一、程序中难以调试的bug
1.野指针
- 同一个对象,两个指针,一个指针将对象释放释放,另一个指针报NPE
- 同一个对象,两个指针,一个指针将对象释放释放,新对象占用原对象未知,另一个指针指向新对象
2.并发问题
- 多线程访问同一个块内存空间
二、垃圾清除算法
Mark-Sweep 标记清除:将垃圾标记之后清除,但会产生碎片化问题
Copying 拷贝:将存活对象收集排列好复制到另一块区域,但会浪费内存
Mark-Compact 标记压缩:在清理垃圾的时候顺便排列存活对象,但效率低
三、堆内存逻辑分区
新生代:采用Copying,频繁GC
老年代:采用Mark-Sweep与Mark-Compact,满了进行FGC
四、GC演化:
1.几兆到几十兆:
- Serial:首先STW(主线程停止),再将未使用垃圾回收。年轻代(采用Copying)
- Serial Old:首先STW(主线程停止),再将未使用垃圾回收。老年代(采用Mark-Sweep与Mark-Compact)
新生代和老年代都使用单线程
2.几十兆到上百兆:
- parallel Scavenge:首先STW(主线程停止),再将未使用垃圾回收
- Parallel Old:首先STW(主线程停止),再将未使用垃圾回收
新生代和老年代都使用多线程并行
3. 1G到数十G:
- Concurrent GC:也会进行STW,但由于多线程,等待时间较短
主要过程
- 初始标记:STW,从根开始寻找
- 并发标记:业务线程不停止,垃圾回收线程标记垃圾
- 重新标记:STW,由于运行过程中存在错标需要修正,即对象再引用和对象不引用
- 并发清理:多线程清理垃圾
CMS:
三色标记算法:运用于并发标记阶段
黑:本对象及其引用的其他对象都访问过。下一次不会扫描该节点
灰:本对象已访问,但其引用对象为全部访问。下一次从未扫描的节点开始
白:未访问的对象
问题:(灰->白)由于线程运行变为(黑->白),此时通过Incremental Update方案,把黑转为灰。但多线程情况下,可能会出现线程1认为黑仍为黑,线程2将黑转灰,但是最终线程1仍将黑标为黑,即出现错误。
注:->表示指向
Epsilon:只做标记。可用于JVM调优debug
G1:物理上不再区分年轻代和老年代,都是以一小块区域存在。逻辑上分区,某块区域在回收之后重新分配可能是伊甸区,s1,s2,也可能是老年区
五、处理漏标方法
CMS:写屏障+增量更新 G1:写屏障+SATB ZGC:读屏障
最小堆内存和最大堆内存保持一致可以避免内存抖动
六、调优案例:
OLD GC耗时较长:
原因:Remark阶段时间较长
优化:-XX:+CMSScavengeBeforeRemark 在重新标记前再进行一次YGC以减少存活对象
YGC耗时增加:
原因:jackson进行反序列化时会将key进行String#intern,导致扫描时,GCRoot变大
解决:禁用jackson的String#intern(String常量池)
YGC次数增加:
原因:-XX:MaxGCPauseMillis参数时间设置过小,导致JVM降低年轻代region
解决:1.调大-XX:MaxGCPauseMillis的值 2.将年轻代region大小设置为固定值
CMS内存碎片化导致FGC:
原因:由于CMS通过标记清除的方法回收垃圾,不进行压缩与整理,会出现碎片化问题,影响大对象的分配,长期之后会出现FGC
优化策略:业务低峰期显式触发FGC,以降低在业务高峰期出现FGC概率
System.gc() 注:没有开启-XX:+DisableExplicitGC,否则失效
Jmap -histo:live pid
YGC和OLD GC频繁:
原因:为了降低服务时延,让YGC较快完成,年轻代设置较小,但造成YGC过多。年轻代设置较小又导致大量对象较早进入老年代,从而导致OLD GC频繁
优化:扩大新生代内存 (虽然单次YGC耗时增加,但是频率大大降低,OLD GC频率也会下降许多)
metaspace导致频繁FGC:
原因:反射调用导致创建大量DelegatingClassLoader,占用较大元空间内存,同时存在碎片化问题,导致元空间利用率不高,触发FGC
优化:1.调大metaspace空间的大小 2.优化不合理的反射调用。如用mapstruct替换BeanUtils.copyProperties()