图解JVM--(二)垃圾回收

垃圾回收

1.如何判断对象可以回收

1.1 引用计数

在对象中添加一个引用计数器,每当有一个地方引用它,计数器值就加一,当引用失效时,计数器值就减一,任何时刻计数器为零的对象就不可能再被使用的,就可以做为垃圾被回收

image

会出现如上图的循环引用,永远清除不了

1.2 可达性分析算法

  • java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象

  • 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收

  • 那些对象可以作为GC Root ? (使用eclipse的分析工具 map 分析 jmap命令的结果)

    使用 jps 查看进程java进程id

    抓取内存并进行转储,b表示二进制,live会在抓取时执行一次垃圾回收,只关心存活对象,file 指定文件名称

    再使用 jmap -dump:format=b,live,file=xx.bin + 线程id

    image

System class ---> 系统的类,jdk自带的类,方法区中的类静态属性引用的对象,以及方法区中的StringTable(常量池)中的引用

Native Stack ----> 本地方法

Thread ----> 线程 (每一个线程都对应一个虚拟机栈,虚拟机栈中(栈帧中的本地变量表)中引用的对象)

Busy Monitor ---> 加锁 (被synchronized关键字)所持有的对象

1.3 四种引用

  1. 强引用

    • 只有所有 GC Roots 对象都不通过【强引用】 引用该对象,该对象才能被垃圾回收
  2. 软引用(SoftReference)

    • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时,会再次触发垃圾回收,回收软引用对象
    • 可以配合引用队列来释放软引用自身
  3. 弱引用

    • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
    • 可以配合引用队列来释放弱引用自身
  4. 虚引用

    • 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由Reference Handler 线程调用虚引用相关方法释放直接内存
  5. 终结器引用

    • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalizer 方法,第二次GC时才能回收被引用对象
    image
    image

软引用的例子:

/**
 * 演示软引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */

private static final int _4MB = 4 * 1024 * 1024;


    public static void soft() {
        // list --> SoftReference --> byte[]

        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());

        }
        System.out.println("循环结束:" + list.size());
        for (SoftReference<byte[]> ref : list) {
            System.out.println(ref.get());
        }
    }
/**
 * 演示软引用, 配合引用队列
 */
public class Demo2_4 {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        List<SoftReference<byte[]>> list = new ArrayList<>();

        // 引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

        for (int i = 0; i < 5; i++) {
            // 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }

        // 从队列中获取无用的 软引用对象,并移除
        Reference<? extends byte[]> poll = queue.poll();
        while( poll != null) {
            list.remove(poll);
            poll = queue.poll();
        }

        System.out.println("===========================");
        for (SoftReference<byte[]> reference : list) {
            System.out.println(reference.get());
        }

    }
}

弱引用例子:

/**
 * 演示弱引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Demo2_5 {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        //  list --> WeakReference --> byte[]
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
            list.add(ref);
            for (WeakReference<byte[]> w : list) {
                System.out.print(w.get()+" ");
            }
            System.out.println();

        }
        System.out.println("循环结束:" + list.size());
    }
}

2.垃圾回收算法

2.1 标记清除

定义: Mark Sweep

  • 速度较快
  • 会造成内存碎片
image

2.2 标记整理

定义: Mark Compact

  • 速度慢
  • 没有内存碎片
image

2.3 复制

定义: Copy

  • 不会有内存碎片
  • 需要占用双倍内存空间
image

3.分代垃圾回收

image
  • 对象首先分配在伊甸园区
  • 新生代空间不足时,触发 minor gc ,伊甸园和 from 存活对象使用 copy算法 复制到 to 中,存活的对象年龄加1,并且交换 from to
  • minor gc 会引发一次 stop the word (STW) :在发生垃圾回收时,会暂停其他用户线程,由垃圾回收线程完成垃圾回收动作后,其他用户线程才可以恢复运行。(由于牵扯到对象的移到,所以需要暂停其他用户线程)
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(对象头长度为 4 bit 即 1111,十进制为15)
  • 当老年代空间不足,会先尝试触发 minor gc ,如果之后空间仍不足,那么触发 full gc,STW时间更长,因为老年代的对象比较大,并且可能采用的垃圾回收算法时(标记+清除)或者是(标记+整理)算法,所以互比较耗时间

3.1 相关 VM 参数

含义 参数
堆初始大小 -Xms
堆最大大小 -Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阈值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

3.2 GC分析

/**
 *  演示内存的分配策略
 */
public class Demo2_1 {
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            ArrayList<byte[]> list = new ArrayList<>();
            list.add(new byte[_8MB]);
            list.add(new byte[_8MB]);
        }).start();

        System.out.println("sleep....");
        Thread.sleep(1000L);
    }
}

