GC的一些知识

1.Java堆内存结构

java将堆内存分为3大部分:新生代,老年代和永久代,其中新生代又进一步划分为Eden,S0,S1(Survivor)三个区。结构图如下:

+---------------------------+-------------------------------+-------------------+
|          |       |        |                               |                   |
|   Eden   |   S0  |   S1   |       Old generation          |      Perm         |
|          |       |        |                               |                   |
+---------------------------+-------------------------------+-------------------+
|<----Young Gen Space------>

我们在程序中new出来的对象一般情况下都会存在新生代的Eden区分配的空间,如果存活时间足够长就会进入Survivor区,进而如果存货时间再长,还会被提升分配到老年代里面,持久带里面存放的是Class类元数据,方法描述等。

1.S0和S1是两个大小相等的区域。分配内存空间只会在其中某一个进行,另外一个空间用来辅助新生代进行垃圾回收的,因为新生代的垃圾回收策略基于复制算法,其思想是将Eden区及两个Survivor中的某个区,如S0区里面需要存活的对象复制到另一个空的Survivor区,如S1区,然后就可以回收Eden和S0区里面的死亡对象。下一次回收就对调S0和S1两个区的角色,S1用来存放活对象,而S0用来辅助回收垃圾,如此循环利用。
2.有些文章并不将永久代纳入java堆内存,其实永久代就是我们所说的方法区,而方法区经常被称为Non-Heap(非堆)。仅仅在HotStop虚拟机的实现中才将GC分代收集扩展至方法区,或者说使用永久代来实现方法区,对于其他的虚拟机是不存在永久代这个概念的。
3.并非所有的对象创建都会在Eden区中分配内存空间。对于Serial和ParNew垃圾收集器,通过指定-XX:PretenureSizeThreshold={size}来设置超过这个阈值大小的对象直接进入老年代。

2.分代回收算法

我们一般讨论的垃圾回收主要针对java堆内存中的新生代和老年代,也正是因为新生代和老年代结构上的不同,所以产生了分代回收算法,即新生代的垃圾回收和老年代的垃圾回收采用的是不同的回收算法。针对新生代,主要采用复制算法,而针对老年代,通常采用标记-清除算法或者标记-整理算法来进行回收。

2.1复制算法(针对新生代)

所谓复制算法(Copying),即将内存平均分成A区、B区两块,进行复制+清除垃圾的操作,算法图解如下:
在这里插入图片描述
算法过程:
1.新生对象被分配到A块中未使用的内存当中。当A块的内存用完了, 把A块的存活对象复制到B块。
2.清理A块所有对象。
3.新生对象被分配到B块中未使用的内存当中。当B块的内存用完了, 把B块的存活对象复制到A块。
4.清理B块所有对象。
5.循环1。
这个算法实现简单,并且也相对高效,但是代价就是需要将牺牲一半的内存空间用于进行复制。有研究表明,新生代中的对象98%存活期很短,所以并不需要以1:1的比例来划分整个新生代,通常的做法是将新生代内存空间划分成一块较大的Eden区和两块较小的Survivor区,两块Survivor区域的大小保持一致。每次使用Eden和其中一块Survivor区,当回收的时候,将还存活的对象复制到另外一块Survivor空间上,最后清除Eden区和一开始使用的Survivor区。假如用于复制的Survivor区放不下存活的对象,那么会将对象存到老年代。

2.2标记-清除算法

“标记-清除”算法是最基础的算法,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。它主要由两个缺点:一个是效率问题,标记和清除过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
在这里插入图片描述

2.3标记-整理算法(针对老年代)

复制收集算法在对象存活率较高时,就需要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用复制收集算法。
	根据老年代的特点,提出了“标记-整理”算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
	标记-整理步骤:
		1.标记阶段
		2.整理阶段:移动存活对象,同时更新存活对象中所有指向被移动对象的指针
整理顺序:
	不同算法中,堆遍历的次数,整理的顺序,对象的迁移方式都有所不同。而整理顺序又会影响到程序的局部性。主要有以下3种顺序:
	1.任意顺序:对象的移动方式和他们初始的对象排列及引用关系无关。
	 	任意顺序整理实现简单,且执行速度快,但任意顺序可能会将原本相邻的对象打乱到不同的高速缓存行或者是虚拟内存页中,会降低赋值器的局限性。任意顺序算法只能处理单一大小的对象,或者针对大小不同的对象需要分批处理;
	 2.线性顺序:将具有关联关系的对象排列在一起。
	 3.滑动顺序:将对象“滑动”到堆的一端,从而“挤出”垃圾,可以保持对象在堆中原有的顺序。
	   所有现代的标记-整理回收器均使用滑动整理,它不会改变对象的相对顺序,也就不会影响赋值器的空间局部性。复制式回收器甚至可以通过改变对象布局的方式,将对象与其父节点或者兄弟节点排列的更近以提高赋值器的空间局部性。    

整理算法的限制,如整理过程需要2次或者3次遍历堆空间;对象头部可能需要一个额外的槽来保存迁移的信息。

部分整理算法: 双指针回收算法:实现简单且速度快,但会打乱对象的原有布局
Lisp2算法(滑动回收算法):需要在对象头用一个额外的槽来保存迁移完的地址
引线整理算法:可以在不引入额外空间开销的情况下实现滑动整理,但需要2次遍历堆,且遍历成本较高
单次遍历算法:滑动回收,实时计算出对象的转发地址而不需要额外的开销

