java垃圾回收相关梳理

学习了jvm的垃圾回收相关的知识,感觉很有意思,做一些记录,争取逻辑清晰,简单有趣。目前的记录不涉及具体使用,需要再次深入后再做补充吧。推荐看看这本书:深入理解Java虚拟机

java给人的美好可能很大程度来源于无需过度关注内存管理,专心业务逻辑就好了。但这对于像我这样一开始就使用java的人来说,大概就是忘记了还需要做内存管理吧!想想还是让人愧疚,所以认真学习一下。基于目前的知识深度撰写,后面再版。

行文逻辑

大概可以清楚的是我们可以通过jvm的启动参数,对采用哪种垃圾回收器进行配置,那就从这里开始吧。

  • 垃圾回收器

    是垃圾回收算法的具体实现,由于算法不同,针对的问题和场景不同就会有很多种垃圾回收器,选择哪种更好呢? 这时候就需要有相应的指标对垃圾回收器进行评价了。

  • 垃圾回收器的分类标准
  • 垃圾回收算法

    算法是回收器的核心,我们可以看到算法是不断优化的,对不同的场景有不同的解决方案。

  • 定义垃圾

    这可能也是一个问题了,实际上也是通过一些算法进行判断的,当jvm认为某个对象没有被引用后,就算做垃圾对象了,等待被回收吧。

  • 定义引用

    上一点中你可能会好奇,那jvm凭什么判断某个对象有没有被引用呢?

这是自上而下的介绍,下面我会自下而上的进行解释,前后都有逻辑关系。

java对象的引用

这个算是理解基础了,java对象的引用有四种类型,但遗憾的是基本上没有好好利用这些特性。

  • 强引用 (Strong reference)。 几乎正常手段得到的对象赋值都是强引用了,比如:String str = “Strong”; 这种引用在任何时候只要存在,对象就不会被回收,即使jvm被迫OOM
  • 软引用 (Soft reference)。 需要用特殊的类SoftReference实现,它的特点是,内存充足时不受影响,若要发生OOM的时候会被清理掉。想想它的应用场景,大概可以做数据缓存一类的~
  • 弱引用 (Weak reference)。 同样需要特殊类WeakReference包装,它就更惨了,每一次垃圾回收器工作都会将该对象收走。在gc频繁的情况下,我想不到它有什么价值,存活的太短了。或许用来从程序内部判断gc的频率,做性能分析?
  • 虚引用 (Phantom reference)。 用PhantomReference类实现,对实例的生存时间不构成任何影响,也无法取得实例。作用是在对象被回收时获取一个系统通知(对不起,你凉了~) ,可以完成一些对象的清理工作(替代没啥用的finalize??)PhantomReference的构造函数有两个参数,一个是引用,另一个是引用队列,引用的对象要被gc时,就被加入到该队列,并不立即回收,从而便于做最后的清理工作。具体的可以看看这里: Java幽灵引用的作用

我了解完的感受就是除了强引用,其他的应用场景很有限,不过了解有利于jvm性能调优并解决特殊场景的引用,可以看这篇文章加深了解 Java reference的种类及使用场景

java对象: 你凭什么说我是个垃圾?!!

区分清楚引用关系后,除了虚引用,其他情况基本都很好处理了,只要jvm认为对象不存在其他引用关系,没有别的对象需要他的时候,恭喜你,垃圾身份坐实。那么,就有个问题了,jvm咋知道没有别的对象需要它呢~ 这里就引入了关于引用计数的判定算法。

  • 引用计数算法

    堆中的每个对象自身有一个引用计数器,有谁动我的地址,我就记他一笔,不用了就减一【<– 我对象可是个记仇的小妖精呢~】 后续算法判定是否为垃圾的时候,直接看这个计数器就好了。 所以优点很明显嘛,简单高效。但略做思考就能发现哪里不对,假设存在循环引用,两个垃圾抱团,在属性里相互引用,这样就能逃过被gc(垃圾回收)的厄运了。对于开发人员来说,莫名奇妙就内存泄漏了。wtf!两个垃圾抱团还是垃圾,关键是这个世界需不需要你们,不是你们需不需要就可以决定的~ 因此出现了一个升级算法

  • GC Roots 【亦称 可达性分析算法】

    GC Roots就是为了解决上述问题出现的产物,相当于jvm认可的老大,只要老大不用的对象统统都是垃圾!所以判断的过程就是遍历GC Roots,它引用过的就不被清理,当一个对象与GC Roots不可达时,就等待被gc吧!这样看这个算法就完美了么? 并不是的,仔细想想,这样对于jvm来说,要做的工作就变多了。比如,要是遍历的过程中引用关系发生了变化怎么办?所以需要打个快照,然后中场休息进行清场。当然又会产生一系列的问题,可以看看这里:虚拟机的算法实现

那如何选择呢?对于jvm的设计标准,当然是不允许任何可避免的内存泄漏发生的,所以jdk1.2以后就不再使用引用计数算法了。是不是引用计数就一无是处了呢?当然是要物尽其用的,要是有些场景本身就不会产生”垃圾抱团”的情景,那就完美了啊,我查了一些,看到这个应用场景:对象引用计数器。 可以引发思考的是,在某些缓存对象的处理上可以使用。

另一个问题,GC Roots 就像公认的贵族,那谁可以成为决定生死的贵族呢?

1.虚拟机栈
2.方法区的静态属性
3.方法区的常量引用
4.本地方法栈(JNI)引用
更完整的看这里GC Roots

