垃圾收集主要是针对堆和方法区进行。算法
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束以后也会消失,所以不须要对这三个区域进行垃圾回收。多线程
判断一个对象是否可被回收并发
给对象添加一个引用计数器,当对象增长一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。框架
两个对象出现循环引用的状况下,此时引用计数器永远不为 0,致使没法对它们进行回收。函数
正由于循环引用的存在,所以 Java 虚拟机不使用引用计数算法。性能
public class ReferenceCountingGC { public Object instance = null; public static void main(String[] args) { ReferenceCountingGC objectA = new ReferenceCountingGC(); ReferenceCountingGC objectB = new ReferenceCountingGC(); objectA.instance = objectB; objectB.instance = objectA; } }
经过 GC Roots 做为起始点进行搜索,可以到达到的对象都是存活的,不可达的对象可被回收。测试
Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 通常包含如下内容:spa
由于方法区主要存放永久代对象,而永久代对象的回收率比新生代低不少,所以在方法区上进行回收性价比不高。线程
主要是对常量池的回收和对类的卸载。翻译
在大量使用反射、动态代理、CGLib 等 ByteCode 框架、 动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都须要虚拟机具有类卸载功能,以保证不会出现内存溢出。
类的卸载条件不少,须要知足如下三个条件,而且知足了也不必定会被卸载:
能够经过 -Xnoclassgc 参数来控制是否对类进行卸载。
finalize() 相似 C++ 的析构函数,用来作关闭外部资源等工做。可是 try-finally 等方式能够作的更好, 而且该方法运行代价高昂,不肯定性大,没法保证各个对象的调用顺序,所以最好不要使用。
当一个对象可被回收时,若是须要执行该对象的 finalize() 方法, 那么就有可能在该方法中让对象从新被引用,从而实现自救。 自救只能进行一次,若是回收的对象以前调用了 finalize() 方法自救,后面回收时不会调用 finalize() 方法。
不管是经过引用计算算法判断对象的引用数量,仍是经过可达性分析算法判断对象是否可达, 断定对象是否可被回收都与引用有关。
Java 提供了四种强度不一样的引用类型。
被强引用关联的对象不会被回收。
使用 new 一个新对象的方式来建立强引用。
Object obj = new Object();
被软引用关联的对象只有在内存不够的状况下才会被回收。
使用 SoftReference 类来建立软引用。
Object obj = new Object(); SoftReference<Object> sf = new SoftReference<Object>(obj); obj = null; // 使对象只被软引用关联
被弱引用关联的对象必定会被回收,也就是说它只能存活到下一次垃圾回收发生以前。
使用 WeakReference 类来实现弱引用。
Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj); obj = null;
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在, 彻底不会对其生存时间构成影响,也没法经过虚引用取得一个对象。
为一个对象设置虚引用关联的惟一目的就是能在这个对象被回收时收到一个系统通知。
使用 PhantomReference 来实现虚引用。
Object obj = new Object(); PhantomReference<Object> pf = new PhantomReference<Object>(obj); obj = null;
将存活的对象进行标记,而后清理掉未被标记的对象。
不足:
让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存。
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另外一块上面,而后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。
如今的商业虚拟机都采用这种收集算法来回收新生代,可是并非将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另外一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。
HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。若是每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时须要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。
如今的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不一样块采用适当的收集算法。
通常将堆分为新生代和老年代。
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器能够配合使用。
Serial 翻译为串行,也就是说它以串行的方式执行。
它是单线程的收集器,只会使用一个线程进行垃圾收集工做。
它的优势是简单高效,对于单个 CPU 环境来讲,因为没有线程交互的开销,所以拥有最高的单线程收集效率。
它是 Client 模式下的默认新生代收集器,由于在该应用场景下,分配给虚拟机管理的内存通常来讲不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间能够控制在一百多毫秒之内,只要不是太频繁,这点停顿是能够接受的。
它是 Serial 收集器的多线程版本。
是 Server 模式下的虚拟机首选新生代收集器,除了性能缘由外,主要是由于除了 Serial 收集器,只有它能与 CMS 收集器配合工做。
默认开启的线程数量与 CPU 数量相同,可使用 -XX:ParallelGCThreads 参数来设置线程数。
与 ParNew 同样是多线程收集器。
其它收集器关注点是尽量缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
停顿时间越短就越适合须要与用户交互的程序,良好的响应速度能提高用户体验。而高吞吐量则能够高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不须要太多交互的任务。
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,致使吞吐量降低。
能够经过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics), 就不须要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。 虚拟机会根据当前系统的运行状况收集性能监控信息, 动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。 自适应调节策略是 Parallel Scavenge 收集器和 ParNew 收集器的一个重要区别。
是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用,采用标记-整理算法。若是用在 Server 模式下,它有两大用途:
是 Parallel Scavenge 收集器的老年代版本,采用标记-整理算法。
在注重吞吐量以及 CPU 资源敏感的场合,均可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
分为如下四个流程:
在整个过程当中耗时最长的并发标记和并发清除过程当中,收集器线程均可以与用户线程一块儿工做,不须要进行停顿,具备并发收集、低停顿的优势。
具备如下缺点:
CMS 已经在 JDK 9 中被标记为废弃( deprecated )。
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是将来能够替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 能够直接对新生代和老年代一块儿回收。
G1 把堆划分红多个大小相等的独立区域(Region),Region的大小是一致的,数值是在1M到32M字节之间的一个2的幂值数,JVM会尽可能划分2048个左右、同等大小的Region,新生代和老年代再也不物理隔离。
经过引入 Region 的概念,从而将原来的一整块内存空间划分红多个的小空间,使得每一个小空间能够单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。经过记录每一个 Region 垃圾回收时间以及回收所得到的空间(这两个值是经过过去回收的经验得到),并维护一个优先列表,每次根据容许的收集时间,优先回收价值最大的 Region。
每一个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。经过使用 Remembered Set,在作可达性分析的时候就能够避免全堆扫描。
若是不计算维护 Remembered Set 的操做,G1 收集器的运做大体可划分为如下几个步骤:
从 GC 算法的角度, G1 选择的是复合算法,能够简化理解为:
具有以下特色:
目前尚处于开发中的 JDK 11, JDK 又增长了两种全新的 GC 方式,分别是:
- Epsilon GC,简单说就是个不作垃圾收集的GC,彷佛有点奇怪,有的状况下,例如在进行性能测试的时候,可能须要明确判断GC自己产生了多大的开销,这就是其典型应用场景。
- ZGC,这是Oracle开源出来的一个超级GC实现,具有使人惊讶的扩展能力,好比支持T bytes级别的堆大小,而且保证绝大部分状况下,延迟都不会超过10 ms。虽然目前还处于实验阶段,仅支持 Linux 64 位的平台,但其已经表现出的能力和潜力都很是使人期待。