当前位置: 首页 > >

深入理解JVM之深入Hotspot了解类加载机制

发布时间:

前言

小编最*涉猎比较广泛,有点贪多嚼不烂的感觉,但是每天有学不完的知识(有的是温故而知新),很充实也很满足,重拾技术的热情。今天为大家带来JVM的类加载机制,首先,是否和小编曾经一样,不知道学*JVM有和作用,好像除了面试的时候通通背一遍做几题关于JVM类加载的面试题,之后进入公司后都是写一些业务代码。这是大家深有感触的。那小编为什么要学*JVM,这边罗列了两点;第一:为了搞清楚Java代码运行的本质。第二:真正遇到JVM相关问题时能够解决,并有相应的能力进行调优。


JVM有几大模块组成:


    类加载的子系统内存模型执行引擎垃圾收集器(GC)JIT(热点代码缓存)

这篇博客跟其他大部分博客可能不太一样,希望大家有耐心可以看完。接下来咱们讲一下最开始的,就是类的加载机制,进入正题。


klass模型

何为klass模型,在说明这个问题之前,大家有没有想过我们编写的java类在JVM中是以何种形式存在的,其实他就是以klass模型存在的,klass模型即java类在jvm中存在的形式。下图为klass模型类的继承结构:

这边小编说明一下



从继承关系上也能看出来,类的元信息是存储在原空间的
普通的Java类在JVM中对应的是instanceKlass类的实例,再来说下它的三个字类


    InstanceMirrorKlass:用于表示java.lang.Class,Java代码中获取到的Class对象,实际上就是这个C++类的实例,存储在堆区,学名镜像类InstanceRefKlass:用于表示java/lang/ref/Reference类的子类InstanceClassLoaderKlass:用于遍历某个加*骷釉氐睦

Java中的数组不是静态数据类型,是动态数据类型,即是运行期生成的,Java数组的元信息用ArrayKlass的子类来表示:


    TypeArrayKlass:用于表示基本类型的数组ObjArrayKlass:用于表示引用类型的数组


小编这边把klazz的理解也分享一下:klazz分为两类
第一种:数组类
instanceKlass:java普通类在jvm中对应的c++类 在方法区中(类的元信息:如访问权限,类的属性,类的方法)
InstanceMirrorKlass:对应的class对象 在堆中
第二种:非数组类
TypeArrayKlass(基本类型数组):boolean,byte,char,int,short,long,float,double
ObjArrayKlass:引用类型


使用HSDB来查看java类对应的klass类(非数组)

HSDB的简单使用:HSDB(Hotspot Debugger),JDK自带的工具,用于查看JVM运行时的状态。
HSDB位于安装目录下与bin同一文件夹的lib里面,小编这里是这样的前面路径是各位的安装目录Javajdk1.8.0_212lib里面,接下来启动HSDB:



java -cp .sa-jdi.jar sun.jvm.hotspot.HSDB
启动后的页面如下图



这边jdk安装方式不同,可能启动连接出现这个报错。(至少小编是这样的,如果没有请自行跳过)

解决方法:去jdk里面找了一下有没有这个dll文件,还真有,我就给copy到外部jre里面对应的目录里面了,接着启动HSDB就没有问题啦。


简单示例代码:


public class LearnJVM {
private int a =2;
private static int b =2;
private final int c =2;
private static final int d =1;

public static void main(String[] args) {
LearnJVM learnJVM = new LearnJVM();
int[] intArr = new int[1];
LearnJVM[] learnJVMS = new LearnJVM[1];
Class learnJVMClass = LearnJVM.class;
while (true);
}
}

启动后查看进程

可以看到刚才运行的代码的PID是5428,我们在HSDB里面去关联进程:
File > Attach to Hotspot process
进入之后内存指针地址:
Tools > Class Browser

LearnJVM 对象的地址是0x00000007c0060828,然后我们去看这个对象的详细信息:
Tools > Inspector

上面是其中一个方法。
还有一个方法是根据类初始化后寻找

然后再使用内存指针地址:
Tools >Inspector


