JVM垃圾回收(一)- 什么是垃圾回收

什么是垃圾回收?java

垃圾回收是追踪全部正在被使用的对象,并标注剩余的为garbage。这里咱们先从JVM的GC是如何实现的提及。程序员

 

手动内存管理算法

在开始介绍垃圾回收以前,咱们先复习一下手动内存管理。它是指你须要明确的为你的数据手动分配须要的空闲内存,可是若是用完后忘了free 掉这些内存,则以后也没法再次使用这部份内存。也就是说,这部份内存是属于被申明但未被继续使用。这种状况称为一个memory leak(内存泄漏)编程

下面是一个C语言写的一个例子,使用手动管理内存:缓存

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

    free(elements)
    return 0;
}

 

忘记free memory 多是一件至关常见的事情。Memory Leak在过去也是一个较为常见的问题,并且仅能经过修改代码才能彻底解决此问题。因此,一个更好的方法是:自动回收未被用的内存,减小人自己可能犯错的可能性。这种自动的机制就是垃圾回收(简称GC)安全

 

智能指针app

自动垃圾回收的第一种方法基于reference counting(引用计数)。对每一个object,简单的计算它被引用了多少次,若是次数为0,则这个object能够被安全的回收。一个很著名的例子是C++的shared pointers:编程语言

int send_request() {
      size_t n = read_size();
      shared_ptr<vector<int>> elements
                             = make_shared<vector<int>>();
       if(read_elements(n, elements) < n) {
              return -1;
       }
       return 0;
}

 

shared_ptr 用于追踪object被引用的数量。这个值会在object被传递时增长,在离开域时减小。一旦这个引用数量值到达0,shared_ptr 会自动删除它底层的vector。然而,这个例子在实际使用中并不广泛,不过做为一个展现的例子足够了。优化

 

自动内存管理spa

在上面的C++ 代码中,咱们已经明确的说明了何时咱们须要考虑好内存管理。若是咱们让全部的object都使用这种自动回收内存的方式的话,确定会方便开发人员作开发,由于他们不须要再去手动释放一些objects。Runtime会自动获取到哪些内存不会再被使用,并释放这些内存。换句话说,它会自动收集这部分垃圾。

第一个垃圾回收器在1959年建立,用于Lisp语言,而且从那时候开始,垃圾回收的技术才有进展。

 

引用计数法

上面介绍的C++ 的shared pointers 能够被应用到全部的objects。不少语言例如Perl,Python或PHP都使用了这种方法。下面的图片很好的展现了这个方法:

 

绿色的小云表示它们指向的objects仍然在被程序员使用。从技术层面来讲,这些多是正在执行的方法中的局部变量,或是静态变量等等。它可能在不一样的编程语言中有不一样的场景,这里咱们不作进一步探讨。

蓝色的小环表明内存里当前活跃的objects,上面的数字表示它的引用计数。最后,灰色的小环表示没有被任何当前在使用的object(也就是之别被绿色的小云引用的)引用的objects。也就是说,灰色的小环就是须要被垃圾回收器清理的垃圾。

这个方法看起来好像很不错,可是它有一个很大的问题,即:若是是存在一个独立的有向回环的话,则这些object永远不会被回收,例如:

 

红色的圆环实际上是须要被收集的垃圾,可是因为相互引用,引用计数不为1,因此不会被回收。因此这个方法仍旧会形成memory leak。

也有一些方法用于克服这个问题,例如使用一个特殊的 ‘weak‘ references 或应用一个单独的算法用于收集这些回环。像以前提到过的语言 – Perl,Python以及PHP,它们都会以某种方式处理这种回环并回收垃圾。固然,这部分超出了在此讨论的范围,咱们仍会以讨论JVM采用的方法为主。

 

标记并清除

首先,JVM对于如何跟踪一个object会有更具体的信息,因此相对于以前模糊定义的绿色的小云,咱们如今能够更清晰的定义那些被称为Garbage Collection Roots(垃圾回收根)的一系列对象:

  1. 局部变量
  2. 活跃的线程
  3. 静态区域
  4. JNI引用

在JVM中跟踪全部可达的(当前活跃的)对象,并确保那些uon-reachable对象申明的内存被再次重复使用的方法,称为Mark and Sweep(标记并清除)算法。它包含两个步骤:

  1. Marking(标记):从GC roots开始,遍历全部reachable对象,并在本地内存保存全部这些对象的一个记录
  2. Sweeping(清除):确保被 non-reachable对象占用的内存空间能够在下一个allocation阶段时被从新使用

在JVM中的不一样的GC 算法,例如Parallel Scavenge,Parallel Mark+Copy 或CMS,都实现了上面两个阶段,可是会存在一些细微的差异。可是从概念层次上,整个过程基本与上面两个步骤相似。

