前言:阅读需要有一定的基础,不太适合初识JVM的人群。有一些语言比较凝练,有些知识点或者是一笔带过,不懂得需要自己详细查阅资料,本文章的目的是补充JVM一些细节,一些稍微深入的地方,一些容易忽视的地方,相信看本篇文章你会有和以往看八股文不同的感觉。后续会补充更多的知识。由于本文章知识帮助补充知识,有些地方如果详细写,可能占用很多篇幅,故精选了几个链接,其文章都不错,本人已经看过,放心食用。适合即将面试的人群。(本文章没有记录非常常规的八股)
内存区域
堆
- 在堆上分配对象的方法:指针碰撞和空闲列表。
指针碰撞需要在堆内存规整的情况下进行指针移动,移动所需要的内存(由垃圾回收器是否有压缩整理来决定)。
空闲列表记录了内存的使用情况。
解决其并发问题的方法有:CAS,失败重试;TLAB(为每一个线程分配一块内存,不够了再申请,各自用各自的)
-
堆上对象的访问方式:句柄和直接指针。
句柄:Java堆会划分一块内存来作为句柄池,reference中存储的是对象的句柄地址,句柄中包含对象实例数据与类型信息的具体地址。
直接指针:reference中存储的就是对象的地址。
前者稳定,当对象的地址改变时,reference指向的句柄地址不用改变。
后者效率高,少了一次寻址时间。(目前HotSpot虚拟机采用的这种)
方法区
用于存储已被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码、运行时常量池等数据。
-
运行时常量池、常量池、字符串常量池(jdk1.8后,将字符串常量池放到了堆中)的区别:
常量池:
java文件被编译成 class文件,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项就是常量池,用于存放编译器生成的各种字面量和符号引用。
运行时常量池:
JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,JVM就会将 class常量池中的内容存放到运行时常量池`中,运行时常量池是每个类都有一个。之后在解析和运行时将其转换为直接引用。
字符串常量池:准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存储到字符串常量池中。
字符串常量池中存的是引用值,而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。
虚拟机栈
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
本地方法栈和程序计数器
JVM调用本地方法(为其他语言提供接口);程序计数器:唯一没有OutOfMemoryError情况的内存区域。
堆外内存
通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,避免在Java堆和Native堆之间来回复制数据。
永久代和元空间
-
永久代(Java7,堆空间):
永久代为方法区的具体实现,存储类相关信息,对于动态生产类的情况,容易出现永久代的内存溢出。
为避免这类问题,将符号引用放到了本地内存;字符串常量池放到了堆中,静态变量放到了堆中
-
元空间(Java8,本地内存)。
-
为什么使用元空间替换永久代
字符串存在永久代,容易出现性能问题和内存溢出。
类及方法的信息难以确定的大小,对永久代的指定,太小容易出现永久代的溢出,太大容易老年代溢出。动态生产类加载容易出现OOM错误。使用元空间,由系统的实际可用空间来控制。
永久代会为GC带来不必要的复杂度,回收效率偏低(加载的类一般不卸载)
-
对象真的死了吗
需要就行两次标记,如果没有在finalize方法中关联上GCRoots,就会被回收。(没有覆盖该方法或者已经调用过,视为没必要执行)(具体查阅资料)
-
GC ROOT的对象包含:
虚拟机栈中引用的对象,方法区中的类静态属性引用的对象,方法区中常量引用对象,本地方法中引用对象。
-
强、软、弱、虚引用
注意加入引用队列的顺序。软引用和软引用,在其对象被垃圾回收后,才加入队列;虚引用在其对象被回收前加入。(具体知识查询资料)
垃圾回收器(三色)
-
三色标记法(具体学习需查询)
白色代表为还没有被垃圾回收器访问过,如果分析结束后,仍旧为白色,则将会被回收
黑色带表对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代 表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
CMS和G1垃圾回收器因为都是并发进行,会出现误标的情况,其都用的三色标记法来进行操作。
CMS解决方案:写屏障+增量更新。记录新增引用,黑色到白色。
G1解决方法:写屏障+SATB。 记录灰色对象到白色对象的引用。
在重新标记中,会对这部分进行重新扫描,防止误标。
-
浮动垃圾
灰色引用被断开后,因为是灰色仍旧会继续扫描,并存活下来。但是其已经与GC ROOT不再相连,因此变为浮动垃圾。
- 注意理解这里的并发与并行。
- 新生代一般采用复制算法(只需要付出少量的对象复制成本就可以完成每次的垃圾收集,因为新生代每次收集都有大量对象的死去),老年代的对象存活几率高,一般采用标记整理算法。
对象何时进入老年代
大对象(需要大量内存连续的)直接进入老年代(如数组)。(因为新生代采用复制算法收集垃圾,大对象直接进入老年代,避免在Eden区和Survivor区发生大量内存复制)
空间分配担保:当Eden区和其中一个survivor区进行垃圾回收,将存活对象放入另一个survivor区时,但又空间不够,只能将Survivor无法容纳的对象直接进入老年代。
年龄判断:1、年龄计数器会为对象记录年龄,每次经过一次GC仍然存活的,年龄+1,当超过设定的值,直接进入老年代。(一般为15)。2、动态对象年龄判定,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到达到要求的年龄。
空间分配担保:
安全的Minor GC:老年代中最大可用的连续空间大于新生代所有对象的空间。
冒险的Minor GC:老年代中最大可用的连续空间大于历代晋升到老年代的平均水平且允许担保失败;如果小于平均值,则直接进行Full GC,让老年代腾出空间
安全点和安全区域
https://www.cnblogs.com/newAndHui/p/12246015.html
逃逸分析
https://cloud.tencent.com/developer/article/1761785
https://blog.csdn.net/chengqiuming/article/details/118639672
双亲委派模型
-
原因、自己所写Java.lang.String会被加载吗
http://fengziji.com/blog/10
-
如何打破双亲委派模型?
因为他的双亲委派过程都是在loadClass方法中实现的,那么想要破坏这种机制,那么就自定义一个类加载器继承ClassLoader,重写其中的 loadClass() 方法,使其不进行双亲委派即可。
使用线程上下文类加载器。(JDBC的源码即是,需查阅资料)
原因:JVM规定某一类加载器加载A类时发现A用到了B(A a=new B( )),那么它就得先去加载B。
Driver类为启动类加载器,实现类也应为启动类加载器加载,但是实现类为厂家自己实现,在用户目录中,无法被启动类加载(只能在自己的范围\lib内查找,所以他无法找到该类),所以便定义了一个上下文件类加载器,默认为应用类加载器。
-
自定义类加载器
继承Class Loader,并且重写 findClass() 方法。
如何进行GC调优
-
优化目标
将进入老年代的对象数量降到最低
减少Full GC的执行时间
优化JVM参数:比如堆栈,设置垃圾收集器的模式。
-
优化策略
将新对象预留在新生代,由于 Full GC的成本远高于 Minor GC,因此尽可能将对象分配在新生代,实际项目中根据GC日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。
大对象进入老年代,虽然大部分情况下,将对象分配在新生代是合理的,但是对于大对象这种做法是值得商榷的,大对象如果首次在新生代分配可能会出现空间不足,导致很多年龄不够的小对象被分配到老年代破坏新生代的对象结构,可能会出现频繁的Full GC,因此对于大对象可以设置直接进入老年代。-XX :PretenureSizeThreshold可以设置直接进入老年代的对象大小。另外大对象在新生代来回复制也是直接进入老年代的原因。(老年代太小,Full GC时间短,但可能out of memory error;老年代大,FullGC发送频率减少,但Full GC时间增长)。
合理设置进入老年代对象的年龄,-XX:MaXTenuringThreshold 设置对象进入老年代的年龄大小,减少老年代的内存占用降低 full gc 发生的频率。
设置稳定的堆大小,堆大小设置有两个参数:-Xms 初始化堆大小,-Xmx最大堆大小。
如果满足下面的指标,则一般不需要进行GC优化:
Minor GC执行时间不到50ms;Minor GC执行不频繁,约10秒一次;
Full GC 执行时间不到1s;Full GC执行频率不算频繁,不低于10分钟1次
杂七杂八
-
内存泄漏:长生命周期的对象持有短生命周期的对象
-
JVM工具使用(举例用一两个)
jstack:死锁检测,死循环
https://blog.csdn.net/sinat_36246371/article/details/53066585
jstat:用于收集HotSpot虚拟机各方面的运行数据。
https://www.cnblogs.com/zhi-leaf/p/10629776.html
-
为什么需要stop the world
因为可达性分析算法必须是在一个确保一致性的内存快照中进行。如果在分析的过程中对象引用关系还在不断变化,分析结果的准确性就不能保证。
-
JVM:字节码运行环境,JDK:在此基础上加上核心类库,JDK:再加上Java工具。