jvm(5)垃圾回收

垃圾对象检测:

  • 引用计数
  • 可达性分析

GC root由哪些对象组成

  1. 本地方法栈中引用的对象
  2. 虚拟机栈中引用的对象
  3. 方法区中类变量引用的对象
  4. 方法区中的常量引用的对象

1. 垃圾收集算法和收集器

垃圾收集算法有哪些:

  1. 标记-清除 产生内存碎片、效率不高
  2. 标记-整理 效率低
  3. 复制 空间利用率低

新生代(Young区)对象朝生夕死,GC时存活概率小,所以适合复制算法

老年代(old区)对象,GC频率低,涉及空间较大,因此适合使用标记清除或者标记整理

垃圾收集器有哪些

评价一个垃圾收集器优劣的指标是 吞吐量 和 停顿时间

一、新生代收集器

  1. Serial 收集器

历史悠久的收集器,单线程运行,运行时会阻塞其他线程,使用复制算法,因此适用于新生代垃圾回收

  1. ParNew 收集器

Serial收集器的并行版,同样采用复制算法,适用于新生代,单CPU性能比Serial差

运行在server模式下的虚拟中首选的新生代收集器

  1. Parallel Scavenge 收集器

注重吞吐量,

吞吐量 = 程序运行时间/(程序运行时间+垃圾回收时间)

二、老年代收集器

  1. Serial old

复制算法的实现,单线程运行

  1. Paraller Old

最关注的点事吞吐量

  1. CMS 并发收集器

Concurrent Mark Sweep—并发标记清理

并发:用户线程和垃圾回收线程一起执行

并行:多条垃圾回收线程同时执行

CMS 最关注的点是GC停顿时间,所以优点是低停顿时间(因为并发收集)

缺点就是会产生大量的内存碎片(因为采用标记-清理算法),且并发阶段会吞吐量降低

流程:初始标记->并发标记->重新标记-> 并发清理

初始标记,stw,标记的事GCroot

并发标记,与用户线程一起执行,执行可达性分析,标记不可达对象

重新标记,stw,标记并发标记阶段产生的新垃圾

并发清理,用户线程一起执行,回收标记的对象

使用 -XX:+UseConcMarkSweepGC 开启CMS

搭配使用:

三、G1 Garbage-First

整体上属于标记-整理算法的实现,不会产生内存碎片

比CMS先进的地方在于用户可以设置停顿时间的大小,G1会按照用户的设置的停顿时间制定回收计划

G1可同时用于新生代和老年代的垃圾回收,核心在于对堆内存的重新划分,不同的内存区域对于G1来说只是逻辑上的区分,在物理层面,G1将内存划分成一个个的region统一进行管理。

G1收集器会先收集存活对象少的区域,也就是垃圾对象多的区域,这样可以有大量的空间可以释放出来,这就

是Garbage First的由来

1584582498419

执行流程为:初始标记–> 并发标记 –> 最终标记 –> 筛选回收

初始标记:stw,标记GC ROOT

并发标记:与用户线程并发执行,执行可达性分析

最终标记:stw,标记并发标记阶段用户线程产生的新垃圾

筛选回收:stw,对各个Region的回收价值和回收成本进行排序,根据用户设定的停顿时间指定回收计划

总结:

Serial 和Serial Old 为串行收集器,适用于内存较小的嵌入式设备

Parallel 和 Parallel Old 为并行收集器,吞吐量优先的收集器组合,适用于科学计算、后台处理等应用场景

CMS 和 G1 为并发收集器,停顿时间优先,CMS适用于老年代收集(标记-清除),G1适用于整个堆内存垃圾回收(标记-整理),适用与对响应时间要求较高的场景,比如Web

使用不同的收集器

1
2
3
4
5
6
7
8
9
(1)串行 
-XX:+UseSerialGC
-XX:+UseSerialOldGC
(2)并行(吞吐量优先):
-XX:+UseParallelGC
-XX:+UseParallelOldGC
(3)并发收集器(响应时间优先)
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC

2、GC分类

