JVM垃圾回收(四)- GC算法:实现(1)

GC算法:实现

上面我们介绍了GC算法中的核心概念,接下来我们看一下JVM里的具体实现。首先必须了解的一个重要的事实是:对于大部分的JVM来说,两种不同的GC算法是必须的,一个是清理Young Generation的算法,另一种是清理Old Generation的算法。

在JVM里有各种各样的这种内置算法,如果你没有特别指定GC算法,则会使用一个默认的、适应当前平台(platform-specific)的算法。接下来我们会解释每种算法的工作原理。

下面的列表提供了一个快速的预览,关于哪些算法可能被结合使用。不过需要注意的是,它仅适用于Java 8,对应Java 8 之前的版本,可能稍有不同。

如果上表看起来很复杂,不要慌。在实际使用中,基本可以归结为上面表中标粗的部分。剩下的不是被弃用,就是不被支持,或是在实际场景中不实用。所以,下面我们仅仅会讨论下面的几种组合:

  1. Serial GC for both the Young and Old generation
  2. Parallel GC for both the Young and Old generation
  3. Parallel New for Young + Concurrent Mark and Sweep (CMS) for the Old Generation
  4. G1 in case of which the generation are not separated between the Young and Old

Serial GC

这种垃圾回收器在Young Generation使用mark-copy,在Old Generation使用mark-sweep-compact。正如它的名字一样,这两种收集器均是单线程的收集器,无法与当前的任务并行工作。这两种收集器均会触发stop-the-world pauses,暂时停止所有应用线程。

这种GC算法无法使用当前主流硬件上多核CPU的优点,不管有多少可用的CPU核数,JVM在GC阶段仅会使用一个核。可以通过指定以下配置应用此机制:

java -XX:+UseSerialGC com.company.testclass

这个选项仅推荐给:

  1. JVM中仅有几百MB的堆大小
  2. 运行的环境是单核CPU

对于大部分的服务端部署来说,很少会使用这种模式,因为大部分服务端的应用一般部署在多核平台,并不适合Serial GC的使用场景,会造成服务器资源的浪费。接下来我们看一下如果使用Serial GC 的话,那GC 收集器的日志会是什么形式。首先我们在JVM下开启GC日志:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps

日志的输出类似以下内容:

2015-05-26T14:45:37.987-0200: 151.126: [GC (Allocation Failure) 151.126: [DefNew: 629119K->69888K(629120K), 0.0584157 secs] 1619346K->1273247K(2027264K), 0.0585007 secs] [Times: user=0.06 sys=0.00, real=0.06 secs]

2015-05-26T14:45:59.690-0200: 172.829: [GC (Allocation Failure) 172.829: [DefNew: 629120K->629120K(629120K), 0.0000372 secs]172.829: [Tenured: 1203359K->755802K(1398144K), 0.1855567 secs] 1832479K->755802K(2027264K), [Metaspace: 6741K->6741K(1056768K)], 0.1856954 secs] [Times: user=0.18 sys=0.00, real=0.18 secs]

这小部分GC日志可以提供我们很多信息,JVM内部当时发生了什么。具体地说,这部分日志片段反应了两轮GC,一个是清理Young Generation,另一个是清理整个堆。我们首先分析第一个在Young Generation发生的GC。

Minor GC

下面的日志片段包括了GC清理Young Generation时的一些信息:

2015-05-26T14:45:37.987-02001:151.1262:[GC3(Allocation Failure4) 151.126: [DefNew5:629119K->69888K6(629120K)7, 0.0584157 secs]1619346K->1273247K8(2027264K)9,0.0585007 secs10][Times: user=0.06 sys=0.00, real=0.06 secs]11

1. 2015-05-26T14:45:37.987-0200 : GC事件发生的时间

2. 151.126 : 相对于JVM的启动时间,GC事件发生的时间,以秒为单位

3. GC: GC类型的标志,用于区别是 Minor GC 还是 Full GC。这次显示的是一次 Minor GC

4. Allocation Failure :GC发生的原因。在这个日志中,表示的是是由于Young Generation 里的任何区域均无法满足一个(对某个数据结构的)空间分配

5. DefNew :使用的 GC 收集器名称。这个缩略名表示的是单线程的、mark-copy、stop-the-world 垃圾回收器,用于清理Young Generation

6. 629119K->69888K :Young Generation的使用情况,分为 GC 前以及 GC 后

