《深入理解JAVA虚拟机》第二版 阅读笔记4 垃圾收集与内存分配(2)

上一篇说到的垃圾收集算法是方法论,具体垃圾收集时如何利用这些算法就要看各个JVM的具体实现了,我们肯定主要讨论HotSpot JVM的实现,首先HotSpot JVM使用了分代收集的思想,并实现了多种收集器,有的收集器适用于新生代,有的适用于老年代,但是它们并不能随便组合使用,如下表,第一行是新生代收集器,第一列是老年代收集器

.SerialParNewParallel ScavengeG1
Serial Old
Parallel Old
CMS
G1

接下来一个一个介绍:

Serial 和 Serial Old

这俩一看就是好哥俩可以配对使用的,一个服务于新生代,一个服务于老年代,新生代的Serial收集器使用复制算法,老年代的Serial Old使用标记-整理算法
这俩也是最基本,历史最悠久的收集器,很简单粗暴,单线程,执行GC的时候STW(停止全世界!)。

ParNew

这个收集器是针对新生代的,其实就是Serial的多线程版本,依然复制算法,注意:只有Serial和ParNew这两种新生代收集器可以和高端的CMS收集器配合使用哟

  • -XX:UseParNewGC 因为不是默认的收集器,想使用它就用这个参数指定
  • -XX:ParallelGCThreads 因为是多线程收集器,通过这个参数指定线程数量,其他多线程收集器同样

Parallel Scavenge

jdk1.8 默认的新生代收集器,多线程,依然复制算法,听起来和ParNew没有区别,但其实它的关注点与其他的收集器都不一样。
CMS等收集器的关注点是尽可能减少GC时用户线程的停顿时间,而Parallel Scavenge在意的是吞吐量

吞吐量=运行代码的时间 /(运行代码的时间 + GC时间)

也就是说此收集器在意的是整个运行期间垃圾手机的比例关系,比较适合后台运算多而交互少的情景,比如我有一个计算任务,光计算得花100秒,因为没有用户交互所以我不在乎你GC一次停多久,我只希望整个运行期间GC总时长尽量少,我宁愿你只一次,哪怕停个10秒,也不想你一次停1秒然后停了20次,总共20秒,因为这样的话相比一次停10秒,我的总执行时间多了10秒,也就是吞吐量变低了。

但是对于用户频繁交互的场景需求就反过来了,你可以GC很多次,但是每次的时间必须很短让用户基本感觉不出来,这种场景下我们就不追求吞吐量了,而追求尽量短的GC时间。

怎么样,区别出来了吧。

  • -XX:GCTimeRatio

    通过参数设置理想的吞吐量。值为大于0小于100的整数,表示我们希望执行用户代码的时间占总执行时间的比率。假如设置为99(默认值),表示最大允许1%的收集时间。

  • -XX:MAXGCPauseMillis

    表示最大垃圾收集时间。设置了这个时间之后,收集器将尽可能的将每次GC时间控制在这个时间之内,但是大家从之前的例子应该知道,这个参数越小,垃圾收集越频繁,可能导致吞吐量变小。

  • -XX:+UseAdaptiveSizePolicy

    打开这个参数让收集器自动调优,你只需要告诉堆的总大小是多少,以及你期望的最大垃圾时间和吞吐量,收集器会自动调整新生代的大小(-Xmn),新生代中Eden和Survivor的比例(-XX:SurvivorRatio),新生代晋升老年代的阈值(-XX:PretenureSizeThreshold)等等。
    这叫做GC的自适应调节策略

总结:Parallel Scavenge与ParNew的区别主要在于:吞吐量控制自适应调节策略

Parallel Old

jdk1.8 默认的老年代收集器,没什么特别的,多线程的标记-整理算法收集器
为什么它会出现呢,大家熟知的很牛的老年代收集器有CMS和G1,孤僻的G1只能与自己使用,CMS只能与Serial和ParNew使用,所以咱们刚介绍过的新生代大牛Parallel Scavenge只能和单线程的Serial Old一起使用,唉被拖累死了,好不容易搞起来的吞吐量都被Serial Old拖累没了,还不如用ParNew+CMS组合,所以Parallel Scavenge表示很尴尬啊
但是Parallel Old出来了,“吞吐量优先”收集器总算有了CP组合了

