JVM--垃圾回收

一、 如何定位垃圾

  • 垃圾回收的核心工作就是回收垃圾,在JVM 的视角来看:垃圾就是无用的对象占用的堆内存空间。那么如何定位垃圾便是垃圾回收的重中之重。

1. 引用计数算法(reference counting)

  • 算法思想:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加 1;当引用失效时,计数器值就减 1;一旦某个对象的引用计数器为 0,则说明该对象已经死亡,便可被回收。
  • 缺陷:无法处理循环引用对象,如下:
    /**
     * testGC() 方法执行后,objA 和 objB是否会被GC?
     * ClassName: ReferenceCountingGC 
     */
    public class ReferenceCountingGC {
        public Object instance = null;
        private static final int _1MB = 1024 * 1024;
        
        private byte[] bigSize = new byte[2 * _1MB];
        
        public static void testGC() {
            ReferenceCountingGC objA = new ReferenceCountingGC();
            ReferenceCountingGC objB = new ReferenceCountingGC();
            objA.instance = objB;
            objB.instance = objA;
            
            objA = null;
            objB = null;
            
            System.gc();
        }
    }

对象 a, b 相互引用,除此之外没有其他引用指向a 或者 b,在这种情况下,a 和 b 实际已经死亡,但是由于他们的引用计数器皆不为 0 ,在引用计数算法的心中,这两个对象还活着。因此,这些循环引用对象所占据的空间将不可回收,从而造成内存泄漏。

  • GC Roots: 可以理解为由堆外指向堆内的引用,包括(但不限于)如下几种:
    1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;
    2. 方法区中类静态属性引用的对象;
    3. 方法区中常量引用的对象;
    4. 本地方法栈中JNI(即Native 方法)引用的对象;
    5. 已启动且未停止的 Java 线程。

2. 可达性分析算法

  • 算法思想:以“GC Roots”的对象作为起始点,若无法到达对象 a 或者 b,则可达性分析便不会将它们加入存活对象合集中。其中搜索走过的路径称为引用链(Reference Chain)
  • 缺陷:在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为 null)或漏报(将引用设置为被访问的对象)
  1. 误报只会使JVM 损失了部分垃圾回收的机会,即当GC标记完成,还未开始回收,你更新了其中一个引用,使之指向 null,那么原来的指向对象本可以被回收(但没有被GC 标记为可回收,只能等待下次标记)。
  2. 漏报是已经被 GC 标记为可回收的对象,更新为被其他对象指向,垃圾回收器直接给回收掉了,则可能会直接导致JVM 崩溃。

3. Stop-the-world 以及安全点

  • 目的是为了解决上述标记过程中堆栈的状态发生改变,JVM 采取安全点机制来实现 Stop-the-world 操作,暂停其他非垃圾回收线程。因此老年代回收有卡顿现象。

4. Java的四种引用:强软弱虚

