JVM类加载机制
Peng's Blog 只记录和技术相关的东西

JVM类加载机制

2017-08-26
JVM

概述

Class文件中描述的各种信息,最终都需要加载到JVM中之后才能运行和使用。这一章主要讲的是JVM如何加载这些Class文件,以及Class文件中的信息进入到JVM中之后会发生一些啥变化。

简单的说就是:JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被JVM直接使用的Java类型。这就是JVM的类加载机制。

在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的。这也是它的一大特点:运行期动态加载和动态连接。

类加载的生命周期和时机

从加载进JVM内存到卸载出JVM内存,主要的生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。

其中(验证,准备,解析)这三部分统称为连接。

那什么时候加载呢?这个没有强制约束,可以交给JVM自己把握。

关于初始化就有要求了:

  • 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,会触发初始化;
  • 使用java.lang.reflect包中的方法对类进行反射调用时,如果类没有进行初始化,那么需要先触发使其初始化;
  • 当初始化一个类的时候,如果发现其父类还没有初始化,那么先初始化它的父类;
  • 当虚拟机启动时,用户需要指定一个主类(含main方法的类),JVM会先初始化这个主类;
  • 当使用Java7动态语言支持时;

类加载的过程

加载、验证、准备、解析、初始化

加载

加载只是类加载的一个阶段。

在这个阶段JVM主要做三件事:

  • 通过类的全限定名来获取定义此类的二进制字节流;
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;

这里有个考点,相对于类加载的其它阶段,一个非数组类的加载阶段是开发人员可控性最强的(可以用系统提供的引导类加载器也可以自定义类加载器)。所谓的自定义类加载器主要是重写一个类加载器的loadClass方法。

但数组类不同,数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。和类加载器的关系的话,数组类里的元素类型需要靠类加载器创建。

加载阶段完成之后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中。

加载阶段与连接阶段的部分内容是交叉进行的,但开始时间还是保持着先后顺序。

验证

目的是为了确保Class文件的字节流中包含的信息符合当前JVM的要求,并且不会危害JVM自身的安全。

Class文件并不一定都是通过Java代码编译而来,可以通过任何途径生成,当然你厉害的话也可以用16进制来写。所以不检查的话,JVM加载错误的字节流,说不定就崩了。

这一阶段主要有:文件格式验证(魔数,版本号,常量池)、元数据验证(是否有父类、是否是抽象类啥的)、字节码验证(对方法体进行验证)、符号引用验证(将符号引用转为直接引用)。

准备

在这一阶段正式为类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。(这里进行内存分配的仅包括类变量(被static修饰的变量))。

比如:

public static int value = 123;

经过准备阶段之后,value的值为0,而不是123。真正赋值的是初始化阶段。

但如果是 public static final int value = 123;

那么准备阶段就会赋值为123了。

解析

解析阶段是JVM将常量池内的符号引用替换为直接引用的过程。

符号引用:

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能正确的定位到目标即可。与JVM的内存布局无关,引用的目标并不一定已经加载到内存。

直接引用:

可以直接指向目标的指针、相对偏移量或者间接定位到目标的句柄。和JVM的内存布局有关,有了直接引用,那么引用的目标必定已经在内存中存在。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 这7类 符号引用 进行。

初始化

在初始化之前的类加载阶段,除了在加载阶段可以用自定义加载器,其它基本上都是JVM控制的。到了初始化阶段才真正开始执行类中定义的Java程序代码(或者说字节码)。

类加载器

重点,非常重点,特别是那双亲委派模型。

类加载器主要是干嘛的?加载类的。通过一个类的全限定名来获取描述此类的二进制字节流,这个动作属于JVM虚拟机外部,是为了方便应用程序自己决定如何去获取所需要的类。

类与类加载器

这一节最重要的一句话:类加载器的作用并不只有加载类,类加载器和类本身一同确定类的唯一性

这句话怎么理解呢?比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则就算这两个类来自同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等。

双亲委派模型

重点.. 重点!!!

timg

启动类加载器:

负责加载 JAVA_HOME/lib下面的东西到虚拟机内存里

扩展类加载器:

负责加载 JAVA_HOME/lib/ext下面的东西到虚拟机内存里

应用程序类加载器:

有时候也被称为系统加载器,负载加载用户类路径(ClassPath)上所指定的类库。如果没有自定义类加载器的话,这个就是默认的了。

解释一下啥叫双亲委派模型:

就是图上那东西,除了顶层的启动类加载器之外,其余的加载器都应当有自己的父类加载器。但这里的父子关系不以继承的关系实现,而是以组合的关系来复用父类加载器的代码。

工作过程:如果一个类加载器收到了一个类加载的请求,那么它首先不会自己去尝试加载这个类,而是把这个类委派给自己的父类加载器去完成,每层都是这样。因此所有的加载请求最终都会传到顶层的启动类加载器,只有当负载反馈说自己没办法完成,其子加载器才会尝试自己去加载。

看上去很蠢,其实很有用。比如,Object类就在lib下面,如果不是顶层加载器加载的话,那么用户完全可以自己写一个Object,那样后果就很严重了。对保证Java程序的稳定运作来说很重要。

破坏双亲委派模型

主要是因为双亲委派模型的灵活性不够。

这里说Java设计团队引入了一个不优雅的设计:线程上下文类加载器:通过Thread.setContentClassLoader()方法进行设置,如果创建线程时还未设置,它将从副线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那么这个类加载器默认就是应用程序类加载器。JDBC就是基于这种实现。

上面是第二次破坏。

第三次破坏是由于用户对程序动态性的追求导致的。


Comments

评论功能暂停使用,如需跟作者讨论请联系底部的GitHub