JVM:类加载器

一、类加载阶段

1. 加载

  • 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
    _java_mirror 即 java 的类镜,例如对 String 来说,就是 String.class,作用是把 class 暴露给 java 使用
    _super 即父类
    _fields 即成员变量
    _methods 即方法
    _constants 即常量池
    _class_loader 即类加载器
    _vtable 虚方法表
    _itable 接口方法表
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和连接可能是交替运行的
  • instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中
    在这里插入图片描述

2. 连接

1、验证

验证类是否符合 JVM规范,安全性检查
用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行
在这里插入图片描述因为修改了魔数,所以报了错误:
在这里插入图片描述

2、准备

为 static 变量分配空间,设置默认值

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

示例1:

public class Load8 {
	//仅仅定义一个变量,为这个变量在准备阶段分配内存,并没有对其赋值
    static int a;
}

反编译后的字节码文件:

{
  //声明静态变量,在准备阶段为变量分配内存
  static int a;
    descriptor: I
    flags: ACC_STATIC

  public cn.itcast.jvm.t3.load.Load8();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t3/load/Load8;
}
SourceFile: "Load8.java"

示例2:

public class Load8 {
	//仅仅为静态变量分配内存
    static int a;
    //为静态变量分配内存并初始化(准备阶段和初始化阶段)
    static int b = 10;
    //为静态final变量分配内存并初始化(准备阶段完成--等号右边编译器即可确定)
    static final int c = 20;
    //为静态final变量分配内存并初始化(准备阶段完成--等号右边编译器即可确定)
    static final String d = "hello";
    //为静态final变量分配内存并初始化(分配内存准备阶段,赋值初始化阶段--等号右边编译器无法确定)
    static final Object e = new Object();
}

通过字节码来分析:

{
  //在类的准备阶段为静态变量a分配空间
  static int a;
    descriptor: I
    flags: ACC_STATIC
    
 //在类的准备阶段为静态变量b分配空间
  static int b;
    descriptor: I
    flags: ACC_STATIC
    
 //在类的准备阶段为静态变量c分配空间并赋值
  static final int c;
    descriptor: I
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: int 20

 //在类的准备阶段为静态变量d分配空间并赋值
  static final java.lang.String d;
    descriptor: Ljava/lang/String;
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: String hello
    
 //在类的准备阶段为静态变量e分配空间
  static final java.lang.Object e;
    descriptor: Ljava/lang/Object;
    flags: ACC_STATIC, ACC_FINAL

  public cn.itcast.jvm.t3.load.Load8();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t3/load/Load8;
            
  //类的构造方法,完成类的初始化过程
  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #2  // Field b:I
         5: new           #3  // class java/lang/Object
         8: dup
         9: invokespecial #1  // Method java/lang/Object."<init>":()V
        12: putstatic     #4  // Field e:Ljava/lang/Object;
        15: return
      LineNumberTable:
        line 7: 0
        line 10: 5
}

通过示例1和示例2的比较可以发现:示例1中仅仅声明变量而没有对变量进行赋值时,字节码文件中没有static{};,因为这个是类的构造方法<cinit>()V,编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 <cinit>()V ,在类加载的初始化阶段被调用。
示例2中有静态变量的赋值,因此会多出一个static{};,在这个里面完成对静态变量的赋值操作。

3、解析

将常量池中的符号引用解析为直接引用

3. 初始化

<cinit>()V 方法
初始化即调用 <cinit>()V ,虚拟机会保证这个类的『构造方法』的线程安全

发生的时机:概括得说,类初始化是【懒惰的】

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况:

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化
public class Load3 {
    static {
        System.out.println("main init");
    }
    public static void main(String[] args) throws ClassNotFoundException, IOException {
//        // 1. 静态常量不会触发初始化
//        System.out.println(B.b);
//        // 2. 类对象.class 不会触发初始化
//        System.out.println(B.class);
//        // 3. 创建该类的数组不会触发初始化
//        System.out.println(new B[0]);
        // 4. 不会初始化类 B,但会加载 B、A
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        cl.loadClass("cn.itcast.jvm.t3.load.B");
//        // 5. 不会初始化类 B,但会加载 B、A
//        ClassLoader c2 = Thread.currentThread().getContextClassLoader();
//        Class.forName("cn.itcast.jvm.t3.load.B", false, c2);
        System.in.read();


//        // 1. 首次访问这个类的静态变量或静态方法时
//        System.out.println(A.a);
//        // 2. 子类初始化,如果父类还没初始化,会引发
//        System.out.println(B.c);
//        // 3. 子类访问父类静态变量,只触发父类初始化
//        System.out.println(B.a);
//        // 4. 会初始化类 B,并先初始化类 A
//        Class.forName("cn.itcast.jvm.t3.load.B");

    }
}

