一篇文章彻底了解Java垃圾收集(GC)机制

垃圾收集(Garbage Collection ,GC),是一个长久以来就被思考的问题,当考虑GC的时候,我们必须思考3件事情:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

那么在Java中,我们要怎么来考虑GC呢?首先回想以下内存区域的划分,其中程序计数器、本地方法栈、虚拟机栈三个区域随线程而生,随线程释放,栈中的栈帧随着方法的进入和退出执行着出栈和入栈的操作,每一个栈帧分配多少内存基本是在类结构确定时就已经固定的(可能会进行一些优化,但是大体上已知),因此这几个区域就不需要考虑回收的问题,因为方法结束或者线程结束时,内存自然都被回收。不需要额外的GC算法等。

然而Java堆和方法区则不一样,一个接口所对应的多个实现类所需要的内存可能不一样,一个方法中的多个分支所需要的内存也可能不一样,我们只有在程序处于运行期间才能知道程序需要创建那些对象,这部分的内存的分配和回收是动态的,因此,垃圾收集器关注的是这方面的内存。

一. 如何确定对象可以回收

1.引用计数算法

最容易想到与理解的算法,即对于每一个对象,每当该对象被引用时,计数器值就+1,引用失效时,计数器就-1。因此,当对象的引用计数为0时,即为不可再被使用的。该算法也在一些领域被使用来进行内存管理,但是JAVA虚拟机中并没有选用该算法。主要是因为不能很好的解决循环引用的问题。

举个简单的例子来说明循环引用:

