Java 中的内存分配及垃圾回收策略。本文是《深入理解 Java 虚拟机: JVM 高级特性与最佳实践 第 2 版 - 第三章 垃圾收集器与内存分配策略》的读书笔记。
GC [Garbage Collection]:垃圾回收器 。Java 使用 GC 进行内存回收理,不用手动释放内存而提升开发效率。GC 主要完成三件事:
- 确定哪些内存需要回收
- 什么时候回收
- 怎么回收
内存分配
虚拟机运行时数据区

如图所示,JVM 内存模型将内存分为:
- 程序计数器
占用一块很小的内存空间,当前线程执行的字节码的行号指示器,用来选取下一条需要执行的字节码。 - 虚拟机栈
每个方法执行时会创建一个栈帧,存储局部变量表,操作数栈等等。局部变量表只保存基本数据类型、对象的引用、返回地址字节码。当方法退出时,虚拟机栈就会自动清空,释放内存。 - 本地方法栈
虚拟机栈是为Java服务的,而本地方法栈针对的是Native方法的。本地方法栈也是在方法退出时就会释放内存。 - 堆
所有的对象实例和数组都是在堆中分配的内存,是垃圾回收的主要区域,有时候也称为GC堆。类的成员变量是在对象实例化时分配在堆中的。 - 方法区
用于保存类信息,常量,静态变量等,也叫Non-Heap(非堆)。
运行时常量池:方法区的一部分,保存类信息,常量池。
通常将内存分为栈和堆,指的就是虚拟机栈和堆,而内存泄露、内存溢出、垃圾回收等都主要针对的是堆内存。程序计数器、虚拟机栈、本地方法栈都是随线程生而生,灭而灭;栈在方法退出时自动清空,而方法区的回收性价比非常低,条件苛刻;所以内存回收主要集中在堆中。
对象访问定位
建立对象后,Java 程序需要通过栈上的 reference 数据来操作堆上的对象。Java 采用的是直接指针访问对象,这要求堆对象布局中需要考虑如何放置访问类型数据的相关信息,而 reference 存储的直接就是对象地址。

内存溢出
OutOfMemoryError
无法申请到足够的存储空间,都会抛出OOM。StackOverflowError
虚拟机栈和本地方法栈,如果请求的栈深度大于虚拟机允许的最大深度,将会抛出StackOverflowError。
它们都不属于 Exception 的子类,所以 catch 时,只能使用 Throwable。
1 | // 1. OutOfMemoryError |
垃圾对象的判断
GC 之前要判断回收哪些内存?Java 中采用的是可达性分析算法来判断的。
引用计数算法
引用计数算法:给对象添加一个引用计算器,每当有一个地方引用它时,计数器自动加 1;当引用失效时,计数器自动减 1。当计算器为 0 时,表示该对象没有被引用了。
引用计数算法使用非常广泛,但是 Java 中并没有选用它来管理内存,最主要的原因是它很难解决对象之间相互循环引用的问题。
可达性分析算法
可达性分析算法:通过一系列的称为 GC Roots 的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链 Reference Chain,当一个对象到 CC Roots 没有任何引用链相连(即图论中的,GC Roots 到达这个对象是不可达的),则证明此对象是不可用的。

如果对象 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。详细划分图如下:

新生代和老年代的比率,以及 Eden 和两个 Survivor 的比率,都可以可以通过 JVM 参数(–XX:NewRatio, –XX:SurvivorRatio)来设置。参数含义详见“ JVM 常见参数”中的描述。
GC 分类
Minor GC
通常指发生在新生代的垃圾回收动作,因为Java对象大多数具有朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也非常快。Full GCFull 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
标记清除算法:也是最基础的收集算法,分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
其他收集算法基本都是基于这种思路并对不足进行改进的,该算法的不足之处有两个:
- 效率问题
标记和清除两个阶段效率都不高。 - 空间问题
标记清除后会产生大量不连续的内存碎片,空间碎片太多会导致后续分配大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

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

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

垃圾收集器
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,它将堆内存区域划分为多个大小相等的独立区域,虽然保留了新生代和老年代的概念,但是新生代和老年代将不再是物理隔离,他们都是独立区域集合。这和其他收集器直接划分为新生代老年代有非常大的区别。

其中 ? 部分就是 G1 收集器。ParNew 和 Parallel Scavenge 最大的区别是,Parallel Scavenge 不能与 CMS 配合工作。参考:收集器分类
堆内存回收策略
分代收集算法
商业虚拟机的垃圾回收通常采用“分代收集 Generational Collection ”算法,它并没有引入新的算法,而是针对新生代和老年代的特点采用适当的算法。
- 新生代
通常会有大量对象创建及死亡,只有少量对象存活,采用复制算法。 - 老年代
对象存活率高,而且也没有额外的空间担保,使用“标记-清理”或者“标记-整理”算法。
对象分配
- 优先
Eden区Java对象大多数具有朝生夕灭的特性,所以新创建的对象通常优先分配在新生代堆中,默认为Eden和From区,也就是说新生代的可用内存空间默认为整个新生代容量的 90%,剩下 10% 为复制算法备用空间。当没有足够空间进行分配时,虚拟机发起一次Minor GC。但是对象回收时,无法确保只有不多于 10% 的对象存活,如果To Survivor空间不够,需要老年代进行分配担保,也就是说超出空间的对象直接会进入老年代。如果新生代GC的平均晋升大小比目前老年代剩余空间大,则会触发一次Full GC。 - 大对象直接分配在老年代
所谓大对象是指需要大量连续内存空间的对象,最典型的大对象就是很长的字符串和数组。JVM可以通过设置-XX:PretenureSizeThreshold来决定对象大小门限值,如果超过指定值的对象直接分配到老年代,避免垃圾回收时在新生代中发生大量的内存复制。不过这个参数只对Serial和ParNew收集器生效。 - 长期存活对象进入老年代
虚拟机给每个对象定义一个对象年龄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 | 新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10 |
| -XX:PretenureSizeThreshold | 老年代门限值,大小大于这个值的对象直接分配进老年代,这个参数只对 Serial 和 ParNew 收集器生效 |
| -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配置参数 ,这篇博客的虚拟机系列写的都不错。
各字段含义