当对象足够大的,超过幸存区,会直接跨级为老年代

image

4.垃圾回收器

4.1 串行

  • 单线程
  • 堆内存较小,适合个人电脑

打开串行垃圾回收器的 jvm参数:

-XX:+UseSerialGC = Serial + SerialOld

Serial: 工作在新生代 , 采用复制算法

SerialOld : 工作在老年代,采用标记-整理算法

上面两个都是单线程垃圾回收器,所以使用时,其他cpu线程需要阻塞

image

4.2 吞吐量优先

  • 多线程
  • 堆内存较大,多核cpu
  • 让单位时间内,STW的实际最短
  • jdk 1.8 默认的垃圾回收器

打开吞吐量优先垃圾回收器的 jvm参数:

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC

UseParallelGC:新生代,(parallel 并行,多个垃圾回收线程并行同时执行 )复制算法

UseParallelOldGC:老年代,标记-整理算法

-XX:+UserAdaptiveSizePolicy

-XX:GCTimeRatio=ratio //调整吞吐量的目标

-XX:MaxGCPauseMillis=ms

-XX:ParallelGCThreads=n

image

4.3 响应时间优先

  • 多线程
  • 堆内存较大,多核cpu
  • 尽可能让 STW 的时间最短 0.1 0.1 0.1 0. 1 0.1 = 0.5
  • 在某些时刻,垃圾回收线程可以与用户线程并发执行,即,可以执行一会儿垃圾回收,再执行一会儿用户线程
  • 基于并发 标记-清除算法的老年代的垃圾回收器

打开响应时间优先垃圾回收器的 jvm参数:

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld

UseConcMarkSweepGC:新生代 (concurrent 并发(在垃圾回收线程时,用户线程也可以执行,两种线程可以并发)Sweep 清除)标记-清除 算法

并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生

UseParNewGC:会退化为 SerialOld ( 单线程 老年代 标记-整理 算法)

-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads

-XX:CMSInitiatingOccupancyFraction=percent

-XX:+CMSScavengeBeforeRemark

image

4.4 G1

定义: Garbage First

jdk 1.9 默认

适用场景:

  • 同时注重吞吐量(Throughput) 和 低延迟(Low latency) ,默认的暂停目标是200ms
  • 超大堆内存,会将堆划分为多个大小相等的Region
  • 整体上时 标记-整理 算法,两个区域之间是复制算法

相关 JVM 参数

-XX:+UseG1GC

-XX:G1HeapRegionSize=size

-XX:MaxGCPauseMillis=time

4.4.1 G1 垃圾回收阶段

image

三阶段时循环过程:先进行新生代垃圾收集,当老年代垃圾达到一定的阈值的时候,会对新生代垃圾收集的同时进行并发的标记(Concurrent Mark),等这个阶段完成之后,会进行一段混合收集(Mixed Collection) (对新生代,幸存区,老年代进行一次规模较大的收集),等内存释放掉,混合收集结束之后,会再次进入新生代收集(Young Collection)

4.4.2 Young Collection

  • 会STW

将内存划分为多个区,每个区都可以表示 E (Eden) S(幸存区) O(老年代)

E (Eden) S(幸存区) O(老年代)

image

​ 以拷贝算法 将对象放入幸存区

image

​ 当幸存区的对象过多,或者超过一定年龄,时间,会触发垃圾回收,幸存区会有一部分进入老年代

image

4.4.3 Young Collection + CM(concurrent mark)

  • 在Young GC 时会进行 GC Root 的初始标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW) ,由下面的JVM参数决定

-XX:InitiatingHeapOccupancyPercent=percent (默认45%)

image

4.4.4 Mixed Collection

会对 E , S , O 进行全面垃圾回收

  • 最终标记(Remark) 会 STW
  • 拷贝存活 (Evacuatio)会 STW

-XX:MaxGCPauseMillis=ms

image

4.4.5 Full GC

  • SerialGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • CMS
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足
  • G1
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足

4.4.6 Young Collection 跨代引用

  • 新生代回收的跨代引用 (老年代引用新生代) 问题
  • 根对象有一些是来自老对象,将老对象分隔,做标记
image
image

4.4.7 Remark

  • pre-write barrier + satb_mark_queue
image

黑色,已经处理完

灰色,尚在处理中

白色,还没处理

image

当处理B时,发现 B 被请引用,所以,标记为黑色,同时,其他线程执行将B 与 C 之间的应用切掉,把C 作为A 的引用,这个时候,由于对象引用发生改变,此时会有一个 写屏障 会出现在 A 与 C之间 (对象引用发生改变就会有写屏障)

image

接着会把有写屏障的对象丢到一个队列中,标记会灰色,对队列取值判断,发现有被引用,标记为黑色

