目录
1、什么是垃圾回收
程序的运行必然要申请内存资源,无效的对象资源如果不及时清理就会一直占用内存资源,最终导致内存溢出,所以对内存资源的关联非常重要。
为了让开发者更多关注代码的实现,而不用过多的考虑内存释放的问题,所以,在Java中有了自动垃圾回收机制,也就是我们熟悉的GC。
有了垃圾回收机制后,程序员只需要关注内存的申请即可,而内存的释放由系统自动识别完成。
2、如何确定垃圾
一般来说,没有被引用的对象就是垃圾。
怎样判断对象是否可以被回收,有两种算法:
1、引用计数法(已被淘汰)
原理:假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失败时,对象A的引用计数器就-1,如果对象A的计数器的值为0,就说明对象A没有引用了, 可以被回收。
目前主流的java虚拟机都摒弃掉了这种算法,最主要的原因是它很难解决对象之间相互循环引用的问题。尽管该算法执行效率很高。
2、可达性分析算法
通过判断对象的引用链是否可达来决定对象是否可以被回收。
可以作为GC Root的对象有:
1、虚拟机栈中引用的对象(栈帧中的本地变量表)
2、方法区中的常量引用对象
3、方法区中的静态属性引用对象
4、本地方法栈中JNI(Native方法)的引用对象
5、活跃线程中的引用对象
3、强、软、弱、虚四种引用
1. 强引用
只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
2. 软引用(SoftReference)
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用
对象
可以配合引用队列来释放软引用自身
3. 弱引用(WeakReference)
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
可以配合引用队列来释放弱引用自身
4. 虚引用(PhantomReference)
必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,
由 Reference Handler 线程调用虚引用相关方法释放直接内存
5. 终结器引用(FinalReference)
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象
暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize
方法,第二次 GC 时才能回收被引用对象
4、常见的垃圾回收算法
1、Mark_Sweep(标记清除算法)
1、思想:标记清除算法分为两个阶段,标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。
2、优缺点:实现简单,容易产生内存碎片。
2、Copying(复制清除算法)
1、思想:将可用内存划分为大小相等的两块,每次只使用其中的一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉。
2、优缺点:不容易产生内存碎片;可用内存空间少;存活对象多的话,效率低下。
3、Mark-Compact(标记-整理算法)
1、思想:先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存。
2、优缺点:不容易产生内存碎片;内存利用率高;存活对象多并且分散的时候,移动次数多,效率低下。
5、分代垃圾回收
前面介绍了多种回收算法,每一种算法都有自己的优点也有缺点,谁都不能替代谁,所以根据垃圾回收对象的特点进行选择,才是明智的选择。
分代算法其实就是这样的,根据回收对象的特点进行选择,在jvm中,年轻代适合使用复制算法,老年代适合使用标记清除或标记压缩算法。
1、对象首先分配在Eden区域。在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor 区“To”是空的。
2、新生代空间不足时,触发 Minor GC,Eden区中存活的对象会使用copy(复制算法)复制到Survivor To区,而Survivor From区中仍存活的对象会根据它们的年龄值决定去向。
年龄达到一定阈值(可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到老年代中,没有达到阈值的对象会被复制到Survivor To区。
3、经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。
同时,存活的对象年龄会加1。
4、Minor GC会引发Stop The World,暂停其他的用户线程,等垃圾回收结束,用户线程才恢复运行。
5、经过多次Minor GC后,当对象寿命超过阈值时,会晋升到老年代,最大寿命是15(4bit),该值存在对象头中(header)。
6、当老年代空间不足时,会先尝试触发Minor GC,如果之后空间仍不足,那么触发Full GC,STW的时间更长。Oracle 官方建议:Oracle recommends that you keep the size for the young generation greater than 25% and less than 50% of the overall heap size.
6、串行、并行、并发、G1垃圾收集器
1、串行收集器
串行垃圾收集器,是指使用单线程进行垃圾回收,垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停,等待垃圾回收的完成。
这种现象称之为 STW(Stop-The-World)。
对于交互性较强的应用而言,这种垃圾收集器是不能够接受的。一般在Javaweb应用中是不会采用该收集器的。
- Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
- Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
-XX:+UseSerialGC 指定年轻代和老年代都使用串行垃圾收集器 -XX:+PrintGCDetails 打印垃圾回收的详细信息
2、并行收集器
并行垃圾收集器在串行垃圾收集器的基础之上做了改进,将单线程改为了多线程进行垃圾回收,这样可以缩短垃圾回收的时间。
并行垃圾收集器在收集的过程中也会暂停应用程序,这个和串行垃圾回收器是一样的,只是并行执行,速度更快些,暂停的时间更短一些。(多核CPU机器)
- ParNew收集器 (复制算法): 新生代并行收集集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
通过-XX:+UseParNewGC参数设置年轻代使用ParNew回收器,老年代使用的依然是串行收集器。
- ParallelGC收集器工作机制和ParNewGC收集器一样,只是在此基础之上,新增了两个和系统吞吐量相关的参数,使得其使用起来更加的灵活和高效。
Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
Parallel Old收集器 (标记-整理算法):老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;JDK8默认使用PS+PO垃圾回收器
-XX:+UseParallelGC 年轻代使用ParallelGC垃圾回收器,老年代使用串行回收器。
-XX:+UseParallelOldGC 年轻代使用ParallelGC垃圾回收器,老年代使用ParallelOldGC垃圾回收器。
-XX:MaxGCPauseMillis 设置最大的垃圾收集时的停顿时间,单位为毫秒,需要注意的是,ParallelGC为了达到设置的停顿时间,可能会调整堆大小或其他 的参数,如果堆的大小设置的较小,就会导致GC工作变得很频繁,反而可能会影响到性能。 该参数使用需谨慎。
-XX:GCTimeRatio 设置垃圾回收时间占程序运行时间的百分比,公式为1/(1+n)。它的值为0~100之间的数字,默认值为99,也就是垃圾回收时间不能超过1%
-XX:UseAdaptiveSizePolicy 自适应GC模式,垃圾回收器将自动调整年轻代、老年代等参数,达到吞吐量、 堆大小、停顿时间之间的平衡。一般用于,手动调整参数比较困难的场景,让收集器自动进行调整。
3、CMS(并发)垃圾收集器
CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,通过参数-XX:+UseConcMarkSweepGC进行设置。
CMS垃圾回收器的执行过程如下:
- 初始化标记(CMS-initial-mark) ,标记root,会导致stw;
- 并发标记(CMS-concurrent-mark),与用户线程同时运行;
- 预清理(CMS-concurrent-preclean),与用户线程同时运行;
- 重新标记(CMS-remark) ,会导致stw;
- 并发清除(CMS-concurrent-sweep),与用户线程同时运行;
- 调整堆大小,设置CMS在清理之后进行内存压缩,目的是清理内存中的碎片;
- 并发重置状态,等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;
4、G1垃圾收集器
G1垃圾收集器是在jdk1.7中正式使用的全新的垃圾收集器,oracle官方计划在jdk9中将 G1变成默认的垃圾收集器,以替代CMS。
G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:
1. 第一步,开启G1垃圾收集器
2. 第二步,设置堆的最大内存
3. 第三步,设置最大的停顿时间
G1中提供了三种模式垃圾回收模式,Young GC、Mixed GC 和 Full GC,在不同的条件下被触发。
原理:
G1垃圾收集器相对比其他收集器而言,最大的区别在于它取消了年轻代、老年代的物理划分,取而代之的是将堆划分为若干个区域(Region),这些区域中包含了有逻辑上的年轻代、老年代区域。
这样做的好处就是,我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够。特点
同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms;
超大堆内存,会将堆划分为多个大小相等的 Region,Region的大小是1MB~32MB;
整体上是 标记+整理 算法,两个区域之间是复制算法。
在 G1 中,有一种特殊的区域,叫 Humongous 区域。
- 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。
- 这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。
- 为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果 一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
1)Young GC
- Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。
- Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。
- 最终Eden空间的数据为空,GC停止工作,应用线程继续执行。
Remembered Set(已记忆集合)
在GC年轻代的对象时,我们如何找到年轻代中对象的根对象呢?
根对象可能是在年轻代中,也可以在老年代中,那么老年代中的所有对象都是根么?
如果全量扫描老年代,那么这样扫描下来会耗费大量的时间。
于是,G1引进了RSet的概念。它的全称是Remembered Set,其作用是跟踪指向某个堆内的对象引用。
每个Region初始化时,会初始化一个RSet,该集合用来记录并跟踪其它Region指向该Region中对象的引用,每个Region默认按照512Kb划分成多个Card,所以RSet需要记录的东西应该是 xx Region的 xx Card。
2)Mixed GC
当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个YoungRegion,还会回收一部分的Old Region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是Mixed GC 并不是 Full GC。
MixedGC什么时候触发? 由参数 -XX:InitiatingHeapOccupancyPercent=n 决定。默认:45%,该参数的意思是:当老年代大小占整个堆大小百分比达到该阀值时触发。
它的GC步骤分2步:
1. 全局并发标记(global concurrent marking)
2. 拷贝存活对象(evacuation)1、全局并发标记(global concurrent marking)
全局并发标记,执行过程分为五个步骤:
- 初始标记(initial mark,STW):标记从根节点直接可达的对象,这个阶段会执行一次年轻代GC,会产生全局停顿。
- 根区域扫描(root region scan):G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
- 并发标记(Concurrent Marking):G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断。
- 重新标记(Remark,STW):该阶段是 STW 回收,因为程序在运行,针对上一次的标记进行修正。
- 清除垃圾(Cleanup,STW):清点和重置标记状态,该阶段会STW,这个阶段并不会实际上去做垃圾的收集,等待evacuation阶段来回收。
2、拷贝存活对象(evacuation)
Evacuation阶段是全暂停的。该阶段把一部分Region里的活对象拷贝到另一部分Region中,从而实现垃圾的回收清理。
3)G1收集器相关参数
-XX:+UseG1GC 使用 G1 垃圾收集器。
-XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是 200 毫秒。
-XX:G1HeapRegionSize=n 设置的 G1 区域的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。目标是根据最小的 Java 堆大小划分出约 2048 个区域。默认是堆内存的1/2000。
-XX:ParallelGCThreads=n 设置 STW 工作线程数的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量相同,最多为 8。
-XX:ConcGCThreads=n 设置并行标记的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads)的 1/4 左右。
-XX:InitiatingHeapOccupancyPercent=n 设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。
4)对于G1垃圾收集器优化建议
1、年轻代大小
- 避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。
- 固定年轻代的大小会覆盖暂停时间目标。
2、暂停时间目标不要太过严苛
- G1 GC 的吞吐量目标是 90% 的应用程序时间和 10%的垃圾回收时间。
- 评估 G1 GC 的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示您愿意承受更多的垃圾回收开销,而这会直接影响到吞吐量。