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 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
标记清除算法:也是最基础的收集算法,分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
其他收集算法基本都是基于这种思路并对不足进行改进的,该算法的不足之处有两个:
- 效率问题
标记和清除两个阶段效率都不高。 - 空间问题
标记清除后会产生大量不连续的内存碎片,空间碎片太多会导致后续分配大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法 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
指令集是基于寄存器的架构。ART
ART: 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参数不识别