Java GC(Garbage Collection,垃圾收集)

这次来说一下关于Java垃圾收集的一些思想和算法。

垃圾收集的触发条件

1)因为GC线程的优先级较低,所以当虚拟机空闲时,即没有线程工作时,会触发GC。
2)Java堆空间不足以给新对象分配内存时。

引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值减1;任何时刻计数器值为0的对象就是不可能再被使用的。

这种算法原理很好理解,逻辑上看也没毛病。

但是,Java语言中没有选用引用计数算法来管理内存,最主要的原因是它很难解决对象之间的相互循环引用的问题。

我想到的最简单明了的例子就是回调,被调用的对象和回调对象之间互相持有对方的引用,形成一个回环,即使其他地方都无法访问(不持有引用)这两个对象,他们的引用计数也不会变成0,使用引用计数算法就无法回收它们。

那应该使用什么算法判定对象是否存活?

根搜索算法

这个算法的基本思路就是通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

看下面这张图就很好理解了
根搜索算法

蓝底表示仍然存活的对象,白底表示判定可回收的对象。

在Java语言里,可作为GC Roots 的对象包括下面几种
1、虚拟机栈(栈帧中的本地变量表)中的引用的对象。
2、方法区中的类静态属性引用的对象。
3、方法区中的常量引用的对象。
4、本地方法栈中JNI(Native方法)的引用的对象。

要真正回收一个对象,至少要经历两次标记过程:如果在上面的根搜索后发现没有与GC Roots引用链相连,那么它会被第一次标记并进行筛选,筛选该对象是否有必要执行finalize()方法。如果对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过了(任何一个对象的finalize()方法只会被系统自动调用一次),就视为“没必要执行”;如果有必要执行,则把它放置在一个名为F-Queue的队列之中,这里的执行仅指虚拟机会触发这个方法,但并不承诺会等待它运行结束。

不过大多数情况下我们不需要关心finalize()方法,如果有需要显示回收的资源,可以使用try-finally或其他方式完成。

引用的分类

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,引用的强度依次逐渐减弱。

强引用是指类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象。
软引用用来描述一些还有用但非必需的对象,在系统将要发生内存溢出异常时,将会把这些对象列入回收范围并进行第二次回收。
弱引用也是描述非必需对象对的,但是被弱引用关联的对象只能生存到下一次垃圾收集发生之前(不安全)。
虚引用,无法通过虚引用来取得一个对象实例,为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被回收时收到一个系统通知。

垃圾收集算法

以下算法笔者没有找到合适的示意图(毕竟是其他大牛辛辛苦苦画的),如果小伙伴想象不出来的话可以直接百度对应算法的示意图。

标记-清除算法

这是最基础的收集算法。分为“标记”和“清除”两个阶段,标记的操作就如上面所提到的。这种算法的主要缺点有两个:一个是效率问题,标记和清除过程的效率不高;另外一个是空间问题,标记清除后会产生大量不连续的内存碎片。

类似旧XP时代有试过在我的电脑里整理磁盘空间时看到的那样,磁盘的实际存储状况是断断续续的,只不过内存本来就小得多,这样零碎的内存可用空间可能导致程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存进而触发另一次垃圾收集。

复制算法

这种算法的思想是把内存分为等大的两块,每次只使用其中一块,当这块内存(暂称为使用中的区域)用完时,就把其中还存活的对象复制到另一块内存中(暂称为保留区域,复制过去之后在保留区域中是连续的),然后把这块使用中的区域内存完全清理掉,接着保留区域就变成使用中的区域,被清理的使用区域变成了保留区域(两者角色互换)。
这种方法不用考虑内存碎片等复杂情况,因为复制过去以后就是连续的,只要移动堆顶指针按顺序继续分配内存即可。只是这种算法的代价是将内存缩小为原来的一半,太高昂。

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM的专门研究表明,新生代中的对象98%是朝生夕死,并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间(上一篇提到过,是指新生代内存的划分),每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性地拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。
如果另一块Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。

标记-整理算法

这是根据老年代的特点(存活率高、没有额外空间对它进行分配担保)提出的算法,过程和“标记-清除”算法类似,只不过不是标记可回收对象直接回收,而是让存活的对象向一端移动,然后直接清理掉端边界以外的内存(也就是把对象往前移,挤在一起后把后面的内存空间直接清空一次)。

分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法。

其实前面说的都是围绕分代来解决垃圾收集的问题。总结来说就是在新生代中,每次垃圾收集只有少量对象存活,选用复制算法。而在老年代中因为对象的存活比较稳定,所以选用“标记-清除”或“标记-整理”算法来进行回收。

内存分配和回收策略

先说明一下:
新生代GC(MinorGC)指发生在新生代的垃圾收集动作;
老年代GC(FullGC)指发生在老年代的GC,速度一般会比MinorGC慢10倍以上。

大多数情况下,对象在新生代Eden区(存在于Java堆中)中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次MinorGC。

为了避免经常出现大对象而使垃圾收集提前触发(指Eden空间其实还有很多空间但是不足以给这种大对象分配内存),在虚拟机中可以设置一个参数令大于这个设置值的对象直接在老年代中分配。

为了识别对象的“年龄”,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden“出生”并经过一次MinorGC后仍然存活并被转移到Survivor空间中,就将该对象的年龄设为1,此后该对象每次经历MinorGC仍然留在Survivor空间中的话,年龄就加1,当它的年龄增长到老年代的年龄阈值(可以通过参数设置)时,就会被转移到老年代。

当然对象不一定非要达到年龄阈值才能转移到老年代,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,就可以把年龄大于等于该年龄的对象转移到老年代。

刚才提到了一个分配担保机制,因为这个机制的存在,虚拟机需要确定老年代是否可以担保下一次GC并决定是否对老年代进行GC(即FullGC)。

在发生MinorGC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次FullGC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败:如果允许,那只会进行MinorGC;如果不允许,则改为进行一次FullGC。

显然这是一种通过经验预测的手段,能有效避免频繁发起FullGC(耗时长),还是有可能出现担保失败的。如果出现担保失败,就只能在失败后再发起一次FullGC咯。

垃圾收集器

垃圾收集器是内存回收的具体实现。新生代和老年代都有多种垃圾收集器(不同厂商、不同版本的虚拟机提供的垃圾收集器会有很大差别),并且一些垃圾处理器可以搭配组合使用。各种垃圾收集器详细的参数信息笔者也没有去了解,有兴趣的小伙伴可以去查找最新的一些垃圾收集器的资料。

小伙伴们若是有指正或者建议欢迎在下方留言。
参考:
1、《深入理解Java虚拟机》

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