Hotspot垃圾回收
从J2SE 5.0开始,HotSpot JVM共包含四种垃圾收集器,它们全部基于分代算法。
一、代的划分
HotSpot JVM中内存被划分为三代:新生代(young generation)、老年代old generation)和永久代(permanent generation)。从逻辑上讲,新生代和老年代共同构成了Java堆,而永久代则被称为方法区(method area)。除了一些大对象可能在老年区中直接分配外,大部分对象都在新生区中创建;而老年区除了那些直接创建的大对象外,大部分对象都是在新生区中历经几次垃圾收集而幸免于难后被提升过来的。永久代中则保存着已载入类型的相关信息,包含了类、方法和其他一些内部使用的元数据,所有这些信息同样以对象的形式来组织和表示,虽然这些对象并不是Java对象,但是它们却象Java对象一样可以被同样的垃圾收集器所收集;另外,java.lang.String类所管理的内在化的字符串缓存池也在该代中分配;虽然名字叫做“永久”代,但其中的对象并不是永久的,只是沿用了历史名称而已。
新生代由一个Eden区和两个更小的Survivor区(还记得复制收集方法吗)。大部分对象在Eden区中创建,少数大对象可能在老年代中直接创建。Survivor区中保存的对象是历经一次或多次对新生代的垃圾收集而未死的幸存者,并且在被认为已足够成熟而提升到老年代之前,它们仍有在稍后的某次垃圾收集过程中牺牲的可能。除非处在垃圾收集过程当中,两个Survivor区中只有一个用来容纳这些幸存者,另一个则保持为空。
二、垃圾收集类型
新生代填满后,一次只针对该代的收集被执行,这样的收集也被称作“次收集(minor collection)”。当老年代或永久代被填满后,一次针对所有代的完整收集被执行,这样的收集也被称作“主收集(major collection)”。通常来说,在一次主收集过程中,新生代首先被收集,收集算法采用当前收集器的新生代收集算法,然后是对老年代和永久代的收集,收集算法都采用当前收集器的老年代收集算法。对于给定收集器所具体使用的新生代和老年代收集算法,另外,主收集过程中如果存在压缩,则每代独自进行。
不过,首先收集新生代的策略在老年代空闲空间太小时会失效,因为老年代已无足够的空间来接纳所有的可能从新生代提升过来的对象;在这种情况下,除CMS外的所有收集器都会放弃原有的新生代收集算法,转而统一采用老年代收集算法对所有代进行收集。(CMS收集器之所以例外是因为它的老年代算法不能用来收集新生代。)
三、快速分配
内存中大块的连续空闲空间用以满足对象的分配请求。这种情形下的分配操作使用简单的“bump-the-pointer”技术,效率很高。按照这种技术,JVM内部维护一个指针(allocatedTail),它始终指向先前已分配对象的尾部,当新的对象分配请求到来时,只需检查代中剩余空间(从allocatedTail到代尾geneTail)是否足以容纳该对象,并在“是”的情况下更新allocatedTail指针并初始化对象。下面的伪代码具体展示了从连续内存块中分配对象时分配操作的简洁性和高效性:
void *malloc(int n){
if( geneTail - allocatedTail < n )
doGarbageCollection();
void * wasAllocatedTail = allocatedTail;
allocatedTail += n;
return wasAllocatedTail;
}
对于多线程应用,分配操作必须是线程安全的。如果使用全局锁为此提供保证,则分配操作必定成为一个性能瓶颈。基于此,HotSport JVM采用了一种被称为“线程局部分配缓冲区”(Thread-Local Allocation Buffers,TLAB)的技术。该项技术为每个线程提供一个独立的分配缓冲区(Eden区的一小部分),借此来提高分配操作的吞吐量。因为针对每个TLAB,只有一个线程从中分配对象,故而分配操作可以使用“bump-the-pointer”技术快速完成,而不必使用任何锁机制;只有当线程将其已有TLAB填满并且需要获取一个新的TLAB时,同步才是必须的。同时,为了减少TLAB所带来的空间消耗,还使用了一些其他技术,例如,分配器能够把TLAB的平均大小限制在Eden区的1%以下。
“bump-the-pointer”和TLAB技术的组合保证了分配操作的高效性,类似new Object()这样的操作在大部分时间内只需要大约10条机器指令即可完成。
四、收集方式
1)串行(serial)和并行(parallel):采用串行同一时间只有一件事情会发生。
2)stop-the-world和并发(concurrent):STW在垃圾回收时要暂停其他用户线程
五、串行收集器(serial collector 新生代 复制 标记整理 stw 串行)
使用串行收集器时,对新生代和老年代的收集都采用串行、STW方式进行。
1)串行收集器收集新生代(复制)
Eden区中的活动对象被拷贝到初始为空的Survivor区2中;已占用的Survivor区1中的活动对象如果仍显年轻,则同样被拷贝到Survivor区2中,否则被直接拷贝到老年代;需要注意的是,Survivor区2一旦被填满,则所有尚未被拷贝的活动对象,不论其来自Eden区还是Survivor区1,也不论其曾经幸免于多少次次收集,统统被拷贝到老年代。按照定义,在活动对象被拷贝之后,Eden区和Survivor区1中的对象就全部成为垃圾对象,无须再被检查。
收集完成后,Eden区和Survivor区1变为空闲空间,活动对象保存在Survivor区2中。此时,两个Survivor区的角色已经发生了互换。
2)串行收集器收集老年代
串行收集器采用标记-清理-压缩算法收集老年代和永久代。在标记阶段,收集器遍历引用树,找出所有活动对象并打上标记;在清理阶段,顺序扫描代空间中所有对象(不论死活),计算出每个活动对象在代空间中的新位置;在压缩阶段,指向活动对象的所有引用被先期更新后,所有活动对象也被逐个滑动到其新的位置。由于所有活动对象都是按照次序朝代空间头部移动的,因此就在代空间尾部自然形成了一个单一而连续的空闲空间。如下图:压缩过程使基于老年代或永久代的分配操作可以使用快速的“bump-the-pointer”技术。
3)串行收集器的选用
简单高效 对于单核来说 没有线程交互开销 常用于客户端模式
六、并行收集器(parallel collector 新生代 复制 标记整理 stw 并行)
吞吐量收集器,标准是可控的吞度量(运行用户代码的时间/gc的时间+用户代码时间)
停顿时间越短越适合做用户交互的程序,良好的响应速度提升用户体验,高吞吐量可以尽快完成程序的运算任务,适合后台运算。
1)并行收集器如何收集新生代?
和串行收集器相比,并行收集器采用了大致相同的新生代收集算法,只是执行的是其并行版本而已。对新生代的收集虽然仍基于拷贝技术、采用STW方式进行,但收集工作是并行展开的,使用了多个CPU,这就降低了垃圾收集开销,从而提高了应用程序的吞吐量。下图展示了并行收集器和串行收集器在执行新生代收集时到底有何不同:
2)并行收集器如何收集老年代?
和串行收集器一样,并行收集器对老年代的收集同样基于标记-清理-压缩算法,同样采用串行、STW方式进行。
七、并发的标记-清理收集器(Concurrent Mark-Sweep(CMS) Collector)
对于许多应用来说,端到端的吞吐量并不象响应时间那么重要。通常来讲,对新生代的收集并不会引起太长时间的停顿。但是对老年代的收集,虽然不常发生,却可能导致停顿时间过长的状况,在堆空间很大时尤其明显。为了解决这个问题,HotSpot JVM包含了一个名叫“并发的标记-清理(CMS)收集器”的收集器,获取最短回收停顿时间为目标,响应速度快,它也被称为低延迟收集器。
1)步骤:初始标记;并发标记;重新标记;并发清除
其中初始标记和重新标记也是stw的,但初始标记仅仅是检查是否直接关联gc Roots速度快,并发标记是向下tracing的过程,整个过程耗时最长的并发标记和并发清除是并发执行的,所以总体上来说cms是并发执行的。
2)cms缺点:
对CPU资源敏感,无法处理浮动垃圾,存在内存碎片
八、G1收集器
适用:面向服务端应用,适用于新生代和老年代。当前收集器技术发展的最前沿成果
特点:
1.并行+并发。可充分利用CPU资源
2.分代收集。
3.空间整合。 G1从整体看是”标记-整理“算法,从局部(两个Region之间)看,是”复制“算法。 不会产生空间碎片。
4.可预测的停顿。建立可预测的态度时间模型,能让使用者明确指定在一个长度为M毫秒的时间内,消耗在垃圾收集的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
Garbage First名称的由来
G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力地避免全区域的垃圾收集。G1将内存划分为Region,跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
难点:虽然内存分为Region,但垃圾收集不能真的以Region为单位进行,因为Region不可能是孤立的,存在某个对象被多个Region的引用,那在做可达性判断确定对象是否存活时,是否需要扫描整个堆空间呢?注意:此问题在所有的收集器中都存在(如存在新生代与老年代之间的引用)。
解决:1.使用Remembered Set来避免圈堆扫描。
过程:G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作是,会产生一个Write Barrier暂时中断操作,检查Reference类型引用的对象是否处于不同的Region(在分代的例子中就是检查是否老年代的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
内存布局:G1的堆内存布局与其他收集器不同,G1将整个堆内存空间划分为多个大小相等的Region,虽然仍然有新生代和老年代的概念,但是新生代和老年代不再是物理隔离的,他们都是一部分Region(不需要连续)的集合。
过程(与CMS相似)
1.初始标记:暂停用户线程,标记GC Roots能直接关联的对象
2.并发标记:用户线程与标记线程并发,进行GC Roots的Trace
3.最终标记修正并发标记阶段,因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录。
4.筛选回收:
算法: 全局标记-整理+局部复制算法