Java|Java JVM类加载机制解读

目录

  • 1.什么是类加载
  • 2.类加载的过程
    • 2.1加载
    • 2.2验证
    • 2.3准备
    • 2.4解析
    • 2.5初始化【重中之重之重中重】
      • 第一段代码:
      • 第二段代码:
      • 第三段代码:
      • 最后一段代码:
  • 总结

    1.什么是类加载 首先你要知道一个类的从被加载到虚拟机内存中开始,到被初始化为止,是为类加载的整个过程。下图就是类加载的整个过程:
    Java|Java JVM类加载机制解读
    文章图片

    一个类只有经历了加载、验证、准备、解析、初始化这五个关卡才能被认为是实现了类加载。这,就是类加载。
    注意一点:上面五个过程并不是按部就班地“完成”,而是按部就班地“执行”(除解析过程外)。执行时一定是先开始加载,再开始验证,但加载过程中也可能会直接开始验证。

    2.类加载的过程
    2.1加载
    “加载”只是是“类加载”过程的第一个阶段,关于在什么时候开始,规范并没有进行强制约束,可以让虚拟机自行把握。在这个阶段中,Java虚拟机需要完成以下三件事:
    【Java|Java JVM类加载机制解读】1)通过一个类的全限定名来获取这个类的二进制字节流
    2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
    3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的方问入口
    可以用一句话概括:加载是一个读取Class文件,将其转化为某种静态数据结构存储在方法区内,并在堆中生成一个便于用户调用的java.lang.Class类型的对象的过程
    Java|Java JVM类加载机制解读
    文章图片


    2.2验证
    验证是连接阶段的第一步,这个阶段的目的是确保Class文件的字节流中包含的信息符合约束要求,,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。
    这一过程了解即可。

    2.3准备
    准备阶段是正式为类中定义的变量(这里说的是静态变量,也就是被static修饰的变量)分配内存,并设置类变量初始值的阶段。
    这里有两点需要强调:
    1)首先这里进行内存分配的仅仅是类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
    2)其次这里设置的初始值“通常情况”下是数据的零值,而不是用户本身对它赋的初值。
    如下代码:
    public static int a = 10;

    变量a在准备阶段后的初始值是0,而不是10,因为现在只是在类加载过程中,还没有执行任何方法。

    上面说到“通常情况”,那就说明还有特殊情况咯,加修饰词final时:
    public static final int a = 10;

    这时在准备阶段虚拟机就会将a设置为10。其实也不难理解:我们将它设置为常量,那就肯定在任何时候都不能修改啊,天子犯法与庶民同罪!

    2.4解析
    解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,这一过程也可能在初始化后进行,并不一定和流程图的执行顺序一样。
    符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
    直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
    这一过程比较复杂,有兴趣可以参考《深入理解Java虚拟机》

    2.5初始化【重中之重之重中重】
    类的初始化阶段是类加载过程的最后一个阶段。在这个阶段Java虚拟机才开始真正执行类中编写的Java程序代码。
    初始化阶段有以下六种情况必须立即对类进行“初始化”:
    • 1)使用new关键字实例化对象的时候
    • 2)读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
    • 3)调用一个类的静态方法的时候
    • 4)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
    • 5)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
    • 6)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
    光说不行,主要看

    第一段代码:
    package com.bit.JVMTest; class Father {publicstatic int a = 10; static {System.out.println("爸爸静态代码块"); }}class Son extends Father{public static int b = 20; static {System.out.println("儿子静态代码块"); }}public class ClassLoaderTest {public static void main(String[] args) {System.out.println(Son.b); }}

    运行结果:
    爸爸静态代码块
    儿子静态代码块
    20
    首先Son.b是在读取Son类自己的静态字段,这点符合上面六中情况的第二种:读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候需要进行初始化。
    其次Son类继承Father类,也就符合第五条:当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化,所以我们先初始化的应该是Father类,然后是Son类。
    因此,打印的内容首先是爸爸静态代码块(父类先初始化),然后是儿子静态代码块(子类再初始化),最后是我们想要打印的b(20)本身。
    再看

    第二段代码:
    package com.bit.JVMTest; classgrandFather{static{System.out.println("爷爷静态代码块"); }}class Father extends grandFather{publicstatic int a = 10; static {System.out.println("爸爸静态代码块"); }}class Son extends Father{public static int b = 20; static {System.out.println("儿子静态代码块"); }}public class ClassLoaderTest {public static void main(String[] args) {System.out.println(Son.a); }}

    运行结果:
    爷爷静态代码块
    爸爸静态代码块
    10
    首先要明确:Son.a是在读取父类Father类的静态字段(注意a字段在Son类的父类中),而不是读取Son类本身的静态字段
    因此这次不会初始化Son类本身。
    因此这次不会初始化Son类本身。
    因此这次不会初始化Son类本身。
    其它的和第一段代码很相似:JVM在初始化Father类的时候,发现这个类还有一个父类没有被初始化,那就先初始化它的父类:grandFather
    因此,打印的内容首先是爷爷静态代码块(Father类的父类先初始化),然后是爸爸静态代码块(Father类再初始化),最后是我们想要打印的a(10)本身。

    第三段代码:
    package com.bit.JVMTest; classgrandFather{static{System.out.println("爷爷静态代码块"); }}class Father extends grandFather{public final static int a = 10; static {System.out.println("爸爸静态代码块"); }}class Son extends Father{public static int b = 20; static {System.out.println("儿子静态代码块"); }}public class ClassLoaderTest {public static void main(String[] args) {System.out.println(Son.a); }}

    运行结果:10
    看到这里是不是想说卧**你*个*。
    别急别急,这里的主函数调用虽然和第二段代码一样,但是注意!!!我们给a这个静态字段加了一个final修饰符
    再看六条中的第(2)条:读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候会触发类加载。
    也就是说我们读取的a是被final修饰的,读取这种静态字段并不会引起任何类的初始化,所以就直接打印a(10)了。
    再看

    最后一段代码:
    package com.bit.JVMTest; class Father {public Father(){System.out.println("爸爸构造方法"); }static {System.out.println("爸爸静态代码块"); }{System.out.println("爸爸普通代码块"); }}class Son extends Father{public Son(){System.out.println("儿子构造方法"); }static {System.out.println("儿子静态代码块"); }{System.out.println("儿子普通代码块"); }}public class ClassLoaderTest extends Son{public static void main(String[] args) {System.out.println("开始"); new Son(); //这里实例化一个Son类的对象System.out.println("结束"); }}

    运行结果:
    爸爸静态代码块
    儿子静态代码块
    开始
    爸爸普通代码块
    爸爸构造方法
    儿子普通代码块
    儿子构造方法
    结束
    看到这里是不是欲哭无泪,我**不学了我。别急先听我细细分析一波~
    这里有一个细节:主类继承了Son类!,这貌似没什么啊,但是还有一个细节:我们的main()方法是主类中的静态方法!看到这里是不是明白了些什么?
    没错!当我们调用main()方法的时候,就引起了主类的初始化,主类继承Son类,Son类继承Father类,所以就先进行Father类的初始化:打印爸爸静态代码块,接着Son类初始化:打印儿子静态代码块,最后该终于我主类初始化了:代码中没什么可以初始化的…(尴尬)。
    接下来是第二阶段:执行main()方法:
    1.先打印:开始字样。

    2.接着是构造 Son()实例,那么就会先构造它的父类Father()的实例:构造实例时按照先执行代码块,再执行构造方法的顺序来。所以就先打印了:爸爸普通代码块、爸爸构造方法 这几个大字。然后再执行构造Son()的实例,构造顺序一样,所以就后打印了:儿子普通代码块、儿子构造方法 这几个大字。

    3.最后打印:结束字样。
    此时main()才方法真正结束。

    总结 我们平常所说的类加载体现在代码上就是初始化这一阶段,我这里结束的也仅限于此,想了解详细的类加载可以参考《深入理解Java虚拟机》这本书,也可以看其他博主的知识总结。感谢你能看到这里!
    到此这篇关于Java JVM类加载机制解读的文章就介绍到这了,更多相关Java JVM 类加载机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

      推荐阅读