JVM原理 | GC机制

CG机制

1. GC发生在何处?

GC一般情况下是发生在方法区, 其中堆的结构如下图:
在这里插入图片描述
堆:

  1. 老年代 : 存放大对象(很多连续的内存)和长期存活的对象,
  2. 新生代 ( 又分为三个区 ) :
    1. Eden区 ( 内存较大 ) 每次使用都是一个Eden 和 其中一个 Survivor
    2. From Survivor区
    3. To Survivor区

方法区: 存放永久代: (对象一般不会被回收 ,永久代的垃圾收集主要回收废弃常量和无用类)

扩展:

Eden区内存不够时: 虚拟机将发起一次 Minor GC
老年代内存不够时: Major GC/Full GC


2. 如何判断一个对象是否需要被回收?

2.1 判断一个类是否“无用”

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
  2. 加载该类的ClassLoader已经被回收
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

这里需要了解一下年龄计数器这个东西:

  1. 虚拟机通过一个对象年龄计数器来判定哪些对象放在新生代,哪些对象应该放在老生代。
  2. 对象每在Survivor中熬过一次Minor GC,年龄就增加1岁,当他的年龄增加到最大值15时,就将会被晋升到老年代中。
  3. 如果在Survivor空间中所有相同年龄的对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

2.2 判断的原理

一般对象是否需要被回收大致分为如下两种算法 :

2.2.1 引用计数器算法

引用计数算法(Reference Counting): 给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的,这就是引用计数算法的核心。

优点: 引用计数算法实现简单,判定效率也很高

缺点: 它很难解决对象之间相互循环引用的问题

2.2.2 可达性分析算法

可达性分析算法(Reachability Analysis): 这是Java虚拟机采用的判定对象是否存活的算法。通过一系列的称为“GC Roots"的对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。

可达性分析的具体步骤:

  1. 在分析时,需保证这个对象引用关系不再变化,否则结果将不准确。因此GC进行时需停掉其它所有java执行线程(Sun把这种行为称为‘Stop the World’),即使是号称几乎不会停顿的CMS收集器,枚举根节点时也需停掉线程

  2. 系统停下来后JVM不需要一个个检查引用,而是通过OopMap数据结构【HotSpot的叫法】来标记对象引用。

  3. 虚拟机先得知哪些地方存放对象的引用,在类加载完时。HotSpot把对象内的偏移量和它是什么类型的数据算出来,在 jit 编译过程中,也会在特定位置记录下栈和寄存器哪些位置是引用,这样GC在扫描时就可以知道这些信息。【目前主流JVM使用准确式GC】

  4. OopMap可以帮助HotSpot快速且准确完成GC Roots枚举以及确定相关信息。但是也存在一个问题,可能导致引用关系变化。

  5. 这个时候有个 safepoint(安全点)的概念:
    1. HotSpot中GC不是在任意位置都可以进入,而只能在safepoint处进入。 GC时对一个Java线程来说,它要么处在safepoint,要么不在safepoint。
    2. safepoint不能太少,否则GC等待的时间会很久
    3. safepoint不能太多,否则将增加运行GC的负担

    安全点主要存放的位置:
    1 循环的末尾
    2 方法临返回前/调用方法的call指令后
    3 可能抛异常的位置


在下图可以看到GC Roots左边的对象都有引用链相关联,所以他们不是死亡对象,而在GCRoots右边有几个零散的对象没有引用链相关联,所以他们就会别Java虚拟机判定为死亡对象而被回收。

img

扩展 : 哪些对象可以为GC Roots ?

① 虚拟机栈(栈桢中的本地变量表)中的引用的对象。
② 方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中。
③ 方法区中的常量引用的对象
④ 本地方法栈中JNI(native方法)引用的对象

即使可达性算法中不可达的对象,也不是一定要马上被回收,还有可能被抢救一下。网上例子很多,基本上和深入理解JVM一书讲的一样对象的生存还是死亡:


2.3 何为死亡对象?

java虚拟机在进行死亡对象判定时,会经历两个过程

  1. 一次标记 : 将对象放入 F-Queue 队列( 低优先级的队列 ) 如果在 finalize 方法中该对象重新与引用链上的任何一个对象建立了关联 , 即该对象连上了任何一个对象的引用链,例如this关键字,那么该对象就会逃脱垃圾回收系统
  2. 两次标记 : 如果该对象在 finalize 方法中没有与任何一个对象进行关联操作,那么该对象会被虚拟机进行第二次标记,该对象就会被垃圾回收系统回收。值得注意的是finaliza方法JVM系统只会自动调用一次,如果对象面临下一次回收,它的 finalize 方法不会被再次执行。

