JVM垃圾回收器

gc

这些不会被垃圾回收

四种引用

HIXqKvshlSgfEjM.png

软引用和虚引用在被 gc时,要进入引用队列然后被gc回收所占用的空间。

只有所有GC Roots对象不通过(强引用)引用该对象,该对象才能被 垃圾回收。

只要能通过gc root找到,就不会被垃圾回收。

只要没有被强引用引用到,在gc时就可能会被回收

普通gc后,如果内存当内存不足时gc

只要没有被强引用引用到,在gc时就可能会被回收

只要发生gc就会被回收。

必须配合引用对象使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存。

虚引用被回收时,就会被加入到引用队列中,当此引用不再被强引用引用时,会调用unsafe中的freeMemory方法回收。

  • 终结器引用

终结方法被重写后,重写的终结方法就可以被gc回收。

无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象。

一些常用参数

  • 堆初始大小

-Xms

  • 堆最大大小

-Xmx或-XX:MaxHeapSize=size

  • 新生代大小

-Xnm或(-XX:NewSize=size + -XX:MaxNewSize=size)

  • 幸存区比例(动态)

-XX:InitialSurvivorRatio=ratio和-XX:-UseAdptiveSizePolicy

  • 幸存区比例

-XX:SurvivorRatio=ratio

  • 晋升阀值

-XX:MaxTenuringThreshold=threshold

  • 晋升详情

-XX:+PrintTemuringDistributtion

  • GC详情

-XX:+PrintGCDetails -verbose:gc

  • FullGC 前MinorGC

-XX:+ScavengeBeforeFullGC

UseGCOverheadLimit

当打开此开关后,如果gc花费98%的时间,也只能回收不到2%的堆空间时,就不再发生gc而是报出此错。

-xx: +DisableExplicitGC

禁用显示的垃圾回收,让代码中的System.gc()无效。

system.gc() 是一种Full GC

gc的常用算法

  • 标记清除

存活对象比较多的话效率高。

需要两遍扫描,效率低。容易 产生碎片。

  • 拷贝

把内存一分为二,有用的拷贝,然后清除一边内存。

适合存活对象少的,只扫描一次,效率高。

需要移动对象,对象的引用也需要调整,

  • 标记压缩

清理的同时压缩调整内存位置。

不会产生碎片,方便分配。

需要扫描两遍,需要移动对象,效率低。

常见的垃圾回收器

  • Serial

单线程回收器

回收时所有线程都停止,单线程清除后继续。

  • PS(默认的回收器)

回收时所有线程停止,多线程清理后继续。

  • ParNew

回收时所有线程停止,可以配合CMS使用。

  • 垃圾回收器跟内存大小的关系

    • serial 几十兆
    • PS 上百兆-几个G(JDK默认的垃圾回收器)
    • CMS 20G
    • G1 上百G
    • ZGC 4T - 16T(JDK13)
  • 常见的垃圾回收器的组合参数设定(1.8)

  • 内存泄露

有废对象占据内存空间,这块空间不被回收也无法使用,

  • 内存溢出

不断地有数据占据内存,最后把内存空间占满。

G1和其他的垃圾回收器的区别

  • G1之前的垃圾回收器,有逻辑上的分带,还有物理上的分带。
  • G1只有逻辑上的分带,没有物理上的分带。
  • ZGC没有逻辑分带和物理分带,只有内存。

JVM调优

调优案例

  • 系统cpu经常100%。如何调优?(面试高频)

cpu 100%那么一定是有线程在占用系统资源。

  1. 找出哪个进程cpu高(top)
  2. 该进程中的哪个线程cpu占用高(top -Hp)
  3. 导出该线程的堆栈(jstack)
  4. 查找哪个方法(栈帧)的消耗时间(jstack)
  5. 工作线程占比高|垃圾回收线程占比高

jvm调优经验

jps 定位具体java进程

jstack 定位线程状态,重点关注 WAITING BOCKED

加入有一个进程中100个线程,很多线程都在waiting on,一定要找到是哪个线程持有这把锁。

jinfo +线程名:显示进程详细信息。

jstat -gc 线程号: 显示gc信息。(不好看)

  • 利用JMX实现的图形化界面工具

利用 JMX会消耗服务器性能,还挺大。

jconsole :jdk自带的可视化工具。

