垃圾回收
断定对象存活
引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1。任什么时候刻计数器为0的对象就是不能再被使用的。客观地说,引用计数算法( Reference Counting)的实现简单,断定效率也很高,在大部分状况下它都是一个不错的算法。可是,至少主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的缘由是它很难解决对象之间相互循环引用的问题。html
在主流的商用程序语言(Java、C#,甚至包括前面提到的古老的Lisp)的主流实现中,都是称经过可达性分析( Reachability Analysis)来断定对象是否存活的。这个算法的基本思路就是经过一系列的称为“ GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象与任何“GC Roots”都不存在引用链相连(用图论的话来讲,就是从“GC Roots”到这个对象不可达)时,则证实此对象是不可用的。java
所谓“GC roots”,或者说tracing GC的“根集合”,就是一组必须活跃的引用。 例如说,这些引用可能包括:程序员
- 全部Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前全部正在被调用的方法的引用类型的参数/局部变量/临时值。
- VM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot VM里的Universe里有不少这样的引用。
- JNI handles,包括global handles和local handles
- (看状况)全部当前被加载的Java类
- (看状况)Java类的引用类型静态变量
- (看状况)Java类的运行时常量池里的引用类型常量(String或Class类型)
- (看状况)String常量池(StringTable)里的引用
注意,是一组必须活跃的引用(在JDK1.2以后,Java对引用的概念进行了扩充,将引用分为强引用( Strong Reference)、软引用( Soft Reference)、弱引用( Weak Reference)、虚引用( Phantom Reference)。),不是对象。算法
做者:RednaxelaFX 连接:https://www.zhihu.com/question/53613423/answer/135743258编程
引用
不管是经过引用计数算法判断对象的引用数量,仍是经过可达性分析算法判断对象的引用链是否可达,断定对象是否存活都与“引用”有关。在JDK1.2之前,Java中的引用的定义很传统也很简略:若是 reference 类型的数据中存储的数值表明的是另一块内存的起始地址,就称这块内存表明着一个引用。一个对象在这种定义下只有被引用或者没有被引用两种状态。但咱们但愿能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;若是内存空间在进行垃圾收集后仍是很是紧张,则能够抛弃这些对象。api
因而在JDK1.2以后,Java对引用的概念进行了扩充,将引用分为强引用( Strong Reference)、软引用( Soft Reference)、弱引用( Weak Reference)、虚引用( Phantom Reference)4种,这4种引用强度依次逐渐减弱。安全
- 强引用就是指在程序代码之中广泛存在的,相似
Object obj= new Object()
这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。 - 软引用是用来描述一些还有用但并不是必需的对象。对于软引用关联着的对象,**在系统将要发生内存溢出异常前,将会把这些对象列进回收范围之中进行第二次回收。**若是此次回收尚未足够的内存,才会抛出内存溢出异常。在JDK1.2以后,提供了
SoftReference
类来实现软引用。 - 弱引用也是用来描述非必需对象的,可是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生以前。当垃圾收集器工做时,不管当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2以后,提供了
WeakReference
类来实现弱引用。 - 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,彻底不会对其生存时间构成影响,也没法经过虚引用来取得一个对象实例。为一个对象设置虚引用关联的惟一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2以后,提供了
PhantomReference
类来实现虚引用。
finalize 方法与两次标记
即便在可达性分析算法中不可达的对象,也并不是是“非死不可”的,这时候它们暂时处于“缓刑”阶段。数据结构
若是某实例在进行可达性分析后被第一次发现没有与 GC Roots 相链接的引用链,则标记而且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize()
方法。当对象没有覆盖 finalize()
方法,或者 finalize()
方法已经被虚拟机调用过,虚拟机将这两种状况都视为“没有必要执行finalize()
方法”。多线程
若是这个对象被断定为有必要执行 finalize()
方法,那么这个对象将会放置在一个叫作F-Queue的队列之中,并在稍后由一个由虚拟机自动创建的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样作的缘由是,若是一个对象在 finalize 方法中执行缓慢(甚至发生了死循环),将极可能会致使 F-Queue队列中其余对象永久处于等待,甚至致使整个内存回收系统崩溃。并发
finalize()
方法是对象逃脱死亡命运的最后一次机会。稍后GC将对 F-Queue 中的对象进行第二次小规模的标记,若是对象想要拯救拯救本身,必须在 finalize()
方法中与引用链上的任何一个对象创建关联,那样第二次标记时它将被移除出“即将回收”的集合。不然基本上它就真的被回收了。
若是一个对象被断定为没有必要执行 finalize 方法,那就直接被可回收。
任何一个对象的 finalize 方法都只会被系统自动调用一次,若是对象曾经由于这个方法死里逃生,那么它第二次面临缓刑这个方法也救不了它。
须要特别说明的是,笔者并不鼓励你们使用上述方法反而建议你们尽可能避免使用它,由于它不是C++中的析构函数,而是Java刚诞生时为了使C++程序员更容易接受它所作出的一个妥协。它的运行代价高昂,不肯定性大,没法保证各个对象的调用顺序。有些教材中描述它适合作“关闭外部资源”之类的工做,这彻底是对这个方法用途的一种自我安慰。finalize()
能作的全部工做,使用try-finally
或者其余方式均可以作得更好、更及时,因此笔者建议你们彻底能够忘掉Java语言中有这个方法的存在。
方法区的垃圾收集
不少人认为方法区(或者 HotSpot 虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过能够不要求虚拟机在方法区实现垃圾收集,并且在方法区中进行垃圾收集的“性价比”通常比较低:在堆中,尤为是在新生代中,常规应用进行一次垃圾收集通常能够回收70%~95%的空间,而永久代的垃圾收集效率远低于此。
永久代(方法区)的垃圾收集主要回收两部份内容:废弃常量和无用的类。回收废弃常量以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,可是当前系统没有任何一个String对象引用常量池中的“abc”常量,也没有其余地方引用了这个字面量,若是这时发生内存回收,并且必要的话,这个“abc”常量就会被系统清理出常量池。常量池中的其余类(接口)、方法、字段的符号引用也与此相似。
断定一个常量是不是“废弃常量”比较简单,而要断定一个类是不是“无用的类”的条件则相对苛刻许多。类须要同时知足下面3个条件才能算是“无用的类”。
- 该类全部的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
- 加载该类的
ClassLoader
已经被回收。 - 该类对应的
java.lang.Class
对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法。
新生代,老生代,永久代
在 Java 中,堆被划分红两个不一样的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、Survivor * 2。 **这样划分的目的是为了使 JVM 可以更好的管理堆内存中的对象,包括内存的分配以及回收。**这种分法并无什么新的思想,只是老年代用来放在好几回GC中都活下来的新生代中的对象——能够预期这些对象再以后的GC中应该也能存活。
在新生代中,新出的对象会在这里,还不是久经考验的对象会在这里。这块内存里每次垃圾收集时都发现有大批对象死去,只有少许存活。于是能够选用下述的“复制”算法,只须要付出少许存活对象的复制成本就能够完成收集。而老年代中由于对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记一清除”或者“标记一整理”算法来进行回收。
永久代是 HotSpot 虚拟机特有的概念,是方法区的一种实现,别的JVM都没有这个东西。在 Java 8 中,永久代被完全移除,取而代之的是另外一块与堆不相连的本地内存——元空间。 永久代或者“Perm Gen”包含了JVM须要的应用元数据,这些元数据描述了在应用里使用的类和方法。注意,永久代不是Java堆内存的一部分。永久代存放JVM运行时使用的类。永久代一样包含了Java SE库的类和方法。永久代的对象在Full GC时进行垃圾收集。
收集算法
“标记一清除”( Mark-Sweep)算法
最基础的收集算法是“标记一清除”( Mark-Sweep)算法,如同它的名字同样,算法分为“标记”和“清除”两个阶段:首先标记出全部须要回收的对象,在标记完成后统一回收全部被标记的对象,之因此说它是最基础的收集算法,是由于后续的收集算法都是基于这种思路并对其不足进行改进而获得的。它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另外一个是空间问题,标记清除以后会产生大量不连续的内存碎片,空间碎片太多可能会致使之后在程序运行过程当中须要分配较大对象时,没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。
“复制”( Copying)算法
为了解决效率问题,一种称为“复制”( Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂状况,只要移动堆顶指针,按顺序分配内存便可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免过高了一点。
不过如今的商业虚拟机都采用这种收集算法的思想,只不过留存不用的比例有所调整。将新生代内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot虚拟机上默认采用 Eden 和 Survivor 空间 8:1 的比例。不过,咱们没有办法保证每次回收都只有很少于10%的对象存活,当 Survivor空间不够用时,须要依赖其余内存(这里指老年代)进行分配担保( Handle Promotion)。
内存的分配担保指:若是另一块 Survivor 空间没有足够空间在放上一次新生代收集下来的存活对象时,这些对象将经过一些机制进入老年代。关于对新生代进行分配担保的内容,在本章稍后在讲解垃圾收集器执行规则时还会再详细讲解。
“标记-整理”( Mark-Compact)算法
复制收集算法在对象存活率较髙时就要进行较多的复制操做,效率将会变低。更关键的是,若是不想浪费太多空间, Survivor 空间必然较小,进而就须要有额外的空间进行分配担保,以防没法复活全部该复活的对象(试想所全部对象都要复活的极端状况)。因此在老年代中不使用此算法。
根据老年代的特色,有人提出了另一种“标记-整理”( Mark-Compact)算法,标记过程仍然与“标记-清除”算法同样,但后续步骤不是直接对全部对象进行清理,而是让全部继续存活的对象都向一端移动,而后直接清理掉端边界之外的内存。
分代收集算法
就是在根据对象的生存能力的基础上划分新生代,老生代,永久代,在在此基础上对不一样”代“用不一样收集算法。
性能考量
必须对GC算法的执行效率有严格的考量,才能保证虚拟机高效运行。
就可达性分析中从 GC Roots 节点找引用链这个操做来讲,其中可做为 GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,如今不少应用仅仅方法区就有数百兆,若是要逐个检查这里面的引用,那么必然会消耗不少时间。
另外,可达性分析对执行时间的敏感还体如今GC停顿上,由于在整个分析期间整个JVM看起来就像被冻结在某个时间点上。这是由于若是在GC分析过程当中出现对象引用关系还在不断变化的状况,GC分析结果准确性就没法获得保证。因而GC进行时必须停顿全部Java执行线程(Sun将这件事情称为“ Stop The World”)。
GC roots 优化
Hotspot 使用 OopMap 避免对栈上彻底的遍历。一个线程意味着一个栈,一个栈由多个栈帧组成,一个栈帧对应着一个方法,一个方法里面可能有多个安全点。 gc 发生时,程序首先运行到最近的一个安全点停下来,而后更新本身的 OopMap ,记下栈上哪些位置表明着引用。枚举根节点时,递归遍历每一个栈帧的 OopMap ,经过栈中记录的被引用对象的内存地址,便可找到这些对象( GC Roots )。
安全点与安全区域
在 OopMap的协助下, HotSpot能够快速且准确地完成 GC Roots枚举,但一个题随之而来:线程中的信息可能/每每在不断变化,进而其中包含许多引用关系变化,若是 OopMap 要响应这样的变化,那么会带来糟糕的性能开销。
所以, HotSpot 没有实时地响应变化,只是”在特定的位置“记录(更新)信息,这些位置称为安全点(safepoint)。因为 GC 须要 OopMap,因此GC只有在到达安全点时才能进行是很天然的推断。于是 Safepoint 的选定既不能太少以至于让GC等待时间太长,也不能过于频繁以至于过度增大运行时的负荷。
因此,安全点的选定基本上是以程序“是否具备让程序长时间执行的特征”为标准进行选定的—由于每条指令执行的时间都很是短暂,程序不太可能由于指令流长度太长这个缘由而过长时间运行,长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,因此具备这些功能的指令才会产生 Safepoint。
在 Java 这种以多线程并发为亮点特性的语言中,多线程显然是常见的运行场景。而多线程意味着各个线程同时到达安全点的可能性极低,因此是 GC 发出请求后,各个线程接收并要求本身停在安全点。这里有两种方案可供选择:抢先式中断( Preemptive Suspension)和主动式中断( Voluntary Suspension),其中抢先式中断不须要线程的执行代码主动去配合,在GC发生时,首先把全部线程所有中断,若是发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。如今几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。而主动式中断的思想是当GC须要中断线程的时候,不直接对线程操做,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志(轮询标志的地方和安全点是重合的),发现中断标志为真时就本身中断挂起。
如若线程处于 Sleep状态或者 Blocked状态,这时候线程没法响应GC的要求,“走”到安全的地方去中断挂起,GC也显然不太可能等待线程从新被分配CPU时间。对于这种状况,就须要安全区域( Safe Region)来解决。安全区域是指在一段代码片断之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的、咱们也能够把 Safe Region看作是被扩展了的 Safepoint。在线程执行到 Safe Region中的代码时,首先标识本身已经进入了 Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识本身为 Safe Region状态的线程了。在线程要离开 Safe Region时,它要检査系统是否已经完成了根节点枚举(或者是整个GC过程),若是完成了,那线程就继续执行,不然它就必须等待直到收到能够安全离开 Safe Region的信号为止。
垃圾收集器:
若是说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现并无任何规定,所以不一样的厂商、不一样版本的虚拟机所提供的垃圾收集器均可能会有很大差异,而且通常都会提供参数供用户根据本身的应用特色和要求组合出各个年代所使用的收集器。这里讨论的收集器基于JDK1.7 Update14以后的 HotSpot虚拟机(在这个版本中正式提供了商用的G1收集器,以前G1仍处于实验状态),这个虚拟机包含的全部收集器如图3-5所示。图3-5展现了7种做用于不一样分代的收集器,若是两个收集器之间存在连线,就说明它们能够搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器仍是老年代收集器。接
Serial 收集器
Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1以前)是虚拟机新生代收集的惟一选择。如名字暗示的同样,这个收集器是一个单线程的收集器,但它的“单线程”的意义并不只仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工做,更重要的是在它进行垃圾收集时,必须暂停其余全部的工做线程,直到它收集结束。
“Stop The World”这个名字也许听起来很酷,但这项工做其实是由虚拟机在后台自动发起和自动完成的,在用户不可见的状况下把用户正常工做的线程所有停掉,这对不少应用来讲都是难以接受的。读者不妨试想一下,要是你的计算机每运行一个小时就会暂停响应5分钟,你会有什么样的心情?
Serial old是 Serial收集器的老年代版本,它一样是一个单线程收集器。
ParNew 收集器
serial 收集器的多线程版本,只是在工做时本身多线程,其它线程仍是要停,即”Stop the world“没有避免。
注意:从 ParDew收集器开始,后面还会接触到几款并发和并行的收集器。在你们可能产生疑惑以前,有必要先解释两个名词:并发和并行。这两个名词都是并发编程中的概念,在谈论垃圾收集器的上下文语境中,它们能够解释以下。
- 并行(Parallel):指多条垃圾收集线程并行工做,但此时用户线程仍然处于等待状态。
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不必定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另外一个CPU上。
Paralle Scavenge 收集器
Parallel Scavenge收集器的特色是它的关注点与其余收集器不一样,CMS等收集器的关注点是尽量地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量( Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即”吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)“,虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
停顿时间越短就越适合须要与用户交互的程序,良好的响应速度能提高用户体验,而高吞吐量则能够高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不须要太多交互的任务。
Parallel Old 是 Parallel Scavenge收集器的老年代版本,使用多线程和“标记一整理”算法。这个收集器是在JDK1.6中才开始提供的。
CMS 收集器
CMS ( Concurrent Mark Swee)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤为重视服务的响应速度,但愿系统停顿时间最短,以给用户带来较好的体验。CMS收集器就很是符合这类应用的需求。
从名字(包含“ Mark Sweep”)上就能够看出,CMs收集器是基于“标记清除”算法实现的,它的运做过程相对于前面几种收集器来讲更复杂一些,整个过程分为4个步骤,包括:
- 初始标记( EMS initial mark)
- 并发标记( CMS concurrent mark)
- 从新标记( CMS remark)
- 并发清除( CMS concurrent sweep)
其中,初始标记、从新标记这两个步骤仍然须要“ Stop The World”。初始标记仅仅只是标记一下 GC Roots能直接关联到的对象,速度很快,并发标记阶段就是在初始标记阶段的基础上完善引用链的过程。而从新标记阶段则是为了修正并发标记期间因用户程序继续运做而致使标记产生变更的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但远比并发标记的时间短。
因为整个过程当中耗时最长的并发标记和并发清除过程收集器线程均可以与用户线程一块儿工做,因此,从整体上来讲,CMS收集器的内存回收过程是与用户线程一块儿并发执行的。经过图3-10能够比较清楚地看到CMS收集器的运做步骤中并发和须要停顿的时间。
CMS存在以下缺点:
- CMS收集器对CPU资源很是敏感。其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会致使用户线程停顿,可是会由于占用了一部分线程(或者说CPU资源)而致使应用程序变慢,总吞吐量会下降。
- 因为CMS并发清理阶段用户线程还在运行着,伴随程序运行天然就还会有新的垃圾不断产生,这一部分垃圾出如今标记过程以后,CMS没法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
- 也是因为在垃圾收集阶段用户线程还须要运行,那也就还须要预留有足够的内存空间给用户线程使用,所以CMS收集器不能像其余收集器那样等到老年代几乎彻底被填满了再进行收集,须要预留一部分空间提供并发收集时的程序运做使用。在JDK15的默认设置下,CMS收集器当老年代使用了68%。能够适当调高参数
-XX:CMSInitiatingOccupancyFraction
的值来提升触发百分比。要是CMS运行期间预留的内存没法知足程序须要,就会出现一次Concurrent Mode Failure
失败,这时虚拟机将启动后备预案:临时启用 Serial old收集器来从新进行老年代的垃圾收集,这样停顿时间就很长了。因此说参数-XX:CMSInitiatingOccupancy Fraction
设置得过高很容易致使大量Concurrent Mode Failure
失败,性能反而下降。 - 还有最后一个缺点,因为CMS是一款基于“标记一清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,每每会出现老年代还有很大空间剩余,可是没法找到足够大的连续空间来分配当前对象,不得不提早触发一次 Full GC。
G1 收集器
Gl( Garbage- First)收集器是当今收集器技术发展的最前沿成果之一。
优势:
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优点,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World 停顿的时间。
- G1收集器运做期间不会产生内存空间碎片。与CMS的“标记一清理”算法不一样,G1从总体来看是基于“标记一整理”算法实现的收集器,从局部(两个 Region之间)上来看是基于“复制”算法实现的,但不管如何,这两种算法都意味着G1运做期间不会产生内存空间碎片,收集后能提供规整的可用内存。
- 这是G1相对于CMS的一大优点,下降停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能创建可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片断内,消耗在垃圾收集上的时间不得超过N毫秒。
在G1以前的其余收集器进行收集的范围都是整个新生代或者老年代,而G1再也不是这样。使用G1收集器时,Java堆的内存布局就与其余收集器有很大差异,它将整个Java堆划分为多个大小相等的独立区域( Region),虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,它们都是一部分 Region(不须要连续)的集合。
Gl收集器之因此能创建可预测的停顿时间模型,是由于它能够有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个 Region 里面的垃圾堆积的价值大小(回收所得到的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据容许的收集时间,优先回收价值最大的 Region(这也就是 Garbage-First 名称的来由)。这种使用Region划份内存空间以及有优先级的区域回收方式,保证子G1收集器在有限的时间内能够获取尽量高的收集效率。
在G1收集器中, Region 之间的对象引用以及其余收集器中的新生代与老年代之间的对象引用,虚拟机都是使用 Remembered Set 来避免全堆扫描的。G1收集器下的堆中每一个 Region 都有一个与之对应的 Remembered Set,虚拟机发现程序在对 Reference 类型的数据进行写操做时,会产生一个 Write Barrier 暂时中断写操做,检查 Reference引用的对象是否处于不一样的 Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),若是是,便经过 CardTable 把相关引用信息记录到被引用对象所属的 Region的 Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入 Remembered Set 便可保证不对全堆扫描也不会有遗漏。
若是不计算维护 Remembered Set的操做,Gl收集器的运做大体可划分为如下几个步骤
- 初始标记( Initial Marking )
- 并发标记( Concurrent Marking)
- 最终标记( Final Marking)
- 筛选回收( Live Data Counting and Evacuation)
G1收集器的前几个步骤的运做过程和CMS收集器有不少类似之处。
初始标记阶段仅仅只是标记一下 GC Roots 能直接关联到的对象,而且修改TAMS( Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的 Region 中建立新对象,这阶段须要停顿线程,但耗时很短。
并发标记阶段是从 GC Root 开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
最终标记阶段则是为了修正在并发标记期间因用户程序继续运做而致使标记产生变更的那一部分标记记录,虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面,最终标记阶段须要把 Remembered Set Logs 的数据合并到 Remembered Set中,这阶段须要停顿线程,可是可要求并行执行。
最后在筛选回收阶段首先对各个 Region的回收价值和成本进行排序,根据用户所指望的GC停顿时间来制定回收计划,从Sun公司透露出来的信息来看,这个阶段其实也能够作到与用户程序一块儿并发执行,可是由于只回收一部分 Region,时间是用户可控制的,并且停顿用户线程将大幅提升收集效率。
线程终结不致使对应实例的GC
当线程处于TERMINATED
状态时,并不会致使该线程对应的实例被GC。可使用以下代码进行尝试:
import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; public class Test extends Thread { public Map<StringBuilder, StringBuilder> map = new HashMap<>(); public StringBuilder key = new StringBuilder("key"); @Override public synchronized void start() { map.put(key,new StringBuilder("hello")); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } super.start(); } }
首先是一个用来测试的线程类。
import java.lang.ref.WeakReference; import java.util.concurrent.TimeUnit; public class Main { public static void main(String[] args) throws InterruptedException { Test t = new Test(); t.start(); WeakReference<Thread> r = new WeakReference<>(t); WeakReference<StringBuilder> s = new WeakReference<>(t.map.get(t.key)); System.gc(); TimeUnit.SECONDS.sleep(3); System.out.println(t != null ? t.getState().toString() : "null"); System.out.println(s.get() != null ? s.get().toString() : "null"); t = null; System.gc(); TimeUnit.SECONDS.sleep(3); System.out.println(r.get() != null ? r.get().getState().toString() : "null"); System.out.println(s.get() != null ? s.get().toString() : "null"); } }
而后是正式测试,输出结果为:
TERMINATED hello null null
其中前两行说明了,即使线程已经处于终结状态,但其对应的实例并不会被GC,进而实例内部的成员变量所保持的强引用的对象不会被GC,进而这些对象内部的……总之就是无限套娃下去。