3. 垃圾回收算法

3.1 标记–清除算法(Mark-Sweep)

在这里插入图片描述

优点:算法执行分为两个阶段标记与清除,所有的回收算法,基本都基于标记回收算法做了深度优化

缺点: 效率问题,会产生内存空间碎片(不连续的空间),这也是标记整理最大的弊端 : 碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

3.2 复制算法(Copying)

为了解决Mark-Sweep算法的缺陷,Copying 算法就被提了出来。**它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。**当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:img

优点: 比较标记清除算法,避免了回收造成的内存碎片问题,

缺点: 这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。以局部的内存空间牺牲为代价,不过空间的浪费比较小,默认8:1的比例1是浪费的。

复制也有一定的效率与空间成本,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低, 所以复制算法一般用于较少对象的内存的复制( 例如: 年轻代 )

3.3 标记整理算法(Mark-Compact)

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact 算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:

在这里插入图片描述

优点: 避免了,空间的浪费,与内存碎片问题。

缺点: 整理时复制有效率成本。

3.4 分代收集算法(Generational Collection)

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。
它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)新生代(Young Generation)

  1. 老年代的特点 : 是每次垃圾收集时只有少量对象需要被回收
  2. 新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
  • 目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
  • 而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法

注意,在堆区之外还有一个代就是永久代(Permanet Generation)
它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

4. 垃圾收集器

年轻代收集器
Serial、ParNew、Parallel Scavenge
老年代收集器
Serial Old、Parallel Old、CMS收集器
特殊收集器
G1收集器[新型,不在年轻、老年代范畴内] — JDK1.7发布的商用垃圾收集器

不同收集器的组合方式 :
收集器,连线代表可结合使用
收集器,连线代表可结合使用

4.1 年轻代

4.1.1 Serial / Serial Old

Serial收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。

优点: 实现简单高效

缺点: 会给用户带来停顿

4.1.2 ParNew

ParNew收集器是Serial收集器的多线程版本,使用多个线程进行垃圾收集。

4.1.3 Parallel Scavenge

Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量

吞吐量 :
该收集器重点关心的是吞吐量【吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间) 如果代码运行100min垃圾收集1min,则为99%】

4.2 老年代

4.2.1 Serial Old

上边介绍了

4.2. Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法

4.2. CMS

CMS(Concurrent Mark Sweep)收集器 是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。【重视响应,可以带来好的用户体验,被sun称为并发低停顿收集器】

启用CMS:-XX:+UseConcMarkSweepGC

正如其名,CMS采用的是"标记-清除"(Mark Sweep)算法,而且是支持并发(Concurrent)的
它的运作分为4个阶段:

  1. 初始标记:标记一下GC Roots能直接关联到的对象,速度很快
  2. 并发标记:GC Roots Tarcing过程,即可达性分析
  3. 重新标记:为了修正因并发标记期间用户程序运作而产生变动的那一部分对象的标记记录,会有些许停顿,时间上比较一般为 初始标记 < 重新标记 < 并发标记
  4. 并发清除

以上初始标记和重新标记需要stop the world(停掉其它运行java线程)

之所以说CMS的用户体验好,是因为CMS收集器的内存回收工作是可以和用户线程一起并发执行。

总体上CMS是款优秀的收集器,但是它也有些缺点

  1. cms堆cpu特别敏感,cms运行线程和应用程序并发执行需要多核cpu,如果cpu核数多的话可以发挥它并发执行的优势,但是cms默认配置启动的时候垃圾线程数为 (cpu数量+3)/4,它的性能很容易受cpu核数影响,当cpu的数目少的时候比如说为为2核,如果这个时候cpu运算压力比较大,还要分一半给cms运作,这可能会很大程度的影响到计算机性能。

  2. cms无法处理浮动垃圾,可能导致Concurrent Mode Failure(并发模式故障)而触发full GC

  3. 由于cms是采用"标记-清除“算法,因此就会存在垃圾碎片的问题,为了解决这个问题cms提供了 -XX:+UseCMSCompactAtFullCollection选项,这个选项相当于一个开关【默认开启】,用于CMS顶不住要进行full GC时开启内存碎片合并,内存整理的过程是无法并发的,且开启这个选项会影响性能(比如停顿时间变长)

浮动垃圾:由于cms支持运行的时候用户线程也在运行,程序运行的时候会产生新的垃圾,这里产生的垃圾就是浮动垃圾,cms无法当次处理,得等下次才可以。

