四、JVM深度解析与排查线上问题(二)

一、怎么判断对象是否存活的算法?

1、引用计数法

  • 算法描述:当有一个对象引用我时,会在计数器上+1,当计数器为0是,说明没被对象引用,故需要被垃圾收集器回收
  • 缺点:每次对对象赋值时需要维护引用计数器,较难处理循环引用(持有并等待,哲学家思考问题)

2、可达性分析算法

(1)、什么是可达性算法(基本概念)?

  • 通过一个根节点RootGC作为一个起点,从这个节点往下搜,搜索走过的路径就是引用链,当一个对象没有引用链的时候,说明根节点到该对象不可达,证明该对象是不可用的。(从根节点往下搜该对象,如果没搜到说明该对象没有被引用了,需要被回收了。)

(2)、Java中可以用作根节点的对象?

  • 虚拟机栈中引用的对象
  • 方法区中静态变量引用的对象
  • 方法区中静态常量引用的对象
  • 本地方法栈JNI引用的对象

(3)、一个对象被确认不可达的时候一定会被回收吗?

  • 不会,当一个对象被确认不可达的时候,会被标记,并且执行finalize方法,若在该方法中与任何一个对象有关联的话,那就不会被回收,若无关联,将等待被垃圾回收。

二、垃圾回收算法

1、元空间是怎么垃圾回收?

  • MetaspaceSize,达到一个阈值就会触发垃圾收集,进行类型卸载。
    • GC会对该值进行动态调整,当回收很多空间时,会适当减少该值大小,当释放很少时,会适当增加该值大小,最大不超过MaxMetaspaceSize,若超过则抛出OOM异常。
  • 每一个类加载器的存储区域就是一个小块的元空间,众多类加载器组成一个大的元空间,当某个类加载器被标记为不可存活时,就会将其卸载,回收。

2、有哪些算法?(可达性分析区标记)

(1)、复制算法

  • 算法描述:年轻代使用,from区(包含eden)复制到to区。因为在该区域,对象存活率低,所以比较适合用,若存活率比较高,则不适合使用,当达到60%会直接放到老年代。
  • 原理思想:将内存分为两块,每次只用其中的一块,当这一块内存用完就开始垃圾回收,并且把还存活的数据复制到另一块空的内存上面。
  • 优点:不会产生内存碎片
  • 缺点:耗空间,空间没有充分利用

(2)、标记清除

  • 算法描述:老年代使用,先标记要回收的对象,然后统一回收这些对象。
  • 原理思想:先埋点,后清除
  • 优点:节约空间,充分利用空间
  • 缺点:产生内存碎片,需要先标记后才能清除,速度上会慢点,效率低。

(3)、标记压缩

  • 算法描述:先标记,在清除,后压缩整理
  • 原理思想:在标记清除的基础上在整理下碎片。
  • 优点:节约空间,没有碎片
  • 缺点:耗时比较长。

3、FullGC触发条件?

  • 代码调用System.gc
  • 老年代空间不足
  • 方法区空间不足
  • 通过MinorGC直接进入老年代,此时老年代没有可用的内存存放该内存
  • MinorGC复制算法后,to区不够放,则把该对象转存到老年代,此时老年代没有可用的内存存放该内存

三、垃圾收集器

1、产生Stop-The-World情况

  • 新生代和老年代GC时
  • 死锁检查
  • jstack工具,Dump线程
  • jmap工具,堆Dump

2、垃圾回收器有哪些?

1、新生代:Serial、ParNew、Parallel Scavenge

(1)、Serial串行收集器
  • 描述:串行执行,单线程收集器,复制算法,可以和Serial Old搭配使用。
  • 详解:在工作的时候会停止其他所有的工作线程(Stop-The-World)
  • 缺点:效率低
  • 参数控制: -XX:+UseSerialGC

(2)、ParNew收集器(常用的)
  • 描述:在Serial基础上,支持多线程的,核心数根据服务器CPU核心数来的,可以和CMS搭配使用
  • 参数控制:
    • -XX:+UseParNewGC,指定年轻代使用该收集器
    • -XX:ParallelGCThreads,限制线程数量

(3)、Parallel Scavenge收集器
  • 描述:和ParNew收集器一样支持多线程并发执行,与之不同的是该收集器的目标是达到一个可控的吞吐量。吞吐量=程序运行时间/(程序运行时间+GC时间)。和Serial Old配合使用。
  • 它是如何保证吞吐量的呢?参数太多了,用到时在研究。
  • 参数控制:-XX:+UseParallelGC
  • 缺点:
    • 一位的追求吞吐量,减少GC的时间,就会到时频繁的进行垃圾回收(因为回收500m肯定比回收2000mGC要短)
    • jdk1.5之前老年代用Serial Old还是比较慢的,性能不太好
    • Jdk1.6以后老年代可以用Paralled Old收集器,效果会好点

