垃圾回收
背景
Java 语言出来之前,大家都在拼命的写 C 或者 C++ 的程序,而此时存在一个很大的矛盾。
C++ 等语言创建对象要不断的去开辟空间,不用的时候又需要不断的去释放控件,既要写构造函数,又要写析构函数,很多时候都在重复的 allocated,然后不停的析构。
于是,有人就提出,能不能写一段程序实现这块功能,每次创建,释放控件的时候复用这段代码,而无需重复的书写呢?
1960 年,基于 MIT 的 Lisp 首先提出了垃圾回收的概念,而这时 Java 还没有出世呢!所以实际上 GC 并不是 Java 的专利,GC 的历史远远大于 Java 的历史!
内存分配
先看一下java内存分配的流程图,什么时间才会发生yongGC,什么时间会发生内存溢出,什么时间发生fullGC
这里可以结合一段代码来看实际的内存回收情况,代码如下:
public class AllocationTest {
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
public 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[4 * _1MB];//出现一次Minor GC
allocation5 = new byte[5 * _1MB]; // 堆内存溢出
}
/**
*VM参数:-verbose:gc-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails-XX:SurvivorRatio=8
*-XX:PretenureSizeThreshold=3145728
*/
public static void testPretenureSizeThreshold () {
byte[] allocation;
allocation = new byte[4 * _1MB];//直接分配在老年代中
}
public static void main(String[] args) {
testAllocation();
// testPretenureSizeThreshold();
}
}
需要设置启动时JVM参数:
方法testAllocation:-verbose:gc表述输出虚拟机内GC详细信息,-xms20M设置启动时内存20M,-Xmx堆内存最大20M,-Xmn堆内新生代的大小为10M,XX:+PrintGCDetails与-verbose:gc表示的意义一样,都是输出GC详细信息设置一个即可,-XX:SurvivorRatio=8表示设置Eden区与Survivor区的比值,默认值为8,也就是说Eden区为8,Survivor区为1,Survivor区一共有两个,所以新生代内存被平均分成了10份,Eden区占去了8,s1占去了1,S2占去了1。
方法解析:创建5个空的byte数组,前三个new了一个大小为2M的对象,Eden区内存8M,已被占用6M,allocation4创建对象的时候,Eden已经放不下了,是时候MinorGC了,先将一部分存活对象放到S1中,发现放不下,只能放到老年代了,然后就放到老年代了6M,然后Eden又new了4M之后,在创建allocation5时,5M已经超过了Eden区一半的大小,所以属于大对象,就要往老年代里放,发现也放不下,然后就爆内存溢出了。
下边是方法执行的结果
[GC (Allocation Failure) [DefNew: 6289K->820K(9216K), 0.0043434 secs] 6289K->4916K(19456K), 0.0044060 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [DefNew (promotion failed) : 7204K->6407K(9216K), 0.0027041 secs][Tenured: 6861K->6861K(10240K), 0.0023662 secs] 11300K->10982K(19456K), [Metaspace: 224K->224K(4480K)], 0.0051184 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [Tenured: 6861K->6844K(10240K), 0.0013907 secs] 10982K->10965K(19456K), [Metaspace: 224K->224K(4480K)], 0.0014196 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4488K [0x05000000, 0x05a00000, 0x05a00000)
eden space 8192K, 54% used [0x05000000, 0x05462298, 0x05800000)
from space 1024K, 0% used [0x05800000, 0x05800000, 0x05900000)
to space 1024K, 0% used [0x05900000, 0x05900000, 0x05a00000)
tenured generation total 10240K, used 6844K [0x05a00000, 0x06400000, 0x06400000)
the space 10240K, 66% used [0x05a00000, 0x060af320, 0x060af400, 0x06400000)
Metaspace used 225K, capacity 2280K, committed 2368K, reserved 4480K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.yonyou.gc.memory.AllocationTest.testAllocation(AllocationTest.java:20)
at com.yonyou.gc.memory.AllocationTest.main(AllocationTest.java:34)
从结果可以看出,发生了两次minorGC,第一次是在allocation4对象创建的时候,将Eden区由6M回收到了820K,对象都放到了老年代,第二次发生在allocation5创建的时候,发生了promotion failed,意思是新生代放不下了,对象直接放到了老年代,然后后边因为老年代也放不下5M了,紧接着又发生了FullGC,FullGC完了对象依然放不下,就内存溢出了。
判断垃圾
引用计数
引用计数法固然是好呀,简单高效,不需要暂停应用程序就可以知道“所有的垃圾”,直接清理就可以了,但是回到上面的图你会发现,引用计数存在很严重的内存问题,就是循环引用的对象,他并不能视他为垃圾,这种对象就永远不会被回收,会造成严重的内存泄露,所以java在1.2之后就弃用了这种方式。
可达性分析
GCroot指的是什么呢?
- 虚拟机栈中引用的对象(本地变量表)
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中引用的对象(Native Object)
Stop-the-world
误报的话,最多就是浪费了一次回收的机会,下次回收继续回收就可以了
但是如果漏报,线程2引用的对象会被垃圾回收掉,严重的可能导致整个程序崩溃,所以就需要在识别垃圾的时候停下所有线程,即Stop the world.
引用类型
引用类型 | 功能特点 |
---|---|
强引用 (Strong Reference) | 被强引用关联的对象永远不会被垃圾收集器回收掉 |
软引用 (Soft Reference) | 软引用关联的对象,只有当系统将要发生内存溢出时,才会去回收软引用引用的对象 |
弱引用 (Weak Reference) | 只被弱引用关联的对象,只要发生垃圾收集事件,就会被回收 |
虚引用 (Phantom Reference) | 被虚引用关联的对象的唯一作用是能在这个对象被回收器回收时收到一个系统通知 |
强引用
只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
Object obj = new Object();
弱引用
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
GC算法
回收算法类型 | 优点 | 缺点 |
---|---|---|
标记清除算法(Mark-Sweep) | 不需要移动对象,简单高效 | 标记-清除过程效率低,GC产生内存碎片 |
复制算法(Copying) | 简单高效,不会产生内存碎片 | 内存使用率低,且有可能产生频繁复制问题 |
标记-整理算法(Mark-Compact) | 综合了前两种算法的优点 | 仍需要移动局部对象 |
分代收集算法(Generational Collection) | 分区回收 | 对于长时间存活对象的场景回收效果不明显,甚至起到反作用 |
标记清除算法
先标记出存货对象,及可回收对象,再从内存中清除对象。优点:简单高效。缺点:容易造成内存碎片化,比如说内存回收之后,断断续续的。假如要分配一个较大对象,需要在内存中寻找一块儿连续的内存空间,会发现找不到~可能报内存溢出了。
复制算法
复制算法,就是将存活的对象复制到另外一块儿干净的连续的内存空间里,另外一块儿直接清除。优点:解决了内存碎片化的问题。缺点:始终有一块儿内存空间被闲置,不能有效的利用有限的内存空间。
标记整理算法
标记整理算法弥补了复制算法的缺点。将清除后的内存空间进行整理压缩,存活对象放在连续的内存空间内,又解决了内存碎片化的问题。缺点也很明显,就是压缩及对象的移动对性能的损耗相当大,影响系统的吞吐量。
分代回收
综合以上算法衍生出的分代回收,将新生代跟老年代分开处理,为什么要这么做呢IBM 公司的专业研究表明,有将近 98% 的对象是朝生夕死,所以针对这一现状,大多数情况下,对象会在新生代 Eden 区中进行分配。所以就将大部分的对象在新生代就回收掉,新生代采用了复制算法,即提高了系统的吞吐,又避免了内存碎片化的问题,老年代采用标记整理的算法,充分利用老年代的空间。
垃圾回收器
回收器类型 | 回收算法 | 特点 | 设置参数 |
---|---|---|---|
Serial New/Serial Old回收器 | 复制算法/标记-整理算法 | 单线程复制回收,简单高效,但会暂停程序导致停顿 | -XX:+UseSerialGC(年轻代、老年代回收器为:Serial New、Serial Old) |
ParNew New/ParNew Old回收器 | 复制算法/标记-整理算法 | 多线程复制回收,降低了停顿时间,但容易增加上下文切换 | -XX:+UserParNewGC(年轻代、老年代回收器为:ParNew New、Serial Old, JDK1.8中无效) -XX:+UseParallelOldGC(年轻代、老年代回收器为:Parallel Scavenge、Parallel Old) |
Parallel Scavenge回收 | 复制算法 | 并行回收器,追求高吞吐量,高效利用CPU | -XX:+UseParallelGC(年轻代、老年代回收器为:Parallel Scavenge、Serial Old) -XX:ParallelGCThreads=4(设置并发线程) |
CMS回收器 | 标记-清理算法 | 老年代回收器,高并发、低停顿,追求最短GC回收停顿时间,CPU占用较高,响应时间快,停顿时间短 | -XX:+UserConcMarkSweepGC(年轻代、年老带回收器为:ParNew New、CMS(Serial Old作为备用)) |
G1回收器 | 标记-整理+复制算法 | 高并发、低停顿,可预测停顿时间 | -XX:+UseG1 GC(年轻代、老年代回收器为:G1、G1) -XX:MaxGCPauseMillis=200 (设置最大停顿时间) |
GC调优
GCEasy
回收调优
- 降低 Minor GC 频率
2、降低 Full GC 的频率
2.1、减少创建大对象
2.2、增大堆内存空间
2.3、选择合适的 GC 回收器
响应时间优先的应用 :尽可能设大,直到接近系统的最低响应时间限制 (根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
吞吐量优先的应用 :尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
垃圾回收统计信息
-XX:+PrintGC虚拟机启动后,只要遇到GC就会打印日志
-XX:+PrintGCDetails 查看详细信息,包括各个区的情况
-XX:+PrintGCTimeStamps 可以将时间和日期也加到GC日志中。表示自JVM启动至今的时间戳
-XX:+PrintGCApplicationStoppedTime 打印垃圾回收期间程序暂停的时间.可与上面混合使用
-XX:+PrintGCDateStamps 每一行就添加上了绝对的日期和时间。(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:…/logs/gc.log 日志文件的输出路径
-XX:+PrintTLAB 查看TLAB空间的使用情况
JVM参数
堆参数
-Xms20M,-XX:InitialHeapSize简写 表示设置堆容量的最小值为20M,必须以M为单位
-Xmx20M -XX:MaxHeapSize简写,表示设置堆容量的最大值为20M,必须以M为单位。将-Xmx和-Xms设置为一样可以避免堆自动扩展,减少程序运行时的垃圾回收次数,从而提供性能。大的项目-Xmx和-Xms一般都要设置到10G、20G甚至还要高
所有JVM关于初始\最大堆内存大小的输出都是使用它们的完整名称:“InitialHeapSize”和“InitialHeapSize”。所以当你查询一个正在运行的JVM的堆内存大小时,如使用-XX:+PrintCommandLineFlags参数或者通过JMX查询,应该寻找“InitialHeapSize”和“InitialHeapSize”标志而不是“Xms”和“Xmx”
稳定的堆大小能减少gc次数,但是每次gc时间增加
震荡的堆大小能增加gc次数,但是每次gc时间减少
-XX:MinHeapFreeRatio 最小空闲比例,当堆空间空闲内存小于这个比例,则扩展
-XX:ManHeapFreeRatio 最大空闲比例,当堆空间空闲内存大于这个比例,则压缩
-Xms和-Xmx相等时,上面的参数失效
-XX:+HeapDumpOnOutOfMemoryError 使得JVM在产生内存溢出时自动生成堆内存快照
-XX:HeapDumpPath=
-XX:OnOutOfMemoryError 当内存发生溢出时 执行一串指令-XX:OnOutOfMemoryError =“sh ~/cleanup.sh”
-XX:PermSize 设置永久代的初始大小
-XX:MaxPermSize 设置永久代的最大大小
-Xmn 或 -XX:NewSize 设置新生代的初始大小
新生代只是堆的一部分 新生代越大老年代越小,一般不允许新生代比老生代还大。考虑到GC最坏的情况 新生代全部复制到老生代会产生OOM错误,这个参数对系统性能以及GC行为有很大影响,新生代大小一般会设置整个堆空间的1/3到1/4左右
-XX:NewRatio 设置新生代和老生代的相对大小。优点是新生代大小会随着整个堆大小动态扩展
不同的堆分布情况对系统执行都会产生不一样的影响,实际配置的基本策略就是尽可能将对象预留在新生代,减少老年代的GC次数。
-XX:SurvivorRatio 设置新生代中eden空间和from/to空间的比例,eden/to=eden/from
-XX:+PrintTenuringDistribution 指定JVM在每次新生代GC时 输出Survivor中对象的年龄分布
-XX:InitialTenuringThreshold 设置老年代阈值的初始值
-XX:MaxTenuringThreshold 设置新生代阈值的最大值(垃圾最大年龄),默认为15。如果设置为0的话,则新生代对象不经过Survivor区,直接进入老年代。对于老年代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则新生代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在新生代即被回收的概念。
另外,大对象(新生代eden区无法装入时,也会直接进入老年代)。
-XX:PretenureSizeThreshold 设置对象的大小超过指定的大小后,直接进入老年代,但是要注意TLAB区域优先分配空间的原则
TLAB 全称Thread Local Allocation Buffer,即线程本地分配缓存,从名字上看是一个线程专用的内存分配区域,是为了加速对象分配而生的。每一个线程都会产生一个TLAB,该线程独享的工作区域,java虚拟机使用这种TLAB区来避免多线程冲突问题,提高了对象分配的效率。TLAB空间一般不会太大,当大对象无法在TLAB分配时,则会直接分配到堆上。
-XX:+NeverTenure 对象永远不会晋升到老年代(当不需要老生代的时候可以这样设置)
-XX:+AlwaysTenure 表示没有Survivor区 对象会直接被移动到老年代中
TLAB
-XX:+UserTLAB 使用TLAB
-XX:+TLABSize 设置TLAB大小
-XX:+TLABRefllWasteFraction 设置维护进入TLAB空间的单个对象大小,他是一个比例值,默认为64,即如果对象大于整个空间的1/64,则在堆中创建对象。
-XX:+PrintTLAB 查看TLAB信息
-XX:+ResizeTLAB 自调整TLABRefllWasteFraction阀值
-XX:TLABWasteTargetPercent TLAB占eden区的百分比
栈参数
-Xss5m 指定线程的最大栈空间,整个参数也直接决定了函数可调用的最大深度
方法区参数
-XX:PermSize=10M 表示JVM初始分配的永久代的容量,必须以M为单位
-XX:MaxPermSize=10M 表示JVM允许分配的永久代的最大容量,必须以M为单位,大部分情况下这个参数默认为64M
方法区,又称永久区,默认情况下-XX:PermSize=64M,如果系统运行时生产大量的类,就需要设置一个相对合适的方法区,以免出现永久区内存溢出的问题
直接内存参数
-XX:MaxDirectMemorySize=128m 直接内存最大容量,M单位,默认为64M
如果不设置默认值为最大堆空间,即-Xmx。直接内存使用达到上限时,就会触发垃圾回收,如果不能有效的释放空间,也会引起系统的OOM
其他
-XX:+UseFastAccessorMethods 原始类型的快速优化
-XX:+DisableExplicitGC 关闭System.gc()
-XX:+AggressiveOpts 加快编译
-XX:+UseBiasedLocking 锁机制的性能改善
-Xnoclassgc 禁用垃圾回收
收集器设置
收集器设置:
-XX:+UseSerialGC 新生代串行(Serial),老年代串行(Serial Old)
-XX:+UseParNewGC 新生代并行(ParNew),老年代串行(Serial Old)
-XX:+UseConcMarkSweepGC 新生代并行(ParNew),老年代串行(CMS),备份(Serial Old)
-XX:+UseParallelGC 新生代并行吞吐(Parallel Scavenge),老年代串行(Serial Old)
-XX:+UseParalledlOldGC 新生代并行吞吐(Parallel Scavenge),老年代并行吞吐(Parallel Old)
并行收集器设置
-XX:ParallelGCThreads=n 设置并行收集器收集时使用的CPU数。并行收集线程数。一般最好和计算机的CPU相当
-XX:MaxGCPauseMillis=n 设置并行收集最大暂停时间
-XX:GCTimeRatio=n 设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
-XX:+CMSIncrementalMode 设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n 设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。