垃圾回收基础知识
什么是gc
- minor gc/young gc:对新生代(eden、from、to----1/3)进行垃圾回收
- major gc(不同地方不一样,需要根据出处的上下文理解,不同地方不一样,有的只是老年代,有的是所有区域)/old gc(更好的说法,只针对老年代,cms):对老年代(tunured----2/3)进行回收
- full gc(不仅是新生代、老年代、还要加上方法区(1.7之前是永久代,jdk1.8及之后是元空间))
分代回收理论
- 1.大部分对象都是朝生夕死 — 新生代(98%)
- 一个方法内创建的对象,只在这个方法里面使用了,当这个方法执行完后,栈就被销毁了,同时引用就不存在了,垃圾回收时根据可达性分析,相关连的对象就会被回收
- 2.熬过多次垃圾回收的就越难回收 — 老年代
- 如果对象能够熬过垃圾回收,说明与静态、常量或者其他有关联,所以熬过一次后,熬过第二次第三次的几率就很大,所以必须要与第一种区别开
垃圾回收算法
- ①复制算法 – 新生代采用的算法
- ②标记清除算法、③标记整理算法 – 老年代采用的算法
复制算法
-
算法执行过程
- 步骤一:把内存空间一分为二,先把一半的空间预留,只在另一半空间里面存放数据,当一半的空间塞满以后,这时就会触发垃圾回收,从gc roots扫描,将存活的copy过去,直接将扫描完的一半整个区域格式化,这比单独一个个删除效率要高,步骤一完成后,两个区的身份交换,预留的变成对象分配区域,对象分配区域变成预留
- 步骤二:重复步骤一
-
特点
- 实现简单、运行高效,只需要进行一次扫描,一次复制,对于新生代而言,存活的对象就很少
- 没有内存碎片,可以选择,把对象推向内存的一端
- 利用率只有一半(缺点)
- 怎么解决?
- 增加一个eden区,没有必要预留一半,eden区占80%,其余的from和to各占10%,标准的eden:from:to = 8:1:1
eden区的来源
-
appel式回收
- 1.先把对象分配在eden区,当eden区满了,将存活的对象复制到from区,因为存活的对象很少,是能够复制过去的,然后将eden区格式化清空
- 2.然后再次创建对象,eden区第二次又满了,将存活的对象复制到to区,同时将from区依然存活的复制到to区
- 3.按照第1步和第2步的过程反复进行
-
提高空间利用率和空间分配担保
-
提高空间利用率
效率由50提高到90,只浪费了10%的空间,同时推迟了复制时间
-
空间分配担保
如果单次回收超过10%会进入老年代
-
-
总结
- 用在新生代
标记清除算法(mark-sweep)
-
老年代存放的是回收不了的对象,是不适合用复制算法的
-
根据可达性分析做标记,将可回收的进行标记,然后像删文件一样的把可回收的区域数据删除掉
-
特点
-
1.位置不连续,产生内存碎片,大对象分配就很困难
-
如果要分配一个大对象?
对象的分配是一个直接指针,不能分成两段,所以需要对象的内存空间是连续的,这就要求有足够的连续内存空间
-
-
2.效率略低(两次)
- 两遍扫描(还要涉及对象的删除)
-
3.对比复制算法,优点是空间利用率100%
-
标记整理算法(Mark-Compact)
- 跟标记清除相似,需要整理内存空间,将存活的对象移动到内存的一端
- 特点
- 没有内存碎片
- 效率偏低,
- 两遍扫描、引用指针需要调整(对象保存的地址发生了变换)
jvm常用的垃圾回收器–以jdk1.8为例
单线程垃圾回收器
-
Serial–回收新生代
-
SerialOld–回收老年代
-
新生代和老年代在回收内存时需要暂停所有的用户线程,只起一个垃圾回收线程来回收内存
-
虚拟机参数
- useSerialGC(鸡肋),只能回收几十兆到一两百兆的东西
stop the world(停顿时间)
-
比如复制算法,因为需要变更引用,所以暂停所有的用户线程
-
缺点
- 如果超过10s,用户体验极其不好,应该不仅仅是为了提高吞吐量
多线程并行垃圾回收器
-
Parallel Scavange(默认)---- 回收新生代
-
Parallel Old (默认)— 回收老年代
-
jdk1.8默认垃圾回收器是Parallel Scavange和Parallel Old
-
虚拟机参数
-
-XX:+UseParallelGC
回收内存是多线程并发执行
-
-XX:MaxGCPauseMillis (没什么鸟用)实际的暂停时间可能还变长了
-
千万不要以为这个值设置的越小,JVM暂停的时间就会越短,实际有可能吞吐量还变低了
500ms 原来是每隔100s回收一次
100ms 现在是每隔10s回收一次
-
-
UseAdaptiveSizePolicy
- 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间)
- 自适应生成大小,为了完成最大吞吐量
- 这个参数默认是开启的
-
-
适用于几百兆到几个g都适用
-
ParNew(单独针对cms的)
- 为了减少stop the world的时间
并发垃圾回收器-cms
-
第一代并发垃圾回收器,具有划时代意义,减少stw的时间
- cms只回收老年代
- 同时JVM推出ParNew用来和cms配对,专门回收新生代
-
cms的垃圾回收过程,其中把标记过程一分为三
- 1.初始标记
- 找到gc roots,只对那些和它有直接关联的进行标记,同时gc roots很少,所以这个过程很快,时间短,同时暂停所有用户线程,暂停时间短,所以可以暂停
- 2.并发标记(让中间标记最长的一段时间并发标记,可以减少stop the wordl的时间)
- 对gc roots直接关联以外的引用链上的元素进行标记,标记的深度很深,数量级很大,所以消耗的时间长,并发的和用户线程跑,但是此时如果用户线程新创建了一部分对象,但是并没有被标记上
- 3.重新标记
- 重新标记用户线程新产生的垃圾,此时也需要暂停所有用户线程(stw),但是新产生的垃圾少,所以也能很快标记完,消耗的时间短
- 4.并发清除
- 用户和gc并发执行,消耗的时间长
- 最后重置线程,把并发清除线程重置为用户线程
- 1.初始标记
-
cms中的问题
-
cpu敏感,对CPU要求很高,4核以下,用户会有很强的卡顿感
-
浮动垃圾,最后一步并发清除的过程中又会产生浮动垃圾,只能等到下一次处理了
- 本来老年代是存储到99.9%才会触发垃圾回收,但是由于浮动垃圾的存在,以前的版本存到68%就触发垃圾回收,jdk1.6提高到92%
- 使得触发垃圾回收的时间相比parallel提前了
- 如果超过了能处理的浮动垃圾
- 此时会用serialOld来取代,本来处理4G垃圾几秒钟就能完成,转跳成SerialOld处理器后就需要几分钟乃至几十分钟处理
-
内存碎片
-
大对象分配非常麻烦
-
UseCMSCompactAtFullCollection
使用SerialOld来整理
-
-
虽然减少了stop the world的时间,但带来的额外问题也非常多,浮动垃圾和内存碎片导致垃圾回收器随时可能被一个单线程的serialold代替,所以以前老版本经常重启,每天晚上进行重启,来清除浮动垃圾和内存碎片,所以jdk1.6、1.7、1.8都没有将cms设置为默认垃圾回收期
-
-
为什么是标记清除不是标记整理?
- 因为清除是并发清除,和用户线程并发进行的,可以确保最好并发清除的效率,减少stop the world的时间。
并发垃圾回收器-g1(garbage first)
设计思想
- 总是跳不出stop the world的问题,所以想着去预测stw、并实现控制。
region
- 将整个堆空间划成一个整体,切割成一个个的region,
- region的大小范围是1m~32m,并且规定是2的次幂大小
- 每一块根据需要扮演不同的角色,分别扮演Eden、Survivor、Old、Humongous
- 针对大对象,多了一个Humongous,假设一个region是1M,如果对象大于等于512k,超过region的一半,判定为大对象,会放到Humongous区,如果更大,就会放到连续的多个Humongous区
- eden、survivor(survivor0就是from区,survivor1就是to区)用复制,老年代使用标记整理,Humongous等同于老年代
筛选回收
- MaxGcPauseMillis 垃圾回收最大暂定时间,是一个软目标,会尽最大努力去实现,不同于parallel scavenge强制去做处理
- 因为划分了区域,不会对整个空间进行垃圾回收,会去选垃圾回收效率比较高的区域进行垃圾回收,这也就是g1名字的来源。
可预测停顿—MaxGcPauseMillis
- 通过筛选回收的方式实现可预测停顿
垃圾回收步骤
-
跟cms一样,也是三次标记
-
①初始标记,gc roots,与cms相同,会stop the world,但是时间很短
-
②并发标记
- 解决回收过程中新分配的对象(不是垃圾),在每一个region内划分一块很小的区域------TAMS(top at mark start),放这些指针(指向哪些并发过程中新new出来的存活的对象)
- 并发标记也会出现漏标问题------SATB(snapshot at the beginning),保存一个快照(内存引用关系)
-
③最终标记,处理漏标对象,stop the world
-
④筛选回收,不会回收整个堆,通过标记已经算出来,每块区域有多少垃圾,哪个回报率最高,stop the world时间最短,可以获得最大的收益,同时会用到标记整理
- 相比于cms,是筛选回收,而cms是简单的并发回收整个老年代
问题
- 解决了cms里面的痛点
- 1.大对象
- 2.内存碎片
- 但是如果region里的对象与另一个region里的对象有依赖怎么办?
- 见java虚拟机5
垃圾回收器总结
新生代
-
回收器 回收对象和算法 回收器类型 Serial 新生代,复制算法 单线程(串行) Parallel Scavenge 新生代,复制算法 并行的多线程回收器 ParNew 新生代,复制算法 并行的多线程回收器
老年代
-
回收器 回收对象和算法 回收器类型 Serial Old 老年代,标记整理算法 单线程(串行) Parallel Old 老年代,标记整理算法 并行的多线程回收器 CMS 老年代,标记清除算法 并发的多线程回收器 G1 跨新生代和老年代,标记整理+化整为零 并发的多线程回收器
回收器的配对处理
-
新生代回收器 老年代回收器 Serial Serial Old Parallel Scavenge Parallel Old Parallel Scavenge Serial Old PraNew CMS+Serial Old PraNew Serial Old Serial CMS+Serial Old G1 G1 -
CMS+Serial Old
- 如果CMS预留空间不足以放浮动垃圾,使用Serial Old取代
-
jdk1.8,cms实际只能跟ParNew组合了
cms与g1应该怎么选
选择
-
serial—serialold 几十兆到一百兆
-
parallel scavenge—parallel old 几百兆到几个g
-
parnw—cms 几个g到几十个g (对g1起到铺垫作用,jdk1.8与g1有竞争之力,到jdk1.9可能就不如g1了)
-
g1 几十g(30g~50g)到一百多个g(jdk官方文档当堆的空间大于6gb以上或更大,推荐采用g1)
-
cms和g1有重合之处,此时可以参考jdk文档
-
假设内存空间就是8个g,怎么选?
jdk官方文档说明:启用垃圾优先(G1)垃圾收集器的使用。它是一种服务器样式的垃圾收集器,适用于具有大量RAM的多处理器计算机。它极有可能满足GC暂停时间目标,同时保持良好的吞吐量。==建议将G1收集器用于需要大堆(大小约为6 GB或更大)==且GC延迟要求有限(稳定且可预测的暂停时间在0.5秒以下)的应用程序
-
并不是cms一定比g1差?
- 为了解决漏标问题和新new出来的对象有地方存放,每个region都需要一块区域去解决并发标记的需要,所以花费的内存空间不一定低(漏标、new出来的对象),甚至付出20%的内存空间来存放这部分内容,像cms就比较单纯,只是追求stop the world的时间
总结
- 1.只有cms是标记清除,其余基本是标记整理
- 2.避免full gc,从一堆不容易删除的对象中找出少数几个可以删除的,同时又要标记又要整理内存空间,相当耗时间
- 3.g1因为对区域进行了划分,所以可以对清除空间进行选择
- 4.G1HeapRegionSize,默认大小会由 g1区会自动设置
- 5.cms达不到追求停顿时间的目的,cms只能说是尽可能减少。
- 6.crud 代码去实现, jvm最重要的关注点是思想
- 7.full gc也会去回收方法区的内容,回收效率非常低,所以后面换成元空间,不再当成对象。
- 8.三色标记?