JVM垃圾回收
如何确定哪些内存需要回收
gc要做的三件事分别是:哪些内存需要回收?什么时候回收? 如何回收?那么首先来看一下如何确定垃圾的问题、也就是哪些对象已经可以回收了。
-
引用计数法
要操作对象必须通过引用进行,所以可以通过看一个对象是否存在引用来判断这个对象是否可以被回收。
引用计数方法存在一种情况,即循环引用问题:比如在一个线程run方法中创建两个对象A和B,分别用成员变量持有对方的引用,这时候就算创建他们的线程执行完毕、线程栈释放,作为线程方法中的临时对象理论上后续没有用处了,但由于他俩互持引用,所以A和B的引用计数至少都是1,按照引用计数不为零的规则,这两个对象不能被释放,这存在问题。 -
可达性分析
也叫根搜索方法,规定JVM栈中的引用、方法区中的静态引用、JNI中的引用这3种引用称为根GC Roots,当一个对象到任何GC Roots没有引用链相连,则称其为不可达对象。不可达对象至少要经历两次标记过程后会变为可回收对象:第一次引用链判断被标记为不可达算1次,后面如果覆盖了finalize方法则有一次通过执行该方法而获得逃逸被回收的机会(比如在finalize方法里对象把自己的this引用赋值给类静态变量。。成功的拯救了自己),如果仍然是不可达的,那么算第2次被标记。GC Roots解决了引用计数法存在的循环引用问题。
垃圾回收算法
- 复制算法,copying,将内存区域分为两个区,每次使用其中1个,满了之后将还存活的对象复制到另一个区域,然后将当前区域清空。显然当存活对象比较多的时候,会进行较多的复制操作而影响性能,且由于分区的关系,内存使用存在一定的浪费。
- 标记清除算法, mark-sweep,内存区域满了之后,将存活对象和可回收对象标记出来,然后把可回收对象的对应的内存区域清空。这个算法的问题在于会产生比较多的内存碎片、导致分配大对象找不到可用的连续的内存空间。此外,就算没有复制操作,但是由于需要逐个的去释放可回收对象、需要一个个的计算释放内存空间的起始位置,所以此算法的效率也是比较低的。
- 标记整理算法, mark-compack,先标记,然后把存活对象移动到一起组成连续空间,之后把剩余部分内存空间清空。减少了内存碎片化。但由于存在复制移动、付出了一定性能开销。
分代收集策略
目前大部分JVM采用的收集算法策略,简单来说就是根据对象存活生命周期的长短将它们分类存放到不同的区域,比如年轻代和老年代,然后在不同的区域使用适合的不同的垃圾回收算法。
- 新生代中对象存活期比较短,都是临时对象为主,每次回收的多、存活的少,所以采用复制算法。因为复制操作不多。
- 老年代中对象存活时间长,每次回收只有少量对象释放,所以使用标记整理算法。因为回收的对象少,所以需要计算起始回收内存区域的次数也少,带来的性能开销可以较低。
综上,根据不同的区域,不同生命周期长短的对象类型,选用适合的不同的垃圾回收算法,对性能的影响还是比较大的。
除了分代收集策略之外,还有一种策略叫做分区收集算法策略,即将堆分为多个小区域,每个小区域可以独立回收。根据计算出来的预估的stop the world时间,每次垃圾回收选择若干个小区域分别执行回收,这样的好处是使得每次stw的时间变得可控。
垃圾收集器
上面讲的都是垃圾回收的算法层面。具体到JVM实现,则是通过各种垃圾收集器来体现的。通过选用不同的垃圾收集器,JVM传达了自己对垃圾回收算法思想的选择。
-
Serial
单线程的新生代垃圾收集器,使用复制算法。 -
ParNew
多线程新生代收集器,复制算法。-XX:ParallelGCThreads参数控制gc线程个数。 -
Parallel Scavenge
也是一个复制算法的多线程新生代收集器。跟ParNew的区别:- Parallel Scavenge比较看重执行用户代码的CPU时间片在总CPU时间上的占比,期望前者占比大一些(也称为吞吐量)。-XX:MaxGCPauseMillis最大期望stw时间,-XX:GCTimeRatio吞吐量比率大小、默认值为99,就是允许最大1%(即1 /(1+99))的垃圾收集时间。
- 此外ParallelScavenge拥有自适应调节策略,也是与ParNew的区别,-XX:+UseAdaptiveSizePolicy自动调节开关参数默认是开启的,比如jdk1.8使用的是parallelGC(Parallel Scavenge+Parallel Old),会自动调节新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数。
如果对于收集器运作原理不太了解,手工优化存在困难的时候,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成将是一个不错的选择。只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用MaxGCPauseMillis参数(更关注最大停顿时间)或GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。
对于清除调优方向的高级技术人员来说,清晰明确可控的gc收集策略要比JVM自动调节所带来的方便要重要的多,且在一些特定Java应用程序时候JVM所自动调节没有手工调节更有效、比如典型无状态应用的新生代可以适当调大、survior适当调大、减少进入老年代对象场景。这些时候,我们需要关闭这个参数,-XX:-UseAdaptiveSizePolicy
-
Serial Old
Serial的老年代版本,单线程、标记整理算法。 -
Parallel Old
Parallel Scavenge的老年代版本,多线程,标记整理算法。同样、也是比较看重吞吐量。 -
CMS - Concurrent Mark Sweep
一种老年代的多线程标记清除算法收集器,其主要诉求是获得最短stw时间,适合交互比较多的对响应时间有要求程序使用。CMS的工作机制相比前面几种收集器更为复杂,一共分为4个步骤:- 初始标记:先stw,然后标记GC roots直接连接的对象,这步虽然stw了但速度非常快。
- 并发标记:不需要stw,继续顺着gc roots引用链进行标记。
- 重新标记:并发标记阶段没stw,标记出来的对象可能会因为同时运行用户线程而发生可达性变化,所以这里还是要stw一下,然后找出这些发生变化的对象、标记出来。漏网之鱼总是少数,所以这步stw也不会太长。
- 并发清除:不stw,清楚之前标记的GC Roots不可达对象。
总结:由于上述过程中最耗时的并发标记和并发清除步骤都是gc线程和用户线程并发工作的没有stw,所以总体上来看,CMS看起来就是垃圾回收和用户线程一起并发执行的一样。
这里值得提一句的是,在并发清除阶段,由于用户线程的并发、清理的同时也会不断产生垃圾,一旦这时候产生的垃圾占满了预留空间,CMS会出现所谓current failed问题,退化为serial old收集器,老老实实的stw然后进行清除。可以说一旦出了这种情况,那CMS性能会直线下降、反而得不偿失了。
-
G1
garbage first垃圾收集器,jdk9默认的收集器,最新成果。相比CMS收集器主要是改进了两点:- 使用标记整理算法,减少内存碎片化。
- 分区收集算法思想,使得stw时间变得可控。“不求一定最少,但求可控”。
先到这里,东西一下太多记不住慢慢来,休息一下泡杯茶~
这个blog里边的关于JVM的文章不错 主要能看出来作者是自己一线心得写出来的,不是纸上谈兵。