JVM系列-02-GC-扫盲

[TOC]

声明

本篇文章是本人阅读《深入理解JVM》和《java虚拟机规范》时的笔记。
记录的都是一些概念性的东西。
JVM是HotSpot,jdk1.7。
大神绕路,不喜勿喷。

1 GC算法

先来走马观花般地浏览一些著名的GC算法。
这里也仅仅是说一下大致过程,具体细节的介绍对于我一个Java程序员来说表示无能为力,因为底层实现要牵扯到具体的实现语言了,而且不同的JVM实现商肯定有不同的实现细节。

1.1 标记/清除算法

这种算法的大概过程是:

  • 标记出所有需要回收的对象
  • 统一回收所有被标记的对象

这种算法很直观,但他的缺点如下:

  • 标记和清除的两个阶段,效率并不是很好,因为回收的粒度太细了
  • 清除后的内存区域一般都是千疮百孔,可用内存区域一般都不连续

1.2 复制算法

上面说的标记/清除算法不太好的主要原因就是其回收粒度太过细微了。
签于此,复制算法的主要做法是:

  • 将内存分为大小相等的两块,暂且称之为内存块
  • 每次当某一内存块(A)占满之后,将该内存块(A)的有用数据复制到另外一块内存块(B)
  • 将A内存块的整个块直接清除

这种算法相比于标记/清除算法的最大特点是:

  • 每次回收都是以内存块为单位(粒度较大)
  • 只有两个内存块,收集完后内存不连续的情况也就不用考虑了
  • 但是,将整个内存分为两块,实际的可用内存也就减半了(这就有点无法接受了)
  • 存在大量的对象复制操作

1.3 标记/整理算法

上面说的复制算法的最大缺点就是对象的复制操作。尤其是在有效的对象很多的情况下。

这里的标记/整理算法的大致过程是:

  • 标记应该回收的区域
  • 将有用的对象/数据集中移动到一块区域,暂且称之为"有效区",有效区的位置往往是在两端的某一端
  • 将"有效区"之外的区域集体清理

1.4 分代收集算法(Generational Collection)

既然上面说集中算法都各有优劣,那么根据他们各自的优点,在不同的情况下使用最优的算法会不会更好呢?

分代收集的大致思路就是这样的:

  • 将JVM堆内存分为新生代和年老代
  • 年老代中的对象存活率一般都很高,采用'标记/清理'或'标记/复制'算法
  • 新生代中一般对象的'死亡率'都很高,采用复制算法

2 GC的代价——Stop The World

上面说了一大堆GC的理论。但是忽略了一点:

怎么确定哪些对象或内存区域是可以被回收的呢???

在java中对于对象是否还“活着”,采用的不是像Python或者其他语言中的"引用计数"的方法。
java中采用的是"可达性分析"。
至于可达性分析的细节没必要去深究,但是由"判断对象是否还存活?"引出的另一个问题却不得不考虑,看下文。

无论采用什么方法去区分哪些对象还活着,不得不做的一个让步就是:这个判定过程中必须暂时让其他所有的线程都暂时停顿,这个现象对于JVM中的各个对象来说就相当于整个世界停止了。也就是所谓的Stop The World

这个停顿当然是有必要的,比如你开始分析对象的存活状态时一个对象是无用的,当你分析完成后那个对象却让其他线程操作了变成有效对象了。

所以,在整个判断过程中,要能够确保一致性。也就免不了Stop The World

当然,应用的规模越大,Stop The World带来的影响越大。
所以,频繁的GC也不见得是好事。

3 垃圾收集器

上面说的都是GC的大致理论知识,现在看看GC的实现:垃圾收集器。

3.1 Serial收集器

Serial收集器是众多垃圾收集器中的元老。是一个单线程的收集器。在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)。
虽然它的出现非常早,但是它依然是虚拟机运行在Client模式下的默认新生代收集器,也有其独特的优点:

  • 简单而高效(与其他收集器的单线程比)
  • 对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销

3.2 ParNew收集器