2、老年代:Serial Old、CMS、Paralled Old

(1)、Serial Old收集器
  • 描述单线程收集器,标记整理算法
  • 补充:如果CMS垃圾回收失败了,会用Serial Old回收

(2)、Paralled Old收集器,
  • 描述:多线程并行的收集器,标记整理
  • 参数控制:-XX:+UseParallelOldGC

(3)、CMS收集器(常用的)
  • 描述:以获取最短回收停顿时间为目标的收集器,标记清除算法
  • 工作流程:
    1. 初始标记:可达性分析,判断对象是否存活,标记为存活的对象,速度很快,需要Stop-The-World
    2. 并发标记:初始标记完并不会立刻垃圾回收,这期间程序不停的有对象创建,需要可达性判断,标记为存活的对象,耗时长
    3. 重新标记:修正程序运行期间导致标记产生变动的那部分对象,耗时长, 需要Stop-The-World
    4. 并发清除:清除标记的对象。
  • 优点:对于耗时的操作,是并发执行,低停顿
  • 缺点:基于标记清除算法,导致内存碎片过多;多线程执行占用CPU
  • 导致并发模式失败的情况(Concurrent Mode Failure)
    1. 对象提升到老年代的速度太快,垃圾收集赶不上它的速度,老年代的空间不足
    2. 老年代碎片化太严重,一个大的对象放不下
  • 浮动垃圾:并发清除期间,用户线程还在运行,这期间产生的对象内存,即为浮动垃圾
  • 参数调优:
    1. -XX:+UseCMSCompactAtFullCollection(默认开启的):由于CMS是基于标记清除算法,会产生会多内存碎片,通过该参数,标记清除完之后再整理内存碎片,该过程无法并发,会占用时间,到时停顿时间变长。
      1. -XX:+CMSFullGCsBeforeCompaction:执行多少次不压缩的FullGC,以后再整理,默认是0,每次都整理
    2. -XX:UseConbcMarkSweepGC:使用CMS垃圾回收器

3、补充G1、ZGC()目前JDK11实现性的收集器

(1)、G1收集器(新生代和老年代):

image-20210221144429580.png

  • 适用场景:
    • 多核CPU、JVM内存占用比较大的(至少大于4G)
      • 为什么?因为在运行过程中会产生大量的内存碎片,需要经常压缩空间。
    • 需要可控的,可预期的GC停顿周期。
  • 回收方式
    • YoungGC,根据预期停顿时间,选定年轻代的部分的Region进行回收,以此来控制时间开销
    • MixedGC,选定所有年轻代Region和根据global concurrent marking找到部分老年代Region进行回收
  • 注意:MixedGC不是FullGC,只会回收部分来年代Region,如果MixedGC是在无法跟上程序分配内存的速度,则会使用Serial Old GC(FullGC)来收集老年代垃圾,故G1是不提供FullGC的。
  • global concurrent marking工作整体过程类似CMS,初始标记、并发标记、最终标记、清理垃圾

(2)、ZGC收集器(不分代)
  • 目标
    1. 支持TB级堆内存(最大16T)
    2. 最大GC停顿10ms
  • ZGC为什么很快?
    1. 染色指针,是否需要垃圾回收放到对象头中
    2. 读屏障+CAS
    3. 使用NUMA架构技术高效分配空间和进行对象的扫描

4、小总结

参考

  • 新生代:

    • Serial(jdk5之前发布,单线程复制算法)
    • ParNew(多线程复制算法)
    • Parallel Scavenge(jdk4发布,多线程标记复制算法,可控的吞吐量=运行代码/(运行代码+垃圾收集时间))
  • 老年代:

    • Serial Old(jdk5之前发布,单线程标记-整理)
    • Concurrent Mark Sweep(CMS jdk5发布,并发低停顿收集器,标记-清除算法,标记-整理(多少次FULLGC以后整理碎片|分配对象内存空间不够直接FULLGC))
    • Paralled Old(jdk6发布,多线程标记-整理)
  • 全堆收集器:

    • Garbage First(G1,jdk1.7.4出现,9时官方推荐)
  • 搭配关系:

    • Serial&Serial Old(jdk5之前)
    • ParNew&Serial Old(jdk5之前)
    • Parallel Scavenge&Serial Old(jdk5之前,浪费性能)
    • Parallel Scavenge&Paralled Old(jdk5之后,吞吐量优先)
    • Serial&CMS(jdk5~8,jdk9取消)
    • ParNew&CMS(jdk5~8官方推荐,9之后合并)
  • 都遵循分代收集理论,G1以前分新生代和老年代,G1全功能收集器,基于Region的堆内存布局,G1优先处理回收价值收益最大的那些Region

  • jdk9以后:G2(推荐),取消ParNew&Serial Old、取消Serial&CMS、此后ParNew合并CMS两者不可以再和其他收集器搭配使用

  • 全功能收集器:G1(局部看两个Region标记-复制、整体看标记-整理)、ZGC(低延迟垃圾收集器)

  • 选型:目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间

