目录
堆垃圾的产生原理
在程序执行过程中,函数调用和返回会导致栈内存的分配与回收。基本数据类型和引用类型变量在栈上分配,当函数返回时,这些栈内存会自动释放。然而,对象是在堆上分配的,栈上仅存储指向这些对象的引用(地址)。当函数返回时,引用本身会被回收,但它们指向的堆上对象不会自动回收,除非所有指向该对象的引用都被回收。
这样,当一个对象的所有引用都不再存在时,该对象就变成了垃圾。以下面的代码为例,函数g()
中创建了两个Person
对象,local
和nonLocal
。local
对象的引用只在g()
函数内部存在,当g()
返回时,这个引用被回收,因此local
对象变成了垃圾。而nonLocal
对象被返回给f()
函数,所以它的引用在f()
中仍然存在,直到f()
中的引用gPerson
也被回收,nonLocal
对象才会变成垃圾。
java
public void f() {
// 在f()方法中,调用g()方法并接收返回的Person对象引用
Person gPerson = g();
// 打印gPerson对象的名称
System.out.println(gPerson.getName());
}
public Person g() {
// 在g()方法中,创建一个新的Person对象并存储在栈上的局部变量local中
Person local = new Person();
// 再次创建一个新的Person对象并存储在栈上的局部变量nonLocal中
Person nonLocal = new Person();
// 在g()方法返回时,栈上的local引用会被自动回收
// 但是分配在堆上的local对象,因为没有其他引用指向它,所以它变成了垃圾,等待垃圾回收器回收
// 而nonLocal引用在g()方法返回后会被回收,但是g()方法会复制一份引用返回给f()方法
// 因此,nonLocal指向的对象在f()方法中仍然有一个引用指向它,所以它还不是垃圾
return nonLocal;
}
堆垃圾的识别方法
-
引用计数(Reference Counting):
-
这种方法的核心思想是为每个对象维护一个计数器,记录指向该对象的引用数量。
-
当一个引用被创建时,计数器增加;当一个引用被销毁时,计数器减少。
-
当一个对象的引用计数器达到0时,意味着没有任何引用指向该对象,因此可以被回收。
-
这种方法简单直观,但是存在一些问题,比如循环引用的问题,即两个对象互相引用,它们的计数器永远不会达到0,导致内存泄漏。
-
-
引用追踪(Tracing):
-
这种方法不依赖于引用计数,而是通过从一组已知的活动对象(通常是栈上的引用)开始,递归地遍历所有可达的对象。
-
这个过程通常被称为"标记-清除"(Mark-Sweep)算法,其中"标记"阶段就是遍历过程,而"清除"阶段则是回收未被标记的对象。
-
引用追踪算法可以是深度优先搜索(DFS)或广度优先搜索(BFS)。
-
这种方法可以解决循环引用的问题,因为它能够识别出所有可达的对象,并将不可达的对象视为垃圾。
-
垃圾的回收策略
垃圾回收策略是垃圾回收器用来识别和回收不再使用的对象以释放内存的方法。以下是三种常见的垃圾回收策略及其说明、优缺点分析:
-
Mark-Sweep(标记-清除):
-
说明:首先标记所有存活的对象,这通常通过引用追踪来实现。然后,遍历整个堆,回收那些没有被标记的对象。
-
优点:实现相对简单,不需要移动对象。
-
缺点:可能会产生内存碎片,因为清除后的空间是分散的,这可能导致大对象无法找到足够的连续空间而需要进行垃圾回收。
-
-
Mark-Compact(标记-整理):
-
说明:先标记所有存活的对象,然后遍历整个堆,为所有存活对象计算新地址,将对象复制到新地址,并修正所有引用。
-
优点:解决了内存碎片化的问题,因为通过整理存活对象,可以释放出连续的内存空间。
-
缺点:实现较为复杂,效率较低,因为需要三步操作:标记、计算新地址和复制对象,这可能涉及到暂停应用程序(Stop-The-World)。
-
-
Copy(复制):
-
说明:将内存分为两半,S1和S2。开始时,所有对象在S1中分配,S2为空。当S1满时,触发垃圾回收,将所有存活对象复制到S2,清空S1。每次回收后,S1和S2的角色互换。
-
优点:效率高,因为只需要一次遍历就可以完成标记、复制存活对象和引用修正。此外,这种方法针对的是存活对象,而不是整个堆,且在触发垃圾回收时,内存使用率可以达到50%。
-
缺点:内存使用率不高,因为只有一半的内存被使用。在最坏的情况下,内存使用率可能不到50%。
-
业务线程和GC线程的关系
在垃圾回收(GC)过程中,业务线程和GC线程之间的相互影响是一个需要特别处理的问题。业务线程在执行时会不断修改栈,这可能会干扰GC的引用追踪过程。同时,GC在进行引用修正时,也可能会干扰业务线程对对象的访问。为了解决这些问题,我们引入了两个关键概念:Stop the World和GC safepoint。
Stop the World是一种大原则,指的是在触发GC时,所有的业务线程会被挂起,直到GC完成。这种做法实际上是让业务线程为GC让路,以确保GC能够无干扰地执行。
而GC safepoint则是在细节层面上,确保业务线程和GC线程之间的协调。程序在执行过程中,堆和栈的状态并不总是同步的。例如,在创建对象时,需要先在堆上分配空间,然后将引用推到栈上。如果在这个过程中堆内存已经分配,但引用还没有被推到栈上,GC就开始了,那么在引用追踪时,新分配的内存会被认为是垃圾,这显然是不正确的。因此,GC需要等待所有业务线程达到一个"安全的状态",即堆和栈的状态一致时,才能开始。这个"安全的状态"就是所谓的GC safepoint。
简而言之,Stop the World确保了业务线程在GC期间不会干扰GC的执行,而GC safepoint则确保了在GC开始前,业务线程已经达到了一个堆和栈状态一致的安全点,从而避免了不一致性带来的问题。这两个机制共同作用,以减少业务线程和GC线程之间的相互影响,确保垃圾回收过程的顺利进行。