这个方法中最重要的是:解决了回环致使内存泄漏的问题。

可是这个方法的一个不太好的点是:在collect发生时,应用的线程须要被暂时stopped(中止),由于若是状态是一直在变化的,则引用计数便不会特别准确。当全部应用被暂时stopped,以便让JVM能够彻底管理这种内部活动时,这个场景被称为Stop The World pause。固然,STWP 发生的缘由可能会有不少种,可是GC是其中最多见的一种。

 

Java 里的垃圾回收

以前对于Mark and Sweep 的垃圾回收的描述是一个最理论的介绍。在实际状况下,为了适应real-world的场景及需求,对此可能须要作大量的调整。做为一个简单的例子,下面看一下在咱们安全的持续分配对象时,JVM所须要作的各种记录与操做。

 

碎片与紧缩

当 Sweeping 发生时,JVM须要确保的是unreachable 对象所占据的空间能够被再次使用。这个(最终必定)会产生内存碎片(相似于磁盘碎片),这样会致使两个问题:

  1. 写操做会更耗时,由于找到下一个有足够空间的空闲块再也不是一个低消耗操做
  2. 当建立一个新对象时,JVM会在连续的内存块上分配内存。因此若是碎片的问题上升到没有单独、空闲、并足够的空间以知足新建立的对象时,会报一个allocation error

为了不这些问题,JVM会去确保碎片问题不会失控。因此,除了作Marking and Sweeping,在垃圾回收时,也会有一个“memory defrag“的工做。这个进程从新分配全部reachable 对象,将它们相邻排列,清除掉(或是减小)内存碎片。下面是一个示意图:

 

 

世代假说

正如以前提到过的,在作垃圾回收时,会牵涉到彻底中止应用。同时,能够明显确认的是:对象越多,回收垃圾的耗时越长。那咱们是否能够只对某些小的内存区域作操做?在研究人员对此作进一步研究后,能够发现:在应用内部,大部份内存分配发生在如下两种场景:

  1. 大部分对象很快变成unused
  2. 对象不长期存活

这个发现促成了 Weak Generational Hypothesis。根据这个假设,VM 里的内存被分红两部分,分别称为Young Generation和Old Generation,后者有时也被称为 Tenured(终身的)。

 

这种分离的、独立的可清理区域,使得大量不一样的算法能够对GC作不少performance上的提高。固然,这并非说,这种方式彻底没有问题。例如,不一样generations的对象可能事实上也是有相互的引用,这样在作垃圾回收时,它们也会被认为是GC roots。

须要着重注意的是,世代假说可能实际上并不适用一些应用。由于GC的算法是对“die young(早逝)”或“有可能一直存在”的对象作优化,但JVM的行为对于(被预期为)“中期”长度生命的对象是不够优化的。

 

内存池

下面是在堆内存里对内存池的划分,可能你们对此已经比较熟悉了。而对于GC如何在不一样的内存池中作回收,可能比较陌生。须要注意的是,不一样的GC算法可能在实现的层面稍有差异,可是从概念层面上,基本是一致的。

 

 

Eden(伊甸园)

在对象被建立时,会从Eden这个内存区域里分配内存。因为通常会有多个线程用于同时建立大量对象,Eden空间会被进一步划分为一个或多个 Thread Local Allocation Buffer(TLAB)(线程本地分配缓冲区)。这些缓存容许JVM在一个线程在它对应的TLAB中直接分配可以分配的最多的对象,避免了与其余线程同步的消耗。

当在一个TLAB中没法完成分配动做时(通常来讲是因为里面没有足够的空间),分配的动做会移动到一个共享的Eden空间。若是那里也没有足够的空间,则在Young Generation里的一个垃圾回收进程会被触发并释放出更多的空间。若是垃圾回收也没法在Eden里释放足够的空间,则对象会被分配到Old Generation。

当Eden 正在被回收时,GC会从GC roots遍历全部可达的对象,并将它们标注为存活(alive)。咱们以前提到过,对象可能存在跨generation的引用,因此一个直接的方法是:检查全部从其余generation指向Eden的引用。可是这个可能会直接影响了以前咱们提到的世代假说(本来已将它们分为两部分,如今这两部分却有了联系)。

JVM里对此作了一个优化,叫作:card-marking(卡片标记)。简单的说,就是对于那些有被Old Generation 引用的、存在于Eden中的“脏”对象,JVM仅仅是对它作一个大体的、模糊的位置标记。

 

 

在标记阶段完成后,全部在Eden中存活的对象会被复制到其中一个Survivor 空间。整个Eden如今会被认为是空的,而且它的空间能够被从新用于分配其余更多的对象。这个方法称为“Mark and Copy”(标记并复制):活跃的对象被标记,而后被复制到(而不是移动)一个survivor 空间。

 

