JVM垃圾回收总结
JVM垃圾回收:
判断对象是否可以回收:
引用计数法:
只要一个对象被其他对象引用一次,计数就+1;当它取消了被引用,计数就-1。当它的引用数等于0的时候,就把它当成垃圾回收。
弊端:循环引用的时候,会导致循环引用的双方计数器都为1,那么他们就一直不能被当成垃圾回收,会造成内存泄漏
JVM采用的可达性分析算法:
根对象:肯定不能被当成垃圾回收的就称为根对象
可达性分析算法:如果一个对象被根对象直接或者间接的引用,那么这个对象就不能被回收;反之则会在将来被回收。
原理:扫描堆中对象,看是否能沿着GC Root为起点的引用链找到该对象,找不到就是可以回收
有哪些可以成为GC Root:使用MAT工具进行分析,使用jmap导出代码运行某一阶段的二进制bin文件,导入MAT分析。
系统类:即核心类都能作为GC Root,比如Object、HashMap、String等类
Native方法类:本地方法的引用类也能作为GC Root
Busy Monitor对象:即加了锁的对象可以作为GC Root,不然一旦清除将无法解锁
线程类:线程需要的对象可以作为GC Root,不然线程无法正常运行。但是栈帧(方法调用)的局部变量的引用对象都可以作为根对象,引用对象并不是对象,就像list = new ArrayList<>(),其中list就是引用对象。当list = null的时候,局部变量被回收,但是list没有被回收。
五种引用:(无论是什么引用,它们本身就是一个对象)
强引用:
我们平时使用的赋值 A = B 就属于强引用。当一个对象被强引用时,它将不可能被回收。
软引用:
当一个对象被弱引用的时候,如果内存足够,将不会被回收;但是当内存不足够的时候,该对象就会被回收。
可以配合引用队列使用,当软引用对象被回收的时候,它就会回到自己所属的引用队列中。
软引用的应用:
软引用需要获取SoftReference<>对象,它里面是一个泛型,用于装载需要被软引用的类,还可以在new的时候绑定队列。当内存不够的时候,软引用所指向的内存空间就会被回收,即软引用了null,释放了资源。如果有队列ReferenceQueue就加入队列。
当使用软引用的时候,垃圾回收之后虽然还有SoftReference对象,但是它再也读取不到之前指向的地址了
弱引用:
比起软引用更短的生命周期。当程序执行了垃圾回收的时候,只要发现了弱引用的对象,无论内存空间够不够,都立刻把它回收。
可以配合引用队列使用,当弱引用对象被回收的时候,它就会回到自己所属的引用队列中。
弱引用的应用:
弱引用需要获取WeakReference<>对象,它里面是一个泛型,用于装载需要被弱引用的类,还可以在new的时候绑定队列。当触发GC的时候,弱引用所指向的内存空间就会被回收,即弱引用了null,释放了资源。如果有队列ReferenceQueue就加入队列。
当使用弱引用的时候,垃圾回收之后虽然还有WeakReference对象,但是它再也读取不到之前指向的地址了
虚引用(必须配合引用队列使用):
虚引用就是形同虚设的引用。当一个对象持有虚引用的时候,无论什么时候都有可能被系统回收。它的唯一作用就是当对象被回收的时候返回一个系统通知。
终结器引用(必须配合引用队列使用)(不建议使用):
所有Java对象都会继承一个Object父类,而Object类有一个finalize方法,也就是终结方法。当当前对象重写了finalize方法并且没有强引用的时候,就可以把它当成垃圾回收。当一个对象有终结器引用时,回收的时候会先把终结器引用加入引用队列中,再由一个优先级很低的线程FinalizeHandler来监听是否有对象被放入引用队列了,如果有就在下一次执行垃圾回收的时候进行回收。
2.垃圾回收算法(三种结合工作)
2.1标记清除算法
标记:寻找那些GC Root不可达的内存空间,标记为垃圾
清除:把找到的垃圾的起始地址和结束地址记录下来,在下一次分配空间的时候可以进行覆盖(也相当于删除,再也读取不到了)
优点:速度快;缺点:容易产生内存碎片,会造成内存溢出
2.2标记整理算法(存活较多使用)
标记:寻找那些GC Root不可达的内存空间,标记为垃圾
整理:避免内存碎片问题,在删除的过程中,把每个非垃圾的数据往前移动,进行整理
优点:避免了内存碎片的问题,使内存使用率更高;缺点:牵扯移动数据,速度慢
2.3复制算法(存活较少的时候才可以使用)
开辟两个空间,一个是FROM,一个是TO。当发生GC的时候,FROM里面非垃圾的元素会复制到TO里面。此时把FROM全部数据删除,并交换FROM和TO的角色,即TO变成了FROM,FROM变成了TO。
优点:不会产生内存碎片;缺点:开辟空间较大
3.分代回收
什么是新生代和老年代?(注意:新生代和老年代都在堆中)
JVM把堆划分成两块,分别是新生代和老年代。
新生代中保存了一些价值较低的对象,例如生命周期较短的对象。
老年代保存了生命周期更长的对象。
新生代里面又划分了三个区域:伊甸园(8),幸存区FROM(1),幸存区TO(1)
当我们创建一个新的对象的时候,都会把它放在伊甸园中。伊甸园放着放着放不下了,就会触发一次Minor GC,对伊甸园中的元素进行复制删除算法,把非垃圾存放到幸存区TO,交换TO和FROM,并清除伊甸园中的垃圾。被放入幸存区TO的数据的年龄会+1,当它超过了一个阈值(最大是15次)或者新生代怎么都放不下了的时候,就会被加入老年代。如果新生代和老年代都放不下了,就会触发Full GC,使得新生代和老年代都进行垃圾回收。
大对象直接晋升机制:
JVM通过计算如果发现一个新的对象在新生代怎么都放不下了,如果老年代还放得下,就直接晋升到老年代,并不需要等阈值。如果老年代也放不下,就抛出OOM错误。
GC类型:
Minor GC:仅针对新生代的GC
Minor GC在触发的时候,把伊甸园中的非垃圾复制存放入幸存区TO,交换FROM和TO,并使得放入幸存区TO的数据年龄+1。
Minor GC会触发Stop the world:也就是说当它在进行垃圾回收的时候,其他线程将停止。等到垃圾回收完成后,再允许其他用户线程执行。因为Minor GC涉及到对象的复制问题,所以当GC的时候需要停止其他用户线程。
Full GC:针对新生代和老年代的GC
当老年代空间不足,会先进行一次Minor GC,但是如果之后空间依然不足,就会触发Full GC。
Full GC也能触发Stop the world。
如果Full GC之后也不能存放数据,就会抛出异常。
线程之间的OOM错误:
如果在子线程里面抛出一个OOM错误,它会直接把子线程占用的空间立即释放掉,并不会影响主线程的正常工作。
4.垃圾回收器
串行回收器:
由于是串行的,即表明它是一个单线程,适合堆内存较小而且CPU内核数小的计算机,例如个人电脑。
吞吐量优先回收器:
吞吐量优先回收器是多线程的。适合堆内存较大的场景,需要多核CPU支持。
使得单位时间内,Stop the world的时间尽可能地短
类比吃饭就是一口尽量吃的多,少餐多食
对于吞吐量优先回收器,它的实现是并行的。当各个CPU在处理到某个需要GC的点的时候,会跳入安全点内,然后数个CPU共同执行垃圾回收线程,阻塞用户线程。这时CPU占用率会暴涨,不过一次性处理的垃圾较多。
响应时间优先回收器:
响应时间优先回收器是多线程的。适合堆内存较大的场景,需要多核CPU支持。
使得单次的Stop the world的时间尽可能地短
类比吃饭就是尽量吃的快,少食多餐
对于响应时间优先回收器,它的与其他用户线程运行是并发的,有可能造成内存碎片。当需要进行垃圾回收的时候,某个CPU会进行初始标记,这是用于标记GC Root对象的,并阻塞用户线程。然后解放用户线程,自己GC Root寻址(并发标记)。并发标记完成后,由于用户进程之前运行的时候可能产生了垃圾,所以进行一次重新标记。最后由垃圾删除线程并发清除垃圾。
有可能会残留垃圾:在并发清除的同时,其他线程产生的垃圾需要等到下一次GC才能清除。
G1回收器:
G1全称是Garbage First,整体上是标记+整理算法,两个区域之间使用复制算法
JDK9开始成为默认回收器,并废弃CMS(响应时间回收器)回收器。内部是并发处理的,追求的是吞吐量和低延迟。适用于超大的堆内存。
G1回收器工作流程:
步骤1:Young Collection 新生代的垃圾收集
当伊甸园被占满的时候,会触发一次新生代的垃圾收集,同时发生STW。当发生新生代的垃圾回收之后,幸存的对象会放入幸存区。当幸存的对象经历了一定周期之后或者幸存区内存不足的时候,也会触发一次新生代垃圾回收,晋升进入老年代的区域。
步骤2:Young Collection + Concurrent Mark 新生代的垃圾回收+并发标记老年代的垃圾
初始标记发生在Young GC,而当老年代在堆内存的储存到达一个阈值的时候,就会触发并发标记,把不在根对象下的垃圾进行标记。
步骤3:Mix Collection 混合垃圾收集,同时进行新生代和老年代的垃圾收集
在Mix Collection中,最终标记(即并发标记后的重新标记)会导致STW;拷贝存活也会导致SWT。
对新生代和老年代内存区域进行垃圾收集,在移动数据的时候采用复制算法。Eden可能去了Survive或者老年代区等。
为了不超过最大GC暂停时间,Mix Collection会依次选择最有价值的老年代非垃圾数据,复制放入新的一片区域中,以减少内存碎片。
各种回收器的Minor GC和Full GC的比较
串行回收器:
新生代内存不足:minor gc
老年代内存不足:full gc
并行回收器(吞吐量优先):
新生代内存不足:minor gc
老年代内存不足:full gc
并发回收器(响应时间优先):
新生代内存不足:minor gc
老年代内存不足:
G1回收器:
新生代内存不足:minor gc
老年代内存不足:老年代内存到达阈值,当回收速度比生产垃圾速度快的话,就只触发并发垃圾收集阶段;反之则触发full gc
Young Collection跨代引用
开发中会出现老年代的引用了新生代的情况,那么如果我们在可达性分析的时候就需要遍历整个老年代内存,来查看那些新生代对象是垃圾,这种设计显然是不合理的。
老年代采用了一个叫卡表的技术,把老年代的区域再进行一次细分,每个细分的卡为512B。如果老年代其中有一个对象引用了新生代的对象,那么就标记为脏卡。在将来GC Root遍历的时候,就只需要遍历脏卡就可以了,减小了搜索范围,提高效率。
Remark(重新标记阶段):
上图在并行标记的时候,黑色表示已经处理完成,灰色表示正在处理,白色表示暂未处理。
而箭头表示引用。
在并发标记的时候,当A -> X的引用断开了,那么X就会被标记成白色,A标记成黑色。但是由于是并发执行,B可以来一个强引用到X,但此时B为黑色表示不会再检查了,那就是说X还是白色。
由于有以上情况,所以引入了重新标记。当发生强引用的时候,给X一个写屏障,并放入队列中,变为灰色。到达重新标记阶段时,查看队列中是否有灰色,并对它进行GC Root判断它是否能被回收。
JDK8的字符串去重:
在字符串进行创建的时候,会自动地放入一个队列中。当G1开始回收的时候,会并发的检查时候有字符串重复,如果有,那就让他们引用同一个地址。
与String.intern方法不同,这个注重的是字符数组,String.inten注重字符串
JDK8的类卸载:
在JDK8u40,当G1在并发标记结束之后,就能知道那些类时不再被使用的。当类实例都被卸载且类加载器的所有类都不再使用,就把这个类加载器加载的所有类都卸载
JDK8的回收巨型对象:
当一个对象大小大于半个储存块的时候,就称它为巨型对象。
G1不会对举行对象进行拷贝,而且回收的时候会优先考虑它。
G1会检测巨型对象被老年代引用的数量,当数量为0的时候,回收巨型对象。
JDK9并发标记起始时间的调整:
并发标记必须在堆空间占满之前完成,否则退化成Full GC。
JDK9可以动态调整堆占用阈值,以令G1开始并发标记阶段。
5.GC调优(各个环境都不相同)
通过java xx:-PrintFlagFinal可以查看当前使用的虚拟机参数
由于GC会触发STW,会导致用户体验不好,所以我们需要使用调优
调优目标:
需要低延迟(响应时间快)还是高吞吐量?
互联网项目就使用低延迟:CMS,G1,ZGC(JDK12)
要科学运算就使用高吞吐量:ParallelGC
最快的GC是不发生GC:
如果系统发生比较多GC/Full GC,需要考虑是否数据量太多,数据表示是否太臃肿,或者是否存在内存泄漏。
新生代调优:
每一个线程都会在伊甸园中给它分配一个私有的空间TLAB(Thread-local allocation buffer),当new一个新对象的时候,就会在对应线程的私有空间中占用一片区域。
新生代死亡对象代价是0,大部分对象用完即死,同时Minor GC的时间远远低于Full GC的时间。
新生代内存越打越好吗?
如果新生代的内存大了,那么老年代占用的就少了,那就是说会更加频繁的触发Full GC。并不是越大越好,最好在总堆内存大小的1/4-1/2,推荐新生代能容纳一次请求-响应中的所有对象*并发量,即 并发量*(请求-响应)中所有的数据。
幸存区调优(JVM会通过幸存区大小调整晋升阈值):
幸存区大小大到能保留当前活跃对象+需要晋升对象,当阈值配置得当的时候,可以让长时间存活的对象尽快晋升。
由于不快点晋升的话,会一直停留在新生代,导致会复制算法的时间会变长,响应时间也会变长。
老年代调优:
老年代使用CMS/G1进行垃圾回收的时候,当并发标记的时候会有额外的垃圾没有清理到位。如果浮动垃圾导致内存不足,会直接退化为SerialOld(串行垃圾回收器)。
优先调优新生代
如果多次发生了Full GC,就将老年代内存预设调大1/4-1/3
GC调优案例:
Full GC和Minor GC频繁:
空间紧张,创建对象很多的话,Minor GC就会多次触发。同时幸存区大小变小了,更多对象挤去了老年代。
先尝试调优新生代空间,后尝试调优老年代空间。
请求的高峰期发生了Full GC,单次暂停时间长:
在初始标记和重新标记的时候,触发了STW,可以在重新标记之前调用一次Minor GC,使它需要重新标记的对象少了,这样STW时间也随之变短。
老年代充裕的情况下,发生Full GC:
JDK1.7之前使用永久代,如果永久代小了,就会导致Full GC。
附录: