《深入理解Java虚拟机》读书笔记4

一、类加载的时机

1、类的生命周期

      类从被加载到虚拟机内存中开始,到卸载出内存为止,总共经历七个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中验证、准备和解析三个部分统称为连接(Linking)。

注:加载、验证、准备、初始化、卸载,这五个阶段的顺序是确定的,类加载过程中这五个阶段按顺序开始(不一定按顺序完成,因为通常会在一个阶段执行中调用或激活另外一个阶段)。解析阶段的执行顺序是不定的(因为存在运行时绑定的情况,此时会在初始化之后才开始解析)。

2、执行第一个阶段“加载”的时机

     虚拟机规范中并没有进行强制约束,这点由虚拟机实现决定。

3、初始化阶段的时机

    这个阶段虚拟机严格规定:有且只有以下四种情况必须立即对类进行“初始化”(注意加载、验证、准备自然需要在此之前开始):

<1>、遇到new 、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先触发器初始化。

注:生成这4条指令的最常见的Java代码场景是使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰的编译器静态字段除外)、调用一个类的静态方法的时候。

<2>、使用java.lang.reflect包的方法对类进行发射调用的时候,如果类没有进行过初始化,则需要先触发器初始化。

<3>、当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

<4>、当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。

 

这四种场景中的行为称为对一个类的“主动引用”,除此之外所有的引用类的方式都不会触发初始化,称为“被动引用”。

以下是被动引用的例子:

【】通过子类引用父类的静态字段,不会导致子类初始化

注:对于静态字段,只有直接定义这个字段的类才会被初始化。

测试代码:


 
  1. public class SuperClass {

  2.  
  3. static {

  4. System.out.println("SuperClass init!");

  5. }

  6.  
  7. public static int value = 123;

  8. }


 
  1. public class SubClass extends SuperClass {

  2. static {

  3. System.out.println("SubClass init!");

  4. }

  5. }


 
  1. public class NotInitialization {

  2.  
  3. public static void main(String[] args) {

  4. System.out.println(SubClass.value);

  5. }

  6. }

运行结果:


 
  1. SuperClass init!

  2. 123

注:虽然不会触发子类的初始化,但是子类是否要被加载和验证,取决于虚拟机的具体实现,通过-XX:+TraceClassLoading参数可以看到Sun HotSpot虚拟机在这种情况下导致子类的加载:


 
  1. ...

  2. [Loaded ClassLoad.SuperClass from file:/E:/java_workspace/BookExercise/JVMTest/bin/]

  3. <span style="color:#ff0000;">[Loaded ClassLoad.SubClass from file:/E:/java_workspace/BookExercise/JVMTest/bin/]

  4. </span>SuperClass init!

  5. 123

  6. [Loaded java.lang.Shutdown from D:\Program Files (x86)\Java\jdk1.8.0_91\jre\lib\rt.jar]

  7. [Loaded java.lang.Shutdown$Lock from D:\Program Files (x86)\Java\jdk1.8.0_91\jre\lib\rt.jar]

【】通过数组定义来引用类,不会触发此类的初始化

测试代码:


 
  1. public class NotInitialization2 {

  2.  
  3. public static void main(String[] args) {

  4. SuperClass[] sca = new SuperClass[10];

  5. }

  6. }

这段代码并没有输出“SuperClass init!”,说明没有触发SuperClass的初始化阶段,但会触发另外一个类的初始化阶段,该类代表了一个元素类型为SuperClass的一维数组,数组中应有的属性和方法都实现在这个类里。Java语言中对数组的访问比C/C++相对安全,是因为这个类包装了数组元素的访问方法,当检测到数组越界会抛出ArrayIndexOutOfBoundsException异常。

【】常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

测试代码:


 
  1. class ConstClass {

  2. static {

  3. System.out.println("ConstClass init!");

  4. }

  5. public static final String HELLOWORLD = "hello world";

  6. }

  7.  
  8. public class NotInitialization3 {

  9.  
  10. public static void main(String[] args) {

  11. System.out.println(ConstClass.HELLOWORLD);

  12. }

  13. }

上述代码运行后,没有输出“ConstClass init!”,这是因为在编译阶段将NotInitialization3中对ConstClass.HELLOEWORLD的引用实际都被转化为NotInitialization3类对自身常量池的引用了,也就是说实际上NotInitialization的Class文件之中并没有ConstClass类的符号引用入口,这两个类在编译成Class之后就不存在任何联系了。

4、接口的初始化时机

       接口也有初始化过程,接口中不能使用“static {}”语句块,但编译器仍然会为接口生成“<clinit>()”类构造器,用于初始化接口中所定义的成员变量。

       接口与类的初始化的真正区别在于:主动引用的四种情况中的第三种:

       *对于类,当一个类在初始化时,要求其父类全部都已经初始化过了;

       *对于接口,当一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口时(如引用父接口中定义的常量)才会初始化。

二、类加载过程

