一般,咱们在写java程序的时候,彷佛不多关注内存分配和垃圾回收的问题。由于,这部分工做,JVM已经帮咱们自动实现了。java
这样看起来,好像很美好,可是任何事情都有两面性。虽然JVM会自动的进行垃圾回收,可是,若是遇到有些问题,JVM本身也处理不了呢?算法
所以,咱们须要了解一下JVM垃圾回收是怎样运做的,这样才能在遇到问题的时候,有的放矢。因此,今天就来聊一聊JVM的垃圾回收吧。数组
首先,思考一下,为何须要进行垃圾回收?安全
咱们知道,在建立对象的时候,Java会把对象的内容放到堆中。随着时间的推移,堆中的对象确定会愈来愈多,可是,堆的大小是有限制的。若是,咱们不进行垃圾回收,也就是把无用的对象进行清除和回收,那么JVM将不堪重负,最终致使内存泄漏。微信
既然咱们须要进行垃圾回收,那么,首先得知道什么是垃圾。spa
在垃圾收集器对堆内存进行回收前,会先判断哪些对象还在“存活”,哪些对象已经“死去”(即不可能再被任何途径使用的对象),这些“死去”的对象,就是咱们须要进行回收的垃圾。.net
那么,经过什么方式去断定是否为垃圾呢?(即断定对象是否存活)3d
引用计数算法(已淘汰)对象
引用计数算法,是指给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1,当引用失效时,计数器的值就减1。当计数器值为0时,该对象就会被回收。blog
能够说,引用计数算法的实现很是简单,断定效率也很高。可是,咱们忽略了一个问题,在Java中,对象之间是能够互相循环引用的。若是,两个对象之间互相循环引用,那么就会致使,它们之间的引用计数都不为0(都在等待对方释放资源),所以,就没法通知垃圾收集器回收它们。
可达性分析算法
这个算法的思想就是,经过一系列被称为“GC Roots”的对象做为起点,而后向下搜索,所走过的路径被称为引用链。当一个对象到 GC Roots之间没有任何引用链时(即从GC Roots到该对象不可达),则证实该对象是不可用的。

这个算法解决了循环引用的问题,只要对象没法与GC Root之间创建直接或间接的链接,就会断定为可回收对象。
那么,什么对象能够做为GC Root呢?通常分为如下四种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中引用的对象。
既然已经肯定了哪些垃圾能够被回收,那么就须要垃圾收集器进行垃圾回收了,咱们来了解一下几种比较常见的的垃圾收集算法。
标记清除算法

是最基础的一种收集算法,分为标记和清除两个阶段。首先,把须要回收的对象标记出来,而后再把他们清除掉。如上图所示,全部可回收的对象会变成未使用的一片区域。
标记清除算法逻辑清晰,易于操做。可是,咱们能够看到,未使用的内存区块都不是连续的,所以,此算法会产生不少的内存碎片。这样,当一些较大的对象须要分配空间的时候,就找不到足够的连续内存来存储,所以会提早触发GC,同时也浪费了不少的内存空间(内存空间过小,致使不可用)。
复制算法

复制算法,是指把内存区域划分为大小相等的两块区域。每次只使用其中的一块,当这一块内存用完了,就把全部存活的对象复制到另外一块上面,最后再把已使用过的内存空间一次清理掉。
这样,就能够保证内存区域的连续性,不会产生内存碎片,实现简单,运行高效。可是,这样的话只有使用原来一半的内存,代价也过高了。
标记整理算法

标记整理算法,标记过程和标记清除算法同样,可是后续不是进行清除,而是先整理,让全部存活的对象都向一端移动,而后再清除另外一端的内存区域。
标记整理算法解决了标记清除算法产生内存碎片的问题,同时也解决了复制算法只能利用一半内存的问题,看似是很是的完美。可是,它却产生了另一个问题。能够看到图中,内存的变更很是频繁,每次整理都有不少存活的对象内存地址发生改变。所以,它的效率会慢不少。
因此,如今通常用分代收集算法。在Java堆中,分为新生代和老年代,能够根据各个代的特色,选择最合适的收集算法。新生代中,每次垃圾收集都有大批对象死去,只有少许对象存活,就能够选择复制算法,只须要付出少许存活对象的复制成本便可。而老年代中,对象存活率高,没有额外空间对它进行分配担保,所以使用标记清除或者标记整理算法。
堆内存模型
Java堆是内存管理中最大的一块区域,也是垃圾回收的重点区域。堆分为新生代、老年代和永久代,新生代又分为Eden区和Survivor区,Survivor又分为S0和S1区。在JDK1.8以后把永久代移除了,而用元空间代替。(永久代使用的是堆内存,而元空间直接使用本机物理内存)

新生代中的对象98%都是朝生夕死的,所以把新生代分为较大的一块Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor(此处Survivor区也叫From区,另外一块空的未使用的空间叫To区,From和To区是会交换的,保证空的老是To区)。
当Eden区没有足够的空间分配时,会进行一次Minor GC,Eden区大部分对象都被回收,而Eden区和From区存活的对象会放入到To区,而后From和To区进行交换。(若是To区空间不够,直接进入老年代)
如下几种状况会进入老年代。
1) 大对象
大对象就是指须要大量连续内存空间的对象,最典型的就是那种很长的字符串和数组。大对象会直接进入到老年代,这样作的目的主要是为了不新生代发生大量的内存复制(大对象的复制成本较高)。
2)长期存活的对象
虚拟机给每一个对象都定义了一个对象年龄计数器。每当进行一次Minor GC,年龄就增长1岁,当年龄超过必定值时(默认是15,能够经过参数配置),就进入到老年代。
3)动态对象年龄判断
虚拟机并不要求对象年龄必定要到达15岁才进入到老年代。若是Survivor空间中有某年龄相同的全部对象大小总和大于Survivor空间的一半,则年龄大于等于该年龄的对象就会直接进入老年代。
空间分配担保
在发生Minor GC以前,虚拟机会检查老年代最大可用的连续空间是否大于新生代全部对象总空间,若是大于,那么Minor GC能够确保是安全的(由于,极端状况下,就算新生代全部对象都存活,也能够保证安全晋升到老年代)。不然,虚拟机会查看HandlePromotionFailure的值是否容许担保失败。若是容许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。若是大于,将尝试进行一次Minor GC(尽管有风险);若是小于或者HandlePromotionFailure设置不容许冒险,那么就先进行一次Full GC。
以上说的有风险,是由于取历次晋升到老年代对象的平均值这种方式只是经验值,并不能保证每次都能担保成功,若是担保成功还好,若是担保失败的话,依然须要进行Full GC。
尽管如此,咱们最好仍是打开HandlePromotionFailure开关,避免过多频繁的Full GC(由于Full GC的执行速度比Minor GC慢的多)。
本文分享自微信公众号 - 烟雨星空(mistyskys)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。