如何判读对象“已死”?
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器值为0的对象就是不可能再被使用的。引用技术法实现简单、效率高,但Java语言并没有选用引用技术法来管理内存,最主要的原因是它很难解决对象之间的相互循环引用的问题。
根搜索算法
在主流的程序语言中,都使用根搜索算法(GC Roots Tracing)判断对象是否是活的。此算法的基本思路就是通过一系列名为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索走过的路径成为“引用链”,当一个对象到“GC Roots”没有任何引用链相连接时,则证明此对象是不可用的。在Java中可作为GC Roots的对象有如下几种:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
Java中的引用定义的很传统:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用和虚引用。这四种引用强度依次逐渐减弱,意义如下:
- 强引用:强引用在程序代码中是普遍存在的,如 Object o= new Object() 中 o 就是强引用,只要强引用还在,垃圾回收器永远不会回收被引用的对象;
- 软应用:软引用用来描述一些还有用,但并非必需的对象。对于软引用所引用的对象,在系统发生内存溢出之前会被列入垃圾回收范围之内并进行第二次回收,使用 SoftReference 类实现软引用。
- 弱引用:也用来描述非必需的对象,被弱引用所引用的对象只能生存到下一次垃圾回收发生之前,使用 WeakReference 类实现弱引用。
- 虚引用:是最弱的一种引用关系,它并不会影响其所引用的对象的生存时间,也无法通过虚引用来获取一个对象的实例。为一个对象设置一个虚引用的唯一目的就是希望能在这个对象被回收时收到一个系统通知。使用 PhantomReference 来实现虚引用。
在根搜索算法中不可达的对象并非必被回收,在被回收前至少要经历两次标记过程:如果对象在进行根搜索后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。如果对象被判定为有必要执行 finalize() 方法,这个对象将被放置在名为 F-Queue 的队列之中。finalize() 方法是对象逃脱被回收的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,然后就进行垃圾回收。
垃圾收集算法
1.标记-清除算法
此算法是最基础的算法,分为两个阶段:首先标记出所有需要被回收的对象,在标记完成后统一回收掉被标记的对象。有两个主要的缺点:一个是效率问题,标记和清除的效率都不高;另一个是空间问题,标记清除后会产生大量不连续的内存碎片,当程序需要分配较大的对象时无法找到足够大的连续内存而不得不进行垃圾回收。
2.复制算法
此算法将内存划分为大小相等的两个区域,每次只是用其中的一块,每当这块内存用完了,就将还存活的对象复制到另一块内存区域中,然后把使用的这块内存给清空,这样使得每次只对其中一块内存进行内存回收,也不会有内存碎片的情况了,只要移动堆顶指针即可顺序分配内存。代价是将内存缩小为原来的一半。
据研究表明,新生代中对象 98% 都是朝生夕死的,所以并不需要按照 1:1 的比例进行内存划分,而是将内存划分为一块较大的 Eden 空间和两个较小的 Survivor 空间,每次只是用 Eden 空间和其中的一块 Survivor 空间。当进行垃圾回收时,将 Eden 和 Survivor 空间中还存活的对象复制到另一块 Survivor 空间中,然后对 Eden 和使用过的 Survivor 空间进行清理。Hotspot 虚拟机默认 Eden 和S urvivor 空间的大小比例为8:1.
3.标记-整理算法
复制算法在对象存活率较高时会进行较多的复制操作,效率会变低且空间浪费严重。因此出现了标记整理算法,其思路是先标记所有需要被回收的对象,然后将存活的对象向一端移动,然后直接清理掉边界以外的内存区域即可。
4.分代收集算法
根据对象的存活周期不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,然后就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次都会回收大量对象,只有少量对象会存活下来,那就选用复制算法,只需少量复制操作即可完成收集。而老年代中的对象存活率较高,没有额外空间对其进行担保,所以就要使用“标记-清理”或“标记-整理”算法进行回收。
垃圾收集器
1.Serial收集器
Serial 收集器是最基本、历史最悠久的收集器,曾经是新生代收集器的唯一选择。这个收集器是一个单线程的收集器,单线程的意思不仅仅只它只能使用一个 CPU 或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾回收时必须暂停所有其他的工作线程,直到它收集结束。在用户不可见的情况下把用户的正常工作的线程全部停掉,这对很多应用来说是不可接受的。它是 jvm 运行在 client 模式下的默认新生代垃圾回收器。
2.ParNew收集器
ParNew 收集器其实就是 Serial 收集器的多线程版,除了使用多条线程进行垃圾回收外,其余行为包括 Serial 收集器可用的所有控制参数(-XX:SurvivorRation、-XX:PretenurSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、对象分配规则、回收策略等都与 Serial 收集器完全一样。它是许多运行在 server 模式下 JVM 的首选新生代收集器,目前它只能与 CMS 收集器配合工作(CMS 收集器是 Hotspot 第一款真正意义上的并发收集器,第一次实现了垃圾回收线程与用户线程基本同时工作)。不幸的的是 CMS 作为老年代收集器,无法与 Parallel Scavenge 收集器配合工作,其只能与 Serial 或 ParNew 进行配合工作。ParNew 也是使用了 -XX:+UseConcMarkSweepGC 选项后的默认新生代收集器,也可以使用 -XX:+UseParNewGC 选项强制使用。
3.Parallel Scavenge收集器
Parallel Scavenge 收集器也是新生代的收集器,也是使用复制算法的收集器,也是并行的多线程的收集器。
CMS 等收集器的关注点在于尽可能的缩短垃圾收集时用户线程暂停时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量。吞吐量就是 CPU 运行用户代码的时间与 CPU 总时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),jvm 总共运行了100分钟,其中垃圾回收用来1分钟,那么吞吐量为 99%。停顿时间越短越适合需要与用户交互的程序,而吞吐量则可以最高效率的使用 cpu 时间,尽可能快的完成程序计算任务,主要适合在后台运算不需与用户有交互的应用。
Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis 参数及设置吞吐量大小的 -XX:GCTimeRatio 参数。MaxGCPauseMillis 参数允许的值是一个大于 0 的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值。GCTimeRatio 参数的值应当是一个大于 0 小于 100 的参数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数,公式为1/(n+1)。Parallel Scavenge 收集器也称为“吞吐量优先”收集器, 它除了上面两个选项外,还有另外一个选项需要注意 -XX:UseAdaptiveSizePolicy,这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最适合的停顿时间或最大的吞吐量,这种调节方式成为GC自适应的调节策略(GC Ergonomics),只需要把基本的内存数据设置好(如 -Xmx 设置最大堆),然后使用 MaxGCPauseMillis 参数或 GCTimeRatioc 参数给虚拟机设立一个优化目标即可。自适应调节策略也是 Parallel Scavenge 收集器与 ParNew 收集器的一个重要区别。
4.Serial Old收集器
Serial Old 是 Serial 收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法,主要在 client 模式下使用,在 server 模式下,可以与 Parallel Scavenge 收集器搭配使用,也可以作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
5.Parallel Old收集器
Parallel Old 是 Parallel Scanvege 收集器的老年代版本,使用多线程和“标记整理”算法,在注重吞吐量和CPU资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
6.CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,重视服务的响应速度,系统停顿时间最短,给用户带来较好的体验。CMS 收集器是基于“标记-清除”算法的,垃圾回收分为4个步骤:1初始标记,2并发标记,3重新标记,4并发清除。其中初始标记和重新标记仍然需要暂停用户线程。初始标记仅仅是标记一下 GC Roots 能关联到的对象,速度很快,并发标记阶段就是进行 GC Roots Tracing 的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
CMS收集器缺点如下:
1.CMS 收集器默认启动的收集线程数是(CPU数量+3)/4,当 CPU 数量在 4 个以上时,并发垃圾回收时垃圾收集线程最多占用不超过 25% 的CPU资源,当时当 CPU 数量在4个一下时,CPU的负载就会比较高。
2.CMS 收集器无法处理“浮动垃圾”,因而会导致 Full GC 的产生。由于 CMS 在并发清理阶段用户线程也在运行,在运行过程中有新的垃圾产生,这一部分垃圾出现在标记过程之后,CMS 无法在本次收集中处理它们,只能留到下一次垃圾回事再处理,之一部分垃圾就成为浮动垃圾。在进行垃圾回收时要预留一部分内存空间给用户线程使用,所以 CMS 收集器默认情况下在老年代内存空间使用 68% 时就会被触发,要是 CMS 运行期间预留的内存无法满足程序需要,就会出现”Concurrent Mode Failure”失败,这是将启动后备预案:调用 Serial Old 收集器来对老年代进行垃圾回收,这样停顿时间就变长了。如果在应用中老年代增长的不是特别快,可以适当的调高参数 -XX:CMSInitiatingOccupancyFraction 的值来提高触发百分比,以便降低内存回收次数以获取更好的性能。
3.CMS 是一款基于“标记-清除”算法实现的收集器,垃圾收集后会产生内存碎片,将会给大对象分配带来很大的麻烦。为解决这个问题,CMS收集器提供了一个 -XX:UseCMSCompactAtFullCollection 的开关参数,用于在 Full GC 后进行一次内存碎片整理,但这会导致停顿时间变长,它还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在经过几次 Full GC 后才进行一次内存碎片整理。
7.G1收集器
G1 收集器是垃圾收集器理论进一步发展的参数,与 CMS 收集器相比有 2 个显著改进:一是 G1 是基于“标记-整理”算法实现的收集器,不会产生内存碎片,二是可以非常精确地控制停顿,既能让使用者明确指定一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
G1 收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,能极力的避免全区域的垃圾回收,原理是它将整个 Java 堆划分为多个大小固定的独立区域,跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域。