JVM底层类加载

前言

本文是作者自己跟着子牙老师学习JVM的笔记。
本文使用到HSDB,HSDB是JDK自带的探索JVM底层的神器,大家如果对这个工具不了解,可以百度一下。

1.Klass模型

Klass类继承结构:
Klass类继承结构
从继承关系上也能看出来,类的元信息是存储在元空间的
MetaSpaceObj中文翻译:元空间对象
MetaSpaceObj这个类是所有类的顶层父类。

InstanceKlass分支:

  • InstanceKlass:普通的Java类的元信息在JVM中对应的是InstanceKlass类的实例(非数组)
  • InstanceMirrorKlass(镜像类):描述Class类的实例,就是Class对象,存在于堆区
  • InstanceRefKlass:引用,比如声明的强软弱虚的引用。因为这些引用在垃圾回收的时候会做一些特殊处理,不能跟普通的类混在一起。
  • InstanceClasssLoadKlass:用于遍历某个加载器加载的类

例子:比如定义了一个A类,类加载器将A类加载到JVM中,就会生成两个类实例。一个是InstanceKlass,一个是InstanceMirrorKlass。InstanceKlass存储A的元信息,元信息就是A的属性信息,方法信息。InstanceMirrorKlass就是Class的对象,就是A的实例,给Java程序的反射用的。

ArrayKlass分支:

  • ArrayKlass:存储数组类的元信息
  • TypeArrayKlass:基本类型的数组在JVM中的存在形式
  • ObjArrayKlass:引用类型的数组在JVM中的存在形式

PS:
JVM中把数据分为两种数据类型:

  • 静态数据类型:jvm中内置的,比如八种基本数据类型
  • 动态数据类型:运行时动态生成的
    Java中的数组是运行时动态生成的,所以是动态数据类型。

Class和Klass区别:

  • Class:Java类(java代码)
  • Klass:java类在JVM的存在形式(C++代码)

2.类加载过程

类加载的过程如图:
类加载的过程

1)加载

加载过程如下:
第一步:通过权限定名加载存储该类的class文件
第二步:生成运行时数据,即InstanceKlass实例
第三步:生成该类class对象,即InstanceMirrorKcass对象(用于反射)
PS:权限定名:类的报名+类名,例:com.test.Demo

何时加载?
JVM加载类是使用的懒加载:主动使用时加载。

什么地方使用懒加载?

  • new、getstatic、putstatic、invokestatic
  • 反射
  • 初始化类的子类时回去加载其父类
  • 启动类(main函数所在的类)

什么地方使用预加载?
包装类,String,Thread

从哪里加载?
因为没有指明必须从某地获取class文件,脑洞大开的工程师们开发了这些:

  • 从压缩包中获取,如:jar,war
  • 从网络中获取,如:Web Applet
  • 动态生成,如:动态代理CGLIB
  • 由其他文件生成,如:JSP
  • 从数据库读取
  • 从加密文件中读取

2)验证

  • 文件格式的验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

3)准备

准备阶段做了什么?
为静态变量分配内存,赋初值

PS:赋初值:某类定义了一个变量,如果没有给它赋值,JVM会给它赋一个初值。例如int i;i的初值为0。

基本类型和引用类型的初值见下表

数据类型零值(初值)
int0
long0L
short(short)0
char‘\u000’
byte(byte)0
booleanfalse
float0.0f
double0.0d
其他引用类型null

特别注意:如果变量被final修饰,在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,即没有赋初值这一步。

4)解析

解析阶段做了什么?
将常量池中的间接引用转为直接引用。
PS:

  • 直接引用:指向运行时常量池的引用,间接引用也叫符号引用
  • 直接引用:指向内存地址的引用

证明:

第一步:运行下面的代码,用idea查看Test8的常量池

public class Test8 {
    public static void main(String[] args) {
        int arr[] = new int[2];
        Test8_A arrs[] = new Test8_A[2];
        while (true);
    }

}

class Test8_A {
    static {
        System.out.println("Test8_A Static Block");
    }
}

在这里插入图片描述
在这里插入图片描述

