作为运维,你不一定要会写Java代码,但是一定要懂Java在生产跑起来之后的各种机制。
本文为《Hi,运维,你懂Java吗》系列文章 第七篇,敬请关注后续系列文章
欢迎关注 龙叔运维(公众号) 持续分享运维经验
前言
本篇对java的JVM堆内存的GC垃圾回收进行讲解。
垃圾回收,最核心的就是两个W一个H,本文也会从这三个点进行讲解:
WHERE:哪些是需要回收的垃圾
WHAT:GC的概念和知识点
HOW:怎么回收垃圾
本篇章节目录:
1.判断堆中哪些是垃圾对象
2.垃圾回收算法
3.垃圾回收器
4.GC日志
1、判断堆中哪些是垃圾对象
判断哪些是无效对象(一个对象不被任何对象或变量引用),这些无效对象就是需要被回收的垃圾
目前有两种方法进行判断,一种是引用计数法,一种是可达性分析法
引用计数法虽然简单,但存在无法解决对象之间相互循环引用的严重问题,且伴随加减法操作的性能影响.
因此,目前主流语言均使用可达性分析方法来判断对象是否有效.
1.1、引用计数法
给对象添加一个引用计数器,有一个地方引用,计数器值加1;引用失效,计数器值减1。计数器值为0的对象不能使用。
但是很难解决两个对象互相引用的问题,计数器永远不会变为0。
public void reference(){
A a = new A();
B b = new B();
a.instance = b;
b.instance = a;
}
1.2、可达性分析法
GC Roots作为起始点,从起始点向下搜索,搜索所走过的路径称为引用链。
当一个对象到GC Roots没有任何引用链时,则证明此对象会被判定为可回收对象。
GC Roots是一些由堆外指向堆内的引用。
哪些可作为GC ROOT呢?下面四个可以作为GC ROOT
-
A、虚拟机栈(栈帧中的本地变量表)中引用的对象
public static void main(String[] args) {
Object ref1 = new Object();
Object ref2 = new Object();
ref1 = ref2;
}
-
B、本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
-
C、方法区中类静态属性引用的对象
-
D、方法区中常量引用的对象
2、垃圾回收算法
2.1、复制算法
将内存分成大小相等两份,只将数据存储在其中一块上
用于年轻代的回收,体现在 Eden、Survior1、Survior2
步骤为:
1、.当需要回收时,首先标记废弃数据
2、然后将有用数据复制到另一块内存
3、最后将第一块内存空间全部清除
2.2、标记清除算法
步骤为:
1、标记需要回收的内存
2、将上述标记的对象进行统一回收
会造成内存碎片,这会导致以后抽象需要进行分配较大空间的对象无法找到足够的空间进行分配,而被迫触发另一次的gc
2.3、标记整理算法
步骤为:
1、标记需要回收的对象
2、将存活的对象向空间内存的一段移动【此时界限就很明确了,同时内存是连续的】
3、清理掉无效标记对象
解决了标记-清除算法产生不连续内存碎片的问题
2.4、分带收集算法
据对象存活周期的不同将Java堆划分为老年代和新生代,根据各个年代的特点使用最佳的收集算法.
1、老年代中对象存活率高,无额外空间对其分配担保,必须使用"标记-清除"或"标记-压缩"算法
2、新生代中存放"朝生夕死"的对象,用复制算法,只需要付出少量存活对象的复制成本,就可完成收集
3、垃圾回收器
如果你不知道当前你的java环境默认用的是什么垃圾回收器,可以用下面命令进行查看,下面可以看到使用的是Parallel并行回收器
java -XX:+PrintCommandLineFlags -version
概念解释:
- STW:Stop The World,JVM GC在清理内存时,整个程序的停顿时间
- 串行:垃圾回收线程和用户线程交替执行,且垃圾回收线程是单线程的,在执行垃圾回收线程时需要暂停用户线程,出现stop the world。GC线程是单线程的并非说明环境是单CPU下,在多核CPU下进行GC的时候只会使用单核CPU。
- 并行:并行是多条垃圾回收线程并行工作,这里肯定是在多核CPU环境下,多条垃圾回收线程同时执行,此时用户线程处于暂停。
- 并发:并发是垃圾回收线程和用户线程同时执行,也是在多核CPU环境下,垃圾回收线程和用户线程并发执行,也就是同一个时刻CPU0上执行用户线程,CPU1上有可能执行垃圾回收线程;
目前应用范围最广的,应该还是JDK8,它默认使用的是 Parallel Scavenge + Parallelo Old 收集器组合。
七种垃圾回收器的概括
垃圾回收器 | 作用区域 | 回收算法 | 开启参数 | 缺点 | 优点 |
---|---|---|---|---|---|
Serial | 年轻代 | 复制算法 | -XX:+UseSerialGC | 串行效率慢 | 最老的年轻代垃圾收集齐 |
Serial Old | 老年代 | 标记整理 | -XX:+UseSerialGC | 串行效率慢 | 最老的老年代垃圾收集齐 |
ParNew | 年轻代 | 复制算法 | -XX:+UseParNewGC | ParNew是Serial收集器的升级版,多线程 | |
Parallel | 年轻代 | 复制算法 | -XX:+UseParallelGC | 并行多线程处理,效率搞,吞吐量优先 | |
Parallel Old | 老年代 | 标记整理 | -XX:+UseParallelOldGC | 并行多线程处理,效率搞,吞吐量优先 | |
CMS | 老年代 | 标记清除 | -XX:+UseConcMarkSweepGC | 1、有浮动碎片(清理阶段用户线程依然在跑) 2、会产生内存碎片 3、因为是和用户线程并发跑,会导致系统GC时间有一定的变慢 | 并发收集,对比上面的收集齐,STW停顿时间降低 |
G1 | 全堆 | 复制算法 标记清除算法 标记整理算法 | -XX:+UseG1GC | 1、不会产生过多内存碎片 2、STW停止时间可控 |
3.1、Serial垃圾回收器
【作用于新生代】【串行】【复制算法】
一般和Serial Old两者搭配使用。
新生代采用Serial,是利用复制算法;老年代使用Serial Old采用标记-整理算法
它为单线程环境设计并且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境。
对应开启的JVM参数:-XX:+UseSerialGC
(上述参数后,会使用: Serial(Young区用) + Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法。)
3.2、Serial Old垃圾回收器
【作用于老年代】【串行】【标记整理算法】
一般和Serial或者ParNew两者搭配使用。
SerialOlid是Serial垃圾收集器老年代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在Client默
的java虚拟机默认的年老代垃圾收集器。
目前主要两个作用:
1.在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用。 ( Parallel Scavenge + Serial Old )
2.作为老年代版中使用CMS收集器的后备垃圾收集方案。
3.3、ParNew垃圾回收器
【作用于新生代】【串行】【复制算法】
ParNew收集器其实就是Serial收集器新生代的并行多线程版本,最常见的应用场景是配合老年代的CMS GC工作,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。
对应开启的JVM参数:-XX:+UseParNewGC
(上述参数后,会使用: ParNew(Young区用) + Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法。)
3.4、Parallel垃圾回收器
【作用于新生代】【并行】【复制算法】
Parallel 收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。
对应开启的JVM参数:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)
(上述参数后,会使用:新生代Parallel+老年代Parallel Old)
3.5、Parallel Old垃圾回收器
【作用于老年代】【并行】【标记整理算法】
Parallel Old收集器是Parallel 的老年代版,Parallel Old正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,JDK1.8后可以优先考虑新生代Parallel 和年老代Parallel Old收集器的搭配策略。
Parallel收集器重点关注的是:可控制的高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务。
对应开启的JVM参数:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)
(上述参数后,会使用:新生代Parallel+老年代Parallel Old)
3.6、CMS垃圾回收器
【作用于老年代】【并行】【标记清除】
用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程。互联网公司多用它,适用于对响应时间有要求的场景。
是一种以获取最短回收停顿时间为目标的收集器。
适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。
CMS非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。
对应开启的JVM参数: -XX:+UseConcMarkSweepGC
(上述参数后,会自动将-XX:+UseParNewGC打开,使用ParNew(Young区用) + CMS(Old区用) + Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器)
CMS垃圾回收期的GC过程整体分为4个步骤:
1、初始标记 - Stop The World
标记一下GC Roots能直接关联到的对象,会"Stop The World"。
2、并发标记 - No Stop The World
GC Roots Tracing,遍历所有GC ROOT 链路,可以和用户线程并发执行。
3、重新标记 - Stop The World
标记期间产生的对象存活的再次判断,修正对这些对象的标记,执行时间相对并发标记短,会“Stop The World”。
4、并发清除 - No Stop The World
清除对象,可以和用户线程并发执行。
3.7、G1垃圾回收器
G1是从JDK9之后的默认垃圾回收器,G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收。
G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:
1、G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
2、G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时
对应开启的JVM参数:-XX:+UseG1GC
G1 依然遵循分代回收的设计理论,但它对堆(Java Heap)内存进行了重新布局,不再是简单的按照新生代、老年代分成两个固定大小的区域了,而是把堆区划分成很多个大小相同的区域(Region),新、老年代也不再固定在某个区域了,每一个Region都可以根据运行情况的需要,扮演Eden、Survivor、老年代区域、或者Humongous区域。
GC回收步骤分为四步骤:
1、初始标记 - Stop The World
只标记 GC Roots 能直接关联的对象,还有一些额外的细节操作例如修改TAMS指针的值,保证后续阶段用户程序并发运行的时候,新对象分配在正确的位置。这个阶段需要暂停用户线程,但耗时很短。
2、并发标记 - No Stop The World
从根节点(GC Root)开始,顺着引用链遍历整个堆,找出存活的对象。这个步骤耗时较长,但用户线程可以和GC线程并发执行。
3、最终标记 - Stop The World
处理并发标记阶段,用户线程继续运行产生的引用变动,这个阶段需要暂停用户线程,支持并行处理。
4、筛选回收 - Stop The World
根据以上三个阶段标记完成的数据,计算出各个Region的回收价值和成本,再根据用户期望的停顿时间来决定要回收多少个Region。回收使用的是复制算法,把需要回收的这些Region里存活的对象,复制到空闲的Region中,然后清理掉旧Region全部空间。因为需要移动存活的对象,所以不可避免的要暂停用户线程,这个步骤支持多条线程并行回收。
4、GC日志
4.1、GC日志参数
GC日志的参数如下:
- -XX:+PrintGC 输出GC日志
- -XX:+PrintGCDetails 输出GC的详细日志
- -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
- -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
- -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
- -XX:+PrintTenuringDistribution 打印出每次GC时Survivor的对象年龄
- -Xloggc:../logs/gc.log 日志文件的输出路径
4.2、日志区分回收器
- Serial收集器:新生代显示 "[DefNew",即 Default New Generation;
- ParNew收集器:新生代显示 "[ParNew",即 Parallel New Generation;
- Parallel Scavenge收集器:新生代显示"[PSYoungGen",JDK1.7使用的即PSYoungGen;
- Parallel Old收集器:老年代显示"[ParoldGen";
- G1收集器:显示”garbage-first heap“;