一段示例 Log:
1 | [GC [PSYoungGen: 6635K->800K(9216K)] 6635K->6944K(19456K), 0.0075650 secs] [Times: user=0.15 sys=0.00, real=0.00 secs] |
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 | xmt@server005:~/$ java -version |
一个怪异现象示例
源码中,堆内存申请大小如下:
1 | public class TestGCLog { |
执行结果:
1 | xmt@server005:~/$ javac TestGCLog.java |
运行时 JVM 的配置为:初始堆和最大堆都是 20M;新生代配置为 10M,比率为 8,也就是说 Eden 为 8M(8192K),From/To 各 1M(1024K),新生代可用总大小为 9M(9216K);这些信息从 Heap 的 Log 中也可以得到验证。
从代码中可以看出 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 | xmt@server005:~/$ java -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintHeapAtGC TestGCLog |
虽然 allocation5 的内存需求只增加了几K,但是触发了 2 次 GC。第一次 GC 前后堆中可以明确看到新生代堆中变量晋升到了老年代 ParOldGen total 10240K, used 7680K; object space 10240K, 75% used,新生代也基本清空。
但是接着又来了一次 Full GC?Java 7 中并没有给出原因,但是使用 Java 8 重新编译执行一次,可以明确看到:[Full GC (Ergonomics),是因为 JVM 认为需要优化下主动触发的 Full GC,直接将新生代完全清空,全部晋升到老年代。
我们也可以在执行时,将 JVM 参数的初始堆和最大堆大小加大避免触发 Full GC。本地验证设置为 -Xms26M -Xmx26M 时,第一次 GC 后 ParOldGen total 16384K, used 7680K; object space 16384K, 46% used,老年代只使用了 46%,JVM 不用优化并触发 Full GC。这也侧面证明 Full GC 的触发多取决于垃圾回收器算法的实现,不同回收器触发机制并不一致。
大对象直接分配在老年代
先看源码:
1 | public class TestLargeObject { |
根据计算 Eden 区完全可以容下三个大对象,执行结果也匹配:
1 | xmt@server005:~/$ java -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintHeapAtGC TestLargeObject |
使用 -XX:PretenureSizeThreshold 参数设置为 3M,当大对象大小超过这个值后直接进入老年代。
1 | xmt@server005:~/$ java -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:PretenureSizeThreshold=3145728 TestLargeObject |
结果显示 -XX:PretenureSizeThreshold 参数并没有生效,allocation3 还是进入了 Eden 区。原因是当前环境下默认的新生代 Parallel Scavenge 收集器并不识别这个参数,而指定新生代收集器为 -XX:+UseSerialGC 则会生效。参考:PretenureSizeThreshol参数不识别
1 | xmt@server005:~/$ java -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:PretenureSizeThreshold=3145728 -XX:+UseSerialGC TestLargeObject |
指定 -XX:+UseSerialGC 后,allocation3 直接进入老年代 tenured generation total 10240K, used 4096K; the space 10240K, 40% used 。
但是当 allocation3 = new byte[5 * _1MB]; 分配 5M 空间时,默认的 Parallel Scavenge 收集器会直接将该对象分配到老年代,避免触发 GC。
1 | xmt@server005:~/$ java -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintHeapAtGC TestLargeObject |
其他垃圾收集器日志
如果想使用或者验证其他垃圾收集器,可以在执行时添加 JVM 参数:-XX:+UseSerialGC, -XX:+UseParNewGC, -XX:+UseConcMarkSweepGC, -XX:+UseParallelGC, -XX:+UseParallelOldGC,各参数含义参考“ JVM 常见参数”。
Android GC 日志
Android GC 日志非常简单,信息并不是很多。参考:解读Android日志消息
Dalvik/ART 简介
Android 的虚拟机是 Google 自己实现的:Dalvik/ART。Android L 开始使用 ART 并废除 Dalvik 。先简单介绍下两者的区别,参考JVM、Dalvik以及ART的区别 :
Dalvik.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统,Dalvik指令集是基于寄存器的架构。ARTART: Android runtime,ART的机制与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_Name:ART 具有可以运行的多种不同的垃圾回收:
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 异常。
参考文档
- 深入理解
Java虚拟机:JVM高级特性与最佳实践 第 2 版 - 第三章 垃圾收集器与内存分配策略 - Java GC、新生代、老年代
- 收集器分类
- 各GC的区别及触发条件
- JVM内存结构
- 解读Android日志消息
- Java虚拟机详解03—-常用JVM配置参数
- Minor GC、Major GC和Full GC之间的区别
- JVM常见参数
- JVM的新生代、老年代、元空间
- JDK8-废弃永久代(PermGen)迎来元空间(Metaspace)
- GC日志分析
- JVM实用参数
- PretenureSizeThreshol参数不识别