1、加载

       “加载”(Loading)阶段是“类加载”(Class Loading)过程的一个阶段,在加载阶段,虚拟机需要完成以下三种事情:

       ** 通过一个类的全限定名来获取定义此类的二进制字节流。

       ** 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

       ** 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

注:加载阶段既可以使用系统提供的类加载器来完成,也可以由用户自定义的类加载器去完成。

 

2、验证

        验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。虚拟机如果不检查输入的字节流,并对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。

       虚拟机规范规定:如果验证到输入的字节流不符合Class文件的存储格式,就抛出一个java.lang.VerifyError异常或其子类异常。

       具体的虚拟机实现,大致都会完成下面四个阶段的检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

<1>、文件格式验证

      验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。

该验证阶段的验证点有:是否以魔数开头?、主次版本号是否能被当前虚拟机处理?、常量类型、常量编码方式等等。

<2>、元数据验证

       该阶段对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,目的是保证不存在不符合Java语言规范的元数据信息。

<3>、字节码验证

       该阶段主要工作时进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。例如,保证跳转指令不会跳转到方法体以外的字节码指令上、保证方法体中的类型转换是有效的等等。

注:如果一个方法体通过了字节码验证,也不能说明其一定是安全的,因为校验程序逻辑无法做到绝对精确。

注:由于数据流校验的高复杂性,耗时较大,所以JDK1.6之后,在Javac中引入一项优化方法(给方法体的Code属性的属性表中增加一项“StackMapTable”属性,该属性描述了方法体中所有基本块开始时本地变量表和操作栈应有的状态,从而将字节码验证的类型推导转变为类型检查从而节省一些时间)。

<4>、符号引用验证

      最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候(注:这个转化动作将在连接的第三个阶段——解析阶段中发生)。符号引用验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,将会抛出一个java.lang.IncompatibleClassChangeError异常的子类,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

      验证的内容有:

      *符号引用中通过字符串描述的全限定名是否能找到对应的类;

      *在指定类中是否存在符号方法的字段描述及简单名称所描述的方法和字段;

      *符号引用中的类、字段和方法的访问性(private、protected、public、default)是否可被当前类访问。

      *….其他情况。

注:验证阶段对应虚拟机类加载机制很重要,但不一定是必须的阶段,在实施阶段可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,来缩短虚拟机类加载的时间。

 

3、准备

           准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。

注意:<1>、这时候进行内存分配的仅包括类变量(static变量),而不包括实例变量(实例变量将会在对象实例化时随着对象一起分配在Java堆中)。

           <2>、这里的初始值通常是数据类型的零值,例如一个类变量的定义为:public static int value = 123; 那么变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作将在初始化阶段才会被执行。

           <3>、在“通常情况”下初始值为零值,一些特殊情况下,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,比如:如果上面的类变量value的定义为:public static final int value = 123; 那么编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。

注;基本数据类型的零值规定如下:

《《深入理解Java虚拟机》读书笔记4》

4、解析

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

符号引用与直接引用的关联:

<>符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是符合约定的任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经架子到内存中。

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

       虚拟机规范没有规定解析阶段发生的具体时间,虚拟机实现会根据需要来判断到底是在类被加载时解析还是等到一个符号引用将要被使用前才去解析。

       同一符号引用进行多次解析请求是很常见的,虚拟机实现可能会对解析结果进行缓存,来避免解析动作重复进行。

      解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。

@1、【类或接口的解析】

      假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,解析过程包含如下3个步骤:

      ** 1、如果C不是一个数组类型,那么虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。如果加载过程出现异常,解析过程将宣告失败。

      ** 2、 如果C是一个数组类型,并且数组的元素类型为对象,就会按照上一条规则加载数组元素类型,然后由虚拟机生成一个代表此数组维度和元素的数组对象。

      ** 3、如果上面的步骤没有任何异常,那么C在虚拟机中已成为一个有效地类或接口了,这时候还要进行符号引用验证,确认C是否具备对D的访问权限,如果发现不具备访问权限,将抛出java.lang.IllegalAccessError异常。

@2、【字段解析】

      首先会对字段表内“class_index”项中索引指向的符号引用(即该字段所属的类或接口的符号引用)进行解析,如果出现异常,会导致字段符号引用解析失败。如果解析成功,虚拟机会按照如下步骤对该类或接口(用C表示)进行后续字段的搜索:

<1>、如果C本身就包含了与目标匹配的字段,则返回这个地段的直接引用,查找结束;

<2>、否则,如果在C中实现了接口,将会按照继承关系从上往下递归搜索各个接口和它的父接口,如果找到了与目标匹配的字段,返回该字段的直接引用,查找结束;

<3>、否则,如果C不是java.lang.Object的话,将会按照继承关系从上往下递归搜索其父类,如果找到匹配的字段,返回该字段的直接引用,查找结束;

<4>、否则,查找失败,抛出java.lang.NoSuchFieldError异常。

