学习JVM垃圾回收机制

垃圾回收是JVM的后台线程完成的,回收内存中不再被引用的那些对象所占用的内存空间。垃圾回收机制在内存达到预先设定的阀值后被触发。经过几个版本的演化,目前主流的算法是CMS,即concurrent mark and sweeping,新一代GC算法G1(garbage first)也在Java6中实现。本文将介绍这几种垃圾回收算法,理解了这些算法,可以帮助我们写出高性能的java程序,并能诊断一些server程序发生的性能问题。有些问题我也不是很懂,将来会继续探讨。

垃圾回收带来的好处是显而易见的,但是这些好处也是需要代价的。垃圾回收算法几经改善,终极目的就是减少这些代价。那些垃圾回收会带来哪些问题呢?最重要的一个问题是“停止世界”,在垃圾回收的某个阶段,所有线程都必须停止工作,直到这个阶段结束。所有的算法都不能完全规避这个问题,但是不同的算法停止的时间不一样。其次的一个问题是会占用额外的内存空间,相对来说这个问题似乎可以忍受。其他的问题是可能造成内存碎片,分配内存的效率降低,或者出现oom。【到底是改变ref地址的时候stop the world还是mark的时候?是mark的时候,我理解jvm的地址应该有一个映射表,将逻辑地址映射到物理地址,sweep的时候,只需要修改映射表的entry,可以快速完成,而且不需要所有对象的sweep完成后再做,因此不需要stop the world。】

所有的垃圾回收算法都基于两个假设:新生成的对象绝大多数是临时对象,新生对象很少被老对象引用。这个符合我们写程序的常理。比如我们写一个函数,计算1000个数字的平均值,假设这些数字是string,那么我们需要把他们转成Number,也就意味着生成了1000个临时对象。但是最终我们只需要一个。其他那些将会被回收掉。

由于这个假设,JVM将heap分成3个区域:新生代,年老代,和永久保存区,在不同的区域实行不同的垃圾回收算法。新生代是新分配的对象,年老代是经过垃圾回收仍然保留下来的那些对象,永久保留区是JVM自己需要的那些对象,比如class文件,静态变量之类。

垃圾回收算法主要有:顺序GC,并行顺序GC(注意不是并发),并发GC,垃圾第一GC。对于前三种来说,内存数据结构是一样的,对于年轻代的gc算法也大致相同。

新生代内存区分成3块,eden,from,to。新对象在eden进行分配,等到到达一定规模的时候,进行垃圾回收,活下来的那些copy到to区间,同时from中一部分进入to区间,一部分promote到年老代。然后from被清空,to变成from,eden被清空。(问题:from中哪些进入to,哪些进入年老代呢?)当然移动不过由于之前的假设,新生代的对象比较少,update比较快,停顿也会比较少。年老代的内存分成很多个page,每个page 512 byte。每次新生代的gc都会导致年老代的内存上升一点点。等到达一定的阀值以后,年老代的垃圾回收开始。

三者的区别在于运行的方式以及对年老代的gc的方法。

顺序GC采用的是mark和compact。对年老代采用的是mark和compact。在释放垃圾的时候会对空间进行压缩。使得释放的内存也是连续的。这种方式适用于client的应用。

并行GC和顺序GC的算法基本相同,不同的是可以多个cpu并发执行。

CMS重点希望减少gc导致的停顿的时间。回收分成3个阶段:初始mark,并发mark,sweeping。初始mark时,jvm停止世界,确认初始可到达的对象集合。然后JVM启动多个线程分头去做并发mark,把所有能到达的对象都打上标记。在并发mark的结尾阶段,再次stop-the-world,对本阶段内新生成的对象进行remark。【在remark之前还有个一个优化】

相对于之前的算法来说,cms不需要在mark的整个阶段stop world,是一个改进。但是这也需要两次扫描所有对象,marking的时间比旧的算法要长,对象被回收的速度也就比较慢。而且在marking阶段,app还在运行,新的对象被生成而不能立即回收。所以改进也不是白来的。

Mark完就是sweeping了。sweeping就是把那些不能被reach的对象释放掉,但是并不移动位置。这就会导致内存是不连续的。而在新生代的内存中是连续的,分配空间比较方便。年老代的内存则不同,需要遍历所有的内存块,找到适合需要的尺寸的内存块。【我记得有个compact阶段的,不知道在哪里?】

总的来说cms算法降低了年老代垃圾回收的停顿,代价是年轻代的停顿略长(why?)整体吞吐量降低(因为有些本该回收的对象没有回收)额外的堆空间(why?),此外由于他是并发访问,还会占用多个cpu。但是对于那些需要快速响应的app来说,还是值得的,比如web server。这就是为什么jvm启动时需要-server这个选项的原因。

垃圾回收的一个重要的数据结构是card table。通过他可以快速找到发生修改的年老代内存块。另外新生代的内存分配是bump-the-pointer机制,因为内存是连续,每次分配只需要移动指针即可。年老代的内存分配则需要搜索可用的内存块。

年轻代的区间是有限的,如果分配的内存太大,会直接分配到年老代的区间中。如果eden进入to时,to的空间不够,会直接进入年老代,导致早熟的promotion,这会导致年老代区充斥这可能可以立即回收的对象。如果年轻代gc导致年老代的区间太大,触发了年老代的垃圾回收,这就是promotion失败。这两种现象是我们需要避免的。

cms的前身是并发顺序gc。他的特点能快速回收内存,但缺点是需要暂停较长的时间。适用于吞吐量优先的应用。cms则是停顿较短。

G1是新一代的垃圾回收机制。下一次将介绍他。

原文地址:https://www.cnblogs.com/alphablox/p/2858042.html