7. (629120K) :Young Generation 的总大小

8. 1619346K->1273247K :堆内存使用的总大小,分为GC前与GC后

9. (2027264K) :堆内存总可用大小

10. 0.0585007 secs :GC事件的持续总长时间,以秒为单位

11. [Times: user=0.06 sys=0.00, real=0.06 secs] :GC事件的持续时间,从三种不同的类别衡量:

A. user:在GC 阶段,GC 线程消耗的整个CPU时间

B. sys:OS 调用消耗的时间,或是等待系统事件的时间

C. real:应用停止的时间。因为 Serial GC 一直使用的是单线程,所以这里 real time 等于 user 与 system 时间的总和

从上面的片段,我们可以精确地了解到在 GC 事件时,JVM内部的内存消耗情况。在这次回收前,heap 使用了总共 1,619,346K 大小的内存,其中 Young Generation 一共占了 629,120K 内存。基于此,我们可以计算出 Old Generation 使用量为 990,227K内存。

另一方面,我们也可以看到,在回收之后,Young Generation 的使用量降了 559,231K,但是整个heap 的使用量仅降了346,099K,由此可以推测出,有 213,132K 的对象从 Young Generation 被提升到了 Old Generation。

这次 GC 事件前后,内存的分布,也可以通过下图表示:

Full GC

在讨论了第一个 Minor GC 事件后,我们再来看看第二个 Full GC 事件日志:

