JVM垃圾回收二-内存布局

Java 中的垃圾回收

以前对标记和清除垃圾收集的介绍主要是理论性的。在实践中,须要进行大量的调整,以适应真实的场景和需求。举个简单的例子,为了安全地继续分配对象,JVM还须要作如下事情。java

碎片和压缩

每次清理垃圾时,JVM必须确保填充了不可访问对象的区域能够从新使用。这可能会致使内存碎片,这与磁盘碎片相似,会致使两个问题:算法

  • 写操做变得更加耗时,由于寻找下一个足够大的空闲块再也不是一个简单的操做。
  • 当建立新对象时,JVM是在连续块中分配内存。所以,若是碎片碎到没有单个空闲碎片的大小足以容纳新建立的对象的时候,就会发生分配错误(事实上有大量空闲内存)。

为了不这些问题,JVM正须要确保碎片不会失控。所以,在垃圾收集期间也会发生“内存碎片整理”过程,而不只仅是标记和清理。这个过程当中须要将全部可访问的对象彼此从新定位,消除(或减小)碎片。下面是一个例子:安全

分代假设

前面说过,执行垃圾收集时须要彻底中止应用程序。很明显,对象越多,收集全部垃圾所需的时间就越长。可是若是咱们有可能使用更小的内存区域呢?有研究人员调查了这种可能性,发现应用程序中的大多数分配能够分为两类:bash

  • 大多数对象很快就不用了
  • 对象一般不能存活很长时间

这些观察结果造成了一个弱分代假设。根据这个假设,虚拟机中的内存被分为年轻代( Young Generation)和老年代(Old Generation)。老年代有时也被称为永久代。性能

拥有这样独立的、单独的可清理区域容许使用许多不一样的算法,这些算法在提升GC性能方面取得了长足的进步。优化

这并非说这种方法没有问题。首先,来自不一样代的对象实际上可能彼此有引用,这些引用在收集代时也被视为“事实上的”GC根。spa

但最重要的是,分代假设可能并不适用于某些应用。因为GC算法是针对“早逝”或“可能永远存在”的对象优化的,因此JVM对那些“中等”预期寿命的对象的回收表现很是糟糕。线程

内存池

稍微了解JVM的对堆中内存池的划分应该很熟悉。不太常见的是垃圾收集如何在不一样的内存池中执行其职责。在不一样的GC算法中,一些实现细节可能会有所不一样,可是本章中的概念其实是相同的。code

Eden区

Eden区一般是内存中建立对象时分配对象的区域。因为一般有多个线程同时建立许多对象,Eden区中进一步划分为一个或多个线程本地分配缓冲区(简称TLAB),这些TLAB缓冲区因为是线程独享的,所以容许JVM在TLAB中直接为一个线程分配对象,避免了与其余线程进行昂贵的同步操做。cdn

当在TLAB中分配对象失败时(一般是由于那里没有足够的空间),分配将转移到共享的Eden区。若是没有足够的空间,就会触发年轻代中的垃圾收集过程来释放更多的空间。若是垃圾收集也没有在Eden中产生足够的空闲内存,那么对象将在老年代中分配。

当Eden区进行垃圾回收时,GC Roots遍历全部可访问的对象,并将它们标记为活动对象。

以前提到到对象能够可能存在跨代的连接,因此一种直接的方法是检查其余区对Eden区的全部引用。不幸的是,这样作将首先破坏分代的意义。JVM有一个诀窍:卡片标记。本质上,JVM只是标记了Eden中“脏”对象的粗略位置,这些对象可能与老年代对象有连接。

标记阶段完成后,Eden区中的全部活动对象都被复制到一个Survivor区。整个Eden区如今是空的,能够重用它来分配更多的对象。这种方法称为“标记和复制”:活动对象被标记,而后复制(而不是移动)到幸存者空间。

Survivor区

紧挨着Eden区的是两个Survivor区,分别称为from和to。须要注意的是,两个Survivor区中的一个老是空的。

当进行新生代收集的时候,空的紧挨着Eden区的是两个Survivor区将开始有数据。新生代的全部活动对象(包括Eden空间和Survivor区中非空的“from”空间)都被复制到Survivor区中“to”空间。在这个过程完成以后,“to”区如今包含对象,而“from”不包含对象。此时他们的角色互换了。

在两个Survivor区之间复制活动对象的过程会重复几回,直到某些对象被认为已经成熟而且“足够老”为止。请记住,根据分代假设,已经存在一段时间的对象预计将继续使用很长时间。

这样的“终身”对象就能够推广到老年代。当这种状况发生时,对象不会从一个Survivor空间移动到另外一个Survivor空间,而是移动到老年代空间,它们将驻留在老年代空间中,直到没法访问为止。

为了肯定对象是否“足够老”到能够考虑将其传播到老年代空间,GC跟踪特定对象存活的集合数量。在每一代对象完成一次GC以后,仍然存活的对象的年龄就会增长。当年龄超过必定的阈值时,对象将被提高到老年代空间。

老年代

老年代内存空间的实现要复杂得多。老年代一般要大得多,而且被占用的对象不太多是垃圾。

老一代的GC比年轻一代发生的频率要低。并且,因为大多数对象都被认为是老一代的活对象,因此没有标记和复制发生。相反只是移动对象以最小化碎片。清理老年代空间的算法一般创建在不一样的基础上。原则上,所采起的步骤以下:

  • 在GC Roots可达的全部对象旁边设置标记位来标记可访问对象
  • 删除全部不可达的对象
  • 经过将活动对象拷贝至以老年代起始空间开头的连续空间来压缩老年代的空间

永久代

在Java 8以前,有一个特殊的空间称为“永久代”,存放的是类信息之类的元数据。此外,一些额外的东西,如内部化字符串,保存在永久代中。永久代实际上给Java开发人员带来了不少麻烦,由于很难预测这些数据到底须要多少空间。这些失败预测的结果以 java.lang.OutOfMemoryError: Permgen space的形式出现。除非这种OutOfMemoryError错误的缘由是实际的内存泄漏,不然解决这个问题的方法就是简单地增长与的permgen大小,以下:

java -XX:MaxPermSize=256m com.mycompany.MyApplication
复制代码

元数据

因为预测永久代数据的需求是一项复杂且不方便的工做,因此在Java8中删除了永久代,代之以元数据空间。此时,大多数杂项都被移到了常规Java堆中。

然而,类定义如今被加载到称为Metaspace中。它位于本机内存中,不会干扰常规堆对象。默认状况下,元数据空间大小仅受Java进程可用的本机内存数量的限制。这将使开发人员避免向应用程序中再添加一个类就会致使 java.lang.OutOfMemoryError: Permgen space错误的问题。

请注意,拥有这样看似无限的空间并非没有代价的——让元空间不受控制地增加,可能会引入大量swap操做并致使本机分配失败。

若是仍然但愿在这种状况下保护程序,能够限制元数据空间的增加,好比限制元空间大小为256 MB:

java -XX:MaxMetaspaceSize=256m com.mycompany.MyApplication
复制代码
相关文章
相关标签/搜索