赞
踩
垃圾回收器是垃圾回收算法的具体实现。由于垃圾回收器分为年轻代和老年代,除了G1(既能管控新生代,也可以管控老年代)之外,新生代、老年代的垃圾回收器必须按照hotspot的要求成对组合进行使用(需要根据JDK的版本来选择相应的组合)
具体的组合关系如下:
JVM底层源码中,在某些特殊情况,CMS回收器会调用Serial Old回收器
不能直接看到使用了什么垃圾回收器,只能根据算法来推断
Serial是一种单线程串行回收年轻代的垃圾回收器。
单CPU处理器下吞吐量非常出色
多CPU下吞吐量不如其他垃圾回收器(单线程,只使用了一个CPU),堆如果偏大会让用户线程处于长时间的等待
Java编写的客户端程序或者硬件配置有限(CPU核数不多)的场景
-XX:+UseSerialGC
:新生代、老年代都使用串行回收器。
SerialOld是Serial垃圾回收器的老年代版本,采用单线程串行回收
单CPU处理器下吞吐量非常出色
多CPU下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待
与Serial垃圾回收器搭配使用,或者在CMS特殊情况下使用
-XX:+UseSerialGC
:新生代、老年代都使用串行回收器。
ParNew垃圾回收器本质上是对Serial在多CPU下的优化,使用多线程进行垃圾回收
-XX:+UseParNewGC
:新生代使用ParNew回收器, 老年代使用串行回收器CMS垃圾回收器关注的是系统的暂停时间(尽量减少STW,优化用户体验),允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间。
1、内存碎片问题
2、退化问题(在某些特定情况,会退化为SerialOld这种单线程回收器)
3、浮动垃圾问题(回收过程,有的垃圾回收不掉)
大型的互联网系统中用户请求数据量大、频率高的场景,比如订单接口、商品接口等
XX:+UseConcMarkSweepGC
,可以分别设置年轻代和老年代的回收器
注:只有初始标记、重新标记阶段会出现STW
缺点:
1、CMS使用了标记-清除算法,在垃圾收集结束之后会出现大量的内存碎片,为了不影响对象的分配,CMS会在Full GC时进行碎片的整理。这样会导致用户线程暂停,可以使用-XX:CMSFullGCsBeforeCompaction=N 参数(默认0)调整N次Full GC之后再整理。
2.、无法处理在并发清理过程中产生的“浮动垃圾”,不能做到完全的垃圾回收(在本次清理的过程中,用户线程并发创建了一些对象,但是很快没有再使用,这些对象在这次清理中没有得到回收,需要等到下一次清理,所以称这些为浮动垃圾)。
3、如果老年代内存不足无法分配对象,CMS就会退化成Serial Old单线程回收老年代。
并发线程数:
在CMS中并发阶段运行时的线程数可以通过-XX:ConcGCThreads
参数设置,由系统计算得出,计算公式为(-XX:ParallelGCThreads定义的线程数 + 3) / 4
, ParallelGCThreads是STW停顿之后的并行线程数
ParallelGCThreads是由处理器核数决定的:
1、当cpu核数小于8时,ParallelGCThreads = CPU核数
2、否则 ParallelGCThreads = 8 + (CPU核数 – 8 )*5/8
我的电脑上逻辑处理器有12个,所以ParallelGCThreads = 8 + (12 - 8)* 5/8 = 10,ConcGCThreads = (-XX:ParallelGCThreads定义的线程数 + 3) / 4 = (10 + 3) / 4 = 3
最终可以得到这张图:
并发标记和并发清理阶段,会使用3个线程并行处理。重新标记阶段会使用10个线程处理。由于CPU的核心数有限,并发阶段会影响用户线程执行的性能。
Parallel Scavenge是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量。为了拉高吞吐量,PS会自动调整堆内存大小(调整新生代、老年代内存大小、晋升的阈值)。
Parallel Scavenge允许手动设置最大暂停时间和吞吐量。Oracle官方建议在使用这个组合时,不要设置堆内存的最大值,垃圾回收器会根据最大暂停时间和吞吐量自动调整内存大小。
-XX:MaxGCPauseMillis=n
设置每次垃圾回收时的最大停顿毫秒数-XX:GCTimeRatio=n
设置吞吐量为n(用户线程执行时间 = n/(n + 1))-XX:+UseAdaptiveSizePolicy
设置可以让垃圾回收器根据吞吐量和最大停顿的毫秒数自动调整内存大小,这个参数默认是开启的(Oracle建议使用PS组合的时候,不要设置堆内存的最大值,让垃圾回收器自动调整)注:最大暂停时间和吞吐量这两个指标是冲突的,垃圾回收器会尽量满足最大暂停时间(有时候设置得太小,是没办法满足的,会超出所设置得最大暂停时间),牺牲吞吐量。如果要同时设置最大暂停时间和吞吐量,要多做测试,让它们比较协调
Parallel Old是为Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集。
JDK8设置参数,就是默认使用该回收器
参数:-XX:+UseParallelGC
或 -XX:+UseParallelOldGC
可以使用Parallel Scavenge + Parallel Old这种组合。
-XX:+PrintFlagsFinal
:可以在程序启动的时候打印所有配置项的最终值,可以看看自动调整功能有没有开启
import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * 垃圾回收器案例3 */ //-XX:+UseSerialGC -Xmn1g -Xmx16g -XX:SurvivorRatio=8 -XX:+PrintGCDetails -verbose:gc -XX:+PrintFlagsFinal //-XX:+UseParNewGC -Xmn1g -Xmx16g -XX:SurvivorRatio=8 -XX:+PrintGCDetails -verbose:gc //-XX:+UseConcMarkSweepGC //-XX:+UseG1GC -Xmn8g -Xmx16g -XX:SurvivorRatio=8 -XX:+PrintGCDetails -verbose:gc MaxGCPauseMillis //-XX:+PrintFlagsFinal -XX:GCTimeRatio = 19 -XX:MaxGCPauseMillis=10 -XX:+UseAdaptiveSizePolicy public class GcDemo2 { public static void main(String[] args) throws IOException { int count = 0; List<Object> list = new ArrayList<>(); while (true){ //System.out.println(++count); if(count++ % 10240 == 0){ list.clear(); } // byte[] bytes = new byte[1024 * 1024 * 1]; list.add(new byte[1024 * 1024 * 1 / 2]); // System.gc(); } } }
【测试最大暂停时间=10】
内存大小如下:
【测试最大暂停时间=1】
结论:堆内存越小,垃圾回收暂停时间越短
JDK9之后默认的垃圾回收器是G1(Garbage First)垃圾回收器。
JDK9之后强烈建议使用G1垃圾回收器。(在JDK7、8上,G1可能不是很成熟,要慎重考虑)
G1出现之前的垃圾回收器,年轻代和老年代一般是连续的,如下图:
G1内存结构发生了变化:G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的。分为Eden、Survivor、Old区。Region的大小通过堆空间大小/2048计算得到,也可以通过参数-XX:G1HeapRegionSize=32m
指定(其中32m指定region大小为32M),Region size必须是2的指数幂,取值范围从1M到32M。
1、年轻代回收(Young GC)
2、混合回收(Mixed GC)
只回收年轻代,回收Eden区和Survivor区中不用的对象。会导致STW,G1中可以通过参数
-XX:MaxGCPauseMillis=n
(默认200) 设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间。
【Young GC步骤】
1、新创建的对象会存放在Eden区。当G1判断年轻代区不足(max默认60%,如果年轻代的内存占了总堆的60%以上,就要Young GC),无法分配对象时需要回收时会执行Young GC。
2、标记出Eden和Survivor区域中的存活对象,
3、根据配置的**最大暂停时间选择某些区域(和其他回收器的区别:其他回收器会回收负责的所有区域)**将存活对象复制到一个新的Survivor区中(年龄+1)(使用复制算法,不会产生内存碎片),清空这些区域。
G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时,以作为下次回收时的参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域了。比如-XX:MaxGCPauseMillis=n
(默认200),每个Region回收耗时40ms,那么这次回收最多只能回收4个Region。
4、后续Young GC时与之前相同,只不过Survivor区中存活对象会被搬运到另一个Survivor区。
5、当某个存活对象的年龄到达阈值(默认15),将被放入老年代。原来的Survivor中年龄没有到达15的对象,还是会迁移到新的Survivor中。
6、部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous(翻译:巨大的)区。比如堆内存是4G,每个Region是2M,只要一个大对象超过了1M就被放入Humongous区,如果对象过大会横跨多个Region(一个Region放不完)。
7、多次回收之后,会出现很多Old老年代区,此时总堆占有率达到阈值时
(-XX:InitiatingHeapOccupancyPercent
默认45%)会触发混合回收Mixed GC,回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法来完成。
混合回收分为如下步骤,看起来和CMS差不多,但是步骤里面都是有区别的:
注意:**如果清理过程中发现没有足够的空Region存放转移的对象,会出现Full GC,**单线程执行标记-整理算法,此时会导致用户线程的暂停,影响用户的正常使用。所以尽量保证应该用的堆内存有一定多余的空间,如果堆内存的占用比较高,就要考虑优化。
参数1: -XX:+UseG1GC
打开G1的开关,JDK9之后默认不需要打开
参数2:-XX:MaxGCPauseMillis=毫秒值
最大暂停的时
适用场景
使用以下代码测试g1垃圾回收器,打印出每个阶段的时间:
package chapter04.gc; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * 垃圾回收器案例3 */ //-XX:+UseG1GC -Xmn8g -Xmx16g -XX:SurvivorRatio=8 -XX:+PrintGCDetails -verbose:gc public class GcDemo2 { public static void main(String[] args) throws IOException { int count = 0; List<Object> list = new ArrayList<>(); while (true){ //System.out.println(++count); if(count++ % 10240 == 0){ list.clear(); } // byte[] bytes = new byte[1024 * 1024 * 1]; list.add(new byte[1024 * 1024 * 1 / 2]); // System.gc(); } } }
每个region大小为2m,一共有84个young区,26个幸存者区。
初始标记花了0.0478秒,初始标记是伴随着年轻代回收的,0.0478秒是两个操作的总时间
并发标记总共耗时10ms,不会产生STW。
最终标记,效率较高
清理阶段
垃圾回收器的组合关系虽然很多,但是针对几个特定的版本,比较好的组合选择如下:
Java中有哪几块内存需要垃圾回收?
方法区回收,Java程序员少关注
有哪几种常见的引用类型?
有哪几种常见的垃圾回收算法?
常见的垃圾回收器有哪些?
Serial+Serial Old
:单线程回收,适用于单核CPU场景ParNew+CMS
:暂停时间较短,适用于大型互联网应用中与用户交互的部分Paraller Scavenge+Parallel Old
:吞吐量高,适用于后台进行大量数据操作G1
:适用于较大堆,具有可控暂停时间该文章是本人学习 黑马程序员 的学习笔记,文章中大部分内容来源于 黑马程序员 的视频黑马程序员JVM虚拟机入门到实战全套视频教程,java大厂面试必会的jvm一套搞定(丰富的实战案例及最热面试题),也有部分内容来自于自己的思考,发布文章是想帮助其他学习的人更方便地整理自己的笔记或者直接通过文章学习相关知识,如有侵权请联系删除,最后对 黑马程序员 的优质课程表示感谢。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。