2015-05-26T14:45:59.690-02001: 172.8292:[GC (Allocation Failure) 172.829:[DefNew: 629120K->629120K(629120K), 0.0000372 secs3]172.829:[Tenured4: 1203359K->755802K 5(1398144K) 6,0.1855567 secs7] 1832479K->755802K8(2027264K)9,[Metaspace: 6741K->6741K(1056768K)]10 [Times: user=0.18 sys=0.00, real=0.18 secs]11

1. 2015-05-26T14:45:59.690-0200 :GC事件开始的时间

2. 172.829 : 相对于JVM的启动时间,GC事件发生的时间,以秒为单位

3. [DefNew: 629120K->629120K(629120K), 0.0000372 secs :类似上一个例子(由于 Allocation Failure触发的一个minor GC),这次对 Young Generation 的回收也是同样由 DefNew 回收器完成。它将 Young Generation的使用量由 629,120K 降为 0。需要注意的是:这里的日志打印有问题,由于一个存在bug的行为,导致它打印的日志为 Young Generation 使用为满的状态。这次回收耗时 0.0000372 秒

4.Tenured :清理 Old 空间时使用的 GC 收集器名称。这里 Tenured 表示GC使用了一个单线程的、stop-the-world、mark-sweep-compact 垃圾回收器

5. 1203359K->755802K :在 GC 事件前后,Old Generation 使用的空间大小

6. (1398144K) :Old Generation 空间的总共大小

7. 0.1855567 secs :清理 Old Generation 的时间

8. 1832479K->755802K :清理 Young 以及 Old Generation 前后,整个 heap 使用的内存大小

9. (2027264K) :JVM 可用的 heap 大小

10. [Metaspace: 6741K->6741K(1056768K)] :类似 Metaspace 空间回收的信息,正如日志打印的,这次回收中,没有Metaspace的垃圾被回收

11. [Times: user=0.18 sys=0.00, real=0.18 secs] :GC事件的持续时间,从三种不同的类别衡量:

A. user:在GC 阶段,GC 线程消耗的整个CPU时间

B. sys:OS 调用消耗的时间,或是等待系统事件的时间

C. real:应用停止的时间。因为 Serial GC 一直使用的是单线程,所以这里 real time 等于 user 与 system 时间的总和

Full GC 与 Minor GC 的不同点显而易见:在 GC 事件中,除了对 Young Generation 做了垃圾回收外,Old Generation 与 Metaspace 也被做了清理。在这个例子中,在 GC 事件前后,内存的分布可如下如表示:

Parallel GC

这种GC收集器的组合(对 Young 与 Old 使用的两种 GC收集器),在 Young Generation 中使用 mark-copy,在Old Generation中使用mark-sweep-compact。对 Young 与 Old Generation的收集均会触发 stop-the-world 事件,暂停应用的所有线程,以运行 GC。两个收集器均会以多线程的方式运行 mark-copy / mark-sweep-compact,所以它的名字为 ‘Parallel’。使用并行的方式,可以明显减少GC的时间。在GC时,使用多少个线程也可以通过参数指定:-XX:ParallelGCThreads=NNN。默认的值是:机器的CPU核数。在启动JVM时使用以下任一配置即可启用ParallelGC:

java -XX:+UseParallelGC com.company.MyClass

java -XX:+UseParallelOldGC com.company.MyClass

java -XX:+UseParallelGC -XX:+UseParallelOldGC com.compay.MyClass

Parallel 垃圾收集器适用与多核机器,所以如果你的主要目标是为了提高吞吐,则Parallel GC是一个较好的选择。可以获得高吞吐是由于此方法高效地使用了系统资源:

  1. 在收集过程中,所有CPU 核均会并行回收垃圾,所以应用暂停时间会更短
  2. 在GC轮数之间,不会有收集器消耗任何资源

另一方面,由于在GC中所有的阶段在运行时不可被打断,所以在你的应用线程暂停时,这些收集器仍容易受到long pause的影响。所以,如果Latency是你需要优先考虑的目标,则你可以考虑下一个垃圾收集器组合。

下面我们看一下使用Parallel GC时,日志输出的信息。下面是一个 minor 和一个 major GC 的日志:

2015-05-26T14:27:40.915-0200: 116.115: [GC (Allocation Failure) [PSYoungGen: 2694440K->1305132K(2796544K)] 9556775K->8438926K(11185152K), 0.2406675 secs] [Times: user=1.77 sys=0.01, real=0.24 secs]

2015-05-26T14:27:41.155-0200: 116.356: [Full GC (Ergonomics) [PSYoungGen: 1305132K->0K(2796544K)] [ParOldGen: 7133794K->6597672K(8388608K)] 8438926K->6597672K(11185152K), [Metaspace: 6745K->6745K(1056768K)], 0.9158801 secs] [Times: user=4.49 sys=0.64, real=0.92 secs]

Minor GC

第一条表示了在 Young Generation里发生的一个GC事件:

2015-05-26T14:27:40.915-02001: 116.1152:[GC3(Allocation Failure4)[PSYoungGen5: 2694440K->1305132K6(2796544K)7]9556775K->8438926K8(11185152K)9, 0.2406675 secs10][Times: user=1.77 sys=0.01, real=0.24 secs]11

1. 2015-05-26T14:27:40.915-0200:GC事件发生的时间

2. 116.115 : 相对于JVM的启动时间,GC事件发生的时间,以秒为单位

3. GC: GC类型的标志,用于区别是 Minor GC 还是 Full GC。这次显示的是一次 Minor GC

4. Allocation Failure :GC发生的原因。在这个日志中,表示的是是由于Young Generation 里的任何区域均无法满足一个(对某个数据结构的)空间分配

5. PSYoungGen:使用的 GC 收集器名称。这里表示的是一个并行的、mark-copy、stop-the-world 垃圾回收器被用于清理Young Generation

6. 2694440K->1305132K:Young Generation的使用情况,分为 GC 前以及 GC 后

7. (2796544K):Young Generation 的总大小

8. 9556775K->8438926K :堆内存使用的总大小,分为GC前与GC后

9. (11185152K):堆内存总可用大小

10. 0.2406675 secs:GC事件的持续总长时间,以秒为单位

11. [Times: user=1.77 sys=0.01, real=0.24 secs] :GC事件的持续时间,从三种不同的类别衡量:

A. user:在GC 阶段,GC 线程消耗的整个CPU时间

B. sys:OS 调用消耗的时间,或是等待系统事件的时间

C. real:应用停止的时间。对于 Parallel GC 来说,它的值应该接近于(user time + system time)/ GC 收集器使用的 CPU 线程数。在这个例子中,GC 收集器使用的是 8 个线程。不过需要注意的是,由于一些活动并不会被并行执行,所以它的真实值会超过一定的比率。

从上面的日志可以看到,在 GC 事件前,整个 heap 中消耗的内存为 9,556,775K,其中 Young Generation 消耗了2,694,440K,也就是说 Old Generation 使用了 6,862,335K。在 GC 后,Young Generation 的使用量降了 1,389,308K,但是整个 heap 的使用量仅降了 1,117,849K。也就是说,有 271,459K 从 Young Generation 提升到了 Old Generation。

Full GC

下面我们继续看下一行 GC 日志,看看 GC 是如何清理整个 heap 内存的:

2015-05-26T14:27:41.155-02001:116.3562:[Full GC3 (Ergonomics4)[PSYoungGen: 1305132K->0K(2796544K)]5[ParOldGen6:7133794K->6597672K 7(8388608K)8] 8438926K->6597672K9(11185152K)10, [Metaspace: 6745K->6745K(1056768K)] 11, 0.9158801 secs12, [Times: user=4.49 sys=0.64, real=0.92 secs]13

1-3 省略

4. Ergonomics:GC 事件发生的原因。这里表示 JVM 的内部功效决定这时候需要做垃圾回收

5. [PSYoungGen: 1305132K->0K(2796544K)]:与之前的例子类似,一个名为“PSYoungGen”的、并行的、mark-copy、stop-the-world GC 回收器被用于清理 Young Generation。Young Generation 的使用情况由 1,305,132K 降为了 0,因为一般一次 Full GC 经常会将 Young GC 完全清理掉。

6. ParOldGen:用于清理 Old Generation 的收集器类型。在这个例子中,一个名为 ParOldGen 的、并行的、mark-sweep-compact、stop-the-world 垃圾回收器被用于清理 Old Generation。

7. 7133794K->6597672K :在 GC 事件前后,Old Generation 使用的空间大小

8. (8388608K) :Old Generation 空间的总共大小

9. 8438926K->6597672K:清理 Young 以及 Old Generation 前后,整个 heap 使用的内存大小

10. (11185152K) :JVM 可用的 heap 大小

11. [Metaspace: 6745K->6745K(1056768K)] :类似 Metaspace 空间回收的信息,正如日志打印的,这次事件中,没有Metaspace的垃圾被回收

12. 0.9158801 secs:GC 事件持续的时间

13. [Times: user=4.49 sys=0.64, real=0.92 secs] GC事件的持续时间,从三种不同的类别衡量:

A. user:在GC 阶段,GC 线程消耗的整个CPU时间

B. sys:OS 调用消耗的时间,或是等待系统事件的时间

C. real:应用停止的时间。对于 Parallel GC 来说,它的值应该接近于(user time + system time)/ GC 收集器使用的 CPU 线程数。在这个例子中,GC 收集器使用的是 8 个线程。不过需要注意的是,由于一些活动并不会被并行执行,所以它的真实值会超过一定的比率。

同样,Full GC 与 Minor GC 的区别较为明显:除了清理 Young Generation,Old Generation 与 Metaspace 也会被清理。在这个例子中,在 GC 事件前后,内存的分布可如下如表示:

References:

https://plumbr.io/handbook/garbage-collection-algorithms-implementations

原文地址:https://www.cnblogs.com/zackstang/p/10093307.html

时间: 2024-09-30 09:54:27

JVM垃圾回收(四)- GC算法:实现(1)的相关文章

JVM垃圾回收机制与算法

JVM内存由几个部分组成:堆.方法区.栈.程序计数器.本地方法栈 JVM垃圾回收仅针对公共内存区域,即:堆和方法区进行,因为只有这两个区域在运行时才能知道需要创建些对象,其内存分配和回收都是动态的. 一.垃圾回收策略 1.1分代管理 将堆和方法区按照对象不同年龄进行分代: (Young Generation and Old Generation)堆中会频繁创建对象,基于一种分代的思想,按照对象存活时间将堆划分为新生代和旧生代两部分,并不是一次垃圾回收新生代存活的对象就放入旧生代, 而是要经过几次

JVM垃圾回收2(垃圾收集算法)

根据<深入理解java虚拟机>这本书总结 一.关于几个概念:(标记垃圾算法.垃圾收集算法.垃圾收集器) 前面说了如何寻找jvm垃圾,有两种方法:引用计数法/可达性算法.这篇准备讲,标记完垃圾之后,回收的算法,这里的算法只是垃圾回收的思想.后面会讲到多种垃圾收集器,这里的垃圾收集器就是运用了垃圾手机算法的思想,可以说是具体实现. 这里还是想多余的说一下这三个概念: 垃圾标记算法:标记垃圾的方法 垃圾收集算法:一种回收思想,供垃圾收集器使用.可能用在年轻代,也可能用在老年代(当然现在来说老年代和年

JVM垃圾回收(GC)整理总结学习

基本回收算法 1. 引用计数(Reference Counting)比较古老的回收算法.原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数.垃圾回收时,只用收集计数为0的对象.此算法最致命的是无法处理循环引用的问题.2. 标记-清除(Mark-Sweep)此算法执行分两阶段.第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除.此算法需要暂停整个应用,同时,会产生内存碎片.3. 复制(Copying)此算法把内存空间划为两个相等的区域,每次只使用其

JVM——垃圾回收(GC)

GC简介 java语言运行在java虚拟机(jvm)上,为了解决有限的空间和性能的保证这个矛盾体,jvm所具备的GC能力,可以有效的清除不用的对象,使空间的利用更加合理.下面介绍该机制的原理. 判断对象已废弃 引用计数法 给每一个对象都配备一个计数器,对于该对象,若增加一个指向它的引用,则计数器加1:每失效一个引用,则计数器减一. 但是,如果两个对象互相引用,但都对于外部都已失去用途,则这样的两个对象是无法被计数为0的,因为他们的计数器永远都只为1. 可达性分析法 为了解决引用计数法的缺陷,我们

JVM垃圾回收概念和算法

GC中的垃圾:特指存在于内存中.不会再使用的对象. 内存泄漏和内存溢出的区别: 内存泄漏:内存空间忘记回收,垃圾对象永远无法被回收 内存溢出:垃圾对象(不满足回收条件)所耗内存持续上升,导致内存溢出. 1 常用的垃圾回收算法 引用计数法.标记压缩法.标记清除法.复制算法和分代.分区 1)引用计数法:

JVM 垃圾回收 Minor gc vs Major gc vs Full gc

关于垃圾回收机制及比较请参见:http://colobu.com/2015/04/07/minor-gc-vs-major-gc-vs-full-gc/ http://colobu.com/2014/12/16/java-jvm-memory-model-and-garbage-collection-monitoring-tuning/

JVM(四)垃圾回收的实现算法和执行细节

全文共 1890 个字,读完大约需要 6 分钟. 上一篇我们讲了垃圾标记的一些实现细节和经典算法,而本文将系统的讲解一下垃圾回收的经典算法,和Hotspot虚拟机执行垃圾回收的一些实现细节,比如安全点和安全区域等. 因为各个平台的虚拟机操作内存的方法各不相同,且牵扯大量的程序实现细节,所以本文不会过多的讨论算法的具体实现,只会介绍几种算法思想及发展过程. 垃圾回收算法 1.标记-清除算法 标记-清除算法是最基础的算法,像它的名字一样算法分为"标记"和"清除"两个阶段

JVM垃圾回收算法 总结及汇总

先看一眼JVM虚拟机运行时的内存模型: 1.方法区 Perm(永久代.非堆) 2.虚拟机栈 3.本地方法栈 (Native方法) 4.堆 5.程序计数器 1 首先的问题是:jvm如何知道那些对象需要回收 ? 目前两种标识算法.三种回收算法.两种清除算法.三种收集器 引用计数法 每个对象上都有一个引用计数,对象每被引用一次,引用计数器就+1,对象引用被释放,引用计数器-1,直到对象的引用计数为0,对象就标识可以回收 这个可以用数据算法中的图形表示,对象A-对象B-对象C 都有引用,所以不会被回收,

JVM垃圾回收算法(最全)

JVM垃圾回收算法(最全) 下面是JVM虚拟机运行时的内存模型: 1.方法区 Perm(永久代.非堆) 2.虚拟机栈 3.本地方法栈 (Native方法) 4.堆 5.程序计数器 1 首先的问题是:jvm如何知道那些对象需要回收 ? 目前两种标识算法.三种回收算法.两种清除算法.三种收集器 引用计数法 每个对象上都有一个引用计数,对象每被引用一次,引用计数器就+1,对象引用被释放,引用计数器-1,直到对象的引用计数为0,对象就标识可以回收 这个可以用数据算法中的图形表示,对象A-对象B-对象C

JVM垃圾回收 GC

一.判断对象是否存活 1.引用计数算法 给对象添加一个引用计数器,每当一个地方引用了该对象,计数器加1,:当引用失效,计数器减1.当计数器为0表示该对象已死,可回收.但是无法解决两个对象互相引用的情况 2.可达性分析算法 通过一系列称为的GC Roots对象为起点,从这些节点往下搜索,搜索走过的路径为引用链,当一个对象到GC Roots没有任何引用链相连是,即对象到GC Roots不可达,则证明对象已死,可回收 可作为GC Roots的对象包括:虚拟机栈中引用的对象,本地方法栈中Native方法