JVM底层原理学习(四)之垃圾回收
java程序员好像从来都不像c++程序员一样在代码中写任何关于垃圾回收的代码,使程序员可以更加专注业务逻辑的开发,那这么说,是不是意味着JAVA在运行过程中没有垃圾?其实不然,这要归功于JVM的垃圾回收机制
垃圾回收
对象的引用类型
-
强引用(强引用对象不能被回收,出现OOM)
对于强引用对象,就算是出现了OOM也不会对该对象进行回收
强引用是我们最常见的普通对象引用,只要还要强引用指向一个对象,就表明这个对象还活着,垃圾收集器不会回收这种对象。 -
弱引用(只要有GC,就回收)
弱引用需要用到java.lang.ref.WeakReference类来实现。对于只有弱引用的对象来说,只要有垃圾回收,不管JVM的内存空间够不够用,都会回收该对象占用的内存空间 -
软引用(内存不够时才回收)
软引用是一种相对强引用弱化了一些的引用,需要java.lang.ref.SoftReference类来实现- 当系统内存充足的时候,不会被回收
- 当系统内存不足时,它会被回收
比如高速缓存就可以用到软引用.内存够用时就保留,不够时就回收
-
虚引用(检测对象的回收机制)
虚引用需要java.lang.ref.Phantomreference类来实现.虚引用的作用主要是跟踪对象被垃圾回收的状态
设置虚引用关联的唯一目的,就是在这个对象被回收的时候收到一个系统通知或者是后续添加进一步的操作处理
判断是否是垃圾的方法
- 引用计数法
只要JVM中该对象被别人所引用(持有该对象的引用),就说明该对象不是垃圾,如果一个对象在JVM中没有任何指针对其引用,它就是垃圾对象。
原理:说白了就是数数。
弊端:如果A对象和B对象互相持有对方的引用,但是没有其他对象对其引用,那么这两个对象就的被引用次数永远是1,永远不会被回收
- 可达性分析
通过GC Root的对象,开始向下寻找,看某个对象是否可达,不可达的对象就是内存垃圾.通过这种方式可以规避引用计数法存在的相互指向的问题.也是目前垃圾回收器默认的垃圾分析标记算法.
JAVA中哪些对象可以作为GC ROOT- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 类静态属性引用的对象
- 方法区中常量引用的对象
- 存活的Thread对象
- Bootstrap ClassLoader
- 通过bootStrap classLoader 或 ext classLoader 加载class对象.
- 正在被synchronized锁定的对象
- 本地方法栈中的对象
GC的分类
根据共享内存区的划分,我们可以推测,可能会有不同的GC
- Minor GC:新生代GC 指young区的垃圾回收过程
- Major GC:老年代GC 指old区的垃圾回收的过程
- Full GC:新生代+老年代 young区和old区一起执行垃圾回收的过程
但是在JVM的运行情况下并不存在单独的Major GC, Major GC 定会伴随着Minor GC 即Full GC
何时执行 GC
GC是由JVM自动完成的,根据JVM系统环境而定,所以触发的时机是不确定的。我们的程序主动调用System.gc()方法告知JVM需要进行一次垃圾回收,但是具体GC的运行时间也是无法指定和控制的。也就是说System.gc()只是通知JVM进行垃圾回收,什么时候触发GC回收由JVM决定。
触发GC的时机
- 当Eden区或者S区不够用了
- 老年代空间不够用了
- 方法区空间不够用了
- System.gc()
如何执行GC(算法)
垃圾回收的算法
- 标记-清除(mark-sweep)
清理前
由上图可知:标记清除之后会产生大量不连续的内存碎片,碎片太多可能会导致程序 运行过程中需要分配较大对象时,无法满足分配要求导致再次进行GC操作,如此往复,恶性循环
- 标记-整理(mark-compact)
问题分析:
最终并不会出现空间碎片和浪费空间的问题,但是整理的过程中带来的计算可不容小觑
- 标记-复制(mark-copy)
始终有一块内存区域是未使用的…造成空间的浪费. 适合大量对象朝生夕死区域
垃圾收集器(Garbage Collector)
是上面垃圾收集算法的落地
垃圾收集原则
那历年来的JVM发布版本中,JVM大多数的垃圾收集器都基于分代垃圾收集的原则
即,不同的区域(老年代,新生代),不同的代,采用不同的垃圾回收算法的垃圾收集器
其中: Young区:以标记复制算法居多.(对象在被分配之后,大部分的对
象生命周期比较短,Young区复制效率比较高) Old区:标记清除或标记整理(Old区对象存活时间比较长)
- Serial(串行)& Serial Old
Serial & Serial Old收集器是最基本、发展历史最悠久的垃圾收集器,在JDK1.3之前唯一的王者(选择)
Serial是针对新生代的垃圾收集器,采用标记-复制算法Serial Old是老年代的垃圾收集器,采用标记-整理算法
Serial这套组合垃圾收集器特点是单线程垃圾收集器,它只会使用单个收集线程去完成垃圾清理工作,更重要的是其在进行垃圾收集的时候需要暂停其他线程(Stop The World) STW 整个世界都安静了
总结:
优点:简单高效,单线程GC收集效率高
缺点:收集过程需要STOP THE WORD, 无法使用到多核的CPU资源,采用单线程收集(设计者设计JVM之处,当时是为嵌入式开发设计的,没想到如今web开发这么火,所以设计者认为STW没什么大问题)
算法:复制算法
- ParNew
ParNew垃圾收集器是新生代的多线程的垃圾收集器.
可以说ParNew只是Serial收集器的多线程版本.在其他的方面几乎跟Serial特性都是一致的.并没有其他的创新点.
ParNew是目前JVM运行在Server模式下的配合CMS首选的新生代垃圾收集器.
因为在JDK1.5版本出现了一个针对Old区跨时代意义的收集器CMS(Concurent Mark Sweep)
除了单线程的Serial收集器外,目前只有ParNew新生代收集器能与CMS收集器兼容配合工作,所有没有办法,你要选择CMS作为老年代收集器的话,只能选择Serial或者ParNew收集器与之匹配.所以ParNew在多CPU的Server场景下首选.
ParNew设定:
-XX:UseParNewGC 强制指定ParNew 收集器
-XX:+UseConcMarkSweepGC 指定Old区采用CMS收集器 会默认设置ParNew收集器与之匹配
-XX:ParallelGCThreads 指定ParNew收集器并行的GC线程数量 - Parallel Scavenge & Parallel Old
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,同时也是并行的多线程收集器,工作机制与我们的ParNew是一致的,可参见上图. 但是Parallel Scanvenge更关注系统的吞吐量。
系统的吞吐量 = 用户工作时间 / 用户工作时间 + GC时间
用户工作时间 99S GC时间 1S
吞吐量 = 99/100 = 99%
GC线程工作的时间
-XX:MaxGCPauseMillis
设置GC线程工作的时间 / 设置GC运行时用户线程暂停的时间 到达时间强制运行.
带来更多的GC操作
Parallel Old
Parallel Old是Parallel Scavenge的老年代垃圾收集器.采用的标记整理算法
是在jdk1.6才开始正式启用的Old区并行垃圾收集器.
所以Parallel Scavenge在Parallel Old出来之前他就很尴尬.因为
Parallel Scavenge + Serial Old的组合很鸡肋. 新生代用并行类的收集器,但是Old区又跟不上脚步.
Parallel Scavenge + Parallel Old 示意图
CMS(concurrent mark sweep)
- CMS垃圾收集器是Old区的垃圾收集器
- 是一个首先实现并发收集的收集器
- 设计的目标以最短停顿时间为设计原则.CMS采用标记-清除算法
CMS的四个收集步骤
- 初始标记-(需要STW)
仅仅标记GCRoots直接指向的对象
STW的时间会很短,因为仅仅标记GCRoots直接关联对象.不用继续追踪,速度非常快. - 并发标记-(GC标记线程与用户线程一起工作)
扫描所有的old区,如果扫描的对象能够找到GCRoot就不需要清理. 如果找不到GCRoot就需要清理
基于GCRoots TRACING 和标记的过程.
但是并发标记过程中,有可能用户会产生新的垃圾或者用户程序运行产品的变动的情况 - 重新标记-(需要STW)
修正并发标记期间,产生的新的垃圾或者变动的情况
STW的时间很会短,因为前面的并发标记阶段已经标记了基于GCRoots所有对象,修正只需修正少量的对象 - 并发清理-(GC清理线程与用户线程一起工作)
与用户一起工作进行,清理工作.
采用标记-清除算法.这种算法只需要清理垃圾的对象就行,不需要做内存的整理
工作流程:
优点:
充分利用CPU资源并发收集、低停顿缺点:
1,标记-清除算法将产生空间碎片问题. -->promotion failure concurrent mode failed ----> Serial Old
2,并发标记和并发清理阶段由于并不是全力进行GC工作一定会带来GC时间过长.影响吞吐量.
3,清理不彻底,会产生浮动垃圾,且浮动垃圾只能在下一次垃圾回收才能处理.
-
G1(Garbadge First) 垃圾优先
G1收集器是一个面向服务端的垃圾收集器,适用于多核处理器、大 内存容量(4G,6G以上)的服务端系统。 它追求短时间GC停顿同时达到一个高的吞吐量,且解决CMS垃圾收集器中针对promotion failure 和 concurrent mode failed 问题.
在JDK7版本中G1已经推出,JDK9 G1成为了默认的垃圾收集器
G1的内存划分
G1垃圾收集器堆的内存结构不再是连续的Old Eden S1 S2了.而是改用Region化的内存分布.
把整个heap的空间划分为一个一个等大的Region. 默认为2048个Region.
Region大小一般为设置为1M ~ 32M 中的2的N幂 (1,2,4,8,16,32)
参数指定方式 -XX:G1HeapRegionSize
每一个Region都可以是以下四种不同的空间存储类型,即这些Region的存储内容是可以基于实际的情况进行改变的- Eden
- Survivor (S1 , S2)
- Old
- Humongous
当一个对象实例化之后整个大小 >= 0.5 Region 就会把这个对象单独拎出来开辟一个新的Region存储.并且把该区域称之为H区. 如果整个对象的大小>1个Region 则会开辟多个连续的Region进行对象的保存.这片连续的区域标记为H区
有可能运行一段时间的情况如下:
G1的垃圾收集过程 - Young GC
基于Region的复制算法.采用多线程并行收集Eden 和 S 的Region (这个过程需要Stop the world) - Mixed GC
在正式讲解G1的Mixed GC过程之前,先给大家普及几个概念
RememberSet 在每一个Region中会额外开辟一块空间用于保存外部引用的Region
Root Region 就GCRoot 对象所在的Region- Initial Mark (Stop The World) – 初始标记
依赖于Yong GC引发的Stop The Word 阶段进行GCRoots的扫描.找出所有的Root Regions; 这个阶段不做GCRoot
Tracing过程,并且是伴随着YoungGC的 Stop The World的过程所以效率很高 - Root Region Scanning (并发阶段) – 存活Region扫描
基于GCRoot Tracing的过程,通过各Region的Remember Set找到需要扫描的Region集合.
Concurrent Marking(并发阶段) --并发标记
从Root Region Scanning 阶段得到的扫描的Region中,区分存活对象和垃圾对象并标记. - Remark(Stop The World) --重新标记
在Concurrent Marking阶段由于是用户与标记线程并发处理.所以需要在这个阶段进行重新的修正工作 - Clean up(Stop The World) – 清理 因为是采用的复制算法.所以需要Stop The world 操作.
G1 采用的是优先级排序的方式进行垃圾的回收.优先回收花费时间少,垃圾比例高的Region
- Initial Mark (Stop The World) – 初始标记
为什么使用G1GC
-
使用G1不再需要关注 Old ,Young ,eden s1 s2 相关的比例可以让程序自适应的去使用各个Region
-
追求可控的响应时间
-XX:MaxGCPauseMillis 200 —200ms之内一定结束垃圾回收机制.
G1 采用的是优先级排序的方式进行垃圾的回收.优先回收花费时间少,垃圾比例高的Region
-XX+UseG1GC -
ZGC(jdk14,美团好像在用)
- 10ms以内的停顿时间
- 支持TB级别的内存
- 堆内存变大后停顿时间还是在10ms以内
垃圾回收器的分类
- 串行收集器 Serial和Serial Old
只能有一个垃圾回收线程执行,用户线程暂停。
- 并行收集器[追求吞吐量]->Parallel Scanvenge、Parallel Old
多条垃圾收集线程并行工作,用户工作暂停等待。
- 并发收集器[停顿时间优先]->CMS、G1
用户线程和垃圾收集线程同时并发执行,垃圾收集线程在执行的时候不会停顿用户线程的运行。