JVM 内存分配及垃圾回收

Java 中的内存分配及垃圾回收策略。本文是《深入理解 Java 虚拟机: JVM 高级特性与最佳实践 第 2 版 - 第三章 垃圾收集器与内存分配策略》的读书笔记。

GC [Garbage Collection]:垃圾回收器 。Java 使用 GC 进行内存回收理,不用手动释放内存而提升开发效率。GC 主要完成三件事:

  • 确定哪些内存需要回收
  • 什么时候回收
  • 怎么回收

内存分配

虚拟机运行时数据区

0076-java-gc-java-memory-model.png

如图所示,JVM 内存模型将内存分为:

  • 程序计数器
    占用一块很小的内存空间,当前线程执行的字节码的行号指示器,用来选取下一条需要执行的字节码。
  • 虚拟机栈
    每个方法执行时会创建一个栈帧,存储局部变量表,操作数栈等等。局部变量表只保存基本数据类型、对象的引用、返回地址字节码。当方法退出时,虚拟机栈就会自动清空,释放内存。
  • 本地方法栈
    虚拟机栈是为 Java 服务的,而本地方法栈针对的是 Native 方法的。本地方法栈也是在方法退出时就会释放内存。

  • 所有的对象实例和数组都是在堆中分配的内存,是垃圾回收的主要区域,有时候也称为 GC 堆。类的成员变量是在对象实例化时分配在堆中的。
  • 方法区
    用于保存类信息,常量,静态变量等,也叫 Non-Heap (非堆)。
    运行时常量池:方法区的一部分,保存类信息,常量池。

通常将内存分为栈和堆,指的就是虚拟机栈和堆,而内存泄露、内存溢出、垃圾回收等都主要针对的是堆内存。程序计数器、虚拟机栈、本地方法栈都是随线程生而生,灭而灭;栈在方法退出时自动清空,而方法区的回收性价比非常低,条件苛刻;所以内存回收主要集中在堆中。

对象访问定位

建立对象后,Java 程序需要通过栈上的 reference 数据来操作堆上的对象。Java 采用的是直接指针访问对象,这要求堆对象布局中需要考虑如何放置访问类型数据的相关信息,而 reference 存储的直接就是对象地址。

0076-java-memory-point.jpg

内存溢出

  • OutOfMemoryError
    无法申请到足够的存储空间,都会抛出 OOM
  • StackOverflowError
    虚拟机栈和本地方法栈,如果请求的栈深度大于虚拟机允许的最大深度,将会抛出 StackOverflowError

它们都不属于 Exception 的子类,所以 catch 时,只能使用 Throwable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 1. OutOfMemoryError
public class TestOutOfMemory {
private static final int _1M = 1024 * 1024;

public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
int count = 0;
boolean flag = true;
while (flag){
try {
list.add(new byte[_1M]);
count++;
}catch (Throwable e){
e.printStackTrace();
flag = false;
System.out.println("count = " + count);
}
}
}
}

// Result
count = 1697
java.lang.OutOfMemoryError: Java heap space
at com.***.jvm.TestOutOfMemory.main(TestOutOfMemory.java:15)

// 2. StackOverflowError
public class TestStackOverFlow {
private static int index;

private void call(){
index++;
call();
}

public static void main(String[] args) {
TestStackOverFlow stackOverFlow = new TestStackOverFlow();
try {
stackOverFlow.call();
}catch (Throwable e){
System.out.println("Stack deep: " + index);
e.printStackTrace();
}
}
}

// Result
Stack deep: 11413
java.lang.StackOverflowError
at com.***.jvm.TestStackOverFlow.call(TestStackOverFlow.java:7)
at com.***.jvm.TestStackOverFlow.call(TestStackOverFlow.java:8)
at com.***.jvm.TestStackOverFlow.call(TestStackOverFlow.java:8)
...

垃圾对象的判断

GC 之前要判断回收哪些内存?Java 中采用的是可达性分析算法来判断的。

引用计数算法

引用计数算法:给对象添加一个引用计算器,每当有一个地方引用它时,计数器自动加 1;当引用失效时,计数器自动减 1。当计算器为 0 时,表示该对象没有被引用了。
引用计数算法使用非常广泛,但是 Java 中并没有选用它来管理内存,最主要的原因是它很难解决对象之间相互循环引用的问题。

可达性分析算法

可达性分析算法:通过一系列的称为 GC Roots 的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链 Reference Chain,当一个对象到 CC Roots 没有任何引用链相连(即图论中的,GC Roots 到达这个对象是不可达的),则证明此对象是不可用的。

0076-java-gc.jpg

如果对象 C 是可达的,则认为该对象是被引用的,GC 不会回收。如果对象 F 或者块 E(多个对象引用组成的对象块)是不可达的,那么该对象或者块则判定是不可达的垃圾对象,GC 会回收。