使用HSDB来查看java类对应的klass类(数组)

数组查询的方式,和非数组的第二种方式类似,如图所示

首先是基本数组类型的类型对应:

引用数组类型的对应类型

其实还有一个是main方法里面的string数组的参数也在里面。
你看小编没骗大家吧,通过工具类证明了。当然大家如果懂c++,看源码会更加清晰。


Hotspot源码的话,大家可以自己搭建openjdk8单步调试环境(加油!)


类加载过程

类的生命周期由七个阶段组成,分别是加载,验证,准备,解析,初始化,使用,卸载。
类的加载说的是前5个阶段,一般我们都会说加载,连接,初始化,因为连接包括了验证准备解析三个阶段。


加载

1、通过类的全限定名获取存储该类的class文件
2、解析成运行时数据,即instanceKlass实例,存放在方法区
3、在堆区生成该类的Class对象,即instanceMirrorKlass实例


从哪获取class文件,由下面这些地方等可以获取
1、从压缩包中读取,如jar、war
2、从网络中获取,如Web Applet
3、动态生成,如动态代理、CGLIB
4、由其他文件生成,如JSP
5、从数据库读取
6、从加密文件中读取


验证

1、文件格式验证
2、元数据验证
3、字节码验证
4、符号引用验证


准备

为静态变量分配内存、赋初值。
实例变量是在创建对象的时候完成赋值的,没有赋初值一说。
如果被final修饰,在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,即没有赋初值这一步


问题:准备阶段为什么要赋初值?
明明可以初始化的时候可以赋值,准备阶段赋初值的意义在哪里,其实是这样的,静态变量假如在准备阶段没有赋初值的话,在InstanceMirrorKlass中他的静态变量就没写进去,在初始化阶段,这个静态变量就没有了,会导致后续报错。


解析

将常量池中的符号引用转为直接引用
解析后的信息存储在ConstantPoolCache类实例中
1、类或接口的解析
2、字段解析
3、方法解析
4、接口方法解析


常量池分为,静态常量池,运行时常量池和字符串常量池,这边解析说的常量池为静态常量池,符号引用为静态常量池的索引。转换为直接引用,就是改写成内存地址信息。
下图为解析完成后的直接引用查看:


初始化

执行静态代码块,完成静态变量的赋值 命令clinit;


1、如果没有静态属性、静态代码段,生成的字节码文件中就没有clinit方法块2、final修饰,不会在clinit方法块中体现3、一个字节码文件只有一个clinit方法块4、clinit方法块中生成的代码顺序与Java代码的顺序是一致的。这个会影响程序最终结果。

初始化主要是在主动使用时,主动使用主要有以下几种方式:
1、new、getstatic、putstatic、invokestatic
2、反射
3、初始化一个类的子类会去加*涓咐
4、启动类(main函数所在类)
5、当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化


关于类初始化先后问题

普通类:



静态变量
静态代码块
普通变量
普通代码块
构造函数



继承的子类:



父类静态变量
父类静态代码块
子类静态变量
子类静态代码块
父类普通变量
父类普通代码块
父类构造函数
子类普通变量
子类普通代码块
子类构造函数



抽象的实现子类: 接口 - 抽线类 - 实现类



接口静态变量
抽象类静态变量
抽象类静态代码块
实现类静态变量
实*类静态代码块
抽象类普通变量
抽象类普通代码块
抽象类构造函数
实现类普通变量
实现类普通代码块
实现类构造函数



接口注意:



声明的变量都是静态变量并且是final的,所以子类无法修改,并且是固定值不会因为实例而变化
接口中能有静态方法,不能有普通方法,普通方法需要用defalut添加默认实现
接口中的变量必须实例化
接口中没有静态代码块、普通变量、普通代码块、构造函数



总结

今天关于类加载就先到这儿了,小编这儿有一些题目,感兴趣的伙伴可以联系我,将代码给大家,看大家能做对几个(说不准是猜对的啊,小编这儿有个奇怪的死锁问题,关键是检测工具检测不到,认为不算死锁【感觉逆天了】)。咱们再接再厉加油!



友情链接: