猝死JVM--2_类加载机制详解

2020-01-04  本文已影响0人  小安的大情调

努力努力再努力xLg

前言
虚拟机吧描述类得数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以呗虚拟机直接使用得Java类型,这就是虚拟机得类加载机制;

​ --引自《深入理解Java虚拟机》

​ 与许多编程语言不同的是,在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的。

那么为什么Java会采取着这样的机制呢?带来了那些好处,又有什么弊端呢?

类加载

​ 为什么Java会采取这样的机制,让程序在运行期间完成类型的加载、连接和初始化工作呢?

好处

坏处

​ 这种预加载的策略会令类加载时稍微增加一些性能开销。(但可以为Java应用提高高度的灵活--值了!!)

类的加载时机

类的生命周期

​ 每一个类被加载到内存,到被卸载出内存为止,这是它的整个生命周期

Java类的整个生命周期包括以下七个阶段:

1578136860789.png

上述的加载、验证、准备、初始化、和卸载这5个阶段的顺序时确定的。类的加载顺序必须按照这5个顺序按部就班的开始。(这里特定的时开始,还不是进行或者是完成,说明这一点,因为是在这些阶段通常是互相交叉混合进行的,通常是在一个阶段执行的过程中激活或者调用另外一个阶段)。

类的加载、连接与初始化

初始化的时机
主动引用(七种)

​ 这里列举的其中情况都是属于JVM规范中。“有且只有”。

  1. 创建类的实例;(使用New关键字实例化对象的时候,“字节马指令对应的也是new”)
  2. 访问某个类或者接口的静态变量时,或者对该变量进行赋值;(其实就是对静态变量进行取值或者赋值时,(这里需要注意的是,被final修饰、已在编译期把结果放入常量池的静态字段除外))
  3. 调用类的静态方法(对应的字节码指令:getstatic(读取)、putstatic(赋值)、invokestatic(应用))
  4. 反射:使用java.lang.reflect包的方法对类进行反射调用的时候。如果类没有进行过初始化,则需要先触发其初始化。
  5. 初始化一个类的子类:(如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化)
  6. Java虚拟机启动时被标明为启动类时。(例如main()函数、Java Test,虚拟机会先初始化这个主类)
  7. JDK1.7开始提供的动态语言支持时,java。lang。invoke。MethodHandle实例的解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则需要先触发初始化。

其他情况均为被动引用

主动引用与被动引用的区别
package com.lg.jvm.classloader;

/**
 * @author by Mr. Li
 * @date 2020/1/4 12:43
 */
// 主动使用: 启动类
public class MyTest1 {
    public static void main(String[] args) throws Exception {
        Singleton singleton = new Singleton();
        Class.forName("com.lg.jvm.classloader.Singleton1");
        new Sun();
        System.out.println(MyChild1.str);
    }

    static {
        System.out.println("Main static block init...");
    }
}

class MyParent1 {
    public static String str = "Hello World";

    static {
        System.out.println("MyParent1 static block init....");
    }
}

class MyChild1 extends MyParent1 {
    public static String str = "MyChild";

    static {
        System.out.println("MyChild1 static block init....");
    }
}

class Singleton {
    public static String str = "Singleton";
}

class Singleton1 {
    public static String str = "Singleton";
}

class Father {
    public static String str = "father";
}

class Sun extends Father {
    public static String str = "sun";
}

执行结果:这里需要给JVM添加参数-XX:+TraceClassLoading,方可看到详细信息

1578149590560.png

可以看出上述7种主动引用都会触发类加载。

被动引用

​ 通过子类引用父类的静态字段,不会导致子类初始化,但是该子类依然会被加载。

package com.lg.jvm.classloader;

/**
 * 通过子类引用父类的静态字段,不会导致子类初始化,但是该子类依然会被加载。
 *
 * @author by Mr. Li
 * @date 2020/1/4 22:55
 */
public class MyTest2 {
    public static void main(String[] args) {
        System.out.println(MyChild2.str);
    }
}

class MyParent2 {
    public static String str = "MyParent2";

    static {
        System.out.println("MyParent2 static block");
    }
}

class MyChild2 extends MyParent2 {

    static {
        System.out.println("MyChild2 static block");
    }
}

执行结果:
[Loaded java.lang.Void from D:\develop\java\jdk_1.8.172\jre\lib\rt.jar]
[Loaded com.lg.jvm.classloader.MyParent2 from file:/E:/idea_workspace/java_legendary/jvm_lecture/build/classes/java/main/]
[Loaded com.lg.jvm.classloader.MyChild2 from file:/E:/idea_workspace/java_legendary/jvm_lecture/build/classes/java/main/]
MyParent2 static block
MyParent2

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

package com.lg.jvm.classloader;

/**
 *  常量在编译阶段会存入调用类的常量池中,
 *  本质上并没有直接引用到定义常量的类。
 *  因此不会触发定义常量的类的初始化。
 *
 * @author by Mr. Li
 * @date 2020/1/4 22:55
 */
public class MyTest3 {
    public static void main(String[] args) {
        System.out.println(MyParent3.str);
    }
}

class MyParent3 {
    public static final String str = "MyParent3";

    static {
        System.out.println("MyParent3 static block");
    }
}

class MyChild3 extends MyParent3 {

    static {
        System.out.println("MyChild3 static block");
    }
}

[图片上传失败...(image-e257e8-1578153420455)]

这里可以讲MyParent3Class文件删除,一样可以运行成功。因为此时的常量在编译时通过常量传播优化,已将将此常量的值存入到调用类的常量池中了。

反编译结果:

1578151004897.png
这里ldc指令是说明:将常量字符串MyParent2 推送到栈顶,
推送到栈顶,表明马上需要用到该常量。
画外音:其实这里的推送栈顶的指令都是来自于JDK最底层的Java类,都是rt.jar 下的。
1578151196200.png 1578151726073.png
public static final int a = 1; // iconst_* : 将int类型的为-1 到5 之间的常量推送到栈顶(-1 为iconst_m1)
public static final int b = -1;
public static final short c = 127;//bipush:将单个字节(-128 -- 127)的常量推送到栈顶
public static final int d = 6; 
// ldc 表示:将int,float或是String类型的常量推送到栈顶

数组实例也属于被动引用。

​ 因为数组实际上是由JVM自动生成的,类型为动态生成的,其父类就是Object

// todo

上一篇下一篇

猜你喜欢

热点阅读