本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力
JVM 一次完整的GC
1.新生代开始执行垃圾回收,堆中的Eden和其中一个Survivor区的存货对象放置到另一个Survivor区中,将这两个区清空。
2.如果Survivor区中放不下这么多对象,就会将多余的对象放置到老年代中(如果是大对象,就直接存放到老年代中)
3.Survivor中的对象每经过一次收集,幸存下来的对象年龄+1,当到达一定的年龄,就会晋升到老年代。(CMS 年龄6, Parallel年龄15)
什么是垃圾
- 什么是垃圾?
没有任何引用指向的一个对象或者多个对象(循环引用);
如何定位垃圾
引用计数法
在对象添加一个引用计数器。当有一个地方引用,就+1,引用失效就-1,。当计数器为0时,代表不可能被使用。
缺点:有很多额外情况需要考虑,需要配合大量的额外处理才能使用。比如,两个对象循环依赖,就不可能回收
可达性分析
从“GC Roots”根对象作为起始节点集,从这些节点触发根据引用关系向下搜索,走过的路径为“引用链”。如果某个对象到起始节点没有任何引用链,说明这个对象不可能再被使用
- 哪些对象可以作为GC Root根节点?
- 虚拟机栈中引用的对象,比如当前正在运行的方法使用到的参数,局部变量,临时变量等。
- 方法区中类静态属性引用的对象。 static String name = new String();
- 方法区中常量引用的对象 final Integer i = new Integer();
- 本地方法栈中JNI(native方法)引用的对象
- 虚拟机内部的引用,如基本数据类型对应的Class对象,常驻的异常对象,NullPointException,系统类加载类。
- 所有被同步锁持有的对象
- 反应Java虚拟机内部情况的JMXBean。JVMTI中注册的回调,本地代码缓存等。
这些都可以放进GC Root根节点集合中,不仅这些,将一些关联区域的对象也一并加入。
局部回收,避免GC Roots包含过多的对象而多度膨胀。
垃圾算法
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。
Mark-Sweep (标记清除)
标记出垃圾之后清除
缺点:位置不连续,产生碎片
Copying(拷贝)
应用场景
用于新生代,高效性是建立在存活对象少、垃圾对象多的前提下的
过程
将活着的内存空间分为两块,每次只使用其中一块,将随机分布的正在引用的内存地址拷贝到连续的内存空间中,然后将之前的随机内存地址全部清空
所以采用的是指针碰撞
优点:没有碎片,速度快
缺点:浪费空间,内存是能使用一半
Mark-Comapct(标记压缩)
应用场景
用于老年代
过程
1.标记出所有垃圾
2.将其他的正在引用的内存地址拷贝到这个垃圾分类地址上,形成连续的正在引用内存地址。
优点: 集合了标记清楚和拷贝的优点
缺点:效率比较拷贝算法低
| Mark-Sweep | Mark-Compact | Copying | |
|---|---|---|---|
| 速度快慢 | 中等 | 最慢 | 最快 |
| 空间开销 | 少(会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍大小(不堆积碎片) |
| 移动对象 | 否 | 是 | 是 |
Generational Collection(分代收集)算法
是目前大部分JVM的垃圾收集器采用的算法。将垃圾区域分成老年代和新生代。
- 特点
老年代的特点是回收时只有少量垃圾需要回收,新生代时有大量垃圾需要回收
- 区域使用的算法
老年代使用标记压缩算法
新生代使用复制算法
垃圾收集器
JDK1.8 默认的垃圾回收器 :Parallel Scavenge + Parallel Old ,所以调优主要针对的就是它。
useParallelGc
新生代垃圾收集器
新生代的垃圾回收器都是基于复制算法
-
1.Serial:单线程的垃圾回收器,STW (stop-the-world)停止所有线程。
- 优点
是客户端模式下默认的新生代收集器,简单高效(相比于其他收集器的单线程)
内存占用最小
最高的单线程收集效率:没有线程交互的开销
-
2.ParNew
-
Serial 的 多线程并行版本,在单线程情况下效率比不上Serial。
处理器核心越多,它的效率越高,因为是并行。默认开启收集线程数 = 处理器核心数。
-
和CMS 配合使用。
-
-
Parallel Scavenge
- 和ParNew非常相似,但是关注点不同:达到一个可控制的吞吐量:运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)这个值默认99,越高越好。
- 和Parallel Old搭配使用
老年代垃圾收集器
老年代的垃圾收集器都是基于 标记压缩算法
-
1.Serial Old
Serial的老年代版本。
-
2.Parallel Old
Parallel Secavenge 的老年代版本,与Parallel Secavenge 搭配使用,组成吞吐量优先的搭配组合
-
3.CMS
CMS
ConcurrentMarkSweep,以获取最短回收停顿时间为目标的收集器。基于标记清理算法(不是标记压缩)
用于服务端,关注服务器响应时间,希望系统停顿时间尽量少,垃圾回收运行期间用户线程也能运行
四个步骤
-
初始标记
- 找到GC Roots能关联的类,这个阶段也会STW ,但是时间很短。
-
并发标记
-
并发的可达性分析,基于三色标记算法,这个最耗时
-
垃圾回收线程和业务线程同时执行,
- 产生的并发问题
1.当业务线程使用对象的时候垃圾线程也标记了这个对象,那么垃圾线程会认为这个线程存活对象,但是业务线程用完后就不再使用了,这个垃圾还是标记为存活对象
2.垃圾线程标记对象为垃圾,但是这时业务线程又使用了这个对象。那么这个在使用的对象就会标记称垃圾。
-
-
重新标记
- 修正漏标和错标,这个阶段STW,但是时间也不长
-
并发清除
- 使用标记清除算法,将标记的对象直接删除,和用户线程并发执行。由于使用清除算法,会产生碎片
- 为何不适用压缩算法? 如果使用压缩算法,会涉及到用户线程正在使用,情况更加复杂,用户线程不停顿很难处理
- 可能会出现浮动垃圾:在清除阶段用户产生的新垃圾,但是CMS在本次垃圾回收中无法处理,只能等到下次CMS回收。
并发模式失败
CMS 无法处理浮动垃圾可能会"Concurrent Mode Failure”失败,导致Full GC
失败原因:
1.对象提升到老年代的速度很快,使得CMS 不能保持老年代足够的空间
2.大量碎片化导致没有足够的空间容纳提升上来的新对象。
年老代将进行垃圾收集以释放可用空间,同时也会整理压缩以消除碎片,这个操作需要停止所有的java应用线程,并且需要执行相当长时间。
- 三色标记
- 黑色:对象已经被收集器访问过,并且它的所有引用也已经访问过,代表它是线程安全的。
- 灰色:表示对象已经被垃圾收集器访问过,但是至少存在一个引用还没有被扫描过
- 白色:这个对象还没有被扫瞄过。
开始的时候,会任务所有的对象都是白色的; 利用根可达算法将根下的引用对象都标记为灰色 移动到 灰色对象中,将本对象 标记为黑色; 将灰色对象中所有引用对象标记为灰色; 重复第3第4步,直至扫完所有灰色对象; 完成后没有被标记为黑色的,或者全部是白色的,就为mark sweep出来的垃圾对象可以进行回收。