5. 对象的最后一次自我拯救

  • 即使在可达性算法中不可达的对象,仍有一次自我拯救的机会。因为宣告一个对象的死亡至少要经历两次标记过程:对象在可达性算法标记为不可达后进行一次筛选,判断此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都是为“没有必要执行”。
  • 在重写的 finalize() 方法中,只要重新与引用链上的任何一个对象建立关联即可,如下:
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    
    public void isAlive() {
        System.out.println("yes, i am still alive:)");
    }
    
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }
    
    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();
        
        // 对象第一次成功自我拯救
        SAVE_HOOK = null;
        System.gc();
        // 由于 finalize() 方法优先级很低,所以暂停 0.5秒等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
        
        // 对象第二次自我拯救 失败!
        SAVE_HOOK = null;
        System.gc();
        // 由于 finalize() 方法优先级很低,所以暂停 0.5秒等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
    }
}
运行结果:
finalize method executed!
yes, i am still alive:)
no, i am dead :(

执行结果一次成功自救,一次失败,这是因为任何一个对象的 finalize() 方法都只会被系统自动调用一次,如果对象面临下一次回收,它的 finalize() 方法不会再次执行。

二、如何回收垃圾

  • 当标记完所有的存活对象是,便可以对死亡的对象进行回收了

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

  • 算法思想:把死亡对象所占据的内存标记为空闲内存,并记录在空闲列表(free list)之中。当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,并划分给新建的对象。


    Sweep
  • 缺点:
    1. 造成内存碎片。由于JVM 的堆中对象必须是连续分布的,因此可能出现总空闲内存足够,但是无法分配的极端情况。
    2. 分配效率较低。如果是一块连续的内存空间,那么可以通过指针加法(pointer bumping)来做分配。而对于空闲列表,JVM 则需要逐个访问列表的项,来查找能够放入新建对象的空闲内存。

2. 压缩(compact)也叫标记 - 整理(Mark-Compact)

  • 算法思想:把存活的对象聚集到内存区域的起始位置,从而留下一段连续的内存空间。


    Compact
  • 优缺点:能够解决内存碎片化的问题,代价就是压缩算法的性能开销。

3. 复制(copy)

  • 算法思想: 把内存区域分为两等分,分别用两个指针 from 和 to 来 维护,并且只是用 from 指针指向的内存区域来分配内存。当发生垃圾回收时,便把存活的对象复制到to 指针指向的内存区域中,并且交换 from 指针 和 to 指针的内容。


    Copy
  • 优缺点: 能够解决内存碎片化的问题,但堆空间的使用效率极其低下。

4. 分代收集(Generational Collection)

  • 算法思想:根据对象存活周期的不同将内存划分为几块。一般为新生代和 老年代,便可根据各个年代的特点采用最适合的收集算法。在新生代中,每次垃圾收集时都会有大批对象死去,只有少量存活,便采用复制算法,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,则使用 “标记 - 清除” 或者 “标记 -整理” 算法。

5. JVM 的堆划分

  • 前面提到了新生代 和 老年代,这是 JVM 对堆的划分,其中新生代又被划分为 Eden 区,以及两个大小相同的 Survivor 区。
  • 默认情况下,JVM采取的是一种动态分配的策略(对应JVM参数 -XX:UsePSAdaptiveSurvivorSizePolicy),根据生成对象的速率,以及 Survivor 区使用情况动态调整 Eden区 和 Survivor 却的比例。
  • 当然,也可以通过参数 -XX:SurvivorRatio 来固定这个比例。不过Survivor区会一直为空,因此比例越低浪费的堆空间将越高。


    堆空间
  • 当 调用new 指令时,它会在 Eden 区中划出一块作为存储对象的内存。由于堆空间是线程共享的,因此直接在这里划空间是需要进行同步的。不然可能出现两个对象公用一段内存的事故。
  • JVM 的解决方法就是:每个线程都可以向JVM 申请一段连续的内存,作为线程私有的TLAB(线程私有缓冲区 Thread Local Allocation Buffer,对应虚拟机参数 -XX:+UseTLAB,默认开启)。这个操作需要加锁,线程需要维护两个指针(可能更多,主要的就两个),一个指向TLAB 中空余内存的起始位置,一个则指向 TLAB 末尾。
  • 接下来的new 指令,便可以直接通过指针加法(bump the pointer)来实现,即把指向空余内存位置的指针加上所请求的字节数。如果加法后空余内存指针的值仍小于或等于指向末尾的指针,则代表分配成功。否则,TLAB 已经没有足够的空间来满足本次新建操作。此时,便需要当前线程重新申请新的 TLAB。
  • 如果 Eden 区的空间耗尽,此时JVM 会触发一次 Minor GC,来收集新生代的垃圾。存活下来的对象,则会被送到 Survivor 区。新生代有两个 Survivor 区,我们分别用 from 和 to来指代。其中 to 指向 Survivor 区是空的。
  • 当发生 Minor GC时,Eden 区和 from指向的 Survivor 区中的存活对象会被复制到 to指向的 Survivor 区中,然后交换 from 和 to 指针,以保证下一次 Minor GC时,to 指向的Survivor 区还是空的。
  • 新生代和老年代的划分: JVM会记录 Survivor 区中的对象一共被来回复制了几次。如果一个对象被复制的次数为 15(对应虚拟机参数 -XX:+MaxTenuringThreshold),那么该对象将被晋升(promote)至老年代。另外,如果 单个 Survivor 区已经被占用了 50%(对应虚拟机参数 -XX:TargetSurvivorRatio),那么较高复制次数的对象也会被晋升至老年代。
    注:为什么是15 而不是其他?原因是 HotSpot会在对象头中的标记字段里记录年龄,分配到的空间只有4位,因此只能记录到15
  • 优缺点:Minor GC 是不用对整个堆进行垃圾回收,此时有一个问题就是老年代的对象可能引用新生代的对象。也就是说,在标记存活对象的时候,我们需要扫描老年代中的对象。如果该对象拥有对新生代对象的引用,你们这个引用也会被作为 GC Roots。如此,岂不是又做了一次全堆扫描?

6. 卡表(Card Table)

HotSpot为了解决Minor GC的时候不用进行全堆扫描而提供的方案

  • 原理:该技术将整个堆划分为一个个大小为512 字节的卡,并维护一个卡表,用来存储每张卡的一个标识位。这个标识位代表对应的卡是否可能存有指向新生代对象的引用。如果存在,你们这张卡就是脏的。
  • 在进行 Minor GC的时候,便可不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到 Minor GC 的 GC Roots中。当完成所有脏卡的扫描后,JVM便可将所有的脏卡的标识位清零。

三、 垃圾回收器

  • 针对新生代的垃圾回收器有三个:Serial、Parallel Scavenge 和 Parallel New。都是采用标记 - 复制算法。其中,Serial 是单线程的,Parallel New 可以看成 Serial 的多线程版本。Parallel Scavenge 和 Parallel New 类似,但更加注重吞吐量(Throughput),吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。此外,Parallel Scavenge 不能与 CMS 一起使用。
  • 针对老年代的垃圾回收器也有三个:Serial Old 和 Parallel Old,以及 CMS。Serial Old 和 Paralled Old 都是标记 - 压缩算法。前者为单线程的,后者可以看成前者的多线程版本。
  • CMS 采用的是标记 - 清除算法,并发收集、低停顿。但是对CPU敏感、而且会产生空间碎片。
  • G1(Garbage First)是一个横跨新生代和老年代的垃圾回收器。它打乱了前面所说的堆结构,直接将堆分为极多个区域。每个区域都可以充当 Eden 区、Survivor 区或者老年代中的一个。采用的是“标记 - 压缩”算法,而且和 CMS 一样都能在应用程序运行过程中并发的进行垃圾回收。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容

  • 0. 前言 JVM笔记系列,以JDK1.7为基准,主要以《深入理解Java虚拟机》(第二版)和《Java虚拟机规范...
    郭寻抚阅读 893评论 0 3
  • GC区域Eden Survivor(from,to), Old Gen和Perm Gen VM区域总体分两类,he...
    Fitz_Lee阅读 409评论 0 0
  • 注意 : 本系列文章为学习系列,部分内容会取自相关书籍或者网络资源,在文章中间和末尾处会有标注 垃圾回收的意义 它...
    lyk2112阅读 327评论 0 0
  • 我今天用那道很多机构都用的那五条数学题给笑妈聊人性。 不知道各位看过没,就是: 1+1=2 2+1=3 2+2=5...
    羽翼成阅读 500评论 0 50
  • 我是日记星球357号星宝宝清远仙人掌,我正在参加日记星球第13期21天蜕变之旅,这是我的第2篇原创日记~ 关注日记...
    清远仙人掌阅读 1,060评论 0 5