JVM学习-类加载

1.加载

将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
_java_mirror 即 java 的类镜像,例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用
镜像起到一个桥梁的作用,Java对象不能直接访问instanceKlass 的信息,它得通过镜像_java_mirror 来访问。例如,对于String,Java只能先找到String.class,String.class实际上就是instanceKlass 的一个对象,它们互相持有指向对方的指针。我们通过java对象,想访问instanceKlass ,需要先访问镜像对象。
_super 即父类
_fields 即成员变量
_methods 即方法
_constants 即常量池
_class_loader 即类加载器
_vtable 虚方法表(各个虚方法的入口地址)
_itable 接口方法
如果这个类还有父类没有加载,先加载父类
加载和链接可能是交替运行的

在这里插入图片描述
类被加载到方法区,方法区的实现就是元空间的实现,所以类的字节码都被加载到元空间中,就构成了instanceKlass数据结构,加载的同时就会在堆内存中生成镜像,就是Person.class对象,这个类对象就是在堆内存中,但是它持有了instanceKlass的地址。反之,instanceKlass也持有了Person.class的地址。如果以后用new关键字创建了一系列的person实例对象。那么它们是怎么联系起来的,每个实例对象都有自己的对象头,16字节,其中,8字节对应着对象的class地址,如果你想通过对象获取class信息,它就会访问对象的对象头。然后通过地址找到java_mirror,然后去元空间找到获取类的信息,如fields,methods等。
instanceKlass保存在方法区。JDK 8以后,方法区位于元空间中,而元空间又位于本地内存中
_java_mirror则是保存在堆内存中
InstanceKlass和.class(JAVA镜像类)互相保存了对方的地址
类的对象在对象头中保存了
.class的地址。让对象可以通过其找到方法区中的instanceKlass,从而获取类的各种信息**

2.链接

2.1.验证是否符合 JVM规范

验证类是否符合 JVM规范,安全性检查 用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行

public class HelloWorld {
    
    
    public static void main(String[] args){
    
    
        System.out.println("hello world");
    }
}

在这里插入图片描述
将CA FE BA BE修改为CA FE BA BA,再来运行一下,看看有没有问题
然后再cmd控制台上运行,报错

E:\git\jvm\out\production\jvm>java cn.yj.jvm.t5.HelloWorld Error: A JNI error has occurred, please check your installation and try again Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 3405691578 in class file cn/itcast/jvm/t5/HelloWorld        at java.lang.ClassLoader.defineClass1(Native Method)        at java.lang.ClassLoader.defineClass(ClassLoader.java:763)        at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)        at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)        at java.net.URLClassLoader.access$100(URLClassLoader.java:73)        at java.net.URLClassLoader$1.run(URLClassLoader.java:368)        at java.net.URLClassLoader$1.run(URLClassLoader.java:362)        at java.security.AccessController.doPrivileged(Native Method)        at java.net.URLClassLoader.findClass(URLClassLoader.java:361)        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)        at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)

它加载完成之后,就要执行链接,执行链接的时候,第一步需要做一个验证,看class字节码的格式是否正确,结果发现magic number是不正确的,所以就报了classformat格式异常。阻止不合法的类继续运行。

2.2.准备阶段:为 static 变量分配空间,设置默认值

static变量在JDK 7以前是存储与instanceKlass末尾。但在JDK 7以后就存储在_java_mirror末尾了即存储在堆中
在这里插入图片描述
如图,JDK1.8的内存结构图,静态变量跟在类对象的后面,跟类对象存储在一起,它们都是存储在堆中的,而在早期的JVM里,静态变量是跟在instanceKlass里面存储在方法区中。

static变量在分配空间和赋值是在两个阶段完成的。分配空间在准备阶段完成,赋值在初始化阶段完成
我们在以下代码里

public class Demo{
    
    
	static int a;
}

我们通过javap反编译它,可以看到生成的字节码里,只有a的声明,没有a的赋值动作,赋值动作体现在cinit类的构造方法中。而类的构造方法是在初始化阶段中被调用的。分配空间和赋值时两个不同的动作。

public class Demo{
    
    
	static int a;
	static int b=10;
}

可以在反编译代码中看到

 static int b;
    descriptor: I
    flags: ACC_STATIC
 ......
    static {
    
    };
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #2                  // Field b:I
         5: return
      LineNumberTable:
        line 5: 0

