jvm学习笔记2--虚拟机类加载机制

虚拟机类加载机制

 

  1. 生命周期
  • 从被加载到虚拟机内存,到卸载内存为止,包含7个阶段: 加载   、 验证解析 准备初始化使用卸载。

 

jvm学习笔记2--虚拟机类加载机制_第1张图片

  • 验证、准备和解析统称为连接。
  •   加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,而解析则不一定,它可以在初始化阶段之后再开始,以支持java的运行时绑定。

2.  3种情况必须对类进行“初始化”

  • 遇到new(实例化对象)、getstatic(读取类静态字段)、putstatic(设置类静态字段)、invokestatic(调用类静态方法)这4条字节码指令
  • 使用Java.lang.reflect包对类进行反射调用
  • 当初始化一个类时,如果父类没有初始化,则对父类初始化
  • 注意点: 
  1.  
    1. 对于静态字段,只有直接定义该字段的类才被初始化,通过子类引用父类的静态字段,不会触发子类的初始化,只对父类初始化
    2. 通过数组定义来引用类,不会触发引用类的初始化
    3. final修饰的静态常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化;

 

3. 类加载的过程

  • 加载
  1.  
    1. 通过类的全限定名获取定义此类的二进制字节流;
    2. 将字节流代表的静态存储结构转化为方法区的运行时数据结构;
    3. Java堆中生成Java.lang.Class对象,作为方法区这些数据的访问入口
  • 验证
  1.  
    1. 目的:确保Class文件的字节流中包含的信息符合当前虚拟机的要求
    2. 4个阶段的校验过程:

1)文件格式验证:譬如是否以魔数0xCAFEBABY开头、主次版本号是否在当前虚拟机处理范围内等。该阶段基于字节流进行,经过该阶段后,字节流才会存储到内存的方法区中。

2)元数据验证:对字节码描述的信息进行语义分析,以保证信息符合Java规范的要求,譬如是否有父类(除了object类之外,所以的类都应该有父类)、是否继承了不允许被继承的类(被final修饰的类)等;

3)字节码验证:保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,譬如保证跳转指令不会跳转到方法体以外的字节码指令上,方法体中的类型转换是有效的等。

4)符号引用验证:该动作在解析阶段发生。可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性的校验,目的是确保解析动作能正常执行。譬如符号引用中通过字符串全限定名是否能找到对应的类,符号引用中的类、字段、方法的访问性是否可被当前类访问等。一般会抛出java.lang.IllegalAccessErrorjava.lang.NoSuchFieldErrorjava.langNoSuchMethodError等异常。

  • 准备
  1.  
    1. 目的:为类变量分配内存并设置类变量的初始值;
    2. 内存都在方法区进行分配,进行内存分配的仅包含类变量(static修饰的),不包含实例变量
    3. 初始值是指数据类型的零值,譬如:

Public static int value = 123;

准备阶段后的初始值为0,而不是123.

注意:如果是常量(被final修饰),

Public static final int value = 123;

准备阶段后的初始值为123

  • 解析
  1.  
    1. 目的:将常量池中的符号引用替换为直接引用。
    2.   解析发生的时间:在执行anewarraycheckcastgetfieldgetstaticinstanceofinvokeinterfaceinvokespecialinvokestaticinvokevitualmultianewarraynewputfieldputstatic13用于操作符号引用的字节码指令之前
    3. 解析过程

1)类或接口的解析:

假设当前代码所处的类为DD的常量池中有一个从未解析过得符号引用N,将N解析为一个类或接口C的直接引用,过程如下: 

  •  
    •  
      • 如果C不是数组类型:将N的全限定名传递给D的类加载器去加载C。如果加载过程出现异常,则解析失败
      • 如果C是数组类型,且数组类型的元素类型为对象,即N的描述符是“[Ljava.lang.Integer”的形式:按照第1点加载Integer,接着由虚拟机生成一个代表此数组维度和元素的数组对象;
      • 如果上面步骤没出异常,则C在虚拟机中已经成为一个有效的类或接口了,但在解析完成之前,还需要进行符号引用验证,确认D是否具备对C的访问权限,否则抛出java.lang.IllegalAccessError异常。

2)字段解析

  •  
    •  
      • 首先对字段表内class_index项索引的CONSTANT_Class_info符号引用进行解析,即解析字段所属的类或接口的符号引用,用C表示
      • 然后对C进行字段搜索: 
  1.  
    1.  
      1.  
        1. 如果C本身包含了简单名称和字段描述符都与目标匹配的字段,则返回该字段的直接引用,查找结束
        2. 否则,如果C实现了接口,则按继承关系从上往下搜索各个接口和他的父接口。如果接口中包含了简单名称和字段描述符都与目标匹配的字段,则返回该字段的直接引用,查找结束
        3. 否则,按继承关系从上往下搜索其父类。如果父类中包含了简单名称和字段描述符都与目标匹配的字段,则返回该字段的直接引用,查找结束
        4. 否则,查找失败,抛出java.lang.NoSuchFieldError异常。
        5. 如果查找过程成功返回应用,则还需要对该字段进行权限验证,不通过,则抛出java.lang.IllegalAccessError异常

3) 类方法解析

4) 接口方法解析

注:后两者类似于字段解析

  • 初始化   
  1.  
    1. 该阶段才真正开始执行类中定义的Java代码(字节码)
    2. 执行类构造器<clinit>()方法:

 1) <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的

2) <clinit>()方法与实例构造器(<init>()方法)不同,它不需要显示的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。(因此虚拟机中第一个执行的<clinit>()方法的类肯定是java.langObject

3) <clinit>()方法不是必须的,如果一个类没有静态语句块,也没有对变量的赋值动作,那么编译器可以不为这个类生成<clinit>()方法

4)接口可以有变量初始化的赋值动作,因此接口也会生成<clinit>()方法。但与类不同的是,不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量被使用时,父接口才被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

5)虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁和同步。

 

4. 类加载器 

  •   对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性。
  • 三种类加载器: 
  1.  
    1. 启动类加载器(Bootstrap ClassLoader): 负责将存放在<JAVA_HOME>\lib目录中的,或被-Xbootclasspath参数所指定的路径中的,且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。 启动类加载器无法被Java程序直接使用。
    2. 扩展类加载器(Extension ClassLoader: sun.misc.Launcher$ExtClassLoader实现 负责加载<JAVA_HOME>\lib\ext目录中的,或者被Java.ext.dirs系统变量所指定路径中的所有类库; 可以直接使用扩展类加载器
    3. 应用程序类加载器(Application ClassLoader): sun.misc.Launcher$AppClassLoader实现 负责加载用户类路径上(ClassPath)所指定的类库; 一般作为默认的类加载器

注意:类加载器之间的父子关系不以继承来实现,使用组合来复用父加载器的代码

jvm学习笔记2--虚拟机类加载机制_第2张图片

  •     双亲委派模型:

如果一个类加载器收到了类加载的请求,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

  • 定义自己的类加载器时,建议把自己的类加载逻辑写到findClass()方法中,而不是去覆盖loadClass()方法。

 

注:本笔记主要参考:深入理解Java虚拟机--Jvm高级特性与最佳实践一书及网络资料

你可能感兴趣的:(学习笔记)