这个ParNew的介绍是来自《深入理解JVM》的作者说的,与本人没任何关系 _ .. _

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、 -XX:PretenureSizeThreshold、 -XX:HandlePromotionFailure等)、 收集算法、 Stop The World、 对象分配规则、 回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。

  • 是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作
  • ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果
    • 甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。
    • 当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。 它默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数

3.3 Parallel Scavenge收集器

3.3.1 简介

他的特点如下:

  • 是一个新生代收集器
  • 使用复制算法的收集器
  • 并行的多线程收集器
  • Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量
    • 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
    • 也经常称为“吞吐量优先”收集器

3.3.2 参数

  • -XX:MaxGCPauseMillis ==> 控制最大垃圾收集停顿时间,大于零的毫秒数
  • -XX:GCTimeRatio ==> 直接设置吞吐量大小,吞吐量的倒数,既然是个比率,也就是个0到100的整数
  • -XX:+UseAdaptiveSizePolicy
    • 是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、 晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了.
    • 虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)

3.4 Serial Old收集器

  • Serial收集器的老年代版本
  • 一个单线程收集器
  • 使用“标记-整理”算法

3.5 Parallel Old收集器

  • 是Parallel Scavenge收集器的老年代版本
  • 使用多线程和“标记-整理”算法

3.6 CMS收集器

  • CMS:Concurrent Mark Sweep
  • 是一种以获取最短回收停顿时间为目标的收集器
    • 此处的停顿指的就是上文提到的"Stop The World"
  • 其名称中的MS指的就是Mark Sweep,它采用的算法就是标记/清除
  • 他有另外一个名字就是:Concurrent Low Pause Collector(并发、低停顿)

他的缺点如下:

  • 对CPU资源非常敏感
    • 它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低
  • 无法处理浮动垃圾
  • 标记/清除------->内存不连续

3.7 G1收集器

  • 是一款面向服务端应用的垃圾收集器
  • 并行与并发
    • G1能充分利用多CPU、 多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行
  • 分代收集
  • 空间整合
    • 不会产生内存空间碎片,收集后能提供规整的可用内存
    • 分配大对象时不会因为无法找到连续内存空间而提前触发下次GC
  • 可预测的停顿:低停顿

《深入理解JVM》一书是这么说的:

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。 使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

4 GC日志

GC日志的格式乍看起来乱七八糟,乌漆嘛黑的。当然他肯定是有格式的。就拿《深入理解JVM》中的这段代码来说吧:

public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    /**
     * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过
     */
    byte[] bigSize = new byte[2 * _1MB];

    public static void main(String[] args) {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        // 假设在这行发生GC,objA和objB是否能被回收?
        System.gc();
    }
}

虚拟机参数:

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

在我的机器(jdk1.7)上输出如下:

