JVM类结构及加载过程
Java的每个类,在JVM中,都有一个对应的Klass类实例与之对应,存储类的元信息如:常量池、属性信息、方法信息。
jvm中类结构
klass
InstanceKlass java类(非数组)普通的Java类在JVM中对应的是instanceKlass类的实例
InstanceMirrorKlass
用于表示java.lang.Class,Java代码中获取到的Class对象,实际上就是这个C++类的实例,存储在堆区,学名镜像类
InstanceRefKlass
用于表示java/lang/ref/Reference类的子类
InstanceClassLoaderKlass
用于遍历某个加载器加载的类
ArrayKlass 动态数据类型,即是运行期生成的, Java数组的元信息用ArrayKlass的子类来表示
TypeArrayKlass
用于表示基本数据类型的数组 jvm中基本类型数组表现形式
ObjArrayKlass
用于表示引用类型的数组 jvm中引用类型数组表现形式
问题:类加载器将.class文件加载进系统,将.class文件解析,生成的是什么,类的元信息如何存储的?
答案:就是 InstanceKlass,元信息存储在InstanceKlass里
jvm中类结构验证
接下来我们需要用到HSDB。
如何使用HSDB?
进入到jdk的lib文件夹下 ,打开cmd,执行命令 java -cp sa-jdi.jar sun.jvm.hotspot.HSDB,会打开一个HSDB的图形化界面
跑一个不会停的项目,使用jps命令找出端口号,1272就是我现在跑的项目
打开HSDB ,file-》attach ,输入1272端口号确认。
报错:Exception in thread "Thread-1" java.lang.UnsatisfiedLinkError: Can't load library: C:\Program Files\Java\jre1.8.0_20\bin\sawindbg.dll
sawindbg.dll这个文件出错,去网上下载一个对应的版本,64位找64位的,32位找32位的。替换这个文件,重新运行HSDB,attach成功。
我这里保存了一份资源上传到csdn了,win10 64位:https://download.csdn.net/download/qq_39404258/13980337。
attach该进程端口号,然后tools-》class brower ,找到运行的类的内存地址
找到该地址后,然后tools-》inspector ,将内存地址放上去回车,就可以看到该类在jvm中就是一个instanceKlass。
而下面的 java_mirror就是java对象。
class和klass区别
class是java中的 (java代码)
klass是jvm中的 (c++代码)
接下里验证数组
比如我的demo是这样的:
public static void main(String[] args) {
int[] arr1 = new int[1];
Test1[] arr2 = new Test1[1];
}
使用HSDB查看main方法里面的堆栈
根据操作查看main的堆栈情况
我们看一下后两个内存地址是什么对象
第一条为TypeArrayKlass,即基本数据类型的数组
第二条为ObjArrayKlass,即引用类型数组
这也验证了jvm中数组的存在形式。
在idea中下载插件,jclasslib,可以看到字节码文件内容。
点击view-》show Bytecode with jclasslib查看字节码文件内容。
如我刚才的demo对应的字节码内容为:
通过查看jvm字节码手册可知,以上数组部分字节码的含义
而后面的数字10代表基本数据的类型
类加载过程
类加载过程分为这几部:
加载-》(验证-》准备-》解析)链接-》初始化-》使用-》卸载
加载阶段
加载:
1、通过类的全限定名获取存储该类的class文件
2、解析成运行时数据,即instanceKlass实例,存放在方法区
3、在堆区生成该类的Class对象,即instanceMirrorKlass实例
(通常来讲如果不使用,这个class只会在那,并没有被加载。而如果使用了肯定创建了实例)
从哪里获取
1、从压缩包中读取,如jar、war
2、从网络中获取,如Web Applet
3、动态生成,如动态代理、CGLIB
4、由其他文件生成,如JSP
5、从数据库读取
6、从加密文件中读取
何时加载
1、new、getstatic、putstatic、invokestatic
2、反射
3、初始化一个类的子类会去加载其父类
4、启动类(main函数所在类)
5、当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化
当然也有一些例外:包装类、String、Thread属于预加载。
验证阶段
1、文件格式验证
2、元数据验证
3、字节码验证
4、符号引用验证
准备阶段
为静态变量分配内存、赋初值。
实例变量是在创建对象的时候完成赋值的,没有赋初值这一过程。
如果被final修饰,在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,即没有赋初值这一步
如何查看常量池
进入到classes目录下执行命令
javap -verbose 全限定名
如: javap -verbose com.chuan.Test1
可以看到constant pool 也是常说的class文件常量池,可以理解为class文件的资源仓库。常量池(类常量池)就是类在编译后的class文件中的一部分
从这里可以看到,final修饰的已经有值了,而其他的都是在之后才赋的值。
常量池主要存放两大类常量:字面量(文本字符串、声明为final的常量值等)和符号引用(有三类:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符)
解析阶段
将常量池中的符号引用转为直接引用
解析后的信息存储在ConstantPoolCache类实例中
解析前常量池和解析后的常量池,一个是引用了一个字符串,一个是指向了内存地址
解析什么内容:
1、类或接口的解析
2、字段解析
3、方法解析
4、接口方法解析
何时解析:
1.加载阶段解析常量池的时候
2.在执行特定字节码之前进行解析。
初始化阶段
执行静态代码块,完成静态变量的赋值
静态字段、静态代码段,字节码层面会生成clinit方法
方法中语句的先后顺序与代码的编写顺序相关
之前在常量池中可以看到,a最终是a:I保存了下来,只赋了初值,接下来在clinit方法里生成了字节码文件,进行初始化操作。
后面使用和销毁就不作分析了。
来几个栗子
demo1:
public class Test_1 {
public static void main(String[] args) {
System.out.printf(Test_1_B.str);
}
}
class Test_1_A {
public static String str = "A str";
static {
System.out.println("A Static Block");
}
}
class Test_1_B extends Test_1_A {
static {
System.out.println("B Static Block");
}
}
结果:
A Static Block
A str
分析:
因为调用Test_1_B.str,其实还是用到了A的属性,和B没关系,所以不加载B,就不会执行B的静态代码块。
demo2:
public class Test_2 {
public static void main(String[] args) {
System.out.printf(Test_2_B.str);
}
}
class Test_2_A {
static {
System.out.println("A Static Block");
}
}
class Test_2_B extends Test_2_A {
public static String str = "B str";
static {
System.out.println("B Static Block");
}
}
结果:
A Static Block
B Static Block
B str
分析:
因为调用的是B里面的属性,但子类加载父类一定加载,所以A的静态代码块也会执行。
demo3:
public class Test_3 {
public static void main(String[] args) {
System.out.printf(Test_3_B.str);
}
}
class Test_3_A {
public static String str = "A str";
static {
System.out.println("A Static Block");
}
}
class Test_3_B extends Test_3_A {
public static String str = "B str";
static {
System.out.println("B Static Block");
}
}
结果:
A Static Block
B Static Block
B str
分析:子类有这个方法就调用子类自己的属性。
demo4:
public class Test_6 {
public static void main(String[] args) {
System.out.println(Test_6_A.str);
}
}
class Test_6_A {
public static final String str = "A Str";
static {
System.out.println("Test_6_A Static Block");
}
}
结果:
A Str
分析:str属性被final修饰,str的值存在了常量池,所以无需这个对象,即不会执行静态代码块。
demo5:
public class Test_7 {
public static void main(String[] args) {
System.out.println(Test_7_A.uuid);
}
}
class Test_7_A {
public static final String uuid = UUID.randomUUID().toString();
static {
System.out.println("Test_7_A Static Block");
}
}
结果:
Test_7_A Static Block
89ee5c04-8146-4be0-b51f-3901391a2019
分析:因为uuid这个值是动态的,不能存在常量池中,所以需要加载A
本文地址:https://blog.csdn.net/qq_39404258/article/details/111976908