image

这样,就避免了当引用被修改时,对象,被设置为白色,当做垃圾清除

4.4.8 JDK 8 字符串去重

  • 优点: 节省大量内存
  • 缺点: 略微多占用了 cpu 时间 ,新生代回收时间略微增加

-XX:+UseStringDeduplication

String s1 = new String("hello"); // char[]{'h','e','l','l','o'} 
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果它们值一样,让它们引用同一个 char[]
  • 注意,与 String.intern() 不一样
    • String.intern() 关注的是字符串对象
    • 而字符串去重关注的是 char[]
    • 在 JVM 内部,使用了不同的字符串表

4.4.9 jdk8 并发标记卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸

载它所加载的所有类

-XX:+ClassUnloadingWithConcurrentMark 默认启用

4.4.10 回收巨型对象

  • 一个对象大于 region 的一半时,称之为巨型对象

  • G1 不会对巨型对象进行拷贝

  • 回收时被优先考虑

  • G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生

  • 代垃圾回收时处理掉

image

4.4.11 jdk9 并发标记起始时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为 FullGC

  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent

  • JDK 9 可以动态调整

    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值

    • 进行数据采样并动态调整

    • 总会添加一个安全的空档空间

5.垃圾回收调优

5.1 调优领域

  • 内存
  • 锁竞争
  • cpu 占用
  • io

5.2 确定目标

  • 【低延迟】 还是 【高吞吐量】,选择合适的垃圾回收器
  • CMS , G1 ,ZGC
  • ParallelGC
  • Zing

5.3 最快的GC是不发送GC

  • 查看 FullGC 前后的内存占用,考虑下面几个问题
    • 数据是不是太多
      • resultSet = statement.executrQuery(“select * from 大表 limit n “) (表数据太多)
    • 数据表是否臃肿
      • 对象图
      • 对象大小 (最小的Object占16字节 包装类型 Integer 大约24 ,而基本类型只有4字节 int 4 )
    • 是否存在内存泄漏
      • 如: static Map map =
      • 长时间存活对象,使用软,硬引用,回收
      • 或者第三方缓存实现

5.4 新生代调优

  • 新生代的优点

    • 所有的new 操作的内存分配非常廉价
      • TLAB thread-local allocation buffer
  • 死亡对象的回收代价为零

  • 大部分对象用过即死

  • Minor GC 的时间远远低于 Full GC

新生代是不是越大越好?

以下是Oracle 对此的原文描述:

-Xmn:设置新生代的初始和最大值的jvm指令,以下时说明

Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery).

GC is performed in this region more often than in other regions. If the size for the young

generation is too small, then a lot of minor garbage collections are performed. If the size is too

large, then only full garbage collections are performed, which can take a long time to complete.

Oracle recommends that you keep the size for the young generation greater than 25% and less

than 50% of the overall heap size.

如果新生代划得太小,那么会由于内存太小引发太多次的minor gc ,每一个minor gc 都会哟式短暂的 STW ,影响效率,如果新生代太大,则会导致老年代内存太小,容易引发多次 Full gc ,Full gc 的占用时间远大于 minor gc ,

建议 新生代 占堆的 25%以上, 50%以下

  • 新生代能容纳所有【并发量 * (请求-响应)】的数据

  • 幸存区大到能保留【当前活跃对象+需要晋升对象】

  • 晋升阈值配置得当,让长时间存活对象尽快晋升

-XX:MaxTenuringThreshold=threshold

-XX:+PrintTenuringDistribution

Desired survivor size 48286924 bytes, new threshold 10 (max 10)
- age 1: 28992024 bytes, 28992024 total
- age 2: 1366864 bytes, 30358888 total
- age 3: 1425912 bytes, 31784800 total 
...

5.5 老年代调优

以CMS 为例

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

推荐阅读更多精彩内容

  • JVM架构 当一个程序启动之前,它的class会被类装载器装入方法区(Permanent区),执行引擎读取方法区的...
    cocohaifang阅读 1,644评论 0 7
  • 垃圾收集基础 Java 语言的一大特点就是可以进行自动垃圾回收处理,而无需开发人员过于关注系统资源,例如内存资源的...
    Austin_Brant阅读 737评论 0 2
  • http://www.cnblogs.com/angeldevil/p/3801189.html值得一看 Clas...
    snail_knight阅读 1,409评论 1 0
  • Java 虚拟机有自己完善的硬件架构, 如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM 屏蔽了与具体操作系...
    尹小凯阅读 1,678评论 0 10
  • GC区域Eden Survivor(from,to), Old Gen和Perm Gen VM区域总体分两类,he...
    Fitz_Lee阅读 406评论 0 0