可作为 GC Roots 的对象包含如下几种:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI (即 Native 方法)引用的对象

换句话说,如果对象被上面这几种对象引用,则无法被回收。通常在内存泄露和优化时,从这几种对象着手分析。

对象引用的四种分类

  • 强引用 Strong Reference
    最常见的应用: Object obj = new Object(),这类引用就是强引用,也就是我们一般声明对象是时虚拟机生成的引用。垃圾回收时需要严格判断当前对象是否被强引用,只要强引用还存在,则不会被垃圾回收。
  • 软引用 Soft Reference
    使用 SoftReference 类来实现软引用,用来描述有用但并非必需的对象。对于软引用关联着的对象在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间;如果剩余内存相对富裕,则不会进行回收。在系统要发生内存溢出异常之前,会将这些对象进行回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。换句话说,虚拟机在发生 OutOfMemory 时,肯定是没有软引用存在的。
  • 弱引用 Weak Reference
    使用 WeakReference 类来实现弱引用,用来描述非必需对象,但是强度比软引用更弱一些。被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾回收发生时,无论当前内存是否足够,被弱引用关联的对象都会被回收,因此其生命周期只存在于一个垃圾回收周期内。
  • 总结
    软引用:当虚拟机内存不足时(内存溢出,OOM 之前),会回收它指向的对象。
    弱引用:随时可能会被垃圾回收器回收,不一定要等到虚拟机内存不足时才强制回收,每次 GC 都会回收。
    软引用多用作来实现缓存机制(cache),而弱引用一般用来防止内存泄漏,要保证内存被虚拟机回收。

在垃圾回收前,弱引用可以确保对象被回收,可以规避内存泄露。

finalize 方法

可达性分析中不可达对象,也并不是必须被回收。GC 在回收对象之前会调用 finalize方法,我们可以在该方法中再次引用对象达到复活。但是 finalize 方法最多只会被系统调用一次,或者不调用。JVM 作者建议忘掉这个方法的存在,这个方法运行的不确定性太大,无法保证 GC 时一定会调用该方法。

堆内存及 GC 分类

堆内存分类