可以看到#2指向指向#27#27的值是“test/jvm/Test8_A”。由此可见:#2现在还是一个间接引用。
第二步:我们利用HSDB查看Test8的常量池
先查找到Test8的进程id
在这里插入图片描述
然后打开HSDB,点击file->Attach to Hotspot …,输入Test8的进程id
在这里插入图片描述
然后点击Tools->Class Browser,输入“Test8”,可以看到下图
在这里插入图片描述
点击public class test.jvm.Test8@0x…,向下拉,找到Constant Pool,点击查看Test8的常量池
在这里插入图片描述
可以看到#2的值为一个真实的内存地址。
所以:解析阶段JVM把间接引用变成了直接引用。

5)初始化

初始化做了什么?
执行静态代码块,完成静态变量的赋值。规则如下:
如果定义了一个static属性,JVM也会自动生成一个clinit方法,生成的clinit方法的代码顺序和定义代码顺序保持一致(也就是说代码执行的顺序和定义代码的顺序一致)。下面会有代码证明。

6)使用

7)卸载

3.代码验证类加载规则

例1:验证初始化规则

public class Test1 {

    public static void main(String[] args) {
        Test1_A instance = Test1_A.getInstance();
        System.out.println("val1---"+Test1_A.val1);
        System.out.println("val2---"+Test1_A.val2);
        while (true);
    }
}
class Test1_A{

    public static int val1 = 3;

    public static Test1_A instance = new Test1_A();

    Test1_A(){
        System.out.println("val1---"+val1);
        System.out.println("val2---"+val2);
        val1++;
        val2++;
        System.out.println("val1---"+val1);
        System.out.println("val2---"+val2);
    }
    public static int val2 = 6;

    public static Test1_A getInstance(){return instance;}
}

输出:
val1—3
val2—0
val1—4
val2—1
val1—4
val2—6
解析:加载时执行代码的顺序是和代码实际顺序是一致的,执行前两组sout的时候,val2只完成了赋初值,然后才会执行public static int val2 = 6;

例2:验证加载规则

public class Test2 {
    public static void main(String[] args) {
        System.out.println(Test2_B.str);
        while (true);
    }
}
class Test2_A {
    public static String str = "a str";
    static {
        System.out.println("A static block");
    }
}
class Test2_B extends Test2_A {
    static {
        System.out.println("B static block");
    }
}

输出:
A static block
a str
解析:JVM加载类是懒加载模式:主动使用时加载。JVM会先判断是否加载,后面才会有初始化动作发生。
这里没有用到Test2_B,也就不会加载Test2_B,也就不会执行Test2_B的静态代码块。所以不会输出“B static block”。

例3:验证加载规则

public class Test3 {
    public static void main(String[] args) {
        System.out.println(Test3_B.str);

    }
}

class Test3_A {
    static {
        System.out.println("A static block");
    }
}

class Test3_B extends Test3_A {
    public static String str = "B str";
    static {
        System.out.println("B static block");
    }
}

输出:
A static block
B static block
B str
解析:使用子类的static属性时,间接使用了父类,按照加载顺序,先加载父类,再加载子类,最后执行main方法里面的输出语句

例4:验证初加载规则和引用类型数组对应ObjArrayKlass

public class Test4 {
    public static void main(String[] args) {
        Test4_A arrs[] = new Test4_A[2];
        Test4_A demo;
        while (true);
    }

}
class Test4_A {
    static {
        System.out.println("Test4_A Static Block");
    }
}

输出:
没有输出
解析:因为Test4_A arrs[] = new Test4_A[2]只是定义了Test4_A数组,并没用使用到Test4_A类。Test4_A demo只是定义了Test4_A类型的变量,并没有使用Test4_A类。所以不会执行Test4_A的静态代码块。

按下面的步骤,查看引用类型的在JVM的存储方式。
操作方式:1选中main函数。2点击第二个按钮。3查看main线程的堆栈。4找到定义的arrs[]变量的地址。5点击tools->Inspector,输入地址。6可以看到arrs[]变量对应的是ObjArrayKlass类型。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

例5:使用被final修饰的常量时?
1.如果常量写死,那么不会执行静态代码块
2.如果常量是别的类动态生成,那么会执行静态代码块

