分代收集算法实际上并不是一个新的实现方式,只是将虚拟机分成几块,每一块根据它的实际作用来选择适合的算法,这些算法可以是标记-清除,复制算法等等。
基于分代的收集思想,将堆内存分为以下几个部分:
将堆内存分为新生代(Young)和老年代(Old),新生代又分为Eden区、from区和to区,默认Eden:from:to=8:1:1。一般情况下,新创建的对象都会被分配到Eden区(一些大对象可能会直接放到老年代),具体的内存分配在后面记录。
HotSpot中的算法实现 枚举根节点在可达性分析中,可作为GC Roots的节点主要是全局性的引用与执行上下文,如果要逐个检查引用,必然消耗时间。
另外可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的时间间隔中进行,这里的“一致性”的意思是指整个分析期间整个系统执行系统看起来就像被暂停在某个时间点,不可以出现分析过程中对象引用关系还在不断变化的情况,也就是在分析过程中用户线程还在工作。这点是导致GC进行时必须暂停所有Java执行线程的其中一个重要原因。
但是目前主流的Java虚拟机都是准确式GC(准确式GC是指就是让JVM知道内存中某个位置数据的类型是什么),所以在执行系统停顿下来之后,并不需要一个不漏的检查执行上下文和全局的引用位置,虚拟机是有办法得知哪些地方存放的是对象的引用。在HotSpot的实现中,是使用一组OopMap的数据结构来达到这个目的的。
安全点在OopMap的协助下,HotSpot可以快速且准确的完成GC Roots的枚举,但可能导致引用关系变化的指令非常多,如果为每一条指令都生成OopMap,那将会需要大量的额外空间,这样GC的空间成本会变的很高。
实际上,HotSpot也的确没有为每条指令生成OopMap,只是在特定的位置记录了这些信息,这些位置被称为安全点(SafePoint)。SafePoint的选定既不能太少,以致让GC等待时间太久,也不能设置的太频繁以至于增大运行时负荷。所以安全点的设置是以让程序“是否具有让程序长时间执行的特征”为标准选定的。“长时间执行”最明显的特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生SafePoint。
对于SafePoint,另一个问题是如何在GC发生时让所有线程都跑到安全点在停顿下来。这里有两种方案:抢先式中断和主动式中断。抢先式中断不需要线程代码主动配合,当GC发生时,首先把所有线程中断,如果发现线程中断的地方不在安全点上,就恢复线程,让他跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程来响应GC。
而主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志的地方和安全点是重合的另外再加上创建对象需要分配的内存的地方。
使用安全点似乎已经完美解决了如何进入GC的问题,但实际情况却并不一定,安全点机制保证了程序执行时,在不太长的时间内就会进入到可进入的GC的安全点。但是程序如果不执行呢?所谓的程序不执行就是没有分配cpu时间,典型的例子就是线程处于sleep状态或者blocked状态,这时候线程无法响应jvm中断请求,走到安全的地方中断挂起,jvm显然不太可能等待线程重新分配cpu时间,对于这种情况,我们使用安全区域来解决。
安全区域是指在一段代码片段之中,你用关系不会发生变化。在这个区域的任何地方开始GC都是安全的,我们可以把安全区域看做是扩展了的安全点。
当线程执行到安全区域中的代码时,首先标识自己已经进入了安全区,那样当在这段时间里,JVM要发起GC时,就不用管标识自己为安全区域状态的线程了。当线程要离开安全区域时,他要检查系统是否完成了根节点枚举,如果完成了,那线程就继续执行,否则他就必须等待,直到收到可以安全离开安全区域的信号为止。
垃圾收集器 Serial收集器Serial是一个单线程的收集器,这表示其Serial只会使用一个CPU或者是一条收集线程进行垃圾回收的工作,同时需要注意的是它在进行回收工作是会停掉所有的其他工作线程(Stop the World),知道它的回收工作结束。