小妖精,跟我归于00吧~

最重要的gc算法登场啦!现在内存区域里鱼龙混杂,想象一下,内存区域就是一个舞台,对象表演完了就成了垃圾,作为jvm钦点的垃圾分拣员,如何高效的把垃圾分拣出来呢?我们依次来认识下这些清理大佬。

  • 标记-清除算法 (Mark-Sweep)

    人如其名,先遍历一次,把垃圾标记出来,然后大喊:收!【<– 当然还是需要一个个去捡的,只是速度太快~】场上就干净了。其他对象也不受影响。 想想这个过程,会不会存在某些问题呢? 首先,标记和清除分开了,需要遍历两次,效率肯定就低下了。另一方面,场上被清理了,只是垃圾占用的位置被空出来了。要是有个胖一点的对象登场,那不就没位置了么?【<– 产生大量空间碎片,没有连续内存,可能引发下一次gc】

  • 复制算法 (Copying)

    为了解决上面的问题,提出了复制算法,大概就是把场地一分为二,每次只使用其中一个,当触发gc时,就翻牌子,把贵族侍宠的对象复制到另一场地,然后直接清场。这个思想简直棒啊!解决了杨贵妃没地方登场的尴尬问题。不过再想想,还是存在问题。第一,场地一分为二了,那实际上可用空间就变小了。当然,实际使用的时候适用于垃圾产生比例高的情景,这样可能就是二八分或者更高的比例了。另一方面,对于垃圾产生比例少的时候,每次清场前都要复制大量还存活的对象,效率降低了很多。所以空间和时间都存在效率问题。

  • 标记-压缩算法 (Mark-Compact)

    从名字上看就和第一种类似,他在标记的基础上,弥补了复制算法的不足。即针对存活对象较多的场景,我们不复制了,也不分区域。像标记-清除算法那样,标记一下,大喊一声,收!这时候还需要加一个操作,请一个巨型胖子上台,把其他对象全部挤到一边去,这样空间也连续了,中和了第一种和第二种算法的优势。【<– 当然,巨型胖子是不存在的,但确实有压缩空间的魔力~】

  • 增量算法 (Incremental Collecting)

    到这里大家可能觉得,哇,有三种算法可以用了,大部分场景都能找到解决方案了,不错哟~ 但工程师是从不止步的。以上算法其实都存在一个问题,就是他们工作的时候,场上是需要停止活动的,这样才能保证不会有误操作。也就是说清理是一个连续的一致性行为,要将jvm世界的时间暂停后再清理。这样程序就会莫名奇妙的顿一下,虽然时间很短,但在强交互式的情景或者频繁gc的时候体验就不太好了。考虑到这里,增量算法将内存分区进行回收,这样能有效减少系统的停顿时间了。问题是暂停jvm的时间,也是需要消耗资源的,即线程切换和上下文转换的消耗会使得垃圾回收的整体成本上升。

  • 分代 (Generational Collecting)

    增量算法从整体的角度提出了解决方案。但实际上优秀的复制算法和标记-压缩算法可以说刚刚好优劣互补,如何能扩大这两者的优势,减少不足呢? 很简单了,一个适用于对象死的早的情景,另一个适用于对象活得久的情境。那我们把舞台分一分,一边死的快,一边活得久,然后两个算法同时上岗,岂不妙哉?!!这就是分代的思想。实际上我们使用的java jvm就是采用了这样的思想,把堆内存分成了新生代、老年代、永久代【<– jdk1.8取消了永久代】,不同区域使用不同算法。详情可以看这里:Java虚拟机:JVM内存分代策略

垃圾回收器战斗力指标~

算法部分就是这些了,可以看到不同的算法有不同的应用场景,之前也提到,垃圾回收器就是对算法的应用,因而实际的垃圾回收器就采用了多种不同的算法进行组合,也正因为组合不同也使得我们对于垃圾回收器的选择也有不同。那么如何选择呢?我们需要对回收器进行标准分类,就像游戏里的战斗力评分一样~

  • 吞吐量:指在应用程序的生命周期内,应用程序所花费的时间和系统总运行时间的比值。系统总运行时间=应用程序耗时+GC 耗时。如果系统运行了 100min,GC 耗时 1min,那么系统的吞吐量就是 (100-1)/100=99%。
  • 垃圾回收器负载:和吞吐量相反,垃圾回收器负载指来记回收器耗时与系统运行总时间的比值。
  • 停顿时间:指垃圾回收器正在运行时,应用程序的暂停时间。对于独占回收器而言,停顿时间可能会比较长。使用并发的回收器时,由于垃圾回收器和应用程序交替运行,程序的停顿时间会变短,但是,由于其效率很可能不如独占垃圾回收器,故系统的吞吐量可能会较低。
  • 垃圾回收频率:指垃圾回收器多长时间会运行一次。一般来说,对于固定的应用而言,垃圾回收器的频率应该是越低越好。通常增大堆空间可以有效降低垃圾回收发生的频率,但是可能会增加回收产生的停顿时间。
  • 反应时间:指当一个对象被称为垃圾后多长时间内,它所占据的内存空间会被释放。
  • 堆分配:不同的垃圾回收器对堆内存的分配方式可能是不同的。一个良好的垃圾收集器应该有一个合理的堆内存区间划分。

常用的垃圾回收器

这部分介绍常用的七种垃圾回收器。但我发现有人写的非常完整了,那就不献丑了。推荐看这里

看完两篇大牛的博客应该都能明白了

参考文章以及扩展深入

Fork me on GitHub