JVM系列:(十)内存分配与回收

日期:2019-01-31       浏览:540

Java技术体系中所提倡的自动内存管理最终可以归结为自动化的解决了两个问题:给对象分配内存和回收分配给对象的内存。

一 分配内存

1.1 新生代和老龄代

对象的内存分配主要就是在堆上进行分配。所以我们先来看一下堆内部布局,堆可以划分为两大块内存:新生代和老龄代,如下图所示:
堆内存布局
堆内存布局
堆内存大小 = 新生代 + 老龄代
针对新生代和老龄代的垃圾回收分别称为Minor GC和Major GC/Full GC。
  • 新生代GC(Minor GC):指发生在新生代的垃圾收集行为,因为Java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。
  • 老龄代GC(Major GC/Full GC):指发生在老龄代的GC,出现Full GC,经常会伴随至少一次的Minor GC,Full GC速度一般比Minor GC慢很多。
新生代和老龄代之间的垃圾回收主要采用的是分代回收算法。即当新生代中的对象被多次Minor GC后还存活并且年龄增长到某个阈值时,就会被移动到老龄代中。

1.2 伊甸园和交换区

新生代还可以再细分,又可以分为两大块:伊甸园(Eden)和交换区(Survivor),默认占比是8:2,如下图所示:
堆内存布局
堆内存布局
堆内存大小 = 伊甸园 + 交换区 + 老龄代
新建的对象主要分配在新生代的伊甸园中,伊甸园中的垃圾回收主要采用的是标记-清除算法,当Minor GC时,如果当前对象还存活就会被移动到新生代的交换区中。

1.3 from空间和to空间

新生代中的交换区也可以再细分一下,分为from空间和to空间,默认占比是1:1,如下图所示:
堆内存布局
堆内存布局
堆内存大小 = 伊甸园 + from空间 + to空间 + 老龄代
交换区中from空间和to空间的垃圾回收主要采用的是复制算法,每次只使用其中的一块,当这一块的内存用完时,就将还存活的对象移动到另一块上面,然后把已使用的内存空间一次性的清理掉。
新生代的总可用空间[9] = 伊甸园[8] + (from[1] || to[1])

1.4 描述

接下来我们所说的对象分配都是针对上图内存模型展开讲解。
大多数情况下,新建的对象主要分配在新生代的伊甸园(Eden)中。当伊甸园中没有足够空间进行分配时,虚拟机将发起一次Minor GC(针对新生代区域的垃圾回收)。
虚拟机针对新生代和老龄代的内存区域,采用了分代收集的思想来管理。那内存回收时就必须能识别哪些对象应当放在新生代,哪些对象应当放在老龄代。为了做到这一点,虚拟机为每个对象定义了一个对象年龄计数器(在对象头数据中)。如果对象在Eden区出生并且经过一次Minor GC后仍然存活,并且能被交换区容纳的话,将被移动到交换区中,并将对象年龄设为1。对象在交换区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老龄代中。对于晋升老龄代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold来设置。
为了能更好的适用不同程度的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老龄代,如果在交换空间中相同年龄所有对象大小的总和大于交换空间的一半(大于from或to空间),年龄大于或等于该年龄的对象将直接进入老龄代,无需等到MaxTenuringThreshold来设置要求的年龄。

二 回收内存

2.1 堆内存回收

介绍堆内存回收前我们先说一下怎么判断对象已死(可以回收),Hotspot虚拟机是通过根搜索算法来判断某个对象是否存活的。
根搜索算法:通过一系列的名为“GC Roots”的对象作为起始点,从这些起始点开始向下搜索,搜索所走过的路径称为引用链,当一个对象不在任何引用链上时,则证明这个对象是不可用的(用图论的说法就是这个对象到GC Roots不可达)。如下图所示:
根搜索算法
根搜索算法
在Java虚拟机中,可以用作GC Roots的对象主要包括下面几种:
  1. 虚拟机栈中(栈帧中的本地方法表)的引用对象;
  2. 方法区中的类静态属性引用的对象;
  3. 方法区中的常量引用的对象;
  4. 本地方法栈中JNI(一般说的native方法)引用的对象;
在根搜索算法中不可达的对象,并不是“非死不可”的,要真正宣布一个对象的死亡,至少要经历两次标记过程。若两次标记都为根不可达对象,才会真正去回收它。

2.2 方法区回收

很多人认为方法区(Hotspot虚拟机中的永久代)是没有垃圾回收的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾回收,而且在方法区进行垃圾回收的性价比比较低,在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾回收效率却远低于此。
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量和回收堆中的对象非常类似。以常量中的字面量的回收为例,加入一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫“abc”的,也就是没有任何String对象引用常量池中的“abc”常量,也没有在其他地方引用这个字面量,如果这个时候发送内存回收,而且必要的话,这个“abc”常量就会被系统从常量池中删除。常量池中的其他类、接口、方法、字段的符号引用也与此类似。
判定一个常量是否是废弃常量比较简单,而要判定一个类是否是无用的类的条件则相对苛刻很多。
  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类任何的实例对象。
  2. 加载该类的 ClassLoader 已经被回收。
  3. 该类对应的 java.lang.Class 对象没有任何地方被引用,无法在任何地方通过反射访问到该类的方法。
虚拟机可以对满足上述3个条件的类进行回收,这里仅仅是可以回收,而不是和对象一样,不使用了就必然要回收。
在大量使用反射,动态代理,CGLib等 bytecode 的场景下,以及动态生成 JSP 和 OSGi 这类频繁自定义 ClassLoader 的场景下都需要虚拟机具备类卸载的功能,以保证永久代不会内存溢出。
扫码关注有惊喜

(转载本站文章请注明作者和出处 qbian)

暂无评论

Copyright 2016 qbian. All Rights Reserved.

文章目录