在这里插入图片描述

2.4分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法并无新的方法,只是根据对象的存活周期的不同将内存划分为几块,一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而年老代中因为对象存活率高、没有额外空间对她进行分配单担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

3.垃圾收集器

3.1.Serial收集器(用于新生代)

Serial收集器作用于新生代,是一个单线程收集器,基于复制算法实现。在进行垃圾回收的时候仅使用单条线程并且在回收的过程中会挂起所有的用户线程(Stop The World)。Serial收集器是JVM client模式下默认的新生代收集器。

特别注意,Stop-The-World会挂起应用线程,造成应用的停顿。

3.2 ParNew收集器(新生代)

ParNew收集器作用于新生代,是一个多线程收集器,基于复制算法实现。相对于Serial收集器而言,在垃圾回收的时候会同时使用多条线程进行回收,但是它跟Serial收集器一样,在回收过程中也是会挂起所有的用户线程,从而造成应用的停顿。

3.3Parallel Scavenge收集器(新生代)

Parallel Scavenge收集器同样作用于新生代,并且也是采用多线程和复制算法来进行垃圾回收。Parallel Scavenge收集器关注的是吞吐量,即使得应用能够充分使用CPU。它与ParNew收集器一样,在回收过程会挂起所有的用户线程,造成应用停顿。

所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。即吞吐量 = 运行用户代码时间 / (运行用户代码时间 +
垃圾收集时间)。如果虚拟机运行了100分钟,其中垃圾收集花了1分钟,那么吞吐量就是99%。

3.4CMS收集器(老年代)------重点

CMS(Concurrent Mark Sweep)收集器是一款真正实现了并发收集的老年代收集器。CMS收集器以获取最短回收停顿时间为目标,采用多线程并发以及标记-清除算法来实现垃圾回收。CMS只在初始化标记和重新标记阶段需要挂起用户线程,造成一定的应用停顿(STW),而其他阶段收集线程都可以与用户线程并发交替进行,不必挂起用户线程,所以并不会造成应用的停顿。CMS收集器可以最大程度地减少因垃圾回收而造成应用停顿的时间。
CMS 处理过程有七个步骤:
CMS垃圾收集分为以下几个阶段:

(1) 初始化标记 (inital mark) SWT
这个阶段仅仅是标记了GC Roots能够直接关联到的对象,速度很快,所以基本上感受不到STW带来的停顿。
(2) 并发标记 (concurrent mark)
并发标记阶段完成的任务是从第一阶段收集到的对象引用开始,遍历所有其他的对象引用,并标记所有需要回收的对象。这个阶段,收集线程与用户线程并发交替执行,不必挂起用户线程,所以并不会造成应用停顿。
(3) 并发预清除 (concurrent-pre-clean)
并发预清除阶段是为了下一个阶段做准备,为的是尽量减少应用停顿的时间。
(4) 重新标记 (remark) SWT
这个阶段将会修正并发标记期间因为用户程序继续运作而导致标记产生变动的那部分对象的标记记录(有可能对象重新被引用或者新对象可以被回收)。这个阶段的停顿时间比初始标记阶段要长一些,但是远比并发标记的时间短。
(5) 并发清除 (concurrent sweep)
这个阶段将真正执行垃圾回收,将那些不被使用的对象内存回收掉。
(6) 并发重置 (concurrent reset)
收集器做一些收尾的工作,以便下一次GC周期能有一个干净的状态。
在这里插入图片描述
缺点:

对CPU资源非常敏感,面向并发设计的程序都会对CPU资源较敏感。CMS默认的回收线程数: (CPU数量+3)/4

无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。并发清理阶段用户程序运行产生的垃圾过了标记阶段所以无法在本次收集中清理掉,称为浮动垃圾。CMS收集器默认在老年代使用了68%的空间后被激活。若老年代增长的不是很快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction 提高触发百分比,但调得太高会容易导致“Concurrent Mode Failure”失败。

基于“标记-清除”算法会产生大量空间碎片。提供开关参数-XX:+UseCMSCompactAtFullCollection 用于在“ 享受”完Full GC服务之后进行碎片整理过程,内存整理的过程是无法并发的。但是停顿时间会变长。

-XX:CMSFullGCsBeforeCompation 设置在执行多少次不压缩的Full GC后,跟来来一次带压缩的

3.5G1收集器 —重点

它是当前收集器技术发展的最前沿成果。与CMS相比有两个显著改进:

基于“标记-整理”算法实现收集器
非常精确地控制停顿
G1收集器可以在几乎不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是Garbage First名称的由来)。区域划分、有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。

4 垃圾回收器组合

youngTenuredJVM options
SerialSerial-XX:+UseSerialGC
ParallelScavenge Serial-XX:+UseParallelGC -XX:-UseParallelOldGC
ParallelScavenge ParallelOld -XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel New或SerialCMS-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
G1-XX:+UseG1GC

5 GC调优常用参数

这篇写的不错https://blog.csdn.net/renfufei/article/details/55102729

参考原文:https://blog.csdn.net/lilong329329/article/details/82222713

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值