CMS Concurrency Mark Sweep

到大名鼎鼎的CMS了,以最短GC停顿时间为目标一直努力了,很适合B/S应用
从全名看出来是使用标记-清除算法
其他收集器都是要STW的,这是第一个号称可以与用户代码并发执行的垃圾收集器,其实也不完全,看一下它的垃圾收集步骤:
1. 初始标记
2. 并发标记
3. 重新标记
4. 并发清除

其中只有带“并发”两个字的第二步和第四步是并发的
第一步是用来遍历GC Roots的,对能与GC Roots直接关联的对象打上标记,因为有我们之前提到的OopMap,所以这一步会肥肠快
第二步是GC Tracing,刚第一步只是对直接关联的对象打了标记,但是我们知道对象关联是树形的,对象还会在关联别的对象,这一步就是一步一步追踪下去,给所有活着的对象打上标记,可想而知这一步耗时会很长,所以会与用户代码并发执行
但是代码执行过程中有的活的对象又会变死唉,所以第三步又要STW来把刚刚变化的那部分取消标记,这一步的时间会比第一步长,但是远比第二步短
最后一步做清除,时间也会比较长,但是是并发的

CMS的缺点如下:
1. CPU敏感。并发执行占了部分CPU,延长垃圾收集时间,吞吐量降低
2. 无法处理浮动垃圾。在第四步并发清除时,因为用户代码也在执行,所以会源源不断产生垃圾,而这部分垃圾无法在此次GC被清除,只能等到下次GC,被称为浮动垃圾。也是因为GC过程中还会有垃圾产生所以CMS收集器不能等到老年代被几乎占满了再FULL GC,默认是到92%开始。
有个很可怕的事情是,CMS执行过程中,老年代被占满了,也就是发生了Concurrent Mode Failure,并发模式特有的失败!此时会临时启用Serial Old收集器重!新!收!集!,很可怕吧,如果发生了这种事情,GC时间可想而知
-XX:CMSInitialingOccupancyFraction 配置触发FULL GC的百分比
3. 产生内存碎片。因为CMS是标记清除算法,可能会出现命名总剩余空间是够的,但是却找不到一块大的连续空间,那怎么办呢,提前Full GC呗!为了解决这个问题,CMS提供了参数:
-XX:+UseCMSCompactAtFullCollection 开启碎片整理功能(默认就是开启的),会导致GC时间变长
-XX:CMSFullGCsBeforeCompaction 设置多少次不带压缩的Full GC后,来一次带压缩的,0表示每次都压缩

G1

终于到大名鼎鼎新生代老年代通吃的G1了
G1独自管理着整个堆,把堆分成了一个个大小相等的Region,新生代对G1来说是一系列Region,老年代也是同样,而且它们各自的一系列Region甚至不需要连续。
之所以划分Region,是因为G1不希望在整个堆上面做GC,它会跟踪每个Region的GC价值(1.可回收的空间大小 2.需要的时间长短),简单地说,可回收空间越大,需要的时间越短,这个Region的价值越大,当用户允许的GC时间有限,那肯定要捡价值最大的Region先回收,所以G1收集器可以预测自己需要的GC时间以及在有限时间内获得最大的收集效率。
上一篇笔记提到了Remembered Set,再提一下,MinorGC是只回收新生代的,当然希望Minor GC的时候只扫描指向新生代对象的引用了,事实上也是这么做的,但是有的新生代对象是被老年代里的对象里的引用的。G1把内存划分为一个一个Region,想以Region为单位做回收,也会遇到类似的问题,就是一个Region的对象被另一个Region里对象的引用了。
这两种情况都通过Remembered Set来解决,虚拟机发现代码对Reference类型的数据做操作时,判断引用对象所在的对象和即将引用的对象是不是在一个代里,或者在一个Region里,如果不在,就记录在被引用对象所属的代或者Region里,搭配根节点枚举,不会遗漏对象。
G1的收集步骤也是四步,前三步和CMS没有大区别,最后一步叫筛选回收,筛选的就是Region,G1要根据用户设置的GC时间挑选收集哪些Region,并实施清理。注意此步骤是需要STW的,因为停止用户线程可以大幅提高GC效率。