Minor GC触发条件:当Eden区满时,触发Minor GC。

Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。

3、调优

JVM调优参数简介

jvm调优一般适用非标准化参数,即-XX参数。除此之外还有标准化参数-、-X参数,此处不表

  • boolean 类型参数:用+/-表示是否开启,例如-XX:+UseG1GC,开启G1收集器
  • 值类型参数:例如-XX:MaxGCPauseMillis=500,表示GC最大允许停顿时间500ms

其他参数

-Xms100M 等价于 -XX:InitialHeapSize = 100M

-Xmx200M 等价于 -XX:MaxHeapSize = 200M

-Xss20K 等价于 -XX:ThreadStackSize = 20k

展示常用的jvm参数:

-XX:+PrintFlagsFinal 启动时打印所有JVM参数

参数怎么修改

  • ide中进行配置
  • java -XX:+UseG1GC
  • tomcat –bin–xxxxx.sh/catalina.sh — jvm参数
  • 实时修改 jinfo,只能修改一部分

启动时打印的参数 = 表示默认参数 :=表示用户修改的参数

常用参数

  • 内存分配相关

    • -Xms100M 初始堆大小
    • -Xmx200M 堆最大size
    • -Xss20K 方法栈大小 经验值3000~5000字节
    • -XX:NewSize=20M 设置Young区大小
    • -XX:MaxNewSize=50M 年轻代最大size
    • -XX:OldSize=50M 设置Old区大小
    • -XX:MetaspaceSize=100M 设置元空间大小
    • -XX:MaxMetaspaceSize=200M 元空间最大size
    • -XX:NewRatio=4 老生代占堆比值 4 表示—新生代:老年代 = 1:4
    • -XX:SurviviorRatio=8 Eden区占Young区的比例 8表示Eden:s0:s1=8:1:1
  • GC相关

    • -XX:+UseSerialGC
    • -XX:+UseSerialOldGC
    • -XX:+UseParallelGC
    • -XX:+UseParallelOldGC
    • -XX:+UseConcMarkSweepGC
    • -XX:+UseG1GC
    • -XX:MaxGCPauseMillis=200ms 设置允许的最大GC停顿时间
    • -XX:G1HeapWastePercent 设置允许G1收集器浪费的堆空间占比
    • XX:ConcGCThreads=n 设置并行的GC线程数量
    • -XX:InitiatingHeapOccupancyPercent 启动并发GC周期时,dui
    • -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps Xloggc:$CATALINA_HOME/logs/gc.log 设置GC日志打印选项

    • -XX:MaxTenuringThreshold =6 对象提升到老年代的年龄阈值

    • -XX:G1OldCSetRegionThresholdPercent=1
    • -XX:G1MixedGCCountTarget=8
    • -XX:G1MixedGCLiveThresholdPercent=65
  • 其他参数

    • -XX:CICompilerCount=3
    • -XX:+HeapDumpOnOutOfMemoryError oom的时候dump内存快照
    • -XX:HeapDumpPath=heap.hprof 内存快照输出路径

常用命令:

  • jps 查看当前jvm中的所有java进程号

  • jinfo 查看和调整指定进程的jvm参数

    • 查看指定名称参数的指令:jinfo -flag
    • 查看所有指令:jinfo -flags
    • 修改参数:
      • jinfo -flag <+/->
      • jinfo -flag =
  • jstat class/gc

  • jstack 查看某进程的线程,可用于排查死锁

  • jmap: 生成堆内存快照

    jmap -heap PID 干嘛要看堆内寸信息呢

    生产环境OOM—->在发生oom的时候能把内存的信息导出来进行分析

    手动 dump文件: jmap -dump:format=b,file=heap.hprof PID

    配置自动dump -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumoPath=dump.hprof

常用工具

  • jdk自带

    • jconsole
    • jvisualvm
  • 连接远端的java

    • 使用jmx连接
  • 第三方
    • arthas
    • mat

GC调优:

GC 发生的timing

  • eden区满
  • old区满
  • 方法区满
  • System.gc()

