原文详细地址:JVM面试题(二):类加载和JVM内存模型
原文详细地址:JVM面试题(二):类加载和JVM内存模型
原文详细地址:JVM面试题(二):类加载和JVM内存模型
原文详细地址:JVM面试题(二):类加载和JVM内存模型
欢迎关注我的公众号【意姆斯Talk】来聊聊Java面试,对线面试官系列持续更新中!
类加载流程
加载
将类的全限定名转换为对应的二进制Class文件,把类加载进JVM虚拟机中,使用到类时,才会加载类,把类信息加载进方法区,同时创建一个对应的Class实例作为访问方法区中该类的统一入口。
主要是通过类加载器来加载该类,如下
-
-
BootstrapClassLoader:位于/lib目录
-
ExtraClassLoader:位于/lin/ext目录
-
ApplicationClassLoader:位于class目录下的
-
自定义ClassLoader:主要是继承抽象类ClassLoader,实现findClass和loadClass两个方法,其中findClass()是为了加载Class对象,loadClass()是双亲委派机制实现的原理,主要保证类的唯一性和安全性,打破双亲委派机制,直接重写loadClass()方法即可。
\
public Launcher() { Launcher.ExtClassLoader var1; var1 = Launcher.ExtClassLoader.getExtClassLoader(); //目的是为了把ApplicationClassLoader的父加载器设置为ExtClassLoader。 this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); Thread.currentThread().setContextClassLoader(this.loader); SecurityManager var3 = null; if (!"".equals(var2) && !"default".equals(var2)) { try { var3 = (SecurityManager)this.loader.loadClass(var2).newInstance(); } } }} //其中的loadClass()方法,是实现了双亲委派机制的方法。
-
主类在运行过程中,如果使用到某个类时, 才会逐步加载这些类, 而不是一次性全部加载的.
**验证
**
-
- 文件格式验证:主要验证方法区中的变量是否有不支持的类型,二进制字节流是否符合规范等等。
- 元数据验证:语法验证,语义验证,检查该类是否继承了final修饰的类,检查该类是否实现了接口的方法等等。
- 字节码验证:字节码的正确性,当前类的字节码不能包含其他类的字节码,比如int类型的字节码就不应该是string类型的字节码,有点隔离性的味道。
- 符号引用验证:符号引用只是一个唯一标识符,能准确无误找到加载的该类。
\
赋值 如果存在被static修饰的变量,则默认给系统默认值,比如0或null,如果存在被final修饰的变量,则默认给设置默认值。
final Integer age=30;//则此时直接给30作为默认值。static Integer score=100;//则此时直接给0作为默认值。
\
解析
****将符号引用转换成直接引用,符号引用只是一个唯一标识,能够准确无误找到某个类对象,此时该类还未在内存中有准确的地址值,个人理解此阶段就是定义正确的地址值。
\
\
初始化 给定义的变量赋值,即自己定义的值,如果存在静态代码块,则静态代码块的优先级初始化(构造方法)。
\
****使用 ****判断一个类是否被引用,则判断是否跟GC roots存在直接/间接的连接,如果存在,则不会被卸载,如果GC Roots不可达,则可以卸载(gc)。
**
**
卸载**** ****一般对象在堆中不会立即清理,主要是判断该类是否实现了finalize()方法,如果实现了,说明还有复活甲的作用,如果在方法中某个引用链跟该类建联了,则该类不会卸载,反之还是会被卸载。**
**
小细节
新创建的类,一般都是会在Eden区中分配的。
OOM的前提一定是进行了一次Full GC。
\
JVM内存模型\
比如:User user=new User()User是存放在方法区中,user是存放在栈中,newUser()是存放在堆中。
程序计数器 多线程情况下,线程上下文切换,程序计数器像一个记录,知道切换线程时,自己执行到了什么步骤,方便于再次切换线程时能够恢复到争取的执行位置,是线程私有的。\
栈 最小单位:栈帧,一个方法就是一个栈帧,栈帧是由局部变量表,方法出口,操作数栈,动态链接组成,并不是所有的对象都是在堆上分配的,栈上分配,标量替换,同步消除都是JDK新出的优化手段。
-
- 操作数栈:如当前方法A()调用方法B(),当B存在返回值,则B()返回值会作为栈元素入A中的操作数栈。
- 方法出口:B()中的返回值,则在方法出口中作为栈元素,入A中的操作数栈,方法出口是作为一个方法结束的代表。
- 局部变量表:基本数据类型,对象引用类型,对象引用一般是指向堆中的对象地址,常见的有两种:句柄池和直接指针。
直接指针:栈栈中的引用对象,直接指向堆中的实例对象。
句柄池:先指向句柄池,句柄池再指向堆中的实例对象。
两者区别:直接指针的效率比句柄池高,少了一次指针的转向,当堆中的实例对象地址发生了变化,对象引用类型的地址也要指向新地址,而句柄池不会让对象引用类型修改,直接自己修改即可,句柄池也是在堆中划分的一块空间。
-
- 动态链接:将当前方法的符号引用转为直接引用。
符号引用:是一组唯一标识,可以准确找到某一个类的地址,在类加载的时,解析阶段会把符号引用转换成直接引用,简单来说:在编译的时候,每个Java类都会编译成class文件,但在编译的时候类还没有加载进内存中,固不知道真正的引用,解析阶段只能把符号引用改成真正的直接引用(地址)。此阶段针对的是对象,而动态链接针对是方法。
若栈的内存大小不允许动态扩展,超过栈中最大深度时,会造成栈溢出。
若栈的内存大小允许动态扩展,超过栈中最大深度,会造成OOM。
\
本地方法栈提供native方法,此方法都是用c++写的,存在栈溢出和oom。\
方法区存储类信息,常量,静态变量,JIT编译后的代码,方法信息等。类加载把Class对象加载到方法区中,会在队内中创建一个Class对象实例,作为方法区的统一入口。
方法区中还存在常量池:
1:String str="abc",现在常量池中判断是否存在该变量,若存在,直接返回常量池中的引用,若不存在,则创建该对象,并返回常量池中的引用。
2:new String("abc"),保证在堆中创建一个该对象,再判断常量池中是否存在"abc"该常量,如果存在,则不创建了,如果不存在,则创建,最终返回堆中的引用。
3:String.intern()方法,当调用intern()方法时,如果常量池中存在该字符串对象,则直接返回常量池中的引用,\
//如果常量池中不存在abc字符串,则在常量池中创建该对象,在堆中也创建//最终返回堆的引用地址比如:String str=new String(“abc”);//假设str.intern()在常量池中和堆中都存在字符串对象,则优先返回常量池的 String str2=str.intern(); //false sout(str2==str);//==比较地址值。str是堆地址,str2是常量池地址
**
**
**堆:**用问题的方式来总结堆知识****在JDK1.8中,堆一般都是分为老年代和年轻代。
1:对象在堆中如何分配地址?
取决于垃圾回收器,若堆中的内存是规整的,连续的,则采用指针碰撞。若堆中的内存不是规整的,连续的,则采用空闲列表。指针碰撞:将内存划分为A区和B区,A区为已用内存空间,B区为空闲内存空间,中间用指针来划分,新分配的对象直接在空闲内存空间中分配,然后用指针挪动,将新分配的地址划分到已用内存地址中区。标记-整理,标记-复制。
空闲列表:记录可用内存列表,分配时找到足够大的内存地址分配即可,如果存在一个大对象无法分配内存,则Full GC。小细节:CMS如何解决内存碎片问题。标记-清除。
**2:对象在内存分配地址时,如何确保线程安全性
**可以把内存地址视作主内存中的共享变量,各个线程去为对象分配内存地址,可能会造成线程不安全问题,JVM采用了TLAB技术,本地分配缓冲区技术,保证每次给对象分配地址时,提前在堆中划分好一块小内存区域,预分配给每个线程,TLAB保证了隔离性。TLAB目的是为了保证对象分配内存地址的线程安全。JVM默认是开启了TLAB参数。
3:对象的内存布局
-
- 对象头:
-
- MarkWord(hashCode值,对象年龄,偏向锁ID,偏向锁标识等)
- 类型指针:指向该类对象方法区的地址,JVM知道该实例是哪个类的
- 数组长度:只有数组对象具备的属性,记录数组的长度。
- 实例数据:当前对象的属性和值。
- 对其填充:起占位符的作用,需要是8字节的整数倍。
4:如何访问堆中的对象在栈帧的局部变量表中引用类型,一般通过句柄池或者直接指针指向堆的引用对象。
5:是不是所有的对象都在堆上分配不是的,JVM虚拟机提供了一种优化手段,采取逃逸分析技术,判断对象的作用于是否存在某个方法中,当一个对象的指针被多个方法访问时,说明发生了逃逸,如果就在一个方法中创建对象,并且未作为返回值返回给其他栈帧使用,则说明未逃逸,则可以直接在栈上分配对象。同时采用标量替换的方式优化对象中存在的包装类对象,比如把Integer优化成int,把Long优化成long。但是在堆上分配的对象,如果不是大对象,肯定是在Eden区中先分配的。
6:进入老年代的前提
-
-
大对象直接进入老年代
-
对象优先是在Eden区分配的,若Eden区分配不了,则进行young GC,若还是分配不了,则直接进入老年代。或通过参数设置,如果对象超过设置大对象的大小阈值时,则直接进入老年代。
-
\
-
年龄大的进入老年代,可以根据参数设置,默认年龄达到15岁时,进入到老年代,CMS垃圾回收器默认是6岁。年龄以在S0,S1区中交替换几次来判断,替换一次则1岁,替换两次则2岁..\
-
动态年龄判断机制
-
假设S区中存在1,2,3,4,5,6岁以上的年龄对象,将对象年龄累加后,累加都某个年龄时,只要超过了S区的一半时(50%),那么比该年龄大的对象,都晋升到老年代去。
-
\
-
\
-
空间担保机制
-
主要是几个条件判断,
-
1:判断老年代剩余空间是否大于年轻代所有存活的对象
-
2:判断是否开启了老年代空间担保机制
-
3:判断老年代剩余空间是不是每次进入老年代对象的平均大小
-
-
7:完整的对象创建流程 ****
\
如何判断一个类是否是无用的?
1:该类在堆中所有的实例对象都被回收了。2:该类没有被任何对象所引用。3:加载该类的加载器也被回收了(一般情况下只有自定义的加载器才会被回收。)\
常见的引用类型****1:强引用 一般我们new的对象都是强引用,强引用只要一直被GC roots可达,就算导致了FullGC,导致了OOM,也不会回收强引用的对象。
2:软引用 触发了Full GC后,若内存中还存在未能分配的空间,则优先回收软引用。
3:弱引用 触发了Young GC/Full GC,都会回收掉弱引用的对象。
**4:虚引用
** 虚引用一般是配合队列一起使用,当GC后,虚引用对象会入队列,目前自己没在项目中用过,不过在Fork/Join中的源码倒是见过。
\
经典的JVM调优思路1:用jstat gc-pid命令计算出相关的数据,pid是进程id,此阶段主要是拿到堆内存大小,年轻代大小,Eden区和S区的比例,老年代大小,大对象阈值,大龄对象进入老年代的阈值等,主要是获取到自己设置的一些参数信息。
2:计算出年轻代对象增长速率,可以执行命令jstat -gc pid 1000 10(美妙执行一次命令,共执行10次),此阶段主要是估算每秒Eden区大概新增多少对象。最好根据不同的时期统计,因为程序页分高峰期和低峰期。
3:计算young GC的触发频率和每次耗时。
4:每次young GC后由多少对象会进入到老年代,目的是计算出老年代的增长速率,这其中就有有四个进入老年代的条件,
5:计算Full GC的触发频率和每次耗时。
优化总结:尽量保证每次young GC后的存活对象是小于S区的50%,都留在年轻代中,尽量别进入老年代,尽量减少Full GC的频率,Full GC会给JVM性能带来印象,引发STW。推荐参数设置:设置最大堆内存和最小堆内存:开发过程中,通常会将 -Xms 与 -Xmx两个参数配置成相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源。设置老年代和年轻代的比例:一般是老年代:年轻代=2:1设置Eden区和S区的大小比例:一般是Eden区:S1:S0=8:1:1。开启TLAB分配,开启老年代空间担保机制,设置年龄阈值,设置大对象阈值。
\