GC(垃圾回收)的假设

垃圾回收器的创建基于以下两个假设(也许称之为推论或前提更合适)

  • 大多数对象的很快就会变得不可达
  • 只有极少数情况会出现旧对象持有新对象的引用

GC内存空间的划分

  • 新生代:大部分的新创建对象分配在新生代。因为大部分对象很快就会变得不可达,所以它们被分配在新生代,然后消失不再。当对象从新生代移除时,我们称之为”minor GC”。
  • 老年代:存活在新生代中但未变为不可达的对象会被复制到老年代。一般来说老年代的内存空间比新生代大,所以在老年代GC发生的频率较新生代低一些。当对象从老年代被移除时,我们称之为”major GC”(或者full GC)。

新生代

  • 新生代又可分为三个部分:一个Eden区,两个Survivor区。
  • 对象在三个区域中的存活过程如下:
  1. 大多数新生对象都被分配在Eden区。
  2. 第一次GC过后Eden中还存活的对象被移到其中一个Survivor区。
  3. 再次GC过程中,Eden中还存活的对象会被移到之前已移入对象的Survivor区。
  4. 一旦该Survivor区域无空间可用时,还存活的对象会从当前Survivor区移到另一个空的Survivor区。而当前Survivor区就会再次置为空状态。
  5. 经过数次在两个Survivor区域移动后还存活的对象最后会被移动到老年代。

堆内存常见分配策略

  1. 对象优先分配在Eden区

  2. 大对象直接进入老年代
    大对象就是需要大量连续内存空间的对象(比如:字符串、数组),为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

  3. 长期存活的对象进入永久代
    既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
    如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

对象如何从新生代进入老年代

对象开始是创建在Eden区,然后经过在Survivor区域上的数次转移而存活下来的长寿对象最后会被移到老年代。

MinorGC 与 Full GC(Major GC)

新生代GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC.
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。

老年代垃圾回收

当老年代数据满时,便会执行老年代垃圾回收。根据GC算法的不同其执行过程也会有所区别,所以当你了解了每种GC的特点后再来理解老年代的垃圾回收就会容易很多。

Serial GC(-XX:+UseSerialGC)

Serial GC务必不要在生产环境的服务器上使用,这种GC是为单核CPU上的桌面应用设计的。使用Serial GC会明显的损耗应用的性能。
Serial GC适用于CPU核数较少且使用的内存空间较小的场景。
老年代GC过程:

  1. 首先该算法需要在老年代中标记出存活着的对象
  2. 然后从前到后检查堆空间中存活的对象,并保持位置不变(把不再存活的对象清理出堆空间,称为空间清理)
  3. 最后,把存活的对象移到堆空间的前面部分以保持已使用的堆空间的连续性,从而把堆空间分为两部分:有对象的和无对象的(称为空间压缩)

如何判断一个对象是否死亡

对象是否已经无效

  1. 引用计数法
    给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
    这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题

  2. 可达性分析算法
    这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

再谈引用

在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生

  1. 强引用
    以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
  2. 软引用
    如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
  3. 弱引用
    如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
  4. 虚引用
    “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
    虚引用主要用来跟踪对象被垃圾回收的活动。

不可达的对象并非“非死不可”

即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

如何判断一个常量是废弃常量

假如在常量池中存在字符串 “abc”,如果当前没有任何String对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,”abc” 就会被系统清理出常量池。

如何判断一个类是无用的类

满足一下三点
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法

标记-清除算法

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,效率也很高,但是会带来两个明显的问题:效率问题 和 空间问题(标记清除后会产生大量不连续的碎片)

复制算法

它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

标记-整理算法

根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

垃圾收集器

Serial收集器

Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
新生代采用复制算法,老年代采用标记-整理算法。
简单而高效(与其他收集器的单线程相比)
Serial收集器对于运行在Client模式下的虚拟机来说是个不错的选择。

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。
新生代采用复制算法,老年代采用标记-整理算法。
它是许多运行在Server模式下的虚拟机的首要选择

Parallel Scavenge收集器

Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。
新生代采用复制算法,老年代采用标记-整理算法。

Parallel GC(-XX:+UseParallelGC)

mark
老年代串行 Serial GC使用单一线程执行GC,而parallel GC则使用多个线程并发执行,因此parallel GC 较serial GC具有更快的速度。Parallel GC适用于多核CPU且使用了较大内存空间的场景。Parallel GC又被称为”高吞吐GC(throughput GC)”

Parallel Old GC(-XX:+UseParallelOldGC)

老年代并行 Parallel Old GC在JDK 5中被引入,与Parallel GC相比唯一的区别在于Parallel的GC算法是为老年代设计的。

CMS GC(-XX:+UseConcMarkSweepGC) 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。
得益于CMS GC的执行方式,在GC期间系统中断时间非常短暂。CMS GC也被称为低延迟GC,适用于所有应用对响应时间要求比较严格的场景。
CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
标记-清除算法
缺点:

  1. 与其他GC相比,CMS GC要求更多的内存空间和CPU资源
  2. CMS GC默认不提供内存压缩,产生大量空间碎片
  3. 无法处理浮动垃圾

G1 GC(重要) 收集器

G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.
并行与并发,分代收集,空间整合,可预测的停顿
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)
mark
如上图所示,每个对象在创建时会分析到一个格子中,后续的GC也是在格子中完成的。每当一个区域分配满对象后,新创建的对象就会分配到另外一个区域,并开始执行GC。在这种GC中不会出现其他GC中的对象在新生代和老生代三区域中移动的现象。G1是为了取代在长期使用中暴露出大量问题且饱受抱怨的CMS GC。