四、内存分配策略

1、对象优先在TLAB/Eden分配?

  • TLAB是本地线程分配的缓冲区,在Eden中独立开出一小块空间(1%)作为TLAB,若对象可以在TLAB上分配则分配,若不能则分配在Eden中,TLAB线程私有的,不存在共享资源竞争的情况

2、什么情况直接进入老年代?

  • 大对象,年轻代的空间经过YoungGC后不够存放时
  • 长期存活的对象,活过了GC分代年龄(最大15次,对象头4字节存放GC分代年龄)

3、什么是空间分配担保?

  • 新生代MinorGC之前,会检查老年代最大可用内存大小是否大于本次新生代的对象大小,若大于则进行MinorGC,若不大于则先进行FullGC

五、常见的JVM参数

六、对象的引用

1、强引用

  • 概念:当内存不足时,JVM抛出oom,也不会回收它。
  • 示例:new出来的对象是强引用,是使用最普遍的引用。

2、软引用

  • 概念:当内存足够时,JVM不会回收它,当内存不够时,再回收。
  • 应用:
    • 实现内存敏感的高速缓存。
    • 可以和引用队列联合使用,如果软引用的对象被垃圾回收了,JVM会把软引用加入到与之关联的引用队列中。SoftReference sf = new SoftReference(obj);

3、弱引用

  • 概念:垃圾回收器扫描到弱引用对象,不管当前内存是否足够,都会清除。(由于垃圾回收器是一个优先级很低的线程,因此不会那么快发现到)
  • 应用:
    • 可以和引用队列联合使用,如果弱引用的对象被垃圾回收了,JVM会把软引用加入到与之关联的引用队列中。WeakReference wf = new WeakReference(obj);

4、虚引用

  • 概念:不会决定对象的生命周期,在任何时候都会被垃圾回收。
  • 应用:
    • 必须和引用队列联合使用。当垃圾回收准备回收一个对象时,如果发现它还有虚引用,就会在回收对象内存之前,把这个虚引用加入到与之关联的引用队列中。PhantomReference pf = new PhantomReference(obj);
    • 由于跟踪垃圾回收的活动,一般利用虚引用感知到jvm垃圾回收动作,然后就可以回收堆外内存

七、常见的OOM以及原因?

1、堆溢出

  • 错误:oom:Java heap space
  • 本质:FullGC之后空间还是不足,则报错
  • 原因:
    • 应用程序保存了太多无法被GC回收的对象,导致无法在堆中分配新的对象
    • 应用程序过度使用finalizer,导致要回收对象没发回收(在该方法中,该对象又引用了其他对象)。
  • 解决:
    • 检查代码是否有递归、大循环、死循环在疯狂new对象。
    • jmap等监控工具
    • 估算堆大小,在机器支持的情况下,适当调整堆大小

2、栈溢出

  • 错误:
    • StackOverflowError
    • oom:如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时,会抛oom
  • 原因:
    • 单个线程下,栈帧太大,或者栈容量太小
    • 不断的创建线程,导致oom
  • 解决:
    • StackOverflowError错误,则检查是否存在递归调用
    • oom,检查是否有哪里疯狂创建线程
    • 在机器支持的情况下,适当调整栈大小

3、方法区

  • 解释:
  • 永久代存放class信息、静态常量、静态变量,元空间存放calss信息。
  • 本质:当运行时产生大量的class时,可能会导致oom
  • 原因:
    • 使用cglib生成了大量的代理类
    • jdk7以前,频繁的错误使用String.intern方法
    • 大量的jsp或者动态产生大量的jsp
  • 解决:
    • 重启,增大内存。

八、JVM常用监控命令

1、jps

  • 进程相关

2、jstat

  • 监控虚拟机统计信息

3、jstack

  • 注重监控线程情况

4、jmap

  • 监控整体内存情况,可以把文件dump下来,用分析工具去分析

5、jinfo

  • java配置信息工具

九、CPU占用过高问题排查

1、查看系统状况

  • top命令查看CPU内存使用情况

2、定位问题的线程

  • ps -mp pid -o THREAD,tid,time

3、查看问题线程的堆栈1

  • jstack pid | grep threadId

4、jstat查看进程内存情况

  • jstat -gcutil pid 2000 10(没2s一次,共计10次)

5、jstack和jmap分析进程堆栈和内存情况

  • 使用jmap命令dump文件,拿到本地使用mat、jprofler等工具分析jmap -dump:format=b,file=dump.bin 6764
  • jstack -l 6764 >> jstack.out
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

showluu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值