目录
一、GC是什么
二、引用计数法详解
三、复制算法详解
四、标记压缩清除算法
JVM中的GC指的是垃圾回收(Garbage Collection)。在Java虚拟机中,程序运行时会产生很多对象,这些对象可能会被程序不再使用,但它们仍然占据着内存空间,如果不及时清理,就会导致内存泄漏或内存溢出等问题。因此,Java虚拟机会通过垃圾回收器对这些无用对象进行自动清理和释放,以保证程序能够正常运行并且不会出现内存相关的问题。垃圾回收器可以根据不同的算法和策略来实现垃圾回收,包括标记-清除算法、复制算法、标记-整理算法等。
JVM中的引用计数法是一种垃圾回收算法。它的基本思想是通过记录每个对象被引用的次数,当引用该对象的变量被销毁时,将该对象的引用计数减1;如果发现某个对象的引用计数为0,则说明该对象已经不再被使用,可以进行垃圾回收了。
然而,由于引用计数法无法处理循环引用的情况,即两个或多个对象互相引用形成一个环,因此在实际应用中很少使用这种垃圾回收算法。目前主流的垃圾回收算法包括标记-清除算法、复制算法、标记-整理算法等。
下面是一个C++的引用计数法代码示例,供参考:
#include
using namespace std;
class ReferenceCountedObject
{
public:
int reference_count;
ReferenceCountedObject() : reference_count(0) {}
virtual ~ReferenceCountedObject() {}
};
class SomeClass : public ReferenceCountedObject
{
public:
SomeClass() { cout << "SomeClass constructed" << endl; }
~SomeClass() { cout << "SomeClass destroyed" << endl; }
};
int main()
{
SomeClass* obj1 = new SomeClass();
obj1->reference_count++;
SomeClass* obj2 = obj1;
obj2->reference_count++;
if (obj1->reference_count == 2)
{
delete obj1;
}
if (obj2->reference_count == 2)
{
delete obj2;
}
return 0;
}
这个示例通过定义一个ReferenceCountedObject
类来维护每个对象的引用计数,SomeClass
类继承自ReferenceCountedObject
类并实现具体功能。在main
函数中,我们创建了两个指向同一个对象的指针,并对它们进行了引用计数。当其中一个指针被销毁时,我们检查其所指向的对象的引用计数是否为0,如果是,则说明该对象已经不再被引用,可以进行垃圾回收。
JVM的复制算法是一种垃圾回收算法,它主要应用于新生代的垃圾回收。在堆内存中,将新生代分为两个相等大小的区域:一个是Eden区,一个是Survivor区(通常有两个Survivor区)。当对象被创建时,会被放入Eden区,如果经过一次Minor GC后,仍然存活的对象会被移动到Survivor区中的一个,并且它们的年龄加1。当Survivor区空间不足时,剩余存活对象会被移动到另一个Survivor区中,并且它们的年龄重置为1。
复制算法的名称来源于其基本思想,即将存活的对象从一个区域复制到另一个区域。这样做的好处是可以避免内存碎片的产生,而且复制算法在进行垃圾回收时非常高效,因为只需要对存活对象进行复制和移动就可以了,不需要扫描整个堆空间。
然而,复制算法也有一些限制,例如需要至少两倍的空间来存放所有存活对象,如果存活对象很多,则可能导致堆空间不足。此外,由于每次垃圾回收都需要将存活对象复制到另一个区域,所以频繁进行垃圾回收会导致复制的开销很大。因此,复制算法主要应用于新生代,而老年代则通常采用其他垃圾回收算法。
JVM的复制算法是一种基于分代的垃圾回收算法,其主要思想是将堆内存分为两个区域:一个存活对象区和一个空闲对象区。在进行垃圾回收时,首先将存活对象从存活对象区复制到空闲对象区,然后清空存活对象区中的所有对象,使其变为空闲对象区。这样,在下一次垃圾回收时,就可以直接使用空闲对象区,而不需要再去扫描存活对象区。
下面是一个简单的JVM复制算法的示例代码:
public class CopyingGC {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB];
}
}
在上面的示例中,我们定义了4个byte数组,并依次分配了8MB、8MB、8MB、16MB的内存。在进行垃圾回收时,JVM会将这些存活对象从存活对象区复制到空闲对象区,并将存活对象区清空。如果对比存活对象区和空闲对象区的内存占用情况,我们会发现存活对象区的内存占用量大约是8MB,而空闲对象区的内存占用量为0。这说明JVM通过复制算法将存活对象从存活对象区复制到空闲对象区,并且成功地回收了不再使用的内存。
JVM的标记压缩清除算法是一种垃圾回收算法,也称为“标记-压缩算法”或“标记-整理算法”。它在标记-清除算法的基础上进行了改进,解决了标记-清除算法中会产生内存碎片的问题。
标记-压缩清除算法的基本流程如下:
1.首先和标记-清除算法一样,从根节点开始遍历所有可达对象,将其标记为“活动对象”。
2.然后,将所有活动对象压缩到堆的一端,也就是将它们移动到连续的地址空间中。这个过程也称为“压缩”或“整理”,相当于把所有活动对象都移到了一起,以便在后续的内存分配时能够使用较大的连续块。
3.最后,对堆的另一端的未被使用的内存进行回收。
相比标记-清除算法,标记-压缩清除算法的优势在于可以减少内存碎片的产生,提高内存的利用率,在应对长时间运行的程序时,效果尤为明显。但同时,由于需要移动对象,所以它的性能开销较大。
首先,一般而言,Java程序员不会直接编写JVM的垃圾回收器。相反,他们使用Java的内存管理机制和垃圾回收器作为后台服务来管理内存。Java垃圾回收器根据堆的大小、内存使用情况等因素来决定何时触发垃圾回收,并根据所选的垃圾回收算法进行垃圾回收。以下是一个简单的Java代码片段,演示了标记压缩清除算法:
public class MyGarbageCollector {
public void collectGarbage(Heap heap) {
// 标记活跃对象
markActiveObjects(heap);
// 压缩所有对象
compactObjects(heap);
// 清除未标记对象
clearUnmarkedObjects(heap);
}
private void markActiveObjects(Heap heap) {
// 遍历所有对象,将可达的对象打上标记
for(Object object : heap.getAllObjects()) {
if(isObjectReachable(object)) {
object.setMarked(true);
}
}
}
private void compactObjects(Heap heap) {
int offset = 0;
// 遍历所有对象,将被标记的对象移动到堆的起始位置,并更新引用的指针
for(Object object : heap.getAllObjects()) {
if(object.isMarked()) {
object.setOffset(offset);
offset += object.getSize();
}
}
}
private void clearUnmarkedObjects(Heap heap) {
// 遍历所有对象,将未被标记的对象从堆中释放
for(Object object : heap.getAllObjects()) {
if(!object.isMarked()) {
heap.free(object);
} else {
// 清除标记,以便下次GC使用
object.setMarked(false);
}
}
}
private boolean isObjectReachable(Object object) {
// 判断对象是否可达
// ...
}
}
在这个示例代码中,我们使用了 markActiveObjects
方法来遍历所有对象,并将可达的对象打上标记。接下来,在 compactObjects
方法中,我们移动了所有被标记的对象到堆的起始位置,并更新引用的指针。最后,在 clearUnmarkedObjects
方法中,我们遍历所有对象,将未被标记的对象从堆中释放,并清除标记以便下次垃圾回收使用。
当然,这是一个非常简单的实现,实际上,Java虚拟机和垃圾回收器的实现要复杂得多,而且包含了很多其他的优化和特性。