[翻译]什么是垃圾回收

作者: 徐士林 | 来源:发表于2018-03-09 16:22 被阅读47次

    原文连接,侵删

    乍一看,垃圾收集的工作应该像其名字一样——找出垃圾并清理垃圾。然而事实恰好相反。GC会追踪仍然活着的对象,然后把剩余的对象标记为垃圾。请记住这一点,然后我们开始深究JVM中实现的GC如何自动的进行内存回收。

    我们一开始应该着手解释GC的特性以及核心概念和方法,而不是上来就进入具体的细节中。

    手动管理内存

    在我们开始讨论GC之前,先回顾一下之前的内存管理。我们必须为程序手动的分配内存,并在不用的时候释放掉该内存。如果程序中忘记释放这块内存,我们就不能重新使用这块内存。内存被申请却没有被使用,这种现象就是内存泄漏
    下面是一个C语言实现的手动内存管理的例子

    int send_request() {
        size_t n = read_size();
        int *elements = malloc(n * sizeof(int));
    
        if(read_elements(n, elements) < n) {
            // elements not freed!
            return -1;
        }
    
        // …
    
        free(elements)
        return 0;
    }
    

    正如我们所见,我们很容易就会忘记释放无用的内存。而出现这种内存泄漏,我们只能通过修改代码的方式修复问题。因此,更好的方法是自动的回收不使用的内存。这种自动回收内存的方法就是Garbag Collection

    Smart Pointers

    自动回收内存的第一个方法就是使用析构器。如下例子,我们可以使用C++中的vector,当vector对象不在作用域的时候,它的析构函数会自动调用进行内存回收。

    int send_request() {
        size_t n = read_size();
        vector<int> elements = vector<int>(n);
    
        if(read_elements(elements.size(), &elements[0]) < n) {
            return -1;
        }
    
        return 0;
    }
    

    但是在更加复杂的例子中,尤其是多线程之间共享某个变量的情况下,析构器是不够高效的。

    最简单的垃圾回收形式是引用计数。对每个对象来说,都可以很清楚的知道这个对象被引用了多少次,当指向这个对象的引用为0时,该对象是可以被回收的。一个例子就是C++中的共享指针

    int send_request() {
        size_t n = read_size();
        auto elements = make_shared<vector<int>>();
    
        // read elements
    
        store_in_cache(elements);
    
        // process elements further
    
        return 0;
    }
    

    为了避免下次调用该函数时还要读取元素,我们可能需要缓存它们。这种情况下,当vector对象超出作用域之后就清理它就不太合理了。因此,这里使用了shared_ptr。它保存了引用该对象的数量。这个数字当该对象被引用时增加,当引用的对象超出作用域时减少。只要引用数量到达0,shared_ptr会自动删除底层的vector。

    自动化内存管理

    在上面的C++代码中,我们仍然必须要明确的说出我们何时要进行内存管理。运行时环境会自动的找出一些不用的内存并且释放它们。换句话说,它自动进行GC

    引用计数

    我们使用的C++中的引用指针可以应用于所有对象。如Perl、Python或者PHP等许多语言都是使用这种方法。 Java-GC-counting-references1.png

    绿色的云彩指向的对象是仍然被程序使用的对象。绿色的云彩可能代表着正在执行的方法中的变量或者静态变量或者其他的东西,具体是什么还可能跟语言本身相关,我们这里先不关注这些。

    蓝的圆圈代表内存中仍然存活的对象,里边的数字表示它的引用计数。最后,灰色的圆圈是不再被引用的对象。所以灰色圈子被认为是垃圾并且会被GC回收。

    引用计数看起来非常好,但是整个方法有一个巨大的缺陷,所有被循环引用涉及到的对象的引用计数都不为0,所以这些对象永远不会被回收。如下图所示


    Java-GC-cyclical-dependencies.png

    标红的对象都没有被程序使用,也就是说这些对象都是垃圾。但是因为引用计数的缺陷,这些对象不会被回收并造成内存泄漏。

    有一些方法可以解决这个问题,例如使用“弱引用”或者对循环引用的对象使用另外的收集算法。前述的Perl、Python等都使用了某些方法解决循环引用的问题。但是这个问题超出了本篇文章的范围,我们不去讨论。相反的,我们将更加详细的介绍JVM采用的GC方式

    Mark and Sweep(标记清理)
    首先,JVM对于对象可达性的定义更具体,不同于之前看到的绿色云彩,JVM有一组非常明确的对象,称为GC root:

    • 本地变量 local variables
    • 活着的线程 active threads
    • 静态属性 static fields
    • JNI 引用 JNI references
      JVM用来跟踪所有可达对象并且确保不可达对象的内存可以使用的方法叫做标记清除算法,该算法由两部分组成
    • 标记阶段从GC root开始遍历所有可达对象,并且在内存中保存路径
    • 清理阶段释放不可达对象的内存,并且保证在下一次申请内存时可以使用

    JVM中不同的GC算法,例如Parallel Scavenge,Parallel Mark+Copy 或者CMS对标记清理的实现可能有略微差别。但是整体的概念上还是一致的。

    这种方法至关重要的一点就是不会造成内存泄漏。


    Java-GC-mark-and-sweep.png

    不过缺点就是在GC的时候,应用线程可能需要暂停一段时间。因为如果对象的引用一直在不停地变化,那么将很难做出正确的统计。而应用程序暂停,JVM开始收集垃圾的这段时间被称为Stop-The Word 阶段

    相关文章

      网友评论

        本文标题:[翻译]什么是垃圾回收

        本文链接:https://www.haomeiwen.com/subject/fjgpfftx.html