jvisualvm: 新的可视化工具(JDK自带)

jprofiler最好用的图形化界面工具。(收费)

  • 如何定位OOM问题

cmdline: arthas

jmap -histo 1736 | head -20

显示前20行的占用cpu的对象。

池线上系统,内存特别大,jmap转dump执行期间会对进程产生很大的影响,甚至卡顿,(电商系统不适合)

  1. 设定参数HeapDump,OOM时会自动产生堆转储文件
  2. 很多服务器备份(高可用),停一台服务器对其他的不影响。

在线分析

  • arthas:阿里的在线jvm分析工具。

heapdump导出堆内存的情况。(也会影响性嫩)

分析dump

  • jhat(jdk自带的dump分析工具)

默认是多大dump文件用多大的内存去分析,分析时最好指定最大内存。

分析完成后它会返回一个port端口,我们可以通过远程连接这个端口来分析dump中的数据。

G1(JDK9的默认回收器)

CMS(老年带回收器)

  • concurrent mark sweep

垃圾回收的线程和工作线程同时运行。

PIS7chgOrfAF5N9.png

  • CMS的缺点

当老年带满时(内存条碎片过多),会调用老年带单线程回收器来清理。(FGC)

  • CMS
  1. 初始标记:通过GCroot找到根对象。(STW的)

  2. 并发标记:不影响主线程的运行,在程序的运行当中来标记要回收的垃圾。

  3. 重新标记:假如之前并发标记的垃圾被又被root重新连接了,(又不能回收)在STW的情况下重新标记一遍。

  4. 并发清理:不影响程序运行的情况下清理。

G1(垃圾优先)

G1是一种服务端应用使用的垃圾回收器,目标是用在多核、大内存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同时还能保持较高的吞吐量。

特点

  • 并发收集
  • 压缩空闲空间不会延长GC的暂停时间
  • 更易预测的GC暂停时间
  • 适用不需要实现很高的吞吐量的场景

把内存分成多个不同的分区,每个分区都可能是年轻代也可能是老年代。同一时间一个分区只能属于一个代。

三色标记算法

  • 白色:未被标记的对象
  • 灰色:自身被标记,成员变量未被标记
  • 黑色:自身和成员变量均已标记完成,

CMS解决三色标记问题

CMS使用增量更新

G1使用SATB

G1的优化

JDK 8u20 字符串去重
  • 优点:节省大量内存
  • 缺点:略微多占用的cpu时间,新生代回收时间略微增加。

-XX:+UseStringDeduplication

1
2
String s1 = new String("hello");
String s2 = new String("hello");
  • 将所有新分配的字符放入一个队列
  • 当新生代回收时,G1并发检查是否由字符串重复
  • 如果他们值一样,让他们引用同一个char[]
  • 注意,与String.intern()不一样
    • String.intern()关注的是字符串对
    • 而字符串去重关注的是char[]
    • 在JVM内部,使用了不同的字符串表

JDK 8u40并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类。

-XX:+ClassUnloadingWithConcurrentMark默认启用。

JDK 8 u60回收巨型对象

  • 一个对象大于region的一半时,称之为巨型对象
  • G1不会对巨型对象进行拷贝
  • 巨型对象回收时会被优先考虑
  • G1会跟踪老年代所有的incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

JDK9 并发标记起始时间调整

  • 并发标记必须在堆空间占满前完成,否则退化为FullGC
  • JDK9之前需使用-XX:InitiatingHeapOccupancyPercent
  • JDK9可以动态调整
    • -XX:InitiatingHeapOccupancyPercent用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间

垃圾收集器和内存分配策略

确认对象需要被回收

引用计数算法

在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可 能再被使用的。

但是主流的JVM都没有使用引用计数算法来管理内存,原因是引用计数器无法处理很多意外情况。例如循环依赖问题。

可达性分析算法

