前言
本文是作者自己跟着子牙老师学习JVM的笔记。
本文使用到HSDB,HSDB是JDK自带的探索JVM底层的神器,大家如果对这个工具不了解,可以百度一下。
1.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。
基本类型和引用类型的初值见下表
数据类型 | 零值(初值) |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
char | ‘\u000’ |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.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]等指令对应的常量池项用。