JVM-------垃圾回收机制(GC)

目录

 

一、什么是垃圾回收机制

二、什么是垃圾

2.1 引用计数法

2.2 根可达法

2.2.1 虚拟机栈,栈帧中局部变量表中的引用的对象

2.2.2 本地方法栈,本地方法栈中JNI(即一般说的 Native 方法)引用的对象

2.2.3 方法区,方法区中类静态属性引用的对象

2.2.4 方法区,方法区中常量引用的对象 

三、怎么回收垃圾

3.1 标记清除法(Mark-Sweep)

3.2 拷贝算法

3.3 标记压缩(Mark-Compact)

四、 什么时候会触发垃圾回收机制(分代的垃圾回收策略)

4.1 新生代里YGC的过程

4.2 老年代里FGC

五、垃圾回收器

5.1 STW(Stop-The-World)

5.2 Serial 收集器

5.3 ParNew 收集器

5.4 Parallel Scavenge收集器(可简称为PS)

GC自适应调整策略

5.5 CMS收集器

步骤

 

CMS优缺点

5.6 G1垃圾收集器

G1的分区概念

G1的两种垃圾回收模式


一、什么是垃圾回收机制

垃圾收集GC(Garbage Collection)是Java语言的核心技术之一, 在Java中,程序员不需要去关心内存动态分配和垃圾回收的问题,这一切都交给了JVM来处理。针对GC我们这篇文章提出以下几个问题,GC中判定为垃圾的标准,标记垃圾的算法以及回收垃圾的算法。由于Java的对象都是堆中创建,因此GC的作用地方主要就是堆内存。

二、什么是垃圾

Java的垃圾主要是在堆。在栈是没有垃圾的,因为栈的元素用完后会出栈。

既然我们要做垃圾回收,首先我们得搞清楚垃圾的定义是什么,哪些内存是需要回收的。通俗而言,垃圾就是没有引用指向的一个或者多个对象

下面提供几个方法:

2.1 引用计数法

引用计数法,是通过在对象头中分配一段空间来保存该对象被引用的次数。

如果该对象被其他对象引用,则它的引用计数(RC)加一,如果删除改对象的引用,那么它的引用减一,当该对象的引用计数为0时,那么该对象就会被回收。

例如:

我通过 String m = new String(“jack”):

然后让 m = null;

可是这种引用计数法拥有致命缺点:

如下图:

当有两个对象,它们如果互相引用着对方,那么它们的RC是不会为0的,意味着它们是不会被垃圾回收掉的。但下图的两个对象的引用a、b都为null,那就意味着这2个对象已经不可能再被访问了,但由于他们相互引用着对方,导致它们的引用计数永远都不会为0,通过引用计数算法,也就永远无法通知GC收集器回收它们。

2.2 根可达法

根可达法的基本思路是,通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为 Reference Chain,当一个对象到GC Roots没有任何引用链项链时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。

那么问题来了,什么样的对象才算是GC Roots呢?

GC Roots主要来自四个部分:(两栈两方法区)

1. 虚拟机栈,栈帧中局部变量表中的引用。

2.本地方法栈,本地方法栈中JNI(即一般说的 Native 方法)引用的对象。

3.方法区,方法区中类静态属性引用的对象。

4.方法区,方法区中常量引用的对象。

具体例子:

2.2.1 虚拟机栈,栈帧中局部变量表中的引用的对象

如下图: s 是在虚拟机栈中产生的(在局部变量表),而 localParameter对象是在堆中产生的,s是GC Roots的一种,所以当s连着localParameter对象时,localParameter对象不会别回收,但当 s=null时,则s不再连着localParameter对象,此时localParameter对象会被回收掉。

2.2.2 本地方法栈,本地方法栈中JNI(即一般说的 Native 方法)引用的对象

任何 Native 接口都会使用某种本地方法栈,实现的本地方法接口是使用 C 连接模型的话,那么它的本地方法栈就是 C 栈。当线程调用 Java 方法时,虚拟机会创建一个新的栈帧并压入 Java 栈。然而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不再在线程的 Java 栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。