从GCroot作为起始节点,通过引用关系向下搜索,搜索过的路径叫做引用链。如果某个对象没有任何引用链,就证明此对象不再被使用。

  • GCROOT
  1. 在栈中的引用对象,每个线程使用到的参数,局部变量,临时变量等。
  2. 在方法区中的静态变量。
  3. 在方法区中常量引用的对象,如字符串池中的对象。
  4. native方法引用的对象。
  5. 虚拟机系统引用的对象,如基本数据类型的class对象,异常对象,系统的类加载器等。
  6. 所有被同步锁((synchronized关键字)持有的对象。
  • 并发情况的可达性分析算法

·白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是 白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。

·黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代 表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对 象不可能直接(不经过灰色对象)指向某个白色对象。

·灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

垃圾回收算法

  • 标记清除算法:最基本的垃圾回收算法,可用于新生代和老年带。
  • 标记复制算法:常用于新生代,目前主流的垃圾回收期新生代都是采用此算法。
  • 标记整理算法:老年带才会用的垃圾回收算法,性能消耗很高。

标记整理和标记清除算法都需要停掉用户线程来处理(STW)

垃圾回收器

2023-2-2318_53_19-1677149598308.png

  • Serial收集器

2023-2-2318_54_13-1677149652489.png

  • ParNew收集器

2023-2-2318_56_13-1677149772287.png

目前只有ParNew和Serial才能和CMS配合使用。

  • CMS收集器

2023-2-2319_01_03-1677150062735.png

  • Garbage First收集器

面对堆内存组成回收集来进行回收,不再管他是哪个分带。

把连续的java堆划分成大小相同的内存区域。

回收过程:

  1. 初始标记:只标记和GCROOT有直接关联的对象。(没有停顿)
  2. 并发标记:从GCroot开始,对堆中的对象进行可达性分析。(与用户进程同步运行)
  3. 最终标记:对用户线程进行暂停,处理2步遗留的有变动的标记。
  4. 筛选回收:暂停用户线程,按照每个内存区域的价值,来决定回收哪个区域的内存。(把回收内存中需要留下的数据复制到新的地方,然后清理掉整个区域的数据)。

2023-2-2319_13_12-1677150791924.png

G1不再追求能够回收所有的垃圾,只要回收速度能追的上使用创建的速度就可以。所以一次不会回收掉全部的垃圾。

低延迟垃圾收集器

2023-2-2319_18_34-1677151113863.png

Shenandoah(谢南多厄)收集器

  1. 初始标记:标记与GCroot直接关联的对象。此阶段STW
  2. 并发标记:遍历对象图,标记出全部可达对象。与用户线程一起运行。
  3. 最终标记:标记并发标记中间变动的对象。小段的STW
  4. 并发清理:清理整个区域一个存活对象都没有的区域。与用户线程一起
  5. 并发回收:把需要回收的内存区域中存活的对象,复制到其他区域。利用读屏障和转发指针,实现此操作和用户线程一起运行。(G1这一步需要暂停用户线程)
  6. 初始引用更新:把堆中所有指向旧对象地址的指针全部指向新的对象(只是统计出哪些对象指针需要被更新)。会有短暂的STW。
  7. 并发引用更新:并发的更新上面统计的引用。与用户线程一起运行。
  8. 最终引用更新:修正GCROOT中的引用。需要STW
  9. 并发清理:清理需要被清理的内存块。与用户线程一起。

2023-2-2319_35_45-1677152145027.png

2023-2-2319_37_34-1677152253748.png

ZGC

目前最强垃圾回收器,回收的停顿时间只与GCROOT的大小有关,与堆内存无关。领先其他回收器一个数量级的差距。吞吐量第一。

引入了染色指针的概念,把少量的信息存在了指针上。但是如果内存超过4TB将无法使用此技术。而且只能在LINUX环境下运行。

  1. 并发标记(Concurrent Mark):与G1、Shenandoah一样,并发标记是遍历对象图做可达性分析的 阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记(尽管ZGC中的名字不叫这些)的 短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。与G1、Shenandoah不同的是,ZGC 的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志 位。
  2. 并发预备重新分配:扫描整个堆内存区域,把所有存活的对象都记录下来。
  3. 并发重分配:把被标记的对象都复制到新的内存块中,在旧的内存块中为这些对象建立一个转发表。如果用户线程这个时候并发访问了就的对象,会被内存屏障截取,把这个对象的引用修正到新的区域。(指针的自愈)。
  4. 并发重映射:如果某个旧对象一直没被用户线程访问,就在下一次垃圾回收的并发标记阶段里把这些对象的引用指向新的地址。

一旦某个内存块中的引用全部指向了新的地址。此转发表被释放,内存区域也被回收。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计