JVM一:类加载过程详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_32625839/article/details/81484262

  前言:我们都知道一个java程序运行要经过编译和执行,但是这太概括了,中间还有很多步骤,今天来说说类加载。
  学完类加载之后,java运行过程就可以分为:编译—>>>类加载—>>>执行。
  类加载主要是由jvm虚拟机负责的,过程非常复杂,类加载分三步:加载—>>>连接—>>>初始化,(这里的加载和本文标题的类加载是不同的,标题的类加载包含了完整的三个步骤),下面详细说说每一步的过程。
一、详细的类加载过程。

1.1加载:这个很简单,程序运行之前jvm会把编译完成的.class二进制文件加载到内存,供程序使用,用到的就是类加载器classLoader,这里也可以看出java程序的运行并不是直接依靠底层的操作系统,而是基于jvm虚拟机。如果没有类加载器,java文件就只是磁盘中的一个普通文件。

1.2、连接:连接是很重要的一步,过程比较复杂,分为三步:验证—>>>准备—>>>解析

   验证:确保类加载的正确性。一般情况由javac编译的class文件是不会有问题的,但是可能有人的class文件是自己通过其他方式编译出来的,这就很有可能不符合jvm的编译规则,这一步就是要过滤掉这部分不合法文件。
   准备:为类的静态变量分配内存,将其初始化为默认值 。我们都知道静态变量是可以不用我们手动赋值的,它自然会有一个初始值 比如int类型的初始值就是0;boolean类型初始值为false,引用类型的初始值为null 。这里注意,只是为静态变量分配内存,此时是没有对象实例的。
   解析:解析阶段即是虚拟机将常量池内的符号引用替换为直接引用的过程.。
   ( 1 )符号引用(Symbolic References)。符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。
   在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
   ( 2 )直接引用。直接引用可以是直接指向目标的指针(比如,指向“类型”Class对象、类变量、类方法的直接引用可能是指向方法区的指针)、相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)、一个能间接定位到目标的句柄。直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
   ( 3 )或者按以下例子理解:直接引用比如在方法A中使用方法B,A(){B();},这里的B()就是符号引用,初学java时我们都是知道这是java的引用,以为B指向B方法的内存地址,但是这是不完整的,这里的B只是一个符号引用,它对于方法的调用没有太多的实际意义,可以这么认为,他就是给程序员看的一个标志,让程序员知道,这个方法可以这么调用,但是B方法实际调用时是通过一个指针指向B方法的内存地址,这个指针才是真正负责方法调用,他就是直接引用

1.3、初始化:为类的静态变量赋予正确的初始值,上述的准备阶段为静态变量赋予的是虚拟机默认的初始值,此处赋予的才是程序编写者为变量分配的真正的初始值,现在java程序的执行就可以分为:
这里写图片描述

二、类加载的内存分析
  类的加载过程我们已经了解,现在来分析一下类加载的内存分配。
  类加载到底是什么呢?其实类加载不过就是jvm虚拟机为类分配了几块内存空间,说的具体一点,就是jvm虚拟机将类的.class文件加载到内存,并将它放到运行时数据区的方法区内,然后在Java堆中创建对应的java.lang.Class类的对象,这个对象作为程序访问方法区中的这些类型数据的外部接口,用来封装类在方法区内的数据结构。
  这里可能很多人不知道什么是运行时方法区。我们简单了解一下jvm虚拟机的内存管理。
  Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。java虚拟机所管理的内存将会包括以下几个运行时数据区域:
这里写图片描述
  很多区域的作用在这里也不多做解释,我们只需要知道其中的两个:
  方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  堆区:存放对象实例,几乎所有的对象实例都在这里分配内存。
  也就是说,类被加载后,方法区会被分出一块内存,存储这个类的所有信息,但是这个内存块存储的依然是.class文件,并不能被我们使用,我们还需要一个能被直接使用的对象,此时堆区就开始发挥作用。类的信息被存储在方法区后,jvm虚拟机又会想堆区创建和.class对应的java.lang.Class对象,这个对象就好像方法区对应类的一个镜子,把方法区存储的类的结构全部反射过来,然后封装起来,成为了一个Class类的对象(此处运用到反射知识)。这个Class对象与对应的类是一对一服务,因为他有类的结构信息,所以他自然可以构造出一个类的对象。我们平时使用的对象就是由这个Class类的对象生成。到此,类的加载已经完成,但是此时依旧没有我们需要使用的对象产生。
  再看下图:
这里写图片描述

总结:
  类的加载的最终产品是位于堆区中的Class对象。
  Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。


  说完了类的加载过程,我们还要绕回去,谈谈类为什么会被加载,如何触发jvm虚拟机加载一个类?
  先来一句看不懂的话:所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化它们。
  什么意思?
Java程序对类的使用方式可分为两种:
—>>>主动使用
—>>>被动使用
  这里说说什么是主动使用,java对类的主动使用有六种情况:
(1)创建类的实例。
(2)访问某个类或接口的静态变量,或者对该静态变量赋值.
(3)调用类的静态方法。
(4)反射(如Class.forName(“com.test.Test”))。
(5)初始化一个类的子类(先初始化所有的父类,最后初始化本身,接口除外,类初始化的时候,它所实现的接口不会初始化,就算子接口初始化,父接口也不会初始化,只有当程序调用接口的静态变量的时候才会导致接口的初始化)。
(6)Java虚拟机启动时被标明为启动类的类。

  这里唯一需要解释的可能就是最后一个,什么叫被标记为启动类的类呢
  比如我们有一个Hello.java文件,但是里面包含了class Hello ,class Person1 ,class Person2,我们在控制台运行的时候会写java Hello,这个class Hello就是被标记为启动类的类,简单说就是拥有main方法的类。

  以上的六个活动在第一次发生时,都会促使jvm虚拟机加载类。

  除了上述六种情况以外,其他情况都属于类的被动调用,主动调用和被动调用的区别请看下面代码。

class Test2{
  public static final int n = 2;
  static{
    System.out.println("test");
  }
}

public class Test1 {
  public static void main(String[] args) {
    System.out.println(Test2.n);
  }
}
//运行结果
2

Test2中的静态代码块是没有运行的,因为Test2并没有初始化。

注意,这里的final关键字不可缺少,我们知道变量被关键字fianl修饰之后就不可修改,亦即此变量相当于编译期常量(是相当于,并非就是常量),常量在java编译期已经确定,不需要初始化,但是把fianal去掉,或者把 final int n = 2 改为 final int n = new Random()。运行的结果将变为 :

test
2

因为n的值为变量或者n值在编译期不能确定,就必须经过初始化才能使用n的值。

猜你喜欢

转载自blog.csdn.net/qq_32625839/article/details/81484262