前言
小编最近涉猎比较广泛,有点贪多嚼不烂的感觉,但是每天有学不完的知识(有的是温故而知新),很充实也很满足,重拾技术的热情。今天为大家带来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里面,小编这里是这样的前面路径是各位的安装目录\Java\jdk1.8.0_212\lib里面,接下来启动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<LearnJVM> 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添加默认实现
接口中的变量必须实例化
接口中没有静态代码块、普通变量、普通代码块、构造函数
总结
今天关于类加载就先到这儿了,小编这儿有一些题目,感兴趣的伙伴可以联系我,将代码给大家,看大家能做对几个(说不准是猜对的啊,小编这儿有个奇怪的死锁问题,关键是检测工具检测不到,认为不算死锁【感觉逆天了】)。咱们再接再厉加油!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/13566.html