前言
之前的学习也是为了让我们更好的理解GC,GC是我们学习Jvm的核心,因为我们后面的优化,为什么会出现oom,怎么调整堆空间的大小等等。
GC算法
第一种标记算法:引用计数法
在对象中添加一个属性用于标记对象被引用的次数,每多一个其他对象引用,计数+1,当引用失效时,计数-1,如果计数=0,表示没有其他对象引用,就可以被回收。
这个算法无法解决循环依赖的问题。比如A,B对象相互引用,这样就会计数增加,不会出现计数减少。
第二种标记算法:可达性算法
通过一系列被称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系链向下搜索,如果某个对象无法被搜索到,则说明该对象无引用执行,可回收。相反,则对象处于存活状态,不可回收。JVM中的实现是找到存活对象,未打标记的就是无用对象,GC时会回收。
哪些对象可以作为GC Root呢:
- 所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
- VM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot VM里的Universe里有很多这样的引用。
- JNI handles,包括global handles和local handles
- (看情况)所有当前被加载的Java类
- (看情况)Java类的引用类型静态变量
- (看情况)Java类的运行时常量池里的引用类型常量(String或Class类型)
- (看情况)String常量池(StringTable)里的引用
Jvm是如何找存活对象的?
3色标记算法
未发生gc时,所有的对象视为白色。
发生gc后,新创建的对象全部视为黑色。
- 白色:尚未访问过。
- 本对象已访问过,而且本对象引用到的其他对象也全部访问过了。
- 本对象已访问过,但是本对象引用到的其他对象尚未全部访问完。全部访问后,会转换为黑色。
从3色标记的说明可以很容易的看出,最后白色未被访问的对象是需要回收的对象。
并发的情况下:3色标记会存在对象被多标记,少标记,漏标记的情况。
多标的情况:因为GC线程和用户并发执行,所以当GC线程刚刚标记上,而用户线程紧接着就断开引用了的情况。
少标的情况:用户线程新创建的对象,默认是黑色的,可以躲过本次GC,下次GC可以被扫描到。
漏标的情况:一个完成黑色标记的对象,指向了一个白色对象,这个时候就产生的漏标。
- 有至少一个黑色对象在自己被标记之后指向了这个白色对象
- 所有的灰色对象在自己引用扫描完成之前删除了对白色对象的引用
多标,少标问题不大,下次GC都可以处理,但是漏标会导致程序报错,这个必须要处理。处理方式有两种:
增量更新:GC过程中发送的引用关系变化都记录下来,等GC标记完成后,再扫描一下记录的对象的引用情况即可。
原始快照:当一个灰色对象去掉对白色对象的引用,这种情况下,这个引用关系会被记录下来。 让这个白色对象变成灰色,后面继续扫描,会产生一点垃圾对象。
第一种GC算法:标记清除算法
没有被打上标记的对象,被清除掉。
第二种GC算法:标记清除-整理算法
第一步:没有被打上标记的对象,被清除掉。
第二步:将内存碎片整理合并。
第三种GC算法:分代+复制算法
Eden区满了触发GC,
第一步:Eden区加From区的对象,进行标记,还被引用的对象打上标记。
第二步:将已经被标记的对象,存放到To区。清空From区和Eden区的对象。
后面的FC 也是一样的道理,只是From 区和To区的位置交换而已。
详细的Jvm,内存模型见:https://www.cnblogs.com/jssj/p/14290060.html
这里有一个重点知识:存活的对象被迁移到新的内存空间,这就牵涉到对象内存地址的变更即对象搬家了,如何保证还是可以让程序正常使用?
Jvm 使用了动态计算指针来实现的,引用指针会指向新的内存地址,内存是连续的,可以通过计算内存移动的距离和自己的长度计算新内存地址的位置。
内存分配算法
指针碰撞:自旋方式获取内存空间。
空闲队列:将内存先划分cell小块,再进行分配。
GC中还有两个概念,安全点和安全区。
垃圾收集器
名词STW:Stop The World。即GC线程与用户线程无法并发运行,GC线程执行期间需要暂停用户线程
Jvm有10种垃圾回收器。
java -XX:+PrintCommandLineFlags -version -- windows 使用该命令可以查看默认使用的垃圾回收器
标红的就是。
1、Serial收集器
串行垃圾收集器,即GC线程与用户线程先后运行,即GC时需要STW(暂停所有用户线程),直至GC结束才恢复用户线程的运行专注于收集年轻代,底层是复制算法
相关参数:-XX:+UseSerialGC
2.Serial Old收集器
Serial Old 和Serial 相比就是应用在老年代的垃圾收集器,也是单线程,但是算法不是copy,而是Mark-Compact 标记整理算法,也是stw:暂停所有线程进行垃圾回收;
所以 Serial 和Serial Old 组合使用,可用内存一般不大(几十M至一两百M)的服务器环境中,不适合当前的大内存了
3.ParNew收集器
ParNew垃圾收集器是Serial收集器的改进多线程版本(因为内存的不断增大),除了多线程外,其余的行为、特点和Serial收集器一样,实现算法跟Serial完全一样(copy算法),也是stw下执行;
但是如果CPU数量为1个或者少于4个时,该种收集器的性能并不会比Serial要好。因为除去上下文切换,以及占用用户线程CPU时间片,导致用户线程被拖慢
在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;
1 ) CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作;
2) 因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码;
设置参数
“-XX:+UseConcMarkSweepGC”:指定使用CMS后,会默认使用ParNew作为新生代收集器;
“-XX:+UseParNewGC”:强制指定使用ParNew;
“-XX:ParallelGCThreads”:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量
4.Parallel Old
这个是Serial Old的多线程版本,应用在老年代的收集器,也是标记-整理算法,并且是stw的执行收集。但是如果CPU数量少的话性能一样不好。但是现在无论是PC还是server CPU数量都不再是性能瓶颈限制了,所以目前它跟Parallel Scavenge的配合是吞吐量优先场景的优先收集器选择。
5.Parallel Scavenge
一种新生代垃圾收集器,与 ParNew相比不可以与cms一起组合使用,PS也是复制算法,它与前两种收集器最大的区别是,它关注的是吞吐量而不是延迟。也被称为是吞吐量优先的收集器。其中,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
主要使用场景:主要适合在后台运算而不是太多交互的任务,高吞吐量则可以最高效率的利用CPU时间,尽快的完成程序的运算任务。当然,如果想要降低停顿时间,相应的也会影响吞吐量
6.CMS
CMS,Concurrent Mark Sweep,这是一款真正的并发收集器,就是在线程执行过程中也可以进行垃圾收集的收集器,在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,CMS 可以用于此场景。 分为四个过程 ,1初始标记 ,2并发标记,3重新标记,4并发清理
CMS采用了多种方式尽可能降低GC的暂停时间,减少用户程序停顿。停顿时间降低的同时牺牲了CPU吞吐量 。
因为并发情况占用大量cpu资源,这是在停顿时间和性能间做出的取舍,可以简单理解为"空间(性能)"换时间,CMS 是一个承前启后的收集器
7.G1(Garbage First)
在JDK7就已加入JVM的收集器大家庭中,成为HotSpot重点发展的垃圾回收技术。同优秀的CMS垃圾回收器一样,G1也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用G1来代替选择CMS。G1最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷
G1收集器,是比前面的更优秀,真正有突破的一款垃圾收集器。其实在G1中还是保留了分代的概念,但是实际上已经在新生代和老年代中没有物理隔离了。在G1中,内存空间被分割成一个个的Region区,所谓新生代和老年代,都是由一个个region组成的。同时G1也不需要跟别的收集器一起配合使用,自己就可以搞定所有内存区域。整体上来讲不是一个分代收集器,是一个通吃收集器。这也是JVM内存管理和垃圾收集的一个发展趋势。从后面zgc中我们可以更清晰的看到这个变化。
G1采用了标记-整理算法,避免了CMS中的内存碎片问题,另外它能达到可控的垃圾时间。是一款优秀的收集器。即便如此,从2004年第一篇论文发表到真正商用推出,也是到了jdk1.7。实现上并不是那么容易的。
G1的工作过程:
初始标记:这个过程跟CMS第一个过程差不多,只是标记一下GC Root关联的对象。
并发标记:这个过程时间比较久,分析GC Root到所有对象的可达性分析。如果从GC Root节点开始遍历所有对象会比较耗时,实际上JVM也不是这么做的。JVM是使用Remembered Set保存了对象引用的调用信息,在可达性分析的时候只需要同时遍历remembered set就好了,不需要从根节点开始挨个遍历。
最终标记:由于并发标记阶段,用户线程仍然在工作,会对标记产生一些偏差,这时候需要通过remembered set log来记录这些改变,在这个阶段将改变合并到remembered set中。完成最终标记。
筛选清除:通过标记整理的算法,根据用户配置的回收时间,和维护的优先级列表,优先收集价值最大的region。收集阶段是基于标记-整理和复制算法实现
记忆集和卡表
8.ZGC
zgc是jdk11中要发布的最新垃圾收集器。完全没有分代的概念,先说下它的优点吧,官方给出的是无碎片,时间可控,超大堆。
9.Shenandoah
Shenandoah是一款concurrent及parallel的垃圾收集器;跟ZGC一样也是面向low-pause-time的垃圾收集器,不过ZGC是基于colored pointers来实现,而Shenandoah GC是基于brooks pointers来实现。
其实低停顿的GC,业界早就出现,只不过Java比较晚
Azul的Zing中C4 GC 土豪选择
oracle中的HotSpot ZGC JDK11的选择
R大说ZGC说抄袭Azul的,两者是等价的。
10.Epsilon
java 11 新的Epsilon垃圾收集器
Epsilon(A No-Op Garbage Collector)垃圾回收器控制内存分配,但是不执行任何垃圾回收工作。一旦java的堆被耗尽,jvm就直接关闭。设计的目的是提供一个完全消极的GC实现,分配有限的内存分配,最大限度降低消费内存占用量和内存吞吐时的延迟时间。一个好的实现是隔离代码变化,不影响其他GC,最小限度的改变其他的JVM代码。
总结
gc 是Jvm非常重要的一环,可以说是最核心的,让代码使用者可以不关心内存分配的问题,作为Java开发者,是必须掌握的核心技术。