Survivor Space(幸存者空间)

邻接Eden空间的是两个Survivor空间,被称为from和to。须要注意的是,这两个Survivor空间中的其中一个必定是一直是空的。

空的Survivor 空间会在Young Generation被回收后开始往里面放入内容。全部从整个Young Generation(包括Eden 空间以及non-empty的“from”Survivor空间)存活的对象会被复制到“to”Survivor空间。在这个过程完成后,“to”Survivor如今会存放对象,而“from”Survivor空间没有对象,它们的角色也会在这时作转换。

 

 

这个在两个Survivor空间中复制存活对象的过程会重复屡次,直到一些对象被认为经历的时间足够久并“old enough”。须要注意的是,根据世代假说,存活时间较长的对象被预期是会继续被长时间使用的。

这些长时间存活的对象所以能够被“提高”到Old Generation。当这个过程发生时,对象并非从一个survivor空间移动到另外一survivor空间,而是被移动到了Old Generation空间。这些对象会在Old Generation空间里长久存在,直到它们unreachable。

为了判断一个对象是不是“old enough”并被移动到Old 空间,GC会跟踪对象在回收后仍然存活的次数。在每一个对象的generation在GC中完成后,这些依旧存活的对象的年纪会增长。当它们的年纪超过了一个特定的“年纪阈值”后,会被移动到Old 空间。

而实际的“年纪阈值”是JVM动态调整的,可是能够经过指定 -XX:+MaxTenuringThreshold 设置一个上限值。若将此参数设置为0,则会致使移动到Old 空间当即生效(也就是说,不会在Survivor空间之间作复制)。默认状况下,这个阈值在主流的JVM中是15轮GC。这也是HotSpot中的最大值。

Promotion(从young 空间移动到old空间)也能够在对象经历的GC轮数达到阈值前发生,若是在Young Generation中的Survivor空间不足以存下全部存活的对象的话。

 

Old Generation(老生代)

Old Generation内存空间的实现更为复杂。Old Generation的空间通常会比Young Generation大得多,而且存放了那些更少可能被回收的对象。

在Old Generation中发生的GC频率要少于Young Generation。而且,因为大部分对象被认为是在Old Generation中应该存活的,因此在这里不会有Mark and Copy(标记并复制)的过程发生。取而代之的是,对象会被四处移动,以最小化内存碎片。Old 空间的清理算法通常基于不一样的基础。基本上,会经历如下几个步骤:

  1. 标记全部可达对象(从GC roots可达的对象),设置标记位
  2. 删除全部不可达对象
  3. 复制存活的对象,并从Old 空间的起始,将它们紧凑、相邻地排在一块儿

从上面的步骤能够看到,在Old Generation的GC会将对象紧凑的排列,以免过分的内存碎片。

 

PermGen(永生代)

在Java 8 之前,会存在一个特殊的空间名为“Permanent Generation”(永久代)。这个地方会存放一些metadata(例如classes)。同时,一些额外的东西,例如Internalized strings(常量字符串)也会存在PermGen。

可是它在过去经常会对Java 开发者产生大量的问题,由于这个区域到底一共须要多少空间是很难被预测的。而预测失败的结果每每会致使 java.lang.OutOfMemoryError:Permgen space 的报错。

除非真正致使这个OutOfMemory报错的缘由是一个内存泄漏,不然修复这个问题的方法通常是增长PermGen的空间分配,例以下面的选项指定了最高容许的PermGen内存空间为256MB:

java -XX:MaxPermSize=256m com.company.MyApplication

 

Metaspace(源空间)

预测metadata所需的空间是一个复杂且不方便的工做,因此Permanent Generation在Java 8 被移除了,并由Metaspace所取代。 今后,大部分杂七杂八的内容被移动到了常规的Java heap中。

可是,类的定义(class definitions),如今被加载到了名为Metaspace的地方。它存在于本地内存而且不干扰常规堆中的对象。默认状况下,Metaspace的空间仅仅由Java进程所拥有的本地可用内存所限制。这种方式解决了在新增长一个或多个类到应用时返回 java.lang.OutOfMemoryError:Permgen space 的问题。

须要注意的是,拥有这种看似无限制的内存空间并非没有开销的,若是让Metaspace无控制的增加的话,则会引入大量的swapping操做并甚至可能触发本地内存分配报错。

考虑到仍旧须要对此场景作控制,咱们能够限制Metaspace的增加,例如,限制它的大小为256MB:

java -XX:MaxMetaspaceSize=256m com.company.MyApplication

 

 

References:

https://plumbr.io/java-garbage-collection-handbook

相关文章
相关标签/搜索