垃圾回收参数小结:

参数含义
PrintGCDetails打印GC详细日志
UseSerialGC使用Seial+Serial Old组合
UseParNewGC使用ParNew+Serial Old组合
UseConcMarkSweepGC使用ParNew+CMS+Serial Old组合,其中Serial Old是CMS并发模式失败的临时方案
UseParallelGC使用Parallel Scavenge+Serial Old组合
UseParallelOldGCjdk1.8默认的,使用Parallel Scavenge+Parallel Old组合
SurvivorRatioEden占新生代的比例,默认是8,表示Eden:Survivor = 8:1
PretenureSizeThreshold提前晋升到老年代的对象大小,超过这大小,对象直接安置在老年代
MaxTenuringThreshold对象在新生代中每经历一次MinorGC年龄会+1,当年龄达到多少时进入老年代
UseAdaptiveSizePolicy自动调优
HandlePromationFailure是否允许分配担保失败
ParallelGCThreads并行GC时的线程数量
GCTimeRatio目标吞吐量
MaxGCPauseMillesGC最大停顿时间
CMSInitialingOccupancyFraction老年代空间被占用多少时触发Full GC
UseCMSCompactAtFullCollection开启CMS的整理功能
CMSFullGCsBeforeCompaction隔几次Full GC整理一次

内存分配

内存回收说完了也提一下内存分配
大多数情况下,新对象在Eden区分配空间,当空间不够了,触发Minor GC。大家知道Minor GC用的都是复制算法,要把存活对象挪到其中一个Survivor中去,那如果不够放怎么办,这时候就通过分配担保机制把放不下的对象提前放到老年代去了。
刚才说的是大多数情况下对象是在Eden区先安家,那少数情况是什么呢,就是大对象,比如很长的数组,会直接放入老年代。这是为了避免大对象在新生代里在Eden和Survivor之间复制来复制去的。书里有句话挺有意思的:

大对象对虚拟机的内存分配来说就是一个坏消息,替JAVA虚拟机抱怨一句,比遇到一个大对象更坏的消息就是遇到一群“朝生夕死”的“短命大对象”,写程序时应当避免

新生代里的对象每熬过一次Minor GC,年龄就会加1岁,当它的年龄到一定程度(默认15岁)就会被挪到老年代了。

冷门知识点:
动态年龄判定
并不是非要达到指定年龄才会进入老年代,还有一种情况是:

对于某一个年龄,Survivor空间中处于这个年龄的所有对象大小的总和大于Survivor空间的一半,那么大于等于该年龄的对象就直接进入老年代了

空间分配担保
在发生Minor GC之前,虚拟机会做个比较,老年代是否有够大的连续空间放下所有新生代对象,如果能放下,说明这次Minor GC是绝对安全的,因为即使新生代存活率100%,即使Survivor存不下,也可以通过空间分配担保放到老年代去。
否则此次Minor GC就是有一定风险的,可能Survivor和老年代都放不下存活对象了,此时虚拟机会查看HandlePromotionFailure参数,是否允许担保失败,如果允许,说明愿意承受这个风险,虚拟机去取得历次晋升到老年代对象的平均大小与此时老年代的剩余最大连续空间作比较,如果发现能放下,就尝试进行Minor GC,尽管是有风险的。
如果发现跟历史经验值比是放不下的,或者HandlePromotionFailure参数是不允许冒险的,就直接进行Full GC。

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/u010588262/article/details/81429782
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