目录
一、什么是垃圾回收
1、C/C++中的垃圾回收
(1)在C/C++语言中,没有自动垃圾回收机制。是通过new关键字申请内存资源,通过delete关键字释放内存资源
(2)在Java语言中,有自动的垃圾回收机制,也就是我们的GC
(3)GC的精髓在于算法,如果算法不合理一样会内存泄漏
二、垃圾回收的常见算法
1、引用计数法
(1)原理
假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失败时,对象A的引用计数器就-1.如果对象A的计数器额值为0,就说明对象A没有引用了,就可以被回收
对于对象A,当对象B/C/D/E/F分别引用它的时候,该对象的程序计数器分别从0变为5
伪代码如下:
int a= 0 ;
int b = a;#对象a的计数器为1
int c = a;#对象a的计数器为2
int d = a;#对象a的计数器为3
int e = a;#对象a的计数器为4
int f = a;#对象a的计数器为5
int b = 0;#对象a的计数器为4
int c = 0;#对象a的计数器为3
...
(2) 优点
a.实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收
b.在垃圾回收的过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报outofmemory错误
c.区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象
(3) 缺点
a.每次对象被引用时,都需要去更新计数器,有一点时间开销
b.浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计
c.无法解决循环引用问题(最大的缺点)
(4) 什么是循环引用及循环引用的演示
当上述main方法中的a = null,b = null的时候,A,B对象的程序计数器只会减1,导致A,B对象始终无法回收
2、标记清除法
(1) 标记清除算法是将垃圾回收分为2个阶段,分别是标记和清除
标记:从根节点开始标记引用的对象
清除:未被标记引用的对象就是垃圾对象,可以被清理
原理:
(2) 优点
标记清除法解决了引用计数算法中的循环计数的问题,没有从root节点引用的对象都会被回收
(3) 缺点
a.效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的
b.通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存时不连贯的
场景举例:相对于在饭店吃饭的顾客,正在吃饭,服务员说,稍等一下我先清理下地下的垃圾;现实场景不可取,尤其是对高并发的环境下
3、标记压缩算法
(1) 原理
清理完之后再将内存中对象压缩到内存中的一段,再清楚边界以外的垃圾,从而解决了碎片化问题
4、复制算法
(1) 原理
将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时将正在回收的对象复制到另一端内存空间当中,然后将该内存空间清空,交换两个内存的角色,从而完成垃圾的回收;如果内存中需要垃圾回收的对象较多,需要复制的对象就较少了,这种情况适合使用该算法。
将from区域中需要垃圾回收的对象做好标记,然后将不需要回收的对象整体移动至另一块区域“to”,然后整体清空"from"区域;不断重复,经过几次之后把还存活的对象放到老年代中去。
(2) JVM中年轻代内存空间
就是JVM内存模型中幸存区的FROM,TO倒换数据就是运用复制算法。
GC开始的时候,对象只存在于伊甸区中,伊甸区满了之后,进行回收之后,把存活的对象放到幸存区中的FROM的区域中,而TO是空的,再经过一轮GC后,会将伊甸区中的存活的对象复制到TO区,而FROM中仍然存活的对象会根据他们的年龄值来决定他们的去向,年龄达到一定值,会移到年老代中去,没有达到年龄值会被复制到TO区域中
(3) 优点
a.在垃圾较多的情况下,效率较高
b.清理后,内存无碎片
(4) 缺点
a.在垃圾对象少的情况下,不适用,如:老年代内存
b.分配的2块内存空间,在同一时刻,只能使用一半,内存使用率较低
三、垃圾收集器及内存分配
1、垃圾收集器种类
串行、并行、CMS、G1垃圾收集器。其中蓝色箭头表示业务线程,黄色箭头表示垃圾收集器线程。
串行垃圾收集器:执行垃圾回收的时候回阻断业务线程的执行,中断的事件较长,严重影响业务正常使用,尤其是并发性比较高的场景下。相对于在饭店吃饭的顾客,正在吃饭,服务员说,稍等一下我先清理下地下的垃圾,等她扫完地的时候,说我扫完地了,你继续吃饭吧
并行垃圾收集器:也会中断业务线程的执行,但是执行垃圾回收的线程数较多,中断的时间相对串行较短。相对于在饭店吃饭的顾客,正在吃饭,突然来了三个服务员。,稍等一下我先清理下地下的垃圾,她们快速的扫完地,说,你继续吃饭吧
CMS垃圾收集器:在大部分的时间和业务线程是并行的,在一头一尾的时候有短暂的中断,但是80%的时间是和业务线程并行的。各大厂商在G1垃圾回收器出现之前,基本上都是使用的CMS垃圾回收器,这种性能是最好的
2、HotSpototSpot虚拟机所包含的垃圾收集器
可以通过-XX参数修改默认的垃圾处理器
3、垃圾收集器后台日志参数说明
4、串行垃圾收集器
(1)串行垃圾收集器是最基本的,发展最悠久的收集器
特点:单线程,简单高效(与其他收集器的单线程相比),对比限定单个CPU的环境来说,Serial收集器没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其它所有的工作线程,直到它结束(Stop The World)
(2)串行垃圾收集器运行示意图
(3)串行垃圾收集器测试案例
思路:while循环,不断拼接字符串,直到发生OOM,查看GC情况
package com.andy.vm;
import java.util.ArrayList;
import java.util.UUID;
/**
* TODO
*
* @author Andy
* @date 2021/4/4 9:02
*/
public class TestOOM {
public static void main(String[] args) {
String str= "Andy";
while(true){
str += str+UUID.randomUUID().toString();
str.intern();
}
}
}
执行上述代码
通过配置,确实实现了串行垃圾收集器
5、并行垃圾收集器
(1)并行垃圾收集器在串行垃圾收集器的基础上做了改进,将单线程改为了多线程并进行了垃圾的回收,这样可以缩短垃圾回收的时间
(2)并行垃圾处理器-ParNew运行示意图
(3)ParNew垃圾收集器
a.通过-XX:+UseParNewGC参数设置年轻代使用ParNew回收器,老年代使用的依旧是串行收集器
b.通过-XX:+ParallelGCThreads可以限制GC线程数量,默认开启和CPU相同的线程数
修改运行参数为ParNewGC
可以看出,垃圾回收器已经改为了并行模式,共进行了8次垃圾回收
未来的版本可以不会使用ParNew,而是CMS
(4)ParallelGC垃圾回收器相关的参数如下:
其中UseParallelGC和UseParallelOldGCldGC会相互激活,配置其中一个,另一个自然被激活
6、CMS垃圾收集器
CMS全称ConcurrentMarkSweep,是一款并发的,使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,通过参数-XX:+UserConcMarkSweepGC来进行设置;
CMS用在老年代里面,和ParNew搭配使用,二者也是相互激活
(1)运行示意图
(2)执行过程
(3)测试
设置启用CMS垃圾回收参数
-XX:+UserCOncMarkSweepGC
注意:开启后将采用ParNew+CMS+SerialOld收集器组合
那么问题来了,新生代使用ParNew,arNew,老年代使用CMS,那SerialOld是干什么的呢?
原因是JVM为了有更好的保障,CMS在收集的时候,应用线程会同时运行,这样就会增加对堆内存的占用,也就是说CMS必须在老年代堆内存用尽之前完成对垃圾的回收,否CMS回收会失败,一旦失败,就会触发担保机制,就会调用SerialOld来进行垃圾回收,这时候应用程序也会中断一段时间,也是为了保底
7、G1垃圾收集器
(1) G1垃圾收集器是在jdk1.7 update4中正式使用的全新的垃圾收集器,oracle官方在jdk9中将G1变成了默认的垃圾收集器,以替代CMS
(2)特点
新生代的垃圾收集仍然采用暂停所有应用线程的方式,将存活对象移动到老年代或者幸存区;由于老年代被划分到不同的区域,G1收集器通过将对象从一个区域复制到另一个区域,完成对象的清理工作,这也意味着正常的处理过程中,G1收集器实现了堆的压缩整理。因此,使用G1收集器的堆不大容易发生碎片化。
那么G1是如何保证无碎片的呢?
即先打散后相同属性聚合,从而避免了碎片化空间
(3) G1垃圾回收模式:MixedGC
分两步:
1、全局并发标记 (global concurrent marking)
2、拷贝存活对象 (evacuation)
四、可视化GC日志工具分析
五、对象内存分配与回收策略
对象的内存分配,往大方向讲,就是在堆上分配〔但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中。
对象优先分配在Eden区,当Eden区可用空间不够时会进行MinorGC
大对象直接进入老年代:大对象即需要大量连续内存空间的对象(例如很长的字符串及数组)。虚拟机提供了一个-XX:PretenureSizeThreshoId参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在Eden区及两个区之间发生大量的内存复制。注意PretenureSizeThreshoId参数只对Serial和ParNew两款收集器有效。
长期存活的对象将进入老年代:虚拟机给每个对象定义了一个对象年龄(Age)计数器(存在于对象头中)。如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survwor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshoId设置。
动态年龄判断:为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进人老年代,无须等到MaxTenuringThreshoId中要求的年龄。
空间分配担保:在发生Minor GC之前,虚拟机会先检查Survivor空间是否够用,如果够用则直接进行Minor GC。否则进行检查老年代最大连续可用空间是否大于新生代的总和,假如大于,那么这个时候发生Minor GC是安全的。假如不大于,那么需要判断HandlePromotionFailure设置是否允许担保失败。假如允许,则继续判定老年代最大可用的连续空间是否大于平均晋升到老年代对象的平均值,如果大于,这个时候可以发生Minor GC ,如果小于或者设置HandlePromotionFailure不允许担保失败,则需要做一次Full GC。通常会把HandlePromotionFailure开关打开,以减少Full GC。
五、对象何时进入新生代、老年代
新分配的对象一般是直接进入新生代的。但是如果出现以下的情况,会让对象进入老年代。
1.新分配的对象占用空间大于-XX:PretenureSizeThreshold时直接分配到老年代
2.MinorGC的时候,Survivor中的内存不足了,允许分配担保时会进入老年代。
3.MinorGC的时候,对象的年龄大于-XX:MaxTenuringThreshold(默认为15)时,进入老年代。对象年龄存在于对象头中,占4bit。
4.当进行MinorGC的时候,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进人老年代。
六、三种GC介绍
(1)MinorGC
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,也叫Young GC。因为Java对象大多具备朝生夕死的特征,所以MinorGC非常频繁,一般回收速度也比较快。一般采用复制算法。
Minor GC触发条件
Eden区域满了
新生对象需要分配到新生代的Eden,当Eden区的内存不够时需要进行MinorGC
(2)Major GC/Full GC
MajorGC:是清理老年代,Major GC发生过程常常伴随一次Minor
FullGC:Full GC可以看做是Major GC+Minor GC共同进行的一整个过程,是清理整个堆空间(包括年轻代和老年代,这里不包含永久代,因为永久代在JDK7之前包含方法区,是一块与堆分离的区域;JDK7将静态变量从永久代移到堆中;JDK8则完全取消永久代,方法区存在元空间MetaSpace中,虽然与堆共享一块内存,逻辑上可以认为在堆中,但仍然与堆不相连)。Full GC的速度一般会比 Minor GC慢10倍以上。一般用的是标记整理和标记清除算法
Full GC触发条件
上面Minor GC时介绍中Survivor空间不足时,判断是否允许担保失败,如果不允许则进行Full GC。如果允许,并且每次晋升到老年代的对象平均大小>老年代最大可用连续内存空间,也会进行Full GC。
MinorGC后存活的对象超过了老年代剩余空间
方法区内存不足时
System.gc(),可用通过-XX:+ DisableExplicitGC来禁止调用System.gc
CMS GC异常,CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,会触发Full GC
图示GC过程
1:初始阶段,对象分配在Eden区(大对象直接进入老年代,通过-XX:PretenureSizeThreshold配置),此时S0和S1是空的(圆圈中的数字代表对象的年龄)
2:当Eden区满了之后,进行MinorGC,经过扫描与标记,不再存活的对象被清除,存活的对象进入Survivor中的S0并且对象年龄+1,此时Eden被清空,S1是空的
3:然后随着对象增多又一次MinorGC后,Eden区和S0区存活的对象进入S1区并且对象年龄+1,Eden和S0区被清空
4:又一次MinorGC后,和上面步骤类似,Eden区和S1区存活的对象进入S0区并且对象年龄+1,Eden和S1区被清空
5:对象每熬过一次MinorGC其年龄就会加1,达到年龄阈值(可通过参数-XX:MaxTenuringThreshold配置)的年轻代对象会晋升到老年代,随着进入老年代的对象越来越多,当老年代内存不够用时会发送MajorGC。