例子:当被final修饰的常量的值写死时

public class Test5 {
    public static void main(String[] args) {
        System.out.println(Test5_A.str);
    }
}

class Test5_A {
    public static final String str = "Test5_A str";
    static {
        System.out.println("Test5_A static block");
    }
}

输出:
Test5_A str
解析:变量str被final修饰,在准备阶段就完成了赋值,然后JVM将常量str写入了Test5_A的常量池中。使用到str常量时,就直接去常量池拿,不会执行Test5_A的静态代码块,也就不会输出Test5_A static block这句话了。
证明:1打开命令行,cd到classes目录,执行:javap -verbose test.jvm.Test5_A。2查看输出,如图:
在这里插入图片描述

例子:当被final修饰的变量的值被别的类动态生成时

public class Test6 {
    public static void main(String[] args) {
        System.out.println(Test6_A.str);
    }
}

class Test6_A {
    public static final String str = UUID.randomUUID().toString();
    static {
        System.out.println("Test6_A static block");
    }
}

输出:
Test6_A static block
9cbab0fd-ca5a-40f9-9f12-d7e97f05f907
解析:public static final String str = UUID.randomUUID().toString();这句话虽然定义了str被final修饰,但它的值却是动态生成的。Test6的字节码中Str的值是一段代码段。当初始化时,就会执行这段代码段。这时候就涉及到Test6_A的主动加载,就会执行Test6_A的静态代码块了。

例6:验证加载规则

public class Test7 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("test.jvm.Test7_A");

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

输出:
Test7_A static block
解析:Class.forName(“test.jvm.Test7_A”)使用了反射,当使用反射时,JVM懒加载。

例7:验证初始化阶段,JVM底层加锁

public class InitDeadLock {

    public static void main(String[] args) {
        new Thread(() -> A.test()).start();
        new Thread(() -> B.test()).start();
    }
}

  class A {
    static {
        System.out.println("class A init");

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new B();
    }

    public static void test() {
        System.out.println("aaa");
    }
}

  class B{

    static {
        System.out.println("class B init");

        new A();
    }


    public static void test() {
        System.out.println("bbb");
    }
}

输出:
如果把new Thread(() -> B.test()).start();注释掉,则输出:
class A init
class B init
aaa
然后程序结束
如果把new Thread(() -> A.test()).start();注释掉,则输出:
class B init
class A init
bbb
然后程序结束
如果都不注释,那么会发生思死锁,输出:
class A init
class B init
或者
class B init
class A init
这就证明了初始化阶段,JVM底层会加锁,A B两个类都在等着对方释放锁,所以程序不会停止,进入了死锁。

4.读取静态属性的底层实现

第一步:运行如下代码

public class Test2 {
    public static void main(String[] args) {
        System.out.println(Test2_B.str);
        while (true);
    }
}

class Test2_A {
    public static String str = "A str";
    static {
        System.out.println("A static block");
    }
}

class Test2_B extends Test2_A {
    static {
        System.out.println("B static block");
    }
}

第二步:查看Test2_A和Test2_B的结构
在这里插入图片描述
在这里插入图片描述

可以发现:静态变量是用InstenceMirrorKlass存储的,而且父类的静态变量只会存储在自己这,子类不会存储父类的静态变量。
PS:oop Klass 类的内存地址
子类是如何访问父类的静态属性的?
1、先去Test_1_B的镜像类中去取,如果有直接返回;如果没有,会沿着继承链将请求往上抛。很明显,这种算法的性能随继承链的death而上升,算法复杂度为O(n)
2、借助另外的数据结构实现,使用K-V的格式存储,查询性能为O(1)
JVM使用哪种方式?
在JVM里,借助另外的数据结构ConstantPoolCache。常量池类ConstantPool中有个属性_cache指向了这个结构。每一条数据对应一个类ConstantPoolCacheEntry。

PS:ConstantPoolCache主要用于存储某些字节码指令所需的解析好的常量项,例如给[get|put]static、[get|put]field、invoke[static|special|virtual|interface|dynamic]等指令对应的常量池项用。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值