详解 Jvm GC 机制

JAVA GC(Garbage Collection,垃圾回收)机制,里面包含的知识点很多,面试中也会经常问道。jvm 虚拟机,回收算法,引用,新生代老年代,什么时候回收,那些需要回收,垃圾回收器和 GC 日志等等。
下面我们来一一介绍研究一下,以后去面试就不怕啦。

对象存活还是已经死去

如何判断那些资源需要回收

引用计数

给对象中添加一个引用计数器,每当一个地方引用它时,计数器就加1。当引用失效时就减1。技术器如果是0,就表示对象不再被使用,可以回收了。
但是,引用计数有个缺点:

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();
    }
}

以上是对象之间循环引用,实际上这两个对象都不可能在被访问,但是互相引用对方,导致引用计数器都不是0。
运行之后,通过 GC 日志可以看出来,虚拟机并没有因为这两个对象互相引用就不回收他们,说明虚拟机比不是用技术法来判断对象是否存活。

可达性分析

这个算法基本思路是通过一系列成为 “GC Root” 的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到 GC Root 没有任何引用链相连,则此对象就可以回收了。


image.png

那么那些点可以作为GC Roots呢?一般来说,如下情况的对象可以作为GC Roots:

1.虚拟机栈(栈桢中的本地变量表)中的引用的对象;
2.方法区中的类静态属性引用的对象;
3.方法区中的常量引用的对象;
4.本地方法栈中JNI(Native方法)的引用的对象。

使用OopMap记录并枚举根节点

HotSpot首先需要枚举所有的GC Roots根节点,虚拟机栈的空间不大,遍历一次的时间或许可以接受,但是方法区的空间很可能就有数百兆,遍历一次需要很久。更加关键的是,当我们遍历所有GC Roots根节点时,我们需要暂停所有用户线程(stop the world),因为我们需要一个此时此刻的”虚拟机快照”,如果我们不暂停用户线程,那么虚拟机仍处于运行状态,我们无法确保能够正确遍历所有的根节点。所以此时的时间开销过大更是我们不能接受的。

基于这种情况,HotSpot实现了一种叫做OopMap的数据结构,这种数据结构在类加载完成时把对象内的偏移量是什么类型计算出,并且存放下位置,当需要遍历根结点时访问所有OopMap即可。

用安全点Safepoint约束根节点

如果将每个符合GC Roots条件的对象都存放进入OopMap中,那么OopMap也会变得很大,而且其中很多对象很可能会发生一些变化,这些变化使得维护这个映射表很困难。实际上,HotSpot并没有为每一个对象都创建OopMap,只在特定的位置上创建了这些信息,这些位置称为安全点(Safepoints)。

为了保证虚拟机中安全点的个数不算太多也不是太少,主要决定安全点是否被建立的因素是时间。当进行了耗时的操作时,比如方法调用、循环跳转等时会产生安全点。此外,HotSpot虚拟机在安全点的基础上还增加了安全区域的概念,安全区域是安全点的扩展。在一段安全区域中能够实现安全点不能达成的效果。

引用

无论哪种引用计数还是可达性分析,都跟引用分不开。一下介绍四种引用

1.强引用(StrongReference)
强引用就是指在程序代码之中普遍存在的,比如下面这段代码中的object和str都是强引用:

Object object = new Object();
String str = "hello";

只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。比如下面这段代码:

public class Main {
    public static void main(String[] args) {
        new Main().fun1();
    }
     
    public void fun1() {
        Object object = new Object();
        Object[] objArr = new Object[1000];
    }
}

当运行至Object[] objArr = new Object[1000];这句时,如果内存不足,JVM会抛出OOM错误也不会回收object指向的对象。不过要注意的是,当fun1运行完之后,object和objArr都已经不存在了,所以它们指向的对象都会被JVM回收。

如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。比如Vector类的clear方法中就是通过将引用赋值为null来实现清理工作的.

public synchronized E remove(int index) {
    modCount++;
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    Object oldValue = elementData[index];

    int numMoved = elementCount - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                 numMoved);
    elementData[--elementCount] = null; // Let gc do its work

    return (E)oldValue;
}

2.软引用(SoftReference)

软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。下面是一个使用示例:

import java.lang.ref.SoftReference;
 
public class Main {
    public static void main(String[] args) {
         
        SoftReference<String> sr = new SoftReference<String>(new String("hello"));
        System.out.println(sr.get());
    }
}

3.弱引用(WeakReference)

弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。下面是使用示例:

import java.lang.ref.WeakReference;
 
public class Main {
    public static void main(String[] args) {
     
        WeakReference<String> sr = new WeakReference<String>(new String("hello"));
         
        System.out.println(sr.get());
        System.gc();                //通知JVM的gc进行垃圾回收
        System.out.println(sr.get());
    }
}

第二个输出结果是null,这说明只要JVM进行垃圾回收,被弱引用关联的对象必定会被回收掉。不过要注意的是,这里所说的被弱引用关联的对象是指只有弱引用与之关联,如果存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象(软引用也是如此)。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。

4.虚引用(PhantomReference)

虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class Main {
    public static void main(String[] args) {
        ReferenceQueue<String> queue = new ReferenceQueue<String>();
        PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
        System.out.println(pr.get());
    }
}

