一、基本概念
1、HotSpot VM :是由 Oracle 公司开发的一种 Java 虚拟机(JVM),是 Java SE 平台上最广泛使用的虚拟机之一。它是 OpenJDK 的一部分,也是 Oracle JDK 的基础之一。使用即时编译(Just-In-Time Compilation,JIT)技术来提高 Java 程序的性能。
2、GC 堆:Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆 (Garbage Collected Heap)。
3、HotSpot VM 的GC 实现分类只有两大种:
部分收集 (Partial GC):
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC):收集整个 Java 堆和方法区。
二、堆空间的基本结构:
Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间一层属于老年代,最下面一层属于永久代(jdk 1.8之后用元空间代替)。
1、新生代(Young Generation):
新生代是堆空间的一部分,主要用于存放新创建的对象。它通常被划分为 Eden 区、两个 Survivor 区(通常是 From 和 To 区)。
新创建的对象首先会被分配到 Eden 区。当 Eden 区空间不足时,触发一次 Minor GC(年轻代垃圾收集),Eden 区中存活的对象将被移动到其中一个 Survivor 区。经过多次 Minor GC 后,仍然存活的对象会被移动到另一个 Survivor 区,同时清空该 Survivor 区。这个过程称为对象的年龄晋升。
对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。
2、老年代(Old Generation):
老年代用于存放经过多次年龄晋升后仍然存活的对象,以及大对象。在一些垃圾收集器中,还可能包含部分永久代(PermGen)或元空间(Metaspace)的数据。
当老年代空间不足时,会触发一次 Full GC(完全垃圾收集),对整个堆空间进行垃圾收集。Full GC 会清理所有的新生代和老年代,以及执行一些额外的清理工作,如永久代的垃圾回收和类卸载。
3、永久代(PermGen)或元空间(Metaspace):
在较早的Java版本中,永久代用于存储类的元数据信息、常量池等。但从Java 8 开始,永久代被移除,取而代之的是元空间(Metaspace)。
元空间用于存储类的元数据信息,包括类的结构、方法、字段等。它位于堆空间之外,通常由操作系统的本机内存管理。
三、内存分配和回收原则
-
对象优先在 Eden 区分配:当一个对象被创建时,它首先会被分配到 Eden 区域,这是新生代中的一部分。大部分对象的生命周期都很短暂,它们很快就会成为垃圾,因此将它们分配到 Eden 区是合理的。
-
大对象直接进入老年代:对于大对象(大于一定大小阈值,字符串、数组、集合对象等),JVM 可能会选择直接将它们分配到老年代,而不是分配到新生代的 Eden 区。这样做的原因是,大对象往往生命周期较长,如果将它们分配到新生代,可能会导致频繁的 Minor GC,影响程序的性能。
-
长期存活的对象将进入老年代:当对象经过多次垃圾收集仍然存活下来时,它们可能会被认为是长期存活的对象。这些对象会逐渐从新生代的 Survivor 区域晋升到老年代。这是因为老年代相对于新生代来说,拥有更大的内存空间,更适合长期存活的对象。
四、判断对象是否死亡的方法
垃圾收集器主要负责回收堆内存中的对象,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡。
1、引用计数法
给对象中添加一个引用计数器:
每当有一个地方引用它,计数器就加 1;
当引用失效,计数器就减 1;
任何时候计数器为 0 的对象就是不可能再被使用的。
缺点:很难解决对象之间循环引用的问题。
2、可达性分析算法
算法的基本思想是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
根对象通常包括以下几种类型:
栈中的引用:包括本地变量表中的引用、方法参数等。
静态变量引用:即类变量,存在于类的静态区域。
JNI(Java Native Interface)引用:通过JNI接口创建的引用。
一旦确定了哪些对象是活动对象,垃圾收集器就可以清理未被标记的对象,释放它们占用的内存空间。通常,被回收的对象将被归入不同的代(新生代或老年代),并根据对象的存活时间来选择不同的垃圾收集算法进行处理。
五、垃圾收集算法
-
标记-清除算法(Mark-Sweep Algorithm):这是最基本的垃圾收集算法之一。它分为两个阶段:标记阶段和清除阶段。在标记阶段,从根对象开始,通过可达性分析标记所有活动对象。在清除阶段,遍历整个堆内存,清除未标记的对象,即垃圾对象。标记-清除算法的缺点是会产生内存碎片,可能导致堆内存的不连续性,影响内存分配效率。
-
复制算法(Copying Algorithm):复制算法将堆内存分为两个大小相等的区域,一部分称为活动区,用于存放存活的对象;另一部分称为闲置区,用于垃圾收集。当活动区的内存空间耗尽时,将活动区中存活的对象复制到闲置区,同时清除活动区中的所有对象。这样可以避免内存碎片的产生,但需要额外的空间用于存放复制后的对象。
-
标记-整理算法(Mark-Compact Algorithm):标记-整理算法结合了标记-清除算法和复制算法的优点。首先,通过可达性分析标记所有活动对象;然后,将活动对象紧凑地移动到堆内存的一端,清除堆内存另一端的所有未标记对象,即垃圾对象。标记-整理算法可以避免内存碎片的产生,并且不需要额外的内存空间,但可能会产生对象移动的额外开销。
一般来说新生代采用标记-复制算法,老年代采用标记-整理或者标记-清除算法。
六、垃圾收集器
JDK 默认垃圾收集器(使用 java -XX:+PrintCommandLineFlags -version 命令查看)。
1、JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代)
Parallel Scavenge 是一种面向新生代的垃圾收集器,它使用复制算法来实现快速的垃圾收集。它会尽可能地利用多线程来加速垃圾收集过程,适用于需要高吞吐量的场景。
Parallel Old 是一种面向老年代的垃圾收集器,同样采用并行方式进行垃圾收集。它适用于大型应用程序,能够在较短时间内完成全局垃圾收集。
2、JDK 9 ~ JDK20: G1
G1 是 JDK 9 及以后版本中引入的全新垃圾收集器,它是一种面向整个堆内存的并发、分代、照顾吞吐量的垃圾收集器。相比于传统的 CMS(Concurrent Mark-Sweep)收集器,G1 在堆内存管理和回收方面提供了更好的性能和可预测性。
G1 垃圾收集器在处理大堆内存时表现出色,它通过将堆内存划分为多个大小相等的区域(Region),并根据垃圾收集的需要来动态调整区域的大小和分配策略,从而有效地避免了堆内存碎片化问题,并且能够在保证较低停顿时间的同时实现高吞吐量的垃圾收集。G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。
3、Serial 收集器
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
4、ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本,它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器配合工作。
其他的就不一一介绍了。