浅谈Java虚拟机(三)之类加载机制

时间:2023-01-03 11:11:11

 在《浅谈Java虚拟机》这篇文章中,我们提到了JVM从操作系统方面来说,可以将其看做是一个进程,分别有类加载器子系统,执行引擎子系统和垃圾收集子系统。这一篇文章就简单的来谈一下类加载器子系统中的类加载机制

第一:什么叫做类加载机制

就是JVM把.class字节码文件加载到内存中,并对其数据进行校验、准备、解析和初始化,最终形成能够被JVM可以直接拿来使用的java类型的一个过程,叫做类加载。如图示:

浅谈Java虚拟机(三)之类加载机制

1:加载

(1)将.class字节码文件加载到内存中

(2)将静态数据结构(即数据存在于.class字节码文件的结构)转化为方法区(详见《浅谈Java虚拟机(二)》)中运行时的数据结构(即数据存在于JVM时的数据结构)

(3)在堆中生成一个代表这个类的java.lang.Class对象,作为数据访问的入口

(4)有一些类是已经提前就被加载到了JVM中的,无需等到运行加载时才加载

2:连接

 连接就是讲java类的二进制代码合并到java的运行状态的过程

(1)验证:确保加载的类符合JVM的规范与安全

(2)准备:为static变量在方法区中分配空间,设置变量的初始值。例如 static int a = 6,在此阶段会将a被初始化为0。(此处特别注意:如果是 static final int a = 6,那么会在此阶段将a的值初始化为6)

(3)解析:JVM将常量池中的符号引用转化为直接引用。例如 "abc"为常量池中的一个值,直接会将"abc"替换成存在于内存中的地址。

 1)符号引用:符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。它是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。

 2)直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在。

 3:初始化

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。

初始化阶段是执行类构造器<client>方法的过程,<client>方法是由编译器自动收集类中的类变量的赋值操作静态语句块中的语句合并而成的。虚拟机会保证<client>方法执行之前,父类的<client>方法已经执行完毕。

4:使用 --- 正常使用

5:卸载 --- GC把无用对象从内存中卸载

第二:类加载与初始化时机

1:类加载时机

当应用程序启动的时候,因为内存资源有限,为避免影响应用程序的正常运行,所有的类是不会被一次性加载的。

当User user = new User()的时候,一个类真正被加载的时机是在创建对象的时候,才会去执行以上的五个过程,去加载类。大家都知道,java中的main方法是程序的入口,所以它最先加载的是拥有main方法的主线程的所在类。

2:类初始化时机

(1)主动引用(有类初始化过程)

1)new 一个对象。

2)调用类的静态成员(除了final常量)和静态方法

3)通过反射(reflect)对类进行调用

4) JVM启动,main方法锁在类被提前初始化

5)初始化一个类,如果其父类没有被初始化,则先初始化父类

(2)被动引用(没有类初始化过程)

1)当访问一个静态变量时,只有真正声明这个变量的类才会被初始化

2)通过数组定义类的引用

3)final变量不会触发此类的初始化,因为在其编译阶段就会存储在常量池中

第三:类加载器

虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,JVM提供了3种类加载器:

(1)启动类加载器(Bootstrap ClassLoader):

负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类

(2)扩展类加载器(Extension ClassLoader):

负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库

(3)应用程序类加载器(Application ClassLoader):

负责加载用户路径(classpath)上的类库

JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。

浅谈Java虚拟机(三)之类加载机制

当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。

采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。

jdk中的ClassLoader的源码实现:

protected synchronized Class<?> loadClass(String name, boolean resolve)
         throws ClassNotFoundException {
     // First, check if the class has already been loaded
     Class c = findLoadedClass(name);
     if (c == null ) {
         try {
             if (parent != null ) {
                 c = parent.loadClass(name, false );
             } else {
                 c = findBootstrapClass0(name);
             }
         } catch (ClassNotFoundException e) {
             // If still not found, then invoke findClass in order
             // to find the class.
             c = findClass(name);
         }
     }
     if (resolve) {
         resolveClass(c);
     }
     return c;
}

1)首先会通过Class c = findLoadClass(name)来判断一个类是否已经被加载过了

2)如果没有被加载过,则执行if(c == null)中的程序,遵循双亲委派规则 ,首先会通过递归从父类加载器开始找,直到父类加载器是Bootstrap ClassLoader为止

3)最后根据resolve的值,判断这个class是否需要解析


在文章的最后,我转载一篇我觉得写的比较好的关于类加载器双亲委派规则的文章, 《类加载的双亲委派机制》(转载于https://my.oschina.net/kavn/blog/1579576)。