内存分区

内存主要被分为三块:新生代(Youn Generation)、老年代(Old Generation)、持久代(Permanent Generation)。三代的特点不同,造就了他们使用的GC算法不同,新生代适合生命周期较短,快速创建和销毁的对象,旧生代适合生命周期较长的对象,持久代在Sun Hotpot虚拟机中就是指方法区(有些JVM根本就没有持久代这一说法)。

image.png

新生代(Youn Generation):大致分为Eden区和Survivor区(8:1),Survivor区又分为大小相同的两部分:FromSpace和ToSpace。新建的对象都是从新生代分配内存,Eden区不足的时候,会把存活的对象转移到Survivor区。当新生代进行垃圾回收时会出发Minor GC(也称作Youn GC)。

旧生代(Old Generation):旧生代用于存放新生代多次回收依然存活的对象,如缓存对象。当旧生代满了的时候就需要对旧生代进行回收,旧生代的垃圾回收称作Major GC(也称作Full GC)。

持久代(Permanent Generation):在Sun 的JVM中就是方法区的意思,尽管大多数JVM没有这一代。

GC 过程

1. Minor GC

(1) Minor GC过程
假设现在Heap内存大小为20M,其中年轻代为10M,老年代为10M,年轻代中Eden区6M,From区2M,To区2M,新创建的对象首先往Eden区分配,当再次分配一个对象,假设大小为1M,此时Eden区已经没有足够空间来给这个对象分配内存,如图所示:


image.png

这时候触发一次Minor GC,把Eden区的存活对象转移到From区,非存活对象进行清理,然后给新创建的对象分配空间,存入Eden区


image.png

随着分配对象的增多,Eden区的空间又不足了:
image.png

这时候再触发一次Minor GC,清理掉Eden区和S1区的死亡对象,把存活对象转移到S2区,然后再给新对象分配内存:
image.png

From区和To区是相对的关系,哪个区中有对象,哪个区就是From区,比如,再进行一次Minor GC,会把存活对象转移到S1区,再为转移之前,S2区是From区,S1区是To区,转移后,S2区中没有存活对象,变为To区,而S1区变为From区:


image.png

2. 对象进入老年代的4种情况

(1) 假如进行Minor GC时发现,存活的对象在ToSpace区中存不下,那么把存活的对象存入老年代


image.png

(2) 大对象直接进入老年代
假设新创建的对象很大,比如为5M(这个值可以通过PretenureSizeThreshold这个参数进行设置,默认3M),那么即使Eden区有足够的空间来存放,也不会存放在Eden区,而是直接存入老年代


image.png

(3) 长期存活的对象将进入老年代
此外,如果对象在Eden出生并且经过1次Minor GC后仍然存活,并且能被To区容纳,那么将被移动到To区,并且把对象的年龄设置为1,对象没"熬过"一次Minor GC(没有被回收,也没有因为To区没有空间而被移动到老年代中),年龄就增加一岁,当它的年龄增加到一定程度(默认15岁,配置参数-XX:MaxTenuringThreshold),就会被晋升到老年代中

(4) 动态对象年龄判定
还有一种情况,如果在From空间中,相同年龄所有对象的大小总和大于From和To空间总和的一半,那么年龄大于等于该年龄的对象就会被移动到老年代,而不用等到15岁(默认):


image.png

3. Full GC

如果某个(些)对象(原来在内存中存活的对象或者新创建的对象)由于以上原因需要被移动到老年代中,而老年代中没有足够空间容纳这个(些)对象,那么会触发一次Full GC,Full GC会对整个Heap进行一次GC,如果Full GC后还有无法给新创建的对象分配内存,或者无法移动那些需要进入老年代中的对象,那么JVM抛出OutOfMemoryError

垃圾收集算法和垃圾收集器

内容较多,篇幅有限不赘述了。建议看这片博文,很详细。注意不光要记住每种算法和不同的收集器,还要几种在新生代老年代分别用的是什么。
http://www.importnew.com/23035.html

参考文献

https://www.jianshu.com/p/314272e6d35b
http://www.importnew.com/23035.html
https://www.cnblogs.com/dolphin0520/p/3784171.html
https://blog.csdn.net/anjoyandroid/article/details/78609971

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

推荐阅读更多精彩内容

  • JVM架构 当一个程序启动之前,它的class会被类装载器装入方法区(Permanent区),执行引擎读取方法区的...
    cocohaifang阅读 1,644评论 0 7
  • JVM架构图分析 JVM被分为三个主要的子系统 (1)类加载器子系统(2)运行时数据区(3)执行引擎 1. 类加载...
    匆匆岁月阅读 1,013评论 1 19
  • 介绍JVM中7个区域,然后把每个区域可能造成内存的溢出的情况说明 程序计数器:看做当前线程所执行的字节码行号指示器...
    jemmm阅读 2,222评论 0 9
  • JVM和GC,是Java的底层虚拟机和垃圾回收器,理解JVM和GC,对于我们理解Java很有帮助。在面试的过程中,...
    步积阅读 1,496评论 0 15
  • JVM笔记 JDK:Java、JVM、Java API类库,是支持java程序开发的最小环境。JRE:Java A...
    一条小袍袍YoY阅读 870评论 0 4