2016-12-17T16:11:19.650+0800: 0.093: [GC [PSYoungGen: 5427K->568K(38400K)] 5427K->568K(124416K), 0.0016819 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2016-12-17T16:11:19.652+0800: 0.095: [Full GC [PSYoungGen: 568K->0K(38400K)] [ParOldGen: 0K->463K(86016K)] 568K->463K(124416K) [PSPermGen: 2514K->2513K(21504K)], 0.0109008 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 38400K, used 998K [0x00000007d5c80000, 0x00000007d8700000, 0x0000000800000000)
  eden space 33280K, 3% used [0x00000007d5c80000,0x00000007d5d79a60,0x00000007d7d00000)
  from space 5120K, 0% used [0x00000007d7d00000,0x00000007d7d00000,0x00000007d8200000)
  to   space 5120K, 0% used [0x00000007d8200000,0x00000007d8200000,0x00000007d8700000)
 ParOldGen       total 86016K, used 463K [0x0000000781600000, 0x0000000786a00000, 0x00000007d5c80000)
  object space 86016K, 0% used [0x0000000781600000,0x0000000781673eb0,0x0000000786a00000)
 PSPermGen       total 21504K, used 2520K [0x000000077c400000, 0x000000077d900000, 0x0000000781600000)
  object space 21504K, 11% used [0x000000077c400000,0x000000077c676178,0x000000077d900000)

解释如下:

2016-12-17T16:11:19.650+0800
    -XX:+PrintGCDateStamps的作用,就是GC的时间了
0.093:表示的从JVM启动以来经过的秒数
GC [PSYoungGen:....
    GC发生的区域
        PSYoungGen表示采用的收集器为Parallel Scavenge
        如果使用的是Serial收集器,新生代名为“Default New Generation”,显示就是“[DefNew”
        如果使用的是ParNew收集器,新生代名称为“[ParNew”,意为“Parallel New Generation”
        如果采用的是Parallel Scavenge收集器,新生代名称就是“PSYoungGen”
“Full”,说明这次GC是发生了Stop-The-World

GC日志,暂时就先写这么多吧,在后续的文章中再详细介绍GC日志。

5 和GC相关的JVM参数

注:以下参数总结来自《深入理解JVM》一书

  • UseSerialGC : 是否使用Serial收集器
    • 启用后将使用Serial + Serial Old的组合来进行垃圾回收
    • 这也是Client模式下的默认值
  • UseParNewGC : 是否使用ParNew收集器
    • 将使用ParNew + Serial Old的组合来进行垃圾回收
  • UseConcMarkSweepGC
    • 启用后将使用ParNew + CMS + Serial Old的组合来进行垃圾回收
    • Serial Old 作为CMS的后备收集器(Concurrent Mode Failure)
  • UseParallelGC
    • 使用Parallel Scavenge + Serial Old的组合来进行垃圾回收
    • 这也是Server模式下的默认值
  • UseParallelOldGC
    • 使用 Parallel Scavenge + Parallel Old的组合来进行垃圾回收
  • SurvivorRatio
    • 新生代中Eden和Survivor的比值
    • 默认为8,即:Eden:Survivor=8:1
  • PretenureSizeThreshold
    • 这个大小值,表示对象大小大于多少之后直接分配到老年代而不进入新生代
  • MaxTenuringThreshold
    • 这个年龄值表示对象在经过多少次Minor GC之后就进入老年代
    • 每次Minor GC之后,对象的该属性值就加1
  • UseAdaptiveSizePolicy
    • 动态调节堆中各个区域的大小和进入老年代的年龄
  • HandlePromotionFailure
    • 是否允许分配担保失败
    • 担保失败指的是: 老年代的剩余空间大小无法容纳新生代中的Eden和Survivor的情况
  • ParallelGCThreads
    • 并行GC的线程数
  • GCTimeRatio
    • GC时间占总时间的比例
    • 只有在使用Parallel Scavenge的情况下生效
    • 默认值:99
  • MaxGCPauseMillis
    • GC的最大停顿时间
    • 只有在使用Parallel Scavenge的情况下生效
  • CMSInitiatingOccupancyFraction
    • CMS收集器在老年代空间被占用多少后触发GC
    • 只对CMS收集器生效
    • 默认值:68%
  • UseCMSCompactAtFullCollection
    • CMS收集器在完成垃圾回收后是否进行内存碎片整理
    • 只对CMS收集器生效
  • CMSFullGCsBeforeCompaction
    • CMS经过多少次GC后再进行碎片整理
    • 也就是设置CMS收集器在进行N次垃圾收集后再进行一次碎片整理
    • 只对CMS收集器生效

6 Minor GC 和 Full GC/Major GC

  • Minor GC指的是新生代的GC
    • Minor GC比较频繁
    • 速度也比较快
  • Full GC/Major GC指的是老年代的GC
    • Full GC的速度一般比Minor GC慢10倍左右
    • Full GC的出现往往会有Minor GC的伴随

参考文章

  • 《深入理解JVM》
  • 《Java虚拟机规范》-JDK1.7
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 原文阅读 前言 这段时间懈怠了,罪过! 最近看到有同事也开始用上了微信公众号写博客了,挺好的~给他们点赞,这博客我...
    码农戏码阅读 5,946评论 2 31
  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 15,528评论 3 83
  • JVM架构 当一个程序启动之前,它的class会被类装载器装入方法区(Permanent区),执行引擎读取方法区的...
    cocohaifang阅读 1,646评论 0 7
  • 1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供...
    简欲明心阅读 89,368评论 17 311
  • 一. 垃圾回收的意义 在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对...
    Stan_Z阅读 1,918评论 0 25