文章目录
- 前言
- 一、死亡对象的判断方法
- 1.1 引用计数算法
- 1.2 可达性分析算法
- 二、垃圾回收算法
- 2.1 标记-清除算法
- 2.2 复制算法
- 2.3 标记-整理算法
- 2.5 分代算法
- 2.6 Minor GC 和 Major GC
前言
JVM 的垃圾回收机制(Garbage Collection)是 Java 中的重要特性之一,它负责在程序运行时自动回收不再使用的内存,以避免内存泄漏和提高程序的性能。垃圾回收机制的设计与实现对于 Java 程序的运行效率和稳定性至关重要。因此,作为一名合格的程序员,对于 JVM 的垃圾回收机制势必要有深入的了解,才能编写设计出合理高效的代码。
一、死亡对象的判断方法
在 JVM 的垃圾回收机制中,判断一个对象是否死亡是非常重要的,因为只有死亡的对象才能够被垃圾回收器回收,释放其占用的内存。其中常用的判断方法包括引用计数算法
和可达性分析算法
。
1.1 引用计数算法
引用计数算法是一种简单的垃圾回收算法,它的基本思想就是为每一个对象都维护一个引用计数器,用于记录当前有多少个引用指向该对象。当引用计数器变为零的时候,表示该对象没有任何引用指向它,因此就可以判断这个对象为死亡状态,就可以进行回收了。
引用计数的实现非常简单,判断效率也很高,在大部分情况下都是一个不错的算法。比如 Python 就是采用的这种方法实现对内存的管理。但是引用计数的最大问题就是循环引用。
例如下面的代码就展示了循环引用:
class Test{
public Test test;
}
public class Demo {
public static void main(String[] args) {
Test test1 = new Test();
Test test2 = new Test();
test1.test = test2;
test2.test = test1;
test1 = null;
test2 = null;
}
}
以上代码就是一个简单循环引用的案例,类Test
中的test
字段是一个引用类型,它指向另一个Test
对象。在 main
函数中,分别常见了两个Test
对象,即 test1
和test2
,然后将其相互赋值给对方的 test
字段。
如果是采用引用计数的方式,由于test1
和test2
相互引用,它们的引用计数器始终不为零,因为它们之间的引用数始终为1。根据引用计数算法的原理,即使这两个对象不再被程序使用,它们的引用计数器也不会变为零,因此不会被垃圾回收器回收。
1.2 可达性分析算法
为了解决循环引用的问题,JVM 采用了更为复杂的可达性分析算法。可达性分析算法的基本思想是以一组称为 GC Roots
的对象作为起始点,通过向下搜索和遍历对象应用链,判断对象是否可达(reachable)。
对象可达的含义:
- 一个对象是可达的,意味着它可以通过一系列引用链从
GC Roots
对象到达。- 如果一个对象不可达,即它没有与任何
GC Roots
对象相连接,那么该对象将被判断为死亡状态,即可以被垃圾回收器回收。
JVM 中的GC Roots
对象包括:
- 已加载类的类对象(Class Object);
- 类静态变量引用的对象;
- 活动线程(Active Thread)的栈帧(Stack Frame);
JNI(Java Native Interface)
中的本地方法栈(Native Method Stack)中引用的对象。
通过可达性分析算法,JVM 能够准确地判断对象的生命周期,避免了引用计数算法的循环引用问题,并有效地进行垃圾回收。
二、垃圾回收算法
垃圾回收算法是 JVM 垃圾回收机制的重要组成部分,它负责回收不再使用的对象,释放内存资源,确保程序的运行效率和稳定性。在 JVM 中,常见的垃圾回收算法包括:标记-清除算法、复制算法、标记-整理算法已经分代算法。
2.1 标记-清除算法
标记-清除算法
是最基本的垃圾回收算法之一。其过程分为两个阶段:标记阶段和清除阶段。
- 标记阶段:从一组称为
GC Roots
的对象出发,遍历所有可达对象,并将它们进行标记,表示这些对象是活动对象,不会被回收。 - 清除阶段:遍历整个堆空间,将没有标记的对象视为垃圾对象,直接回收这些垃圾对象的内存。回收后的内存形成一些不连续的碎片,可能会造成内存碎片化问题。
2.2 复制算法
复制算法
是为了解决标记-清除算法
的内存碎片化问题而设计的一种垃圾回收算法。它将堆空间划分为两个大小相等的区域,每次只使用其中的一半空间。其过程分为三个阶段:标记阶段、复制阶段和角色互换阶段。
- 标记阶段:与
标记-清除算法
相同,从GC Roots
对象出发,遍历所有可达对象,标记活动对象。 - 复制阶段:将所有活动对象从一个区域复制到另一个区域,使得复制后的内存是连续的,不会出现内存碎片化问题。
- 角色互换:完成复制后,两个区域的角色互换,原来的存活对象成为新的空闲区域,原来的空闲区域成为新的工作区域。
现在的商用虚拟机,包括
HotSpot
都是采用这种收集算法来回收新生代区域的对象。
- 新生代中 98% 的对象都是
朝生夕死
的,所以并不需要按照1 : 1
的比例来划分内存空间,而是将内存(新生代内存)分为一块较大的Eden
(伊甸园)空间和两块较小的Survivor
(幸存者)空间。- 每次使用
Eden
和其中一块Survivor
(两个Survivor
区域一个称为From(S0)
区,另一个称为To (S1)
区域)。- 当回收时,将
Eden
和Survivor
中还存活的对象一次性复制到另一块Survivor
空间上,最后清理掉Eden
和刚才用过的Survivor
空间。- 当
Survivor
空间不够用时,需要依赖其他内存(如老年代)进行分配担保。
关于HotSpot
:
HotSpot
默认Eden
与Survivor
的大小比例是 8 : 1
,也就是说Eden : Survivor From : Survivor To = 8:1:1
。所以每次新生代可用内存空间为整个新生代容量的 90%,而剩下的 10% 用来存放回收后存活的对象。
HotSpot是Java平台上使用最广泛的Java虚拟机(JVM)的一种实现。它由Oracle(前身是Sun Microsystems)开发,是Java >Development Kit(JDK)的一部分,也是OpenJDK的默认JVM实现之一。
HotSpot
实现的复制算法流程如下:
- 当
Eden
区满的时候,会触发第一次Minor GC
,把还活着的对象拷贝到Survivor From
区;当Eden
区再次触发Minor GC
的时候,会扫描Eden
区和From
区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To
区域,并将Eden
和From
区域清空。 - 当后续
Eden
又发生Minor GC
的时候,会对Eden
和To
区域进行垃圾回收,存活的对象复制到From
区域,并将Eden
和To
区域清空。 - 部分对象会在
From
和To
区域中来回复制
,如此交换15次(由 JVM 参数MaxTenuringThreshold
决定,这个参数默认是15),最终如果还是存活的,就存入到老年代。
2.3 标记-整理算法
复制收集算法
在对象存活率较高时会进行比较多的复制操作,效率会变低,因此在老年代一般不能使用复制算法。
针对老年代的特点,提出了标记-整理算法
。标记过程仍与标记-清除
过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。流程图如下:
2.5 分代算法
以上三种算法都存在一些共同的问题:
-
效率问题:
标记-清除算法
和标记-整理算法
需要对整个堆空间进行遍历,可能导致垃圾回收的效率较低。 -
内存碎片问题:
标记-清除算法
在回收阶段会产生内存碎片,造成内存空间的浪费和不连续的内存布局。
而分代算法
的诞生为了解决这些问题的,分代算法
是一种综合利用多种垃圾回收算法的策略,通过对堆内存进行区域划分,针对不同区域采用不同的垃圾回收策略,从而实现更好的垃圾回收效果。
分代算法
的设计思想:
在 Java 程序中,不同对象的生命周期
往往是不同的。大部分新创建的对象很快就会变成垃圾,而一些对象可能会长时间存活。因此,根据这个特点就将堆划分为不同的代,分别处理不同生命周期的对象,这样就能更好地优化垃圾回收的效率和性能。
分代算法通常将堆内存划分为以下几个代:
-
新生代(Young Generation):新创建的对象通常被分配到新生代。新生代使用
复制算法
进行垃圾回收,因为这些对象的生命周期短,产生的垃圾较多。 -
老年代(Old Generation):经过多次 GC 仍然存活的对象被移到老年代。老年代使用
标记-清除
或标记-整理算法
进行垃圾回收,因为这些对象的生命周期较长,产生的垃圾相对较少。 -
永久代(Permanent Generation):永久代用于存放类的元数据和常量等信息,在Java 8之后,永久代被元空间(Metaspace)所取代。
通过分代算法,不同代采用不同的垃圾回收策略,能够针对不同生命周期的对象进行更精细的垃圾回收,避免将整个堆空间都遍历,提高垃圾回收效率。这样的设计使得垃圾回收器能够根据应用程序的运行情况动态调整回收策略,从而更好地适应不同的应用场景。
2.6 Minor GC 和 Major GC
在 JVM 中,垃圾回收(Garbage Collection,GC)可以根据执行的任务不同而分为两种类型:即 Minor GC
和 Major GC(也称为 Full GC)
。
Minor GC
:
Minor GC
是针对新生代
的垃圾回收过程。新生代
是 Java 堆内存中的一部分,用于存放刚被创建的对象。通常,新创建对象的生命周期较短,因此在新生代
使用复制算法
进行垃圾回收。
Minor GC
的工作过程如下:
-
标记阶段:从
GC Roots
对象出发,标记所有在新生代中存活的对象。 -
复制阶段:将所有存活的对象从
Eden
区复制到Survivor
区。 -
角色互换:完成复制后,
Eden
区和Survivor
区的角色互换,使得原来的Eden
区成为新的空闲区,原来的Survivor
区成为新的工作区。
Minor GC
的目的是清理新生代中的垃圾对象,使得新生代能够为新的对象分配空间,尽量保证新生代的空间是连续的,避免产生内存碎片。
Major GC
:
Major GC
是针对老年区的垃圾回收过程,老年区用于存放长时间存活的对象。用于老年区中的对象生命周期较长,如果使用复制算法进行垃圾回收可能就会导致较大的复制成本,因此通常使用的是标记-清除
或标记-整理算法
。
Major GC
的工作过程如下:
-
标记阶段:从
GC Roots
对象出发,标记所有在老年代中存活的对象。 -
清除或整理阶段:根据采用的算法进行相应的垃圾回收操作。
标记-清除算法
将清除未标记的垃圾对象,标记-整理算法
将移动存活对象并清理未标记的垃圾对象。
Major GC
的目的是清理老年代中的垃圾对象,以避免老年代占用过多内存资源,同时也是为了确保老年代的空间是连续的,避免内存碎片化问题。