涉及问题
- 垃圾回收设计问题
JVM有哪些垃圾回收算法,各自优劣 CMS垃圾回收器怎么工作,有哪些阶段 服务卡顿的问题症结
JVM GC
- GC触发条件
内存达到一定条件,会自动触发 GC只与活跃对象有关,进行记录;与堆的大小无关
GC流程
标记(Mark)
- 定义
定义:GC Roots遍历所有可达对象。 找出活跃对象进行标记,且GC过程是逆向(先找到活跃对象,再根据触发条件进行回收)
- 图示
圆圈代表对象。绿色是GC Roots,红色的代表可以追溯的对象。灰色是被回收的对象。
清除(sweep)
- 清除阶段就是把未被标记对象回收掉
- 存在问题
这种简单问题,在清除对象后,会带来内存碎片问题,如下图-未标记对象(2k和4k对象)清除前后 如申请了1k,2k,3k,4k,5k内存,在对2k,4k进行回收后交给垃圾回收器。当再次一个5k内存时,无法申请,因为其未对内存空间进行整理,回收的内存仍是2k,4k 此时,空间有6k内存。但是无法申请5k空间(因空间碎片存在),系统运行时间越长,碎片就越多。
复制(copy)
- 解决内存碎片方法
1.方法 解决问题只能通过内存整理。 2.思路 提供一个对等内存空间,将存活的对象复制过去,然后清除原内存空间。3.缺点 会造成一倍空间浪费 4.应用 HashMap内存扩容思路(声明同容量的数组,将其复制过去),Redis rehash
- 实现方法
整理(compact)
- 思路
1.复制清除方法弊端 需要分配对等额外空间,完成内存整理工作。其实也可以不分配,仍能完成内存整理工作。 2.思路: 将内存当做非常大数组,根据随机index删除一些数据。那么对整个数组进行清理,故不需要另外一个数组来进行支持,使用程序即可。 3.步骤: 移动所有存活对象,且按照内存地址顺序依次排列,然后将末端内存地址以后的内存全部收回。 4.注意 仅有复制清除方式进行内存整理是理想状态。对象引用关系一般非常复杂。从效率上讲,一般整理算法低于复制算法(拿空间换时间)。 5.代码模拟 last=0 for(i=0;i<mems.length;i++){ if(mems[i]!=null){ mems[last++]=mems[i]; changeReference(mems[last]); } } clear(mems,last,mems.length);
- 图示
分代
-
常见朴素内存回收算法优劣
1.复制(copy) 复制算法是所有算法效率最高的算法,缺点会造成一定空间浪费 2.标记-清除(Mark-Sweep) 效率一般,缺点会造成内存碎片问题 3.标记-整理(Mark-Compact) 效率较前两者差,但无空间浪费,消除内存碎片问题
-
JVM对象分类
1.大部分对象声明周期很长。如类信息,字符串对象 2.其他对象存活时间很短。如局部变量,临时变量
-
弱代假设
1.定义: 大部分对象存朝生夕灭,剩余的活很久 2.代际划分 年轻代(young generation): 死得快的对象所占区域 老年待(old generation):剩余活得久对象所占区域,又叫Tenured Generation。
-
某应用对象存活时间统计
年轻代
- 垃圾回收算法
复制算法,因为年轻代(minor gc)发生GC后,只有较少对象存储,通过申请相同大小空间(虽然空间浪费,但空间换时间),复制对象到另一相同大小空间,是非常高效的。
- 具体分区
1.分区 一个伊甸园空间(Eden),两个幸存者空间(survivor),其中survivor必须时刻有一个是空的。 2.Eden空间满时,触发年轻代GC (minor gc) 流程如下 (a).Eden执行第一次GC之后,存活对象会被移动到其中一个survivor分区(from survivor) (b).Eden再次GC,此时采用复制算法,将Eden和from区一起清理。存活对象会复制到to区,接下来,只需要情况from区域。 (c)此过程中总有一个survivor分区是空的。Eden、from、to默认比例是8:1:1,所有会有10%空间浪费。比例有 -XX:SurvivorRatio设置,默认是8
- Thread Local Allocation Buffer
1.Young Generation回收流程 关于内存回收,除了上述年轻代Eden在第一次将活对象移动from区,二次minor gc时采用复制方法,将Eden和from区活对象复制到to区后,from变空。多次这样,知道to区变满会移动Old generation。 2.TLAB 还有TLAB,JVM默认给每个线程开辟一个buffer区域,用来加速对象分配,这个buffer放置Eden区。该作风同 java ThreadLocal相似,属线程私有,避免对共享区操作,以及一些锁竞争。 TLAB通常很小,所以对象较大时,会在Eden共享区域进行分配。 TLAB是一种优化技术,类似优化有对象的栈上分配(线程逃逸分析,默认开启)
- TLAB分配图示
老年代(Old Generation)
- 算法
对象存活率较高,空间比较大,拷贝不划算,适合就地收集,因此采用标记-清除,标记-整理算法
- 对象进入老年代途径
- 提升(Promotion)
对象够老,会通过”提升”进入老年代。 通过它的年龄(age)来判断对象是否老。每当发生minor gc,存活对象年龄都会+1.知道达到一定阈值,就会把仍然活跃的对象提升到老年代 如果这些对象不可达,知道老年代发生Major GC时,才会被清理 年龄阈值通过-XX:+MaxTenuringThreshold(=15默认)配置。因为是用4bit存储。故将该值调大无根据
- 分配担保
年轻的对象,每次存活对象,都会放入其中一个survivor区域(比例是10%)。但是我们无法保证每次存活对象都是10%,当survivor空间不够,就需要依赖其他内存(老年代)进行分配担保。这时,对象会在老年代上分配
- 大对象直接在老年代分配
超出参数值(-XX:PretenureSizeThreshold)的对象,直接在老年代分配。默认是0,即全部首选在Eden区进行分配。
- 动态对象年龄判断
有的垃圾回收算法,并不要求age=15才能晋升到老年代,会使用动态计算方法。如survivor区相同年龄对象大小的和,大于survivor的一半,大于或等于age的对象直接进入老年代。
卡片标记(card marking)
- 统计对象引用
对象引用关系时一个巨大的网状。有的对象可能在Eden区,有的可能在老年代。对于跨代引用(如年轻代对象minor gc在单独发生时要被回收,但老年代对象引用了它,要确保年轻代对象存活). 对于是、否的判断,通常用位图和布隆过滤器来加快搜索速度,jvm也是用了类似方法。
- 卡页,老年代是被分成众多的卡页(card page)的(一般数量是2的次幂)
- 卡表(card table)就是用于标记卡页状态的一个集合,每个卡表项对应一个卡页。
如果年轻代有对象分配,而且老年代有对象指向这个新对象,那么老年代所对应的卡页,就会标识为dirty,卡表值需要非常小的存储空间就可以保留这些状态。 垃圾回收时,先读卡表,进行快速判断Eden代的对象是否被Tenure代引用,进而决定是否被回收。
HotSpot垃圾回收器
年轻代垃圾回收器
Serial 垃圾收集器
- 串行垃圾收集器,GC时只有一条线程,并且垃圾回收时暂停一切线程(STW),会出现明显卡顿。
- 使用在java开发的客户端上,不会频繁创建对象,用户不会感觉明显卡顿。使用资源少,更轻量级
- 年轻代serial 垃圾收集器使用复制算法(适用对象少情况,不担心空间浪费,用时间换空间)
ParNew 垃圾收集器
- 并行垃圾收集器(parallel gc),是Serial多线程版本。由多条GC线程并行地进行垃圾清理,清理过程仍然要暂停用户线程,产生卡顿
- ParNew追求通过多线程进行垃圾回收,追求”低停顿时间”。多CPU环境性能会比串行垃圾收集器优异;但涉及线程切换需要额外开销,单核CPU表现不如串行收集器
Parallel Scavenge垃圾收集器
- 多线程垃圾收集器,与ParNew区别如下
Parallel Scavenge:追求CPU吞吐量,能在较短时间完成指定任务,适合无交互后台计算、弱交互计算 ParNew:追求降低用户卡顿时间,适合交互式应用、强交互计算。
老年代垃圾收集器
Serial old垃圾收集器
- 与年轻代serial垃圾收集器对应,都是单线程,同样适合客户端使用
- 老年代serial使用标记-整理(空间有限,避免内存问题,回收时进行内存整理)算法
Parallel old
- 是parallel scavenge 老年代版本,追求CPU吞吐量
CMS 垃圾收集器
-
并发标记清除(concurrent mark sweep)垃圾收集器以获取最短GC停顿时间为目标的收集器,保证GC线程和用户线程并发执行,尽量减少卡顿
-
长期来看CMS要被G1等垃圾回收器替换,java8 后使用其回收老年代对象将抛出警告
JavaHotSpot(TM)64-BitServerVMwarning:OptionUseConcMarkSweepGCwasdeprecatedinversion9.0 and will likely be removed in a future release.
垃圾收集器配置参数
-
除了上面几个GC,我们还有G1,ZGC等更高级GC,有专门配置参数设置来生效
1.查看当前Java版本默认使用垃圾收集器。 -XX:+PrintCommandLineFlags 2.创建配置参数 -XX:+UseSerialGC 年轻代和老年代都用串行垃圾收集器 -XX:+UseParnew 年轻代用ParNew,老年代用Serial Old -XX:+UseParalleGC年轻代用ParallelGC,老年代默认用serial old -XX:+UseParallelOldGC年轻代和老年代都用并行收集器 -XX:+UseConcMarkSweepGC 年轻代用ParNew,老年代用CMS -XX:+UseG1GC 都使用G1垃圾回收器 -XX:UseZGC 都使用ZGC垃圾回收器
-
java9 中,-XX:UseParNewGC已被抛弃,有些程序(ES)会报错,需留意
-
参数对应分代垃圾收集器类型
STW
- jvm进行垃圾回收出现的卡顿就和STW有关
垃圾回收(标记或整理复、制)时,如果有新的对象进来,理想情况下是,为了保证程序不乱套,最后办法是暂停用户一切线程。这段时间,不能new对象,只能等待。具体现象就是jvm出现短暂卡顿,即Stop the world ,简称STW
- GC标记阶段,大多数需要STW
如果不暂停用户进程,在标记对象时,有可能有其他用户线程会产生一些新的对象引用而未被统计到,造成混乱。
- 优化方向
现在垃圾收集器,都尽量减少STW时间。但即使是最先进的ZGC,也会有短暂STW。因此要做的是,在现有基础设施,尽量减少GC停顿
- STW影响例子
某高并发峰值流量10w /秒,后面10台负债均衡机器,那么每台需要1w/s.加上某天机器在一段时间发生STW,持续1s,那么本来需要10ms就可返回的1w个请求,至少需要等1s。 在用户那里,表现为系统卡顿。如果GC频繁,卡顿明显,严重影响用户体验。
小结
- 小结
归根接地,各色垃圾收集器为解决STW问题,减少GC时间,停顿更小,吞吐量更大 现在的垃圾回收器,基于弱代假设,大多数是分代回收理念。针对年轻代、老年代,有多种不同垃圾回收算法,可组合使用
- 年轻代垃圾回收
1.年轻代是GC重灾区 2.面试需要 3.为理解G1,ZGC
- 算法
1.标记-Mark 2.清除-Sweep 3.复制-Copy 4.整理-Compact
- 分代
1.Young generation 2.Survivor 3.Eden 4.Old generation|Tenured Generation 5.GC (1)Minor GC (2)Major GC
- 名词
1.weak generation hypothesis 2.分配担保 3.提升 4.大对象分配在永久代 5.卡片标记年轻代对象被老年代引用 6.STW