前言
对Java 中垃圾回收相关知识,予以记录!
一、基础知识储备
1、如何查看并设置默认的垃圾回收器
查看:java -XX:+PrintCommandLineFlags -version
设置:java -XX:+PrintCommandLineFlags 【垃圾回收器参数】 -version
设置某个垃圾回收器参数如下:
2、LOG中GC参数含义
3、Server/Client模式
4、如何确定某个对象是可回收垃圾?
参考:Java JVM垃圾回收确定垃圾的两种方式,GC Root
二、三大垃圾回收算法思想
当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:
1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。
显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;
如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
1、标记清除Mark-Sweep
分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
缺点:
-
执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
-
内存空间碎片化,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2、标记复制Mark-Copy
常被简称为复制算法它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:不产生内存碎片
缺点:
- 可能存在大量的内存间复制的开销,如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效。
- 将可用内存缩小为了原来的一半,空间浪费太多。
3、标记压缩Mark-Compact
标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
标记-压缩算法其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存:
缺点:
- 可能是大负重操作,标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策:如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作
- 这种对象移动操作必须全程暂停用户应用程序(STW问题)才能进行 。
三、七种垃圾收集器
①UseSerialGC
②UseSerialOldGC(被弃用)
③UseParNewGC
④UseParallelGC
⑤UseParallelOldGC
⑥UseConcMarkSweepGC
⑦UseG1GC
源码分析:
下表图表示新生代与老年代垃圾收集器关联(重要):
G1之前的收集器特点:
- 年轻代和老年代是各自独立且连续的内存块
- 年轻代收集使用单eden+S0+S1进行复制算法
- 老年代收集必须扫描整个老年代区域
- 都是以尽可能少而快速地执行GC为设计原则
以JVM参数配置与下列代码查看对应的垃圾收集器:
public static void main(String[] args) {
byte[] bytes = new byte[30 * 1024 * 1024];
}
1、串行垃圾回收器Serial:不适合服务器使用
①UseSerialGC
一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有工作线程直到它收集结束。
JVM参数配置:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC
开启后会使用Serial(Young区用) + Serial Old(Old区用)的收集器组合,新生代老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法
(ParNew+Tenured)
备注情况: Java HotSpot™ 64-Bit Server VM warning:Using the ParNew young collector with the Serial old collector is deprecatedand will likely be removed in a future release
②UseSerialOldGC(被弃用)
(理论知道即可,实际中已经被优化了,没有了)
是Serial垃圾收集器老年代版本,同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在Client默认的java虚拟机默认的老年代垃圾收集器
- 在JDK 1.5之前与新生代的Parallel Scavenge收集器搭配使用(Parallel Scavenge + Serial Old)
- 作为老年代版中使用CMS收集器的后备垃圾收集方案
JVM参数配置:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldGC
2、并行垃圾回收器Parallel:适用于大数据开发
③UseParNewGC
使用多线程进行垃圾回收,在垃圾收集时,会Stop-the-World暂停其他所有的工作线程直到它收集结束
JVM参数配置:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC
(ParNew+Tenured)
启用ParNew收集器,只影响新生代的收集,不影响老年代,会使用ParNew(Young区用)+ Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法
备注
-XX:ParallelGCThreads:限制线程数量,默认开启和CPU数目相同的线程数
-Xms 10m -Xmx 10m -XX:+PrintGCDetails -XX:+UseParNewGC
④UseParallelGC
串行收集器在新生代与老年代的并行化
类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。
可控制的吞吐量(Thoughput=运行用户代码时间/(运行用户代码时间+垃圾收集时间),即程序运行100分钟,垃圾收集1分钟,吞吐量就是99%)。高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务。
自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMills)或最大吞吐量(这也是ParallelScavenge收集器与ParNew收集器的一个重要区别)
JVM参数配置:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC
(PSYoungGen+ParOldGen)
备注
-XX:ParallelGCThreads=数字N 表示启动多少个GC线程
cpu>8 N = cpu数*5/8
cpu<8 N = cpu实际个数
⑤UseParallelOldGC
Parallel收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,Parallel Old收集器在JDK1.6才开始提供。
JVM参数:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC
(PsYoungGen+ParOldGen)
设置该参数后,新生代Parallel + 老年代Parallel Old
3、并发垃圾回收器CMS(ConcMarkSweep):适用于对响应速度有要求再次标记也会有STW
⑥UseConcMarkSweepGC
一种以获取最短回收停顿时间为目标的收集器,适用于互联网网站或B/S系统服务器上,希望系统停顿时间最短。CMS非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器
JVM参数配置:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC
开启该参数后会自动将-XX:+UseParNewGC打开
使用ParNew(Young区用) + CMS(Old区用) + Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器
备注:
-XX:CMSFullGCsBeForeCompaction(默认0,即每次都进行内存整理,来指定多少次CMS收集之后,进行一次压缩的Full GC)
4、G1垃圾回收器:JDK9及更高版本默认垃圾收集器
⑦UseG1GC
G1(Garbage-First)收集器,是一款面向服务端应用的收集器,主要应用在多CPU和大内存环境中,极大的减少垃圾收集的停顿时间,全面提升服务器的性能,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求,逐步替换java8以前的CMS收集器
- 它具有以下特性:
- 像CMS一样,能与应用程序线程并发执行
- 整理空闲空间更快
- 需要更多的时间来预测GC停顿时间
- 不希望牺牲大量的吞吐性能
- 不需要更大的Java Heap
- 它相比CMS,在以下方面表现更为出色:
- G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片
- G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间
-
底层原理:
主要改变是Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region,每个region从1M到32M不等。一个region有可能属于Eden,Survivor或者Tenured内存区域
区域化region区域,整体编为了一些列不连续的内存区域,避免了全内存区的GC操作
核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小,
在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代与老年代之间切换。启动时可以通过参数 -XX:G1HeapRegionSize=n 可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区
大小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为32MB * 2048 = 64G内存 -
G1与CMS的区别:
- G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW
- G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片
- 宏观上看G1之中不再区分年轻代和老年代。把内存划分成多个独立的子区域(Region),可以近似理解为一个围棋的棋盘
- G1收集器里面将整个的内存区都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代但它们不再是物理隔离的,而是一部分Region的集合且不需要Region是连续的,也就是说依然会采用不同的GC方式来处理不同的区域
- G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换
-
四步过程
-
JVM参数配置:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC
四、如何选择合适的垃圾收集器?
1、单CPU或小内存,单机程序
- -XX:+UseSerialGC
2、多CPU,需要最大吞吐量,如后台计算型应用
- -XX:+UseParallelGC 或者-XX: +UseParallelOldGC
3、多CPU,追求低停顿时间,需快速响应如互联网应用
- -XX:+UseConcMarkSweepGC
- -XX:+ParNewGC