class A {
    static int a = 0;
    static {
        System.out.println("a init");
    }
}

class B extends A {
    final static double b = 5.0;
    static boolean c = false;
    static {
        System.out.println("b init");
    }
}

关于类的初始化,我之前写过一个很详细的博客:类的初始化过程

字节码角度分析使用 a,b,c 这3个常量是否会导致 E 初始化?

public class Load4 {
    public static void main(String[] args) {
        System.out.println(E.a);
        System.out.println(E.b);
        System.out.println(E.c);

    }
}

class E {
    public static final int a = 10;
    public static final String b = "hello";
    public static final Integer c = 20;  // Integer.valueOf(20)
    static {
        System.out.println("init E");
    }
}

字节码:

{
  //为静态变量a分配内存并初始化
  public static final int a;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 10
    
  //为静态变量b分配内存并初始化
  public static final java.lang.String b;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: String hello
    
  //为静态变量c分配内存
  public static final java.lang.Integer c;
    descriptor: Ljava/lang/Integer;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

  cn.itcast.jvm.t3.load.E();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 12: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t3/load/E;

  //类的构造方法,完成类的初始化工作
  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: bipush        20
         2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         5: putstatic     #3                  // Field c:Ljava/lang/Integer;
         8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: ldc           #5                  // String init E
        13: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        16: return
}

完成懒惰初始化单例模式

public class Load9 {
    public static void main(String[] args) {
//        Singleton.test();
        Singleton.getInstance();
    }
}

class Singleton {

    public static void test() {
        System.out.println("test");
    }
    
    private Singleton() {}
    
    private static class LazyHolder{
        private static final Singleton SINGLETON = new Singleton();
        static {
            System.out.println("lazy holder init");
        }
    }
    public static Singleton getInstance() {
        return LazyHolder.SINGLETON;
    }
}

二、类加载器

在这里插入图片描述

1. 启动类加载器

public class F {
    static {
        System.out.println("bootstrap F init");
    }
}
public class Load5_1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
        System.out.println(aClass.getClassLoader()); // AppClassLoader  ExtClassLoader
    }
}

结果:对于启动类加载器是由C++编写的,不能通过java代码直接访问,所以打印null就代表是由启动类加载器加载的

F:\JVM虚拟机\JVM_黑马\JVM_01\out\production\JVM_01>java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5_1
bootstrap F init
null

注意:-Xbootclasspath 表示设置 bootclasspath
其中 /a:. 表示将当前目录追加至 bootclasspath 之后

2. 扩展类加载器

自定义一个类,自定义的类都是在classpath路径下面的:

public class G {
    static {
        System.out.println("classpath G init");
//        System.out.println("classpath G init");
    }
}
public class Load5_2 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
        System.out.println(aClass.getClassLoader());
    }
}

结果:因为是自定义的类,自定义的类是由系统类加载器加载的

classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2

下面我们想这个类放在扩展类加载路径下面:

1、通过打包指令将这个类打包:

F:\JVM虚拟机\JVM_黑马\JVM_01\out\production\JVM_01>jar -cvf my.jar cn\itcast\jvm\t3\load\G.class
已添加清单
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)

2、将这个打包好的my.jar放置到C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext目录下:
在这里插入图片描述
3、重新运行后结果:是由扩展类加载器加载的

ext G init
sun.misc.Launcher$ExtClassLoader@29453f44

这里就是双亲委派模型

3. 双亲委派模型

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

4. 线程上下文类加载器

见我的另一篇博客:线程上下文类加载器

5. 自定义类加载器

见我的另一篇博客:自定义类加载器

©️2020 CSDN 皮肤主题: 点我我会动 设计师:上身试试 返回首页