Java 中为了方便优化 GC 性能,将堆划分成两个不同的区域:

  • 新生代 Young
    新生代内部被划分为三个区域:Eden['i:dn], From Survivor[sərˈvaɪvə(r)], To Survivor,即 Eden 区和两个 Survivor 区。
  • 老年代 Old

JVM 默认配置中,通常新生代占用 1/3, 老年代占用 2/3;在新生代中 Eden:From:To 的比率为 8:1:1。详细划分图如下:

0076-yong-old-eden-survivor.png

新生代和老年代的比率,以及 Eden 和两个 Survivor 的比率,都可以可以通过 JVM 参数(–XX:NewRatio, –XX:SurvivorRatio)来设置。参数含义详见“ JVM 常见参数”中的描述。

GC 分类

  • Minor GC
    通常指发生在新生代的垃圾回收动作,因为 Java 对象大多数具有朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也非常快。
  • Full GC
    Full GC 前通常默认至少会执行一次 Minor GC(并非绝对),Full GC 速度一般会比 Minor GC 慢 10 倍以上。Full GC 是对整个 Java 堆回收,包含:新生代,老年代,永久代(或者元空间)。Major GC 通常是跟 Full GC 是等价的,但有时特指回收老年代。

GC 停顿,GC 进行时停顿所有的 Java 执行线程,即 STW: Stop The World 。所以 GC 过于频繁或者时间过长,将会显著影响到程序的运行。

参考:各GC的区别Minor GC、Major GC和Full GC之间的区别

常见垃圾收集算法

垃圾收集算法的实现各虚拟机平台实现各不相同,但是算法思想基本一致。

标记清除算法 Mark-Sweep

标记清除算法:也是最基础的收集算法,分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
其他收集算法基本都是基于这种思路并对不足进行改进的,该算法的不足之处有两个:

  • 效率问题
    标记和清除两个阶段效率都不高。
  • 空间问题
    标记清除后会产生大量不连续的内存碎片,空间碎片太多会导致后续分配大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

0076-gc-mark-sweep.png

复制算法 Copying

复制算法:将内存按容量划分为大小相等的两块,每次只使用其中一块。当这块内存用完后,将还存活的对象复制到另一块上面,然后将使用完的这块内存空间一次全部清理掉。
这样每次都是对整个半区进行内存回收,只要移动堆顶指针,按顺序分配内存即可,实现简单高效。解决了 Mark-Sweep 的效率问题,同时也不用考虑内存碎片。但是 Copying 算法是将内存缩小为原来的一半,代价太高。

0076-gc-copying.png

标记整理算法 Mark-Compact

标记整理算法:标记过程仍然与“标记-清除”算法一样,但后续并不会直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。

0076-gc-mark-compact.png

垃圾收集器

  • Serial 收集器
    Serial 收集器是最基本的,发展最悠久的收集器。它是一个单线程收集器,使用复制算法。
  • ParNew 收集器
    它是 Serial 的多线程版本。
  • Parallel Scavenge 收集器
    使用复制算法,并行多线程收集器。它有一个更重要的功能:GC 自适应调节策略,由虚拟机来决定内存调优。该收集器相对于其他收集器更关注吞吐量(运行代码时间 / (运行代码时间 + 垃圾收集时间)),也就是它可能为了吞吐量主动触发 Full GC,或者不触发 GC,以确保吞吐量最优。
  • Serial Old 收集器
    它是 Serial 收集器的老年代版本,单线程,标记整理算法。
  • Parallel Old 收集器
    它是 Parallel Scavenge 收集器的老年代版本,多线程,标记整理算法。
  • CMS 收集器
    CMS: Concurrent Mark Sweep,是一种以获取最短回收停顿时间为目标的收集器。基于标记-清除算法,并发收集,低停顿。
  • G1 收集器
    G1: Garbage First,它将堆内存区域划分为多个大小相等的独立区域,虽然保留了新生代和老年代的概念,但是新生代和老年代将不再是物理隔离,他们都是独立区域集合。这和其他收集器直接划分为新生代老年代有非常大的区别。

0076-gc-collectors.jpg

其中 ? 部分就是 G1 收集器。ParNewParallel Scavenge 最大的区别是,Parallel Scavenge 不能与 CMS 配合工作。参考:收集器分类

堆内存回收策略

分代收集算法

商业虚拟机的垃圾回收通常采用“分代收集 Generational Collection ”算法,它并没有引入新的算法,而是针对新生代和老年代的特点采用适当的算法。

  • 新生代
    通常会有大量对象创建及死亡,只有少量对象存活,采用复制算法。
  • 老年代
    对象存活率高,而且也没有额外的空间担保,使用“标记-清理”或者“标记-整理”算法。

对象分配

  • 优先 Eden
    Java 对象大多数具有朝生夕灭的特性,所以新创建的对象通常优先分配在新生代堆中,默认为 EdenFrom 区,也就是说新生代的可用内存空间默认为整个新生代容量的 90%,剩下 10% 为复制算法备用空间。当没有足够空间进行分配时,虚拟机发起一次 Minor GC。但是对象回收时,无法确保只有不多于 10% 的对象存活,如果 To Survivor 空间不够,需要老年代进行分配担保,也就是说超出空间的对象直接会进入老年代。如果新生代 GC 的平均晋升大小比目前老年代剩余空间大,则会触发一次 Full GC
  • 大对象直接分配在老年代
    所谓大对象是指需要大量连续内存空间的对象,最典型的大对象就是很长的字符串和数组。JVM 可以通过设置 -XX:PretenureSizeThreshold 来决定对象大小门限值,如果超过指定值的对象直接分配到老年代,避免垃圾回收时在新生代中发生大量的内存复制。不过这个参数只对 SerialParNew 收集器生效。
  • 长期存活对象进入老年代
    虚拟机给每个对象定义一个对象年龄 Age 计数器。如果对象在 Eden 出生并经历过一次 Minor GC 后仍然存活,并且有足够的空间移动到 To Survivor 区,此时将对象年龄设为 1 。后续对象在 To Survivor 区,每熬过一次 Minor GC ,年龄都会加 1,当年龄到达一定程度(默认为 15),它将会被晋升到老年代中。可以通过设置 -XX:MaxTenuringThreshold 来设置这个晋升阀值。
  • 动态对象年龄判断
    To Survivor 空间相同年龄所有对象大小的总和大于 To Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到 -XX:MaxTenuringThreshold 要求的年龄。

实际上:对象优先分配到 Eden 堆,但是是否进入老年代或者晋升老年代,这些和垃圾收集器强相关,和下面的 GC 触发机制一样,并没有严格定义。

GC 触发机制

根据上面对象分配规则,不同场景触发 GC 类型不一样。大体的触发条件:Eden 区满了就会触发新生代 GC;新生代晋升时如果老年代剩余空间不足会触发 Full GC。具体参考:各GC的区别及触发条件
总的来讲:GC 触发条件取决于 JVM 垃圾收集器的算法实现,并没有严格的定义。如果不对收集器算法做深入研究,不用过于关注。Java 8 中调试时,GC Log 会显示基本的触发原因。
对象分配和 GC 触发和垃圾收集器的关系,可以参考最下面的 GC 日志中的实例。

方法区回收策略

方法区回收条件

方法区也存在垃圾回收,只是回收效率和性价比非常低。方法区会回收两部分内容:废弃常量和无用的类。类被回收时才会卸载,但是方法区回收类时条件比较严格,只有同时满足以下三个条件,才会回收:

  • 该类所有的实例都已经被回收(GC),也就是虚拟机中不存在该 Class 的任何实例
  • 加载该类的 ClassLoader 已经被回收(GC
  • 该类的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问改类的方法
    静态变量在类没有被销毁,也没有置 null 的情况下,是不会被回收的(GC)。

回收废弃常量和回收堆中对象非常类似,判断一个废弃常量比较简单:假如这个常量没有被任何地方引用,当方法区出现内存回收并且有必要的情况下,该常量就会被清理出常量池。

方法区存储空间各版本的区别

方法区的存储空间在不同的虚拟机中有不同的实现, HotSpot 中被称为永久代(PermGen Space),而 JRockit(Oracle) 等其他虚拟机并没有永久代。GC 不会在主程序运行期对永久代进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM: java.lang.OutOfMemoryError: PermGen space 异常。
而在 Java 8 开始已经不存在永久代,而使用的是元空间 Metaspace。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入堆中。这样可以加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制。

JVM 常见参数

选项 描述
-Xms 初始堆大小。如:-Xms256m
-Xmx 最大堆大小。如:-Xmx512m
-Xmn 新生代大小。通常为最大堆的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + From,即 90% 的空间
-XX:NewRatio 新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的 1/3,老年代占 2/3
-XX:SurvivorRatio 新生代中 EdenSurvivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10
-XX:PretenureSizeThreshold 老年代门限值,大小大于这个值的对象直接分配进老年代,这个参数只对 SerialParNew 收集器生效
-XX:MaxTenuringThreshold 对象从新生代晋升到老年代的年龄阀值,默认为 15
-XX:+PrintGC 开启 GC 打印简单信息,别名 -verbose:gc
-XX:+PrintGCDetails 打印 GC 详细信息,通常默认会包含 -XX:+PrintGC 的信息
-XX:+PrintHeapAtGC 打印 GC 前后的堆信息
-Xss JDK1.5+ 每个线程堆栈大小为 1M 。如果超出抛出 StackOverflowError
-XX:PermSize 永久代(方法区)的初始大小,Java 8 中已经使用元空间,降级
-XX:MaxPermSize 永久代(方法区)的最大值,Java 8 中已经使用元空间,降级
-XX:+HeapDumpOnOutOfMemoryError 让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用
-XX:+UseSerialGC 使用 Serial + Serial Old 收集器组合回收内存
-XX:+UseParNewGC 使用 ParNew + Serial Old 收集器组合回收内存
-XX:+UseConcMarkSweepGC 使用 ParNew + CMS + Serial Old 收集器组合回收内存,Serial Old 作为 CMS 失败时的备用收集器
-XX:+UseParallelGC 使用 Parallel Scavenge 收集新生代
-XX:+UseParallelOldGC 使用 Parallel Old 收集老年代,默认同时开启 -XX:+UseParallelGC

GC 日志

阅读和分析 GC 日志是处理 Java 虚拟机内存问题的基本技能,每种收集器的日志都是由其自身决定,也就是说格式可能不一样,但是虚拟机为了方便用户阅读,各个收集器的日志有很大的共性。
参考:Java虚拟机详解03—-常用JVM配置参数 ,这篇博客的虚拟机系列写的都不错。

各字段含义

0076-gc-log-gc.jpg

0076-gc-log-full-gc.jpg

一段示例 Log

1
2
3
4
5
6
7
8
9
10
11
[GC [PSYoungGen: 6635K->800K(9216K)] 6635K->6944K(19456K), 0.0075650 secs] [Times: user=0.15 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 800K->0K(9216K)] [ParOldGen: 6144K->6371K(10240K)] 6944K->6371K(19456K) [PSPermGen: 2733K->2732K(21504K)], 0.0107750 secs] [Times: user=0.12 sys=0.01, real=0.02 secs]
Heap
PSYoungGen total 9216K, used 2214K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 27% used [0x00000000ff600000,0x00000000ff829848,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 6371K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 62% used [0x00000000fec00000,0x00000000ff238d38,0x00000000ff600000)
PSPermGen total 21504K, used 2739K [0x00000000f4600000, 0x00000000f5b00000, 0x00000000fec00000)
object space 21504K, 12% used [0x00000000f4600000,0x00000000f48aced8,0x00000000f5b00000)
  • GC 类型
    疑问:根据垃圾收集器中的介绍,所有 GC 都会出现停顿,而“深入理解虚拟机”一文中特意强调了 [GC[Full GC 并不是用来区分新生代和老年代的,而是用来区分是否停顿(Stop The World)的?但是搜了下大部分网络博客或问答上基本都认为 [GC 特指新生代,而 [Full GC 是指全堆。参考:各GC的区别
  • 垃圾收集器类型以及堆位置
    [PSYoungGen] 表示使用 Parallel Scavenge 收集器回收新生代堆;[ParOldGen] 表示使用 Parallel Old 收集器回收老年代;[PSPermGen] 表示使用 Parallel Scavenge 收集器回收永久代。可以通过设置 JVM 参数来指定对应的垃圾收集器,PS 是当前环境下默认的收集器。

Java 版本及环境

如下示例都是在 Ubuntu Java 1.7 中执行的。

1
2
3
4
xmt@server005:~/$ java -version
java version "1.7.0_91"
OpenJDK Runtime Environment (IcedTea 2.6.3) (7u91-2.6.3-0ubuntu0.12.04.1)
OpenJDK 64-Bit Server VM (build 24.91-b01, mixed mode)

一个怪异现象示例

源码中,堆内存申请大小如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class TestGCLog {
private static final int _1MB = 1024 * 1024;

private static void testAllocation(){
byte[] allocation1, allocation2, allocation3,allocation4,allocation5;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[1572864]; //1.5M

int i = 0;
for (i = 0; i < allocation1.length; i++ ){
allocation1[i] = 1;
}

for (i = 0; i < allocation2.length; i++ ){
allocation2[i] = 1;
}

for (i = 0; i < allocation3.length; i++ ){
allocation3[i] = 1;
}

for (i = 0; i < allocation4.length; i++ ){
allocation4[i] = 1;
}


allocation5 = new byte[20542];
System.out.println(allocation1.length);
System.out.println(allocation2[1]);
System.out.println(allocation3[2]);
System.out.println(allocation4[8]);
}

public static void main(String[] args) {
testAllocation();
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
xmt@server005:~/$ javac TestGCLog.java
xmt@server005:~/$ java -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintHeapAtGC TestGCLog
{Heap before GC invocations=1 (full 0):
PSYoungGen total 9216K, used 8191K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 99% used [0x00000000ff600000,0x00000000ffdfffb8,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
PSPermGen total 21504K, used 2733K [0x00000000f4600000, 0x00000000f5b00000, 0x00000000fec00000)
object space 21504K, 12% used [0x00000000f4600000,0x00000000f48ab7a0,0x00000000f5b00000)
[GC [PSYoungGen: 8191K->1024K(9216K)] 8191K->1080K(19456K), 0.0014510 secs] [Times: user=0.04 sys=0.00, real=0.00 secs]
Heap after GC invocations=1 (full 0):
PSYoungGen total 9216K, used 1024K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffe00000)
from space 1024K, 100% used [0x00000000ffe00000,0x00000000fff00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 56K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 0% used [0x00000000fec00000,0x00000000fec0e000,0x00000000ff600000)
PSPermGen total 21504K, used 2733K [0x00000000f4600000, 0x00000000f5b00000, 0x00000000fec00000)
object space 21504K, 12% used [0x00000000f4600000,0x00000000f48ab7a0,0x00000000f5b00000)
}
2097152
1
1
1
Heap
PSYoungGen total 9216K, used 1271K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 3% used [0x00000000ff600000,0x00000000ff63de78,0x00000000ffe00000)
from space 1024K, 100% used [0x00000000ffe00000,0x00000000fff00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 56K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 0% used [0x00000000fec00000,0x00000000fec0e000,0x00000000ff600000)
PSPermGen total 21504K, used 2741K [0x00000000f4600000, 0x00000000f5b00000, 0x00000000fec00000)
object space 21504K, 12% used [0x00000000f4600000,0x00000000f48ad4b8,0x00000000f5b00000)

运行时 JVM 的配置为:初始堆和最大堆都是 20M;新生代配置为 10M,比率为 8,也就是说 Eden8M(8192K)From/To1M(1024K),新生代可用总大小为 9M(9216K);这些信息从 HeapLog 中也可以得到验证。
从代码中可以看出 allocation5 再分配空间时出现 GC,但是从 [GC [ 前后的堆中看到的 Log 出现了一个怪异现象:

  • GC 前堆
    PSYoungGen total 9216K, used 8191K; eden space 8192K, 99% used; from/to space 1024K, 0% used:表明新生代中 Eden 区基本已经全部用完,而 From 空置。再分配 allocation5 的空间会导致新生代触发一次 GC
  • GC 后堆
    PSYoungGen total 9216K, used 1024K; eden space 8192K, 0% used; from space 1024K, 100% used; to space 1024K, 0% used:表明 GC 清空了 Eden 区,allocation5 的堆内存需求都分配到了 From 区。

诡异问题:既然新生代 GC 后基本被清空,allocation1/2/3/4 四个对象此时也并没有被回收,而 ParOldGen total 10240K, used 56K; object space 10240K, 0% used 表明老年代的堆也没有被使用。allocation1/2/3/4 这部分堆既没有回收,又没有晋升到老年代,那它到底去哪了呢?

我们将源码中的 allocation5 = new byte[25542]; 内存需求改大到 25542,重新编译并执行,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
xmt@server005:~/$ java -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintHeapAtGC TestGCLog
{Heap before GC invocations=1 (full 0):
PSYoungGen total 9216K, used 8171K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 99% used [0x00000000ff600000,0x00000000ffdfaf68,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
PSPermGen total 21504K, used 2734K [0x00000000f4600000, 0x00000000f5b00000, 0x00000000fec00000)
object space 21504K, 12% used [0x00000000f4600000,0x00000000f48ab930,0x00000000f5b00000)
[GC [PSYoungGen: 8171K->960K(9216K)] 8171K->8640K(19456K), 0.0059180 secs] [Times: user=0.17 sys=0.01, real=0.01 secs]
Heap after GC invocations=1 (full 0):
PSYoungGen total 9216K, used 960K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffe00000)
from space 1024K, 93% used [0x00000000ffe00000,0x00000000ffef0000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 7680K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 75% used [0x00000000fec00000,0x00000000ff380040,0x00000000ff600000)
PSPermGen total 21504K, used 2734K [0x00000000f4600000, 0x00000000f5b00000, 0x00000000fec00000)
object space 21504K, 12% used [0x00000000f4600000,0x00000000f48ab930,0x00000000f5b00000)
}
{Heap before GC invocations=2 (full 1):
PSYoungGen total 9216K, used 960K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffe00000)
from space 1024K, 93% used [0x00000000ffe00000,0x00000000ffef0000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 7680K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 75% used [0x00000000fec00000,0x00000000ff380040,0x00000000ff600000)
PSPermGen total 21504K, used 2734K [0x00000000f4600000, 0x00000000f5b00000, 0x00000000fec00000)
object space 21504K, 12% used [0x00000000f4600000,0x00000000f48ab930,0x00000000f5b00000)
[Full GC [PSYoungGen: 960K->0K(9216K)] [ParOldGen: 7680K->7907K(10240K)] 8640K->7907K(19456K) [PSPermGen: 2734K->2733K(21504K)], 0.0077670 secs] [Times: user=0.07 sys=0.01, real=0.01 secs]
Heap after GC invocations=2 (full 1):
PSYoungGen total 9216K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 7907K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 77% used [0x00000000fec00000,0x00000000ff3b8d48,0x00000000ff600000)
PSPermGen total 21504K, used 2733K [0x00000000f4600000, 0x00000000f5b00000, 0x00000000fec00000)
object space 21504K, 12% used [0x00000000f4600000,0x00000000f48ab5a0,0x00000000f5b00000)
}
2097152
1
1
1
Heap
PSYoungGen total 9216K, used 275K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 3% used [0x00000000ff600000,0x00000000ff644f68,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 7907K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 77% used [0x00000000fec00000,0x00000000ff3b8d48,0x00000000ff600000)
PSPermGen total 21504K, used 2779K [0x00000000f4600000, 0x00000000f5b00000, 0x00000000fec00000)
object space 21504K, 12% used [0x00000000f4600000,0x00000000f48b6da0,0x00000000f5b00000)

虽然 allocation5 的内存需求只增加了几K,但是触发了 2 次 GC。第一次 GC 前后堆中可以明确看到新生代堆中变量晋升到了老年代 ParOldGen total 10240K, used 7680K; object space 10240K, 75% used,新生代也基本清空。
但是接着又来了一次 Full GCJava 7 中并没有给出原因,但是使用 Java 8 重新编译执行一次,可以明确看到:[Full GC (Ergonomics),是因为 JVM 认为需要优化下主动触发的 Full GC,直接将新生代完全清空,全部晋升到老年代。
我们也可以在执行时,将 JVM 参数的初始堆和最大堆大小加大避免触发 Full GC。本地验证设置为 -Xms26M -Xmx26M 时,第一次 GCParOldGen total 16384K, used 7680K; object space 16384K, 46% used,老年代只使用了 46%,JVM 不用优化并触发 Full GC。这也侧面证明 Full GC 的触发多取决于垃圾回收器算法的实现,不同回收器触发机制并不一致。

大对象直接分配在老年代

先看源码:

1
2
3
4
5
6
7
8
9
10
public class TestLargeObject {
private static final int _1MB = 1024 * 1024;

public static void main(String[] args){
byte[] allocation1, allocation2, allocation3;
allocation1 = new byte[1 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[4 * _1MB];
}
}

根据计算 Eden 区完全可以容下三个大对象,执行结果也匹配:

1
2
3
4
5
6
7
8
9
10
xmt@server005:~/$ java -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintHeapAtGC TestLargeObject
Heap
PSYoungGen total 9216K, used 7823K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 95% used [0x00000000ff600000,0x00000000ffda3f18,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
PSPermGen total 21504K, used 2740K [0x00000000f4600000, 0x00000000f5b00000, 0x00000000fec00000)
object space 21504K, 12% used [0x00000000f4600000,0x00000000f48ad158,0x00000000f5b00000)

使用 -XX:PretenureSizeThreshold 参数设置为 3M,当大对象大小超过这个值后直接进入老年代。

1
2
3
4
5
6
7
8
9
10
xmt@server005:~/$ java -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:PretenureSizeThreshold=3145728 TestLargeObject
Heap
PSYoungGen total 9216K, used 7823K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 95% used [0x00000000ff600000,0x00000000ffda3f18,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
PSPermGen total 21504K, used 2740K [0x00000000f4600000, 0x00000000f5b00000, 0x00000000fec00000)
object space 21504K, 12% used [0x00000000f4600000,0x00000000f48ad158,0x00000000f5b00000)

结果显示 -XX:PretenureSizeThreshold 参数并没有生效,allocation3 还是进入了 Eden 区。原因是当前环境下默认的新生代 Parallel Scavenge 收集器并不识别这个参数,而指定新生代收集器为 -XX:+UseSerialGC 则会生效。参考:PretenureSizeThreshol参数不识别

1
2
3
4
5
6
7
8
9
10
11
xmt@server005:~/$ java -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:PretenureSizeThreshold=3145728 -XX:+UseSerialGC TestLargeObject
Heap
def new generation total 9216K, used 3727K [0x00000000f4600000, 0x00000000f5000000, 0x00000000f5000000)
eden space 8192K, 45% used [0x00000000f4600000, 0x00000000f49a3f08, 0x00000000f4e00000)
from space 1024K, 0% used [0x00000000f4e00000, 0x00000000f4e00000, 0x00000000f4f00000)
to space 1024K, 0% used [0x00000000f4f00000, 0x00000000f4f00000, 0x00000000f5000000)
tenured generation total 10240K, used 4096K [0x00000000f5000000, 0x00000000f5a00000, 0x00000000f5a00000)
the space 10240K, 40% used [0x00000000f5000000, 0x00000000f5400010, 0x00000000f5400200, 0x00000000f5a00000)
compacting perm gen total 21248K, used 2740K [0x00000000f5a00000, 0x00000000f6ec0000, 0x0000000100000000)
the space 21248K, 12% used [0x00000000f5a00000, 0x00000000f5cad158, 0x00000000f5cad200, 0x00000000f6ec0000)
No shared spaces configured.

指定 -XX:+UseSerialGC 后,allocation3 直接进入老年代 tenured generation total 10240K, used 4096K; the space 10240K, 40% used

但是当 allocation3 = new byte[5 * _1MB]; 分配 5M 空间时,默认的 Parallel Scavenge 收集器会直接将该对象分配到老年代,避免触发 GC

1
2
3
4
5
6
7
8
9
10
xmt@server005:~/$ java -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintHeapAtGC TestLargeObject
Heap
PSYoungGen total 9216K, used 3727K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 45% used [0x00000000ff600000,0x00000000ff9a3f08,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 5120K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 50% used [0x00000000fec00000,0x00000000ff100010,0x00000000ff600000)
PSPermGen total 21504K, used 2740K [0x00000000f4600000, 0x00000000f5b00000, 0x00000000fec00000)
object space 21504K, 12% used [0x00000000f4600000,0x00000000f48ad158,0x00000000f5b00000)

其他垃圾收集器日志

如果想使用或者验证其他垃圾收集器,可以在执行时添加 JVM 参数:-XX:+UseSerialGC, -XX:+UseParNewGC, -XX:+UseConcMarkSweepGC, -XX:+UseParallelGC, -XX:+UseParallelOldGC,各参数含义参考“ JVM 常见参数”。

Android GC 日志

Android GC 日志非常简单,信息并不是很多。参考:解读Android日志消息

Dalvik/ART 简介

Android 的虚拟机是 Google 自己实现的:Dalvik/ARTAndroid L 开始使用 ART 并废除 Dalvik 。先简单介绍下两者的区别,参考JVM、Dalvik以及ART的区别

  • Dalvik
    .dex 格式是专为 Dalvik 设计的一种压缩格式,适合内存和处理器速度有限的系统,Dalvik 指令集是基于寄存器的架构。
  • ART
    ART: Android runtimeART 的机制与 Dalvik 不同。在 Dalvik 下应用每次运行的时候,字节码都需要通过即时编译器(just in time ,JIT)转换为机器码,这会拖慢应用的运行效率。而 ART 在应用第一次安装时就预编译字节码到机器语言,移除解释代码这一过程,应用程序执行更有效率启动更快,但是 ART 占用空间比 Dalvik 大 10%~20%,属于空间换时间。

Dalvik 日志格式

格式:D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>
示例:D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms

垃圾回收的原因:

  • GC_CONCURRENT
    在堆开始占用内存时可以释放内存的并发垃圾回收。
  • GC_FOR_MALLOC
    堆已满而系统不得不停止应用并回收内存时,应用尝试分配内存而引起的垃圾回收。
  • GC_HPROF_DUMP_HEAP
    当请求创建 HPROF 文件来分析堆时出现的垃圾回收。
  • GC_EXPLICIT
    显式垃圾回收,例如当调用 gc() 时(应避免调用,而应信任垃圾回收会根据需要运行)。
  • GC_EXTERNAL_ALLOC
    这仅适用于 API <= 10 。外部分配内存的垃圾回收(例如存储在原生内存或 NIO 字节缓冲区中的像素数据)。

ART 日志格式

格式:I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>
示例:I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms

GC_Reason:垃圾回收原因

  • Concurrent
    不会暂停应用线程的并发垃圾回收。此垃圾回收在后台线程中运行,而且不会阻止分配。
  • Alloc
    应用在堆已满时尝试分配内存引起的垃圾回收。在这种情况下,分配线程中发生了垃圾回收。
  • Explicit
    由应用明确请求的垃圾回收,例如通过调用 gc()。与 Dalvik 相同,在 ART 中,最佳做法是信任垃圾回收并避免请求显式垃圾回收。不建议使用显式垃圾回收,因为它们会阻止分配线程并不必要地浪费 CPU 周期。如果显式垃圾回收导致其他线程被抢占,那么它们也可能会导致卡顿(应用中出现间断、抖动或暂停)。
  • NativeAlloc
    原生分配(如位图或 RenderScript 分配对象)导致出现原生内存压力,进而引起的回收。
  • CollectorTransition
    由堆转换引起的回收;此回收由运行时切换垃圾回收引起。回收器转换包括将所有对象从空闲列表空间复制到碰撞指针空间(反之亦然)。当前,回收器转换仅在以下情况下出现:在 RAM 较小的设备上,应用将进程状态从可察觉的暂停状态变更为可察觉的非暂停状态(反之亦然)。
  • HomogeneousSpaceCompact
    齐性空间压缩是空闲列表空间到空闲列表空间压缩,通常在应用进入到可察觉的暂停进程状态时发生。这样做的主要原因是减少 RAM 使用量并对堆进行碎片整理。
  • DisableMovingGc
    这不是真正的垃圾回收原因,但请注意发生并发堆压缩时,由于使用了 GetPrimitiveArrayCritical,回收遭到阻止。一般情况下,强烈建议不要使用 GetPrimitiveArrayCritical,因为它在移动回收器方面具有限制。
  • HeapTrim
    这不是垃圾回收原因,但请注意,堆修剪完成之前回收会一直受到阻止。

GC_NameART 具有可以运行的多种不同的垃圾回收:

  • Concurrent mark sweep (CMS)
    整个堆回收器,会释放和回收映像空间以外的所有其他空间。
  • Concurrent partial mark sweep
    几乎整个堆回收器,会回收除了映像空间和 zygote 空间以外的所有其他空间。
  • Concurrent sticky mark sweep
    生成回收器,只能释放自上次垃圾回收以来分配的对象。此垃圾回收比完整或部分标记清除运行得更频繁,因为它更快速且暂停时间更短。
  • Marksweep + semispace
    非并发、复制垃圾回收,用于堆转换以及齐性空间压缩(对堆进行碎片整理)。

Objects_freed 释放的对象:此次垃圾回收从非大型对象空间回收的对象数量。
Size_freed 释放的大小:此次垃圾回收从非大型对象空间回收的字节数量。
Large_objects_freed 释放的大型对象:此次垃圾回收从大型对象空间回收的对象数量。
Large_object_size_freed 释放的大型对象大小:此次垃圾回收从大型对象空间回收的字节数量。
Heap_stats 堆统计数据:空闲百分比与(活动对象数量)/(堆总大小)。
Pause_time 暂停时间:通常情况下,暂停时间与垃圾回收运行时修改的对象引用数量成正比。当前,ART CMS 垃圾回收仅在垃圾回收即将完成时暂停一次。移动的垃圾回收暂停时间较长,会在大部分垃圾回收期间持续出现。

如果在 logcat 中看到大量的垃圾回收,请注意堆统计数据的增大(上面示例中的 25MB/38MB 值)。如果此值继续增大,且始终没有变小的趋势,则可能会出现内存泄漏。或者如果看到原因为 Alloc 的垃圾回收,那么意味着操作已经快要达到堆容量,并且将很快出现 OOM 异常。

参考文档

0%