因此和Java的栈帧中的局部变量表一样,JNI方法的变量或参数也会称为GC Roots。

2.2.3 方法区,方法区中类静态属性引用的对象

如下图: s是栈帧中局部变量表的参数,所以s自然就是GC Roots,在s=null时,s指向的 “properties”对象由于无法跟GC Roots连接,所以被回收。

但类的静态成员m(存放在方法区)由于和“parameter”对象连接,所以“parameter”不会被回收。

2.2.4 方法区,方法区中常量引用的对象 

如下图:由于 m是被 final修饰的,所以定义为常量,存储在方法区的常量池,属于GC Roots。

class MethodAreaStaicProperties {
    public static final MethodAreaStaicProperties m = new MethodAreaStaicProperties("final");
    public MethodAreaStaicProperties(String name){}
}

三、怎么回收垃圾

知道了哪些内存空间是垃圾后,就要开始做垃圾回收了,垃圾回收的算法有:

1. 标记清除法(Mark-Sweep)

2. 拷贝算法(copying)

3. 标记压缩(Mark-Compact)

下面具体说明

3.1 标记清除法(Mark-Sweep)

标记清除法是最基础的一种方法,如下图:

先把内存中可回收的((即垃圾)标记出来,然后再把它们擦除,然后就变成未使用的区域,等待下次被使用。

缺点:会产生内存碎片。从上图其实可以看出,这个方法会把内存分类成大小不一的一块块,上图中等方块的假设是 2M,小一些的是 1M,大一些的是 4M。等我们回收完,内存就会切成了很多段。我们知道开辟内存空间时,需要的是连续的内存区域,这时候我们需要一个 2M的内存区域,其中有2个 1M 是没法用的。这样就导致,其实我们本身还有这么多的内存的,但却用不了。

3.2 拷贝算法

拷贝算法其实是在标记清除法上演变过来的。解决了标记清除产生内存碎片的问题。

它将可用的内存分成大小相同的两块,每次只使用其中一块,当这一块用完了,就把还存活的对象拷贝到另一块去,这时候,是不会产生内存碎片的,如下图,然后再把已经使用过的内存空间一次清理掉,循环往复。但是缺点也很明显,就是太浪费内存空间了,因为每次只能使用其中的一半。

优点:不会造成内存碎片。且效率较高。

缺点:浪费空间,例如需要5个G的内存,但至少要10G。

3.3 标记压缩(Mark-Compact)

如下图:这种算法,一开始跟标记清除一样,先标记出可以回收的对象,然后进行整理,就是强行把存货的对象挪到一起,再清除可回收的内存,这样虽然解决了内存碎片问题,但是效率会很低。

优点:解决了内存碎片问题,内存利用率也高

缺点:效率偏低。

 

四、 什么时候会触发垃圾回收机制(分代的垃圾回收策略)

上面我们已经知道了具体回收垃圾的方法,这节探讨一下什么时候会触发垃圾回收。这种策略的理据是:不同对象的生命周期是不一样的。

几点知识点:

1. Java堆 = 新生代 + 老年代 ,默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。

2. 新生代 = 伊甸区 + s0区+s1区,内存大小的比例是 8:1:1。 

3. 新对象的创建,优先考虑在伊甸区里创建,若伊甸区里放不下,会触发新生代的垃圾回收(即YGC),YGC之后再会判断伊甸区是否足够内存分配新对象,若还是不能,就会把新对象放到老年代去创建。

4. 如果对象在Eden出生,并经过第一次YGC后仍然存活,并且被S区容纳的话,年龄设为1,每熬过一次YGC,年龄+1,若年龄超过一定限制(具体取决于哪个垃圾回收器,CMS为6岁,PS即并行,为15岁),则被晋升到老年态。即长期存活的对象进入老年代。

5. 若老年代满了,无法容纳更多的对象,那么很可能会在YGC后,触发FGC(Full GC即老年代的GC),FGC会清理整个堆内存的垃圾,包括新生代和老年代。FGC比YGC慢十倍以上。

6. 在新生代的对象有个特点,就是大部分生命周期都很短,有可能一次YGC就把很多对象清理掉了。

整体流程如下图:

4.1 新生代里YGC的过程

刚刚我们已经知道了,新生代是分成伊甸区和s0和s1区的。一般来说新对象都在伊甸区生成,伊甸区满了会触发YGC,YGC的过程是,把伊甸区存活的对象复制到s0区,然后擦除伊甸区和s1区,然后下次YGC,就把伊甸区和s0存活的对象复制到s1区,然后擦除伊甸区和s0区,循环往复。

我们看到新生代的伊甸区、s0、s1区和YGC的原理,有没有想起拷贝算法?没错,就是用YGC就是用拷贝算法来清理垃圾的。选用拷贝算法的原因也很简单,因为YGC不同于FGC,YGC是频繁触发的,所以效率要求就要比较高。

4.2 老年代里FGC

全堆范围的gc。默认堆空间使用到达80%(可调整)的时候会触发fgc。以我们生产环境为例,一般比较少会触发fgc,有时10天或一周左右会有一次。

五、垃圾回收器

在最常用的HotSpot虚拟机中,囊括的垃圾回收器有以下几种:

用于新生代的垃圾回收器:Serial、parNew、Parallel Scavenge

用于老年代的垃圾回收器:CMS、Serial Old、Parallel Old

其余:G1

下图的垃圾回收器都是新生代和老年代配合使用的,它们之间的连线表示它们能配合一起用。例如Serial+CMS

 

5.1 STW(Stop-The-World)

STW是GC过程中常会出现的一种现象,具体是在GC时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。这样想或许能理解:你妈在打扫房间,但是你一直吃瓜子往地上吐皮,这时候又有新的垃圾产生,房间永远打扫不干净,只有让你停止嗑瓜子,才能打扫干净。GC也是一个道理。

5.2 Serial 收集器

Serial收集器是最基本的、发展历史最悠久的收集器。

特点:单线程、简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。

过程:触发YGC的时候,停下所有的用户进程,然后垃圾回收线程上场,回收完垃圾后,程序再继续执行。

例子:Serial + Serial Old 收集器运行示意图。

可以看到,在安全点处,用户线程停止(STW),然后GC线程上场回收垃圾,回收结束后,用户线程继续执行。

 

5.3 ParNew 收集器

ParNew是多线程版本的Serial。

除了使用多线程外其余行为均和Serial收集器一模一样。

特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题。

垃圾回收的时候程序是没法运行的,这也是为什么有些java程序跑着跑着会卡顿。

 

例子: ParNew + Serial Old 收集器

可以看到,ParNew是当用户线程停止时,上场的GC是多个线程的。对比Serial Old(用在老年代的Serial),Serial Old就只有一个单线程的GC。

5.4 Parallel Scavenge收集器(可简称为PS)

PS和ParNew很相似,都是多线程的GC,都是用拷贝算法进行垃圾回收。但是PS是更关心吞吐量的垃圾回收器,拥有GC自适应调整策略

吞吐量: CPU运行用户程序的时间 / (CPU运行用户程序的时间+垃圾回收的时间)

特点:跟ParNew很像,且面向吞吐量。拥有GC自适应调整策略。

GC自适应调整策略

虚拟机会根据系统的运行状况收集性能监控信息,动态设置下面某些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。

如:

  • XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间。直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是GC停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。比如原来10秒收集一次,每次停顿100毫秒。但是线程编程每5秒收集一次,每次停顿70毫秒,停顿时间下降的同时,吞吐量也下降了。即根本原因是GC发生更频繁了。

  • XX:GCRatio 直接设置吞吐量的大小。

5.5 CMS收集器

一种以获得最短STW时间的收集器。由于STW时间短,所以程序停顿时间也短

特点:基于标记清除法,并发收集,低停顿

步骤

具体如下:

1.初始标记:初始标记只是标记一下能由GC Roots直接关联的对象,速度很快,需要STW

2.并发标记:进行GC Roots tracing,找出所有存活的对象,这时候 GC线程和用户线程是可以一起执行的。没有STW

3.重新标记:由于 并发标记 过程中,用户线程还在执行,所以有可能存在 上一刻还是垃圾,被标记成垃圾了,但下一刻又被某些对象连上了的情况。或者说 上一刻还不是垃圾,然后下一刻就变成了垃圾的情况。所以需要 重新标记 来修正。那 重新标记 难道就不会标记错吗?不会的,因为 重新标记 过程,是STW的,用户程序不能运行。

4.并发清除:清除垃圾对象。这时候用户线程是可以一起运行的。

 

CMS优缺点

优点:并发、低停顿

缺点:

1.由于CMS是使用 标记清除法 的,所以会存在大量的内存碎片,容易导致大对象提前放不进内存,这样就会导致FGC,那结果就是FGC发生的频率会更高。

2. 会产生浮动垃圾情况 ,就是在初始标记过程中,一个对象在上一时刻没被标记成垃圾,但是下一时刻又变成了垃圾,这时候垃圾收集器是不会对它动手的。只能等到下一次GC时再处理它。

 

5.6 G1垃圾收集器

G1是逻辑上分代,但是物理上已经不分代了。

G1的分区概念

 G1的堆区在分代的基础上,引入分区的概念。G1将堆分成了若干Region,以下和”分区”代表同一概念。(这些分区不要求是连续的内存空间)。Region的大小可以通过G1HeapRegionSize参数进行设置,其必须是2的幂,范围允许为1Mb到32Mb。

Eden regions(年轻代-Eden区)

Survivor regions(年轻代-Survivor区)

Old regions(老年代)

Humongous regions(巨型对象区域)

Free regions(未分配区域,也会叫做可用分区)-上图中空白的区域

G1中的巨型对象是值 占用了 50%region容量的对象。Humongous区,就专门用来存储巨型对象。如果一个H区装不下一个巨型对象,则会通过连续的若干H分区来存储。

特点:

1.  G1 从多个region 中复制存货的对象,然后集中放入一个region中,同时整理清除内存。(拷贝算法)

2. 对比使用 “标记清除” 的CMS,G1使用 “拷贝算法” 不会造成内存碎片。

3. G1的最大特点是,高效的执行回收,优先去执行那些大量垃圾对象可回收的区域(region),这也是G1名字的又来,Garbage First。

4.在G1前的垃圾收集器要么对新生代进行GC,要么对老年代GC。而G1 则没有这个概念,G1把堆分成多个region,不会说这次GC只对新生代或者老年代执行。

5.重点:

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

 

G1的重要概念:

1.收集集合(CSet):一组可被回收的分区的集合。在CSet中存活的对象会在GC中被移动到另一个可用分区。

2.已记忆集合(RSet):RSet记录了其他region中的对象是否引用 本region中对象的关系。价值在于 使得垃圾收集器不需要扫描整个堆空间,只需要扫描RSet就能找到谁引用了当前分区中的对象。

G1的两种垃圾回收模式

G1的两种垃圾回收模式是:Young GC (即YGC) 和 Mixed GC,两种都是有 STW的。

YGC:

1.当eden数据满了,则触发g1 YGC 
2.并行的执行: 
YGC 将 eden region 中存活的对象拷贝到survivor,或者直接晋升到Old Region中;将Survivor Regin中存活的对象拷贝到新的Survivor或者晋升old region。 

Mixed GC:

在G1的 GC 中,主要是为了 Mixed GC提供标记服务的,其实很像CMS。

步骤

G1收集器的运作大致可划分为以下几个步骤:

  • 1)初始标记(Initial Marking):初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,这阶段需要停顿线程(STW),但耗时很短。
  • 2)并发标记(Concurrent Marking):并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
  • 3)最终标记(Final Marking):最终标记阶段是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set(即RSet)中,这阶段需要停顿线程,但是可并行执行。

  •  

    4)筛选回收(Live Data Counting and Evacuation):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划(可并发执行)

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值