注:实际应用中,虚拟机编译器实现可能更严格,如果有一个同名字段同时出现在C的接口和父类中,或者同时在自己或父类的多个接口中出现,那编译器将可能拒绝编译。

 

     如果上述过程成功,还会对这个字段进行权限验证。

@3、【类方法解析】

       首先解析类方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,会进行如下类方法搜索:(注:这个类用C表示):

<1>、如果发现class_index中索引的C是一个接口,那就抛出java.lang.IncompatibleClassChangeError异常。

<2>、如果通过第一步,接下来在类C中查找是否有匹配的方法,如果有则返回这个方法的直接引用,查找结束;

<3>、否则,在类C的父类中递归查找匹配的方法,如果有则返回该方法的直接引用,查找结束;

<4>、否则,在类C实现的接口列表及它们的父接口之中递归查找匹配的方法,如果有匹配的方法,说明类C是一个抽象类,查找结束,抛出java.lang.AbstractMethodError异常;

<5>、否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError;

       最后,如果查找到方法的直接引用,将会对该方法进行权限验证。

 

@4、【接口方法解析】

      首先解析出接口方法表的class_index项中索引的方法所属的类或接口的符号引用,如果成功,按以下步骤搜索接口方法:(这个接口用C表示):

<1>、如果在接口方法表中发现class_index中的索引C是类而不是接口,就抛出java.lang.IncompatiableClassError异常;

<2>、否则,在接口C中查找匹配的方法,如果找到,返回该方法的直接引用,查找结束;

<3>、否则,在接口C的父接口中递归查找,直到ava.lang.Object类(查找范围会包含Object类)为止。如果找到,返回该方法直接引用,查找结束;

<4>、否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。

注:由于接口中的方法默认为public的,所以不存在访问权限的问题。不用进行访问权限的验证。

 

5、初始化

        类初始化阶段是类加载过程的最后一步。初始化阶段就是执行类构造器<clint>()方法的过程

关于类构造器:

        ** 、<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并而产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。

        ** 、虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object类。

        **、由于父类的<clinit>()方法先执行,就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

        **、<clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

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

        **、虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步,同一时刻,只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待。

 

三、类加载器

        虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现。实现这个动作的代码模块称为“类加载器”。

1、类与类加载器

        对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。如果两个类来源于同一个Class文件,只要加载它们的类加载器不同,那么这两个类就必定不相等

 

2、双亲委派模型

类加载器的分类:

         从Java虚拟机的角度分为:启动类加载器(Bootstrap ClassLoader)和其他类加载器。其中启动类加载器,使用C++语言实现,是虚拟机自身的一部分;其余的类加载器都由Java语言实现,独立于虚拟机之外,并且全都继承自java.lang.ClassLoader类。

        从开发者角度分:可以分为三类:

        ** 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且能被虚拟机识别的类库加载到虚拟机内存汇总。启动类加载器无法被Java程序直接引用;

        ** 扩展类加载器(Extension ClassLoader):这个类加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。开发者可以直接使用该类库;

        ** 应用程序类加载器(Application ClassLoader):这个类加载器由sun,misc.Launcher$AppClassLoader来实现。这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,一般情况下是程序的默认类加载器。

 

类加载器的双亲委派模型如下:

《《深入理解Java虚拟机》读书笔记4》

        双亲委派模型(Pattern Delegation Model),要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器。(注意:这里父子关系通常是子类通过组合关系来复用父加载器的代码)。

        双亲委派模型是Java设计者们推荐给开发者们的一种类加载器实现方式。

        双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,先把这个请求委派给父类加载器去完成(所以所有的加载请求最终都应该传送到顶层的启动类加载器中),只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己去加载。

注:实现双亲委派模型的代码都集中在java.lang.ClassLoader的loadClass()方法之中。

 

3、破坏双亲委派模型

<1>、第一次破坏是因为类加载器和抽象类java.lang.ClassLoader在JDK1.0就存在的,而双亲委派模型在JDK1.2之后才被引入,为了兼容已经存在的用户自定义类加载器,引入双亲委派模型时做了一定的妥协:在java.lang.ClassLoader中引入了一个findClass()方法,所以JDK1.2之后不提倡用户去覆盖loadClass()方法,而是把自己的类加载逻辑写到findClass()方法中,如果loadClass()方法中如果父类加载失败,则会调用自己的findClass()方法来完成加载。

<2>、第二次破坏是因为模型自身的缺陷,现实中存在这样的场景:基础的类加载器需要求调用用户的代码(比如JNDL),而基础的类加载器可能不认识用户的代码。为此,Java设计团队引入的设计时“线程上下文类加载器(Thread Context ClassLoader)”。这样可以通过父类加载器请求子类加载器去完成类加载动作。已经违背了双亲委派模型的一般性原则。参看:http://blog.csdn.net/zhoudaxia/article/details/35897057

<3>、第三次破坏是Java引入“代码热替换”、“模块热部署”而产生的。OSGi是当前业界“事实上”的Java模块化标准,OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现。每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/suyimin2010/article/details/81295055
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