class Container{    public Object obj ;
}public class ReferTest {    public static void main(String[] args){
        Container c1 =new Container();
        Container c2 =new Container();
        c1.obj = c2 ;
        c2.obj = c1 ;
        
        c1 = null ;
        c2 = null ;        //此时c1 c1会被判定为死亡对象么?    }
}

事实上会被判定为死亡对象,因为JAVA虚拟机不是采用引用计数来进行判断的,因此如果发生垃圾回收,c1,c2 都会被回收内存。

2.可达性分析

Java、C#的主流实现都是采用该种方式,来判断对象是否存活。

这个算法的基本思路就是一系列“GC Roots”作为起始点,从这些节点向下搜索,搜索到的所有引用链中的对象都是可达的,其余的对象都是不可达的,如上例,即使c1,c2互相引用,但是c1,c2都不属于GC Roots对象,因此都不可达。

Java中,以下几种对象可以作为GC Roots:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 本地方法栈JNI方法引用的对象。
  • 方法区类的静态属性引用的对象。
  • 方法区常量引用的对象。
3.引用的分类

了解了GC Roots之后,我们可能会希望存在这么一种对象,内存够的时候不进行回收,当需要内存时再将其回收。JDK 1.2 中对引用进行了扩充。将引用分为了4种,从强到弱依次为;

强引用(Strong Reference)

我们一般情况下使用的都是强引用,如Object o = new Object(),之类的代码。只要强引用还在,垃圾收集器就永远不会回收被引用的对象。

软引用(Soft Reference)

SoftReference类来实现,用来描述一些还有用但是不必须的对象,在系统如果不回收就会发生OOM时才会对软引用进行内存回收。

弱引用(Weak Reference)

WeakReference类来实现,描述非必需的对象,强度弱,只能活到下一次发生垃圾回收前,无论那时内存是否短缺,都会对软引用对象进行内存回收

虚引用(Phantom Reference)

PhantomReference类实现,不会对生存时间发生任何影响,唯一目的时能在这个对象被收集器回收时得到一个通知。

4.其他

及其不建议使用finalize()方法,虽然可以在回收时被调用,但是finalize()方法的执行代价高昂,不确定性大,无法保证各个对象的调用顺序。使用finalize()能做的工作,使用try()finally()或其他方式可以执行的更好。大家可以忘记JAVA中有这个方法的存在。本身就是在JAVA刚诞生时向C/C++程序员做的妥协,但是未得到优化。

方法区(永久代)进行GC的效率极低,花费较大,但是在大量使用反射、动态代理等场景都需要虚拟机具备类卸载的功能,以保证永生代的空间。

二.垃圾收集算法

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

算法分为两个阶段,标记与清除。

标记阶段:标记出所有需要回收的对象。回收阶段:将所有标记区域回收。由于该算法不对空间进行整理,因此会产生大量的内存碎片,内存空间碎片过多会导致在分配较大的对象时,因为没有连续的内存而不得不提前触发一个GC。另外,标记与清除的过程效率都不高。这也是最基础的GC算法。

2.复制算法(Copying)

将内存的总容量分为两块,每次只使用其中的一块,当这一块用完了,触发GC,此时将还存活的对象转移到另一块内存中,之前使用的那一块内存完全清理掉。这样每次对一个半区进行回收,也不会存在内存碎片,实现简单,运行高效,但是一次只能使用半块内存可能会造成浪费。

在新生代中,绝大部分的对象时“朝生夕死”的,因此,不需要按照1:1来划分空间。而是将内存分为一块较大的Eden区以及两个Survivor区,HotSpot虚拟机中,Eden:Survivor=8:1 ,每次使用一个Eden区以及一个Survivor区,90%的空间,触发GC后,将剩余的对象转移到未使用的Survivor中,然后清理Eden区和用过的Survivor区,空间不够时,会担保分配到老年代。这样一次可以使用90%的内存空间,极大的提高了内存的使用率。因此,新生代一般采用这种算法来回收。

3.标记整理算法(Mark-Compact)

如果回收时空间内的对象存活率较高,那么使用复制算法一次只能使用50%的空间(以应对所有对象都存活的情况),因此老年代采用标记整理算法。先对需要清理的对象进行标记,然后将存活的对象都向一端移动,直接清理掉端边界以外的内存。这种方式也不会留下内存碎片。

标记整理算法没有复制算法快。

三. Java垃圾收集器

(了解即可,需要时可以网上细查)

新生代收集器:Serial收集器、ParNew收集器(Serial的多线程版本)、Parallel Scanvenge收集器(控制吞吐量,提高相应速度)

老年代收集器:Serial Old收集器、Parallel Old收集器、CMS收集器(最短停顿)、G1(新生代、老年代都可回收)

四. 内存的分配与回收

新生代:即复制算法中提到的Eden区以及2个Survivor区。

老年代:新生代存活足够长时间后进入老年代。堆上的另一块区域。

Minor GC:发生在新生代的垃圾收集动作。因为Java对象存活时间一般较短,故Minor GC非常频繁,一般回收速度也较快。

Full GC:发生在老年代的垃圾收集动作,伴随着最少一次的Minor GC,且速度较慢(比Minor GC慢10倍以上)

1.空间的分配

1)对象优先在新生代Eden区分配。当Eden区没有足够空间时,将发动一次Minor GC.

2)较大对象需要连续的空间,如长字符串或数组,如果放在新生代会提前触发GC。故大对象直接进入老年代区域,避免频繁的GC。

3)长期存活的对象进入老年代,每个对象有一个年龄,在对象头Mark Word中记录,刚被创建时年龄为0,当它活过一次Minor GC,并且转移到Survivor中,年龄变为1,此后,在Survivor区中每活过一个Minor GC,年龄就会+1,当年龄达到某个程度(默认为15),就会晋升到老年代。

4)此外,为了适应内存的复杂情况,年龄不一定达到规定值才能进入老年代。当Survivor区的相同年龄所有对象大小大于Survivor区大小的一半时,此年龄就会被作为判定标准,大于等于该年龄的都会进入老年代。

2.空间的回收--GC

这里我用一张图来彻底解释清除:

需要解释的地方有:担保失败,这个的作用在图上已经解释的很清楚了,可以在JVM参数设置。

另外一个地方就是平均大小来作比较,因为有多少对象晋升到老年代是无法知道的,所以只好取之前每一次晋升到老年代的对象的容量的平均值大小来作为经验值,来决定是否进行Full GC来让老年代腾出更多空间。如果仍然失败,那么只能进行一次Full GC。在我个人开来,之所以使用担保,经验值来尽可能的只进行MinorGC,所有的一切,都是为了尽可能不执行Full GC的情况下将需要申请的内存空间搞定

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

推荐阅读更多精彩内容