如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
如果我们加上final类型的关键字时

public class Demo{
    
    
	static int a;
	static final int c=20;
}

但是此时在类的构造方法中,没有变量c的赋值语句,但是在前面准备阶段就有

static final int c;
    descriptor: I
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: int 20

final比较特殊,在准备阶段就可以完成赋值,无需等到后续初始化阶段。因为我已经知道它之后值不会变了。
而对于String类型的话

static final String s="ssss";
static final java.lang.String s;
    descriptor: Ljava/lang/String;
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: String ssss

如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

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 o:Ljava/lang/Object;
        15: return

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

2.3.1.HSDB的使用

先获得要查看的进程ID

jps

打开HSDB

java -cp F:\JAVA\JDK8.0\lib\sa-jdi.jar sun.jvm.hotspot.HSDB

运行时可能会报错,是因为缺少一个.dll的文件,我们在JDK的安装目录中找到该文件,复制到缺失的文件下即可
在这里插入图片描述
定位需要的进程
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.3.2.解析的含义

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

/**
 * 解析的含义
 */
public class Load2 {
    
    
    public static void main(String[] args) throws ClassNotFoundException, IOException {
    
    
        ClassLoader classloader = Load2.class.getClassLoader();
        Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
        //new C();
        System.in.read();
    }
}

class C {
    
    
    D d = new D();
}

class D {
    
    

}

使用loadClass加载类C的时候,只会导致类C的加载,而不会导致类C的解析以及初始化。从而类D也不会被加载和初始化。而如果我们加上new C()的时候,他会让C类加载解析和初始化,从而间接的让D类加载解析和初始化。

在这里插入图片描述
可以看到进程id为7452
然后打开HSDB进程工具,连接到7452,。

打开HSDB
可以看到此时只加载了类C

在这里插入图片描述
查看类C的常量池,可以看到类D未被解析,只是存在于常量池中的符号
在这里插入图片描述
类D是未被解析的类。

我们查看类D被解析的情况

       new C();
       System.in.read();

解析以后,会将常量池中的符号引用解析为直接引用

可以看到,此时已加载并解析了类C和类D
在这里插入图片描述
在这里插入图片描述

解析阶段将常量池中的符号引用解析为直接引用,符号引用仅仅是个符号,它并不知道类,也有可能是属性和方法在内存的哪个位置,但是经过了解析之后,变成了直接引用,它就能确切的知道它们在内存中的位置。

3.初始化

3.1.初始化的理解

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

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

注意

编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如
在这里插入图片描述

3.2.发生的时机

类的初始化的懒惰的,以下情况会初始化

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

以下情况不会初始化

访问类的 static final 静态常量(基本类型和字符串)
它们是在链接的准备阶段完成的。
类对象.class 不会触发初始化
类对象.class实际上类加载时就会生成mirror对象,并不是在初始化阶段完成的。
创建该类对象的数组
类加载器的.loadClass方法
Class.forNamed的参数2为false时

实验

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");
    }
}

注意:在测试第一条的时候, System.out.println(B.b);我们虽然可以打印出B.b=5,但是B的静态代码快没有执行,说明B没有初始化。所以我们可以判断B的静态初始化块有没有执行来判断B有没有初始化

注意:子类的初始化会间接导致父类的初始化,而且父类的初始化在子类的初始化之前

4.练习

从字节码分析,使用 a,b,c 这三个常量是否会导致 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和b都不会导致E的初始化,因为他们都是静态常量,基本类型和字符串常量,都是在类链接的准备阶段赋值的。而c不一样,c是包装类型,赋值虽然为基本类型的形式,但是在底层会自动装箱,Integer.valueOf(20),把20这个基本类型转化为包装类型。所以在准备阶段无法完成,只能在初始化阶段完成。

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");
        }
    }
	 // 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员 
    public static Singleton getInstance() {
    
    
        return LazyHolder.SINGLETON;
    }
}

单例模式保证类的对象实例只有一个。懒惰是指只有在第一次要使用到这个类的时候才会实例化对象,而不会事先把对象创建好。
以上的实现特点是:
懒惰实例化 初始化时的线程安全是有保障的(类加载器保证)

猜你喜欢

转载自blog.csdn.net/qq_39736597/article/details/113617263
今日推荐