缺点总结 : 1. CPU敏感 2. 无法处理浮动垃圾 3.可能有垃圾碎片

4.3 G1

G1( garbage first : 尽可能多收垃圾,避免full gc )收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

g1的特别之处在于它强化了分区,弱化了分代的概念,是区域化、增量式的收集器,它不属于新生代也不属于老年代收集器, 用到的算法为标记-清理复制算法

同cms一样也是关注降低延迟,是用于替代cms功能更为强大的新型收集器,因为它解决了cms产生空间碎片等一系列缺陷。

G1的工作流程 :

  1. g1通过并发(并行)标记阶段查找老年代存活对象,通过并行复制压缩存活对象【这样可以省出连续空间供大对象使用】。
  2. g1将一组或多组区域中存活对象以增量并行的方式复制到不同区域进行压缩,从而减少堆碎片,目标是尽可能多回收堆空间【垃圾优先】,且尽可能不超出暂停目标以达到低延迟的目的。
  3. g1提供三种垃圾回收模式young gcmixed gcfull gc,不像其它的收集器,根据区域而不是分代,新生代老年代的对象它都能回收。
4.3.1 Minor GC、Major GC、FULL GC、mixed gc
4.3.1.1 Minor GC

在年轻代Young space(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC,Minor GC只会清理年轻代.

4.3.1.2 Major GC

Major GC清理老年代(old GC),但是通常也可以指和Full GC是等价,因为收集老年代的时候往往也会伴随着升级年轻代,收集整个Java堆。所以有人问的时候需问清楚它指的是full GC还是old GC。

4.3.1.3 Full GC

full gc是对新生代、老年代、永久代【jdk1.8后没有这个概念了】统一的回收。

【知乎R大的回答:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)、元空间(1.8及以上)等所有部分的模式】

4.3.1.4 mixed GC【g1特有】

混合GC

收集整个young gen以及部分old gen的GC。只有G1有这个模式


5.扩展

5.1 什么时候触发GC?

简单来说,触发的条件就是GC算法区域满了或将满了。

minor GC(young GC) : 当年轻代中eden区分配满的时候触发[值得一提的是因为young GC后部分存活的对象会已到老年代 (比如对象熬过15轮) ,所以过后old gen的占用量通常会变高]
full GC:
①手动调用System.gc()方法 [增加了full GC频率,不建议使用而是让jvm自己管理内存,可以设置-XX:+ DisableExplicitGC来禁止RMI调用System.gc]
②发现perm gen(如果存在永久代的话)需分配空间但已经没有足够空间
③老年代空间不足,比如说新生代的大对象大数组晋升到老年代就可能导致老年代空间不足。
④CMS GC时出现Promotion Faield[pf]
⑤统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间。
这个比较难理解,这是HotSpot为了避免由于新生代晋升到老年代导致老年代空间不足而触发的FULL GC。
比如程序第一次触发Minor GC后,有5m的对象晋升到老年代,姑且现在平均算5m,那么下次Minor GC发生时,先判断现在老年代剩余空间大小是否超过5m,如果小于5m,则HotSpot则会触发full GC(这点挺智能的)

5.2 CMS收集器是否会扫描年轻代

会,在初始标记的时候会扫描新生代。

虽然cms是老年代收集器,但是我们知道年轻代的对象是可以晋升为老年代的,为了空间分配担保,还是有必要去扫描年轻代。

5.3 什么是空间分配担保

在minor gc前,jvm会先检查老年代最大可用空间是否大于新生代所有对象总空间,如果是的话,则minor gc可以确保是安全的

如果担保失败,会检查一个配置(HandlePromotionFailire),即是否允许担保失败。

如果允许 : 继续检查 老年代最大可用可用的连续空间 > 之前晋升的平均大小 是否成立,比如说剩10m,之前每次都有9m左右的新生代到老年代,那么将尝试一次minor gc(大于的情况),这会比较冒险。

如果不允许,而且还小于的情况,则会触发full gc。【为了避免经常full GC 该参数建议打开】

这边为什么说是冒险是因为minor gc过后如果出现大对象,由于新生代采用复制算法,survivor无法容纳将跑到老年代,所以才会去计算之前的平均值作为一种担保的条件与老年代剩余空间比较,这就是分配担保。

这种担保是动态概率的手段,但是也有可能出现之前平均都比较低,突然有一次minor gc对象变得很多远高于以往的平均值,这个时候就会导致担保失败【Handle Promotion Failure】,这就只好再失败后再触发一次FULL GC