获取GC日志

配置参数

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:gc.log

若想要获取tomcat的Gc日志,编辑catalina.bat,在第一行加上参数:

1
set JAVA_OPTS=%JAVA_OPTS% -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:gc.log

启动tomcat后能在当前目录拿到gc.log文件

GC日志文件分析工具

  • gceasy
  • GCViewer

G1调优指南(官方)

  1. 不要手动设置新生代和老年代的大小,只要设置整个堆的大小

    G1收集器在运行过程中,会自己调整新生代和老年代的大小

    其实是通过adapt代的大小来调整对象晋升的速度和年龄,从而达到为收集器设置的暂停时间目标

    如果手动设置了大小就意味着放弃了G1的自动调优

  2. 不断调优暂停时间目标

    一般情况下这个值设置到100ms或者200ms都是可以的(不同情况下会不一样),但如果设置成50ms就

    不太合理。暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度。最终退化成Full GC。所以

    对这个参数的调优是一个持续的过程,逐步调整到最佳状态。暂停时间只是一个目标,并不能总是得到

    满足。

  3. 使用-XX:ConcGCThreads=n来增加标记线程的数量

    IHOP如果阀值设置过高,可能会遇到转移失败的风险,比如对象进行转移时空间不足。如果阀值设置过

    低,就会使标记周期运行过于频繁,并且有可能混合收集期回收不到空间。

    IHOP值如果设置合理,但是在并发周期时间过长时,可以尝试增加并发线程数,调高 ConcGCThreads。

  4. MixedGC调优

    1
    2
    3
    4
    -XX:InitiatingHeapOccupancyPercent   
    -XX:G1MixedGCLiveThresholdPercent
    -XX:G1MixedGCCountTarger
    -XX:G1OldCSetRegionThresholdPercent
  5. 适当增加堆内存大小

常见问题:

1、内存泄漏与内存溢出的区别

内存泄漏是指不再使用的对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。

2、young gc会有stw吗?

不管什么 GC,都会发送 stop-the-world,区别是发生的时间长短。而这个时间跟垃圾收集器又有关系,Serial、PartNew、Parallel Scavenge 收集器无论是串行还是并行,都会挂起用户线程,而 CMS和 G1 在并发标记时,是不会挂起用户线程的,但其它时候一样会挂起用户线程,stop the world 的时间相对来说就小很多了。

3、major gc和full gc的区别

Major Gc 在很多参考资料中是等价于 Full GC 的,我们也可以发现很多性能监测工具中只有 Minor GC和 Full GC。一般情况下,一次 Full GC 将会对年轻代、老年代、元空间以及堆外内存进行垃圾回收。触发 Full GC 的原因有很多:当年轻代晋升到老年代的对象大小,并比目前老年代剩余的空间大小还要大时,会触发 Full GC;当老年代的空间使用率超过某阈值时,会触发 Full GC;当元空间不足时(JDK1.7永久代不足),也会触发 Full GC;当调用 System.gc() 也会安排一次 Full GC。

4、G1与CMS的区别是什么

CMS 主要集中在老年代的回收,而 G1 集中在分代回收,包括了年轻代的 Young GC 以及老年代的 MixGC;G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生;在初始化标记阶段,搜索可达对象使用到的 Card Table,其实现方式不一样。

5、什么是直接内存

Java的NIO库允许Java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。

6、垃圾判断的方式

引用计数法:指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为0就会回收但是JVM没有用这种方式,因为无法判定相互循环引用(A引用B,B引用A)的情况引用链法: 通过一种GC ROOT的对象(方法区中静态变量引用的对象等-static变量)来判断,如果有一条链能够到达GC ROOT就说明,不能到达GC ROOT就说明可以回收

7、不可达的对象一定要被回收吗?

即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 fifinalize 方法。当对象没有覆盖 fifinalize 方法,或fifinalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

8、方法区中的无用类回收

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是无用的类:

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。

  • 加载该类的 ClassLoader 已经被回收。

  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

9、为什么要区分新生代和老年代

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。