5.4 为什么复制算法要分两个Survivor,而不直接移到老年代

这样做的话效率可能会更高,但是old区一般都是熬过多次可达性分析算法过后的存活的对象,要求比较苛刻且空间有限,而不能直接移过去,这将导致一系列问题(比如老年代容易被撑爆)

分两个Survivor(from/to),自然是为了保证复制算法运行以提高效率

5.5 常用回收器参数设置:

img

5.6 stop the world具体是什么,有没有办法避免

stop the world简单来说就是gc的时候,停掉除gc外的java线程

无论什么gc都难以避免停顿,即使是g1也会在初始标记阶段发生,stop the world并不可怕,可以尽可能的减少停顿时间。

5.6 新生代什么样的情况会晋升为老年代

对象优先分配在Eden区,Eden区满时会触发一次minor GC

对象晋升规则

  1. 长期存活的对象进入老年代,对象每熬过一次GC年龄+1(默认年龄阈值15,可配置)。
  2. 对象太大新生代无法容纳则会分配到老年代
  3. eden区满了,进行minor gc后,eden和一个survivor区仍然存活的对象无法放到(to survivor区)则会通过分配担保机制放到老年代,这种情况一般是minor gc后新生代存活的对象太多。
  4. 动态年龄判定,为了使内存分配更灵活,jvm不一定要求对象年龄达到MaxTenuringThreshold(15)才晋升为老年代,若survior区相同年龄对象总大小大于survior区空间的一半,则大于等于这个年龄的对象将会在minor gc时移到老年代

5.6 怎么理解g1,适用于什么场景

G1 核心关键词: 自适应
G1 GC 是区域化、并行-并发、增量式垃圾回收器,相比其他 HotSpot 垃圾回收器,可提供更多可预测的暂停。增量的特性使 G1 GC 适用于更大的堆,在最坏的情况下仍能提供不错的响应。G1 GC 的自适应特性使 JVM 命令行只需要软实时暂停时间目标的最大值以及 Java 堆大小的最大值和最小值,即可开始工作。

g1不再区分老年代、年轻代这样的内存空间,这是较以往收集器很大的差异,所有的内存空间就是一块划分为不同子区域,每个区域大小为1m-32m,最多支持的内存为64g左右,且由于它为了的特性适用于大内存机器。


g1回收时堆内存情况, 如下图:
在这里插入图片描述

适用场景:

  1. 像cms能与应用程序并发执行,GC停顿短【短而且可控】,用户体验好的场景。
  2. 面向服务端,大内存,高cpu的应用机器。【网上说差不多是6g或更大】
  3. 应用在运行过程中经常会产生大量内存碎片,需要压缩空间【比cms好的地方之一,g1具备压缩功能】。

5.7 内存分配方面

img
  对象的内存分配,往大方向上讲就是在堆上分配,对象主要分配在新生代的 Eden SpaceFrom Space,少数情况下会直接分配在老年代。如果新生代的Eden Space和From Space的空间不足,则会发起一次GC,如果进行了GC之后,Eden Space和From Space能够容纳该对象就放在Eden Space和From Space。在GC的过程中,会将Eden Space和From Space中的存活对象移动到To Space,然后将Eden Space和From Space进行清理。如果在清理的过程中,To Space无法足够来存储某个对象,就会将该对象移动到老年代中。在进行了GC之后,使用的便是Eden space和To Space了,下次GC时会将存活对象复制到From Space,如此反复循环。当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到老年代中。

一般来说,大对象会被直接分配到老年代,所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组,比如:

byte[] data = new byte[4 * 1024 * 1024]

这种一般会直接在老年代分配存储空间。

当然分配的规则并不是百分之百固定的,这要取决于当前使用的是哪种垃圾收集器组合和JVM的相关参数。

5.8 Eden / From / To区的传递步骤

Eden空间From SurvivorTo Survivor 三者默认比例为8:1:1
优先使用Eden区,若Eden区满,则将对象复制到第二块内存区上。但是不能保证每次回收都只有不多于10%的对象存活,所以Survivor区不够的话,则会依赖老年代年存进行分配 ( 担保机制 )。

GC开始时,对象只会存于Eden和From Survivor区域,To Survivor【保留空间】为空。

GC进行时,Eden区所有存活的对象都被复制到To Survivor区,而From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阈值(默认15是,新生代每熬过一次垃圾回收,年龄+1),则移到老年代,没有达到则复制到To Survivor

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值