Java垃圾回收器笔记

memory leak:未回收的垃圾

oom:memory leak达到一定程度无法再分配内存

一、判断垃圾

  1. 引用计数

无法解决循环引用问题,比如A->B->C->A。python是这样判定的

  1. Root可达判定

GC Roots:

  • 方法区类静态属性
  • 方法区常量
  • 虚拟机栈局部变量表
  • 本地方法栈JNI

引用:有4种,引用强度依次减弱

  • 强引用,只要存在就不会被回收
  • 软引用,系统将要发生OOM前,把这些对象列到回收范围进行第二次回收,若还没有足够的内存则OOM
  • 弱引用,GC只要工作就会回收这些对象
  • 虚引用,不会影响对象生命周期,不能通过虚引用取得一个对象实例,唯一目的是能在虚引用对象被回收时收到一个系统通知

如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为"没有必要执行",若有必要执行,则会调用对象的finalize(),此时是对象的最后一次自救机会

二、GC算法

堆回收:

  • 分代算法:mark-sweep、copy、mark-compact,分新生代和老年代,各自用不同的GC算法
  • 分区算法:适用于大内存,不同区用同一种GC算法,分区内部分代

方法区回收:

  • 废弃常量:如果没有对象引用该常量,即回收
  • 无用类:同时满足以下三条件,根据-Xnoclassgc控制是否回收
    • 该类所有实例已被回收
    • 加载该类的ClassLoader已被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

1. GC Root扫描

在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了,这样可以很快完成GC root扫描

2. 如何发起GC

为防止使用OopMap时出现引用关系变化影响GC,因此就要停顿下来,但为了防止影响效率,HotSpot没有为每条指令都生成OopMap,只是在"特定的位置"记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。

如何在GC发生时让所有线程都跑到安全点再停顿下来?主要使用抢断式中断,不需要线程的执行代码主动配合,在GC发生时首先把所有线程全部中断,如果有线程中断的地方不在安全点上,就继续运行直至到安全点。但当线程处于sleep或blocked状态时,线程无法响应中断,所以扩展安全点到安全区域,安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。线程执行到安全区域时首先标记自己进入了此区域,GC发生时这些线程会正常停顿,当线程要离开此区域前先检查是否完成了GC root扫描,只有完成扫描了才可以离开

3. 典型的分代算法

分配内存的规则:

  • 对象优先在新生代的Eden分配
  • 大对象直接进入老年代(-XX:PretenureSizeThreshold 默认是0即任何情况下都先分到Eden区,也可设置一个大于0的值表示大于此值时直接分到老年代)
  • 长期存活的对象将进入老年代(-XX:MaxTenuringThreshold 默认经过15次GC仍未回收的)
  • 动态年龄判断(相同年龄的总和大于Survivor一半,年龄不小于此的对象直接进入老年代)
  • Young GC后存活对象大于Survivor,直接放到老年代
  • 老年代空间分配担保(依次进行两类判断,在执行任何一次Young GC之前,先检查一下老年代可用内存是否大于新生代所有对象的总大小。如果大于则可放心Young GC,如果小于看-XX:HandlePromotionFailure,若为false则直接Full GC,若为true则检查老年代可用内存是否大于之前每次Young GC后进入老年代的平均大小,若小于则Full GC,若大于则Young GC,存活对象若大于老年代可用内存则又Full GC。若还是不够用则触发OOM)

Minor GC:新生代GC

Full GC/Major GC:老年代GC,会产生stop-the-world

整体详细过程:

部分对象可以直接分配到栈上,栈帧pop后直接回收而不需要GC复杂算法,效率很高,但需要满足两个条件:1、通过逃逸分析;2、标量替换

TLAB(Thread Local Allocation Buffer 线程本地分配缓冲区)如果直接把新生成的对象放到Eden区会产生竞争,为分配对象时提升效率,给每个线程分配个TLAB,每个线程的对象优先分配到这里

三、GC垃圾回收器

1. serial、serialOld

单线程,使用copying算法,只适用于小内存比如几K到几十M

2. parallel scavenge、parallel marksweep

多线程,java8默认,适用于几G

新生代和老年代的默认比例是1:2

3. ParNew、CMS (concurrent mark sweep)

适用于几十G

使用ParNew和CMS垃圾回收器,ParNew与Parallel scavenge类似,只是为了配合CMS增强了部分功能而已。

新生代和老年代的默认比例是1:2,ParNew默认使用与CPU核心数相同的线程数并发处理新生代,CMS默认启动的垃圾回收线程数是(CPU核心数+3)/4 处理老年代

并发、低STW,第一种支持GC与工作线程并发的垃圾回收器,有很多缺陷,未被任何一种JVM作为默认垃圾回收器

过程:

  • 初始标记:STW,只标记root可达的第一级对象,工作量小,因此STW很短
  • 并发标记:工作线程与GC线程并发,使用三色标记算法,容易出现两种情况:1.浮动垃圾,标记时不是垃圾后来又变成垃圾,会在下次GC时被清除,不影响GC正确性;2.漏标,灰白消失黑白出现,标记时是垃圾后来不是垃圾,出现野指针
  • 重新标记:STW,为应对并发标记漏标,CMS使用Incremental Update重新标记黑变灰的节点
  • 并发清理:清理垃圾对象(白色对象),产生碎片

并发标记阶段产生漏标的原因:不同垃圾回收线程间会存在覆盖标记的现象,比如两个GC线程分别处理一个对象两属性,A1属性指向D后,GC1将A变灰,GC2扫描完A2属性后,将A对象置黑,覆盖了GC1的标记,此时会漏标D

Concurrent Mode Failure问题:

老年代根据-XX:CMSInitiatingOccupancyFaction默认预留8%内存给并发清理期间新进入老年代的对象,若预留空间仍不够用则会出现Concurrent Mode Failure问题,自动使用Serial Old替换CMS执行STW,重新进行单线程GC Roots追踪并回收垃圾,系统卡死的时间可能很长

内存碎片问题:

标记清理算法会产生很多内存碎片。-XX:+UseCMSCompactAtFullCollection默认打开,即Full GC后STW进行碎片整理。-XX:CMSFullGCsBeforeCompaction,指定执行多少次Full GC后进行碎片整理,默认每次执行Full GC后都碎片整理,两个参数一般不用改

4. G1

逻辑分代,物理不分代,适于于上百G大内存

把JVM堆内存分为多个大小相等的Region,且某些Region属于新生代,某些属于老年代,是动态变化的

JVM默认分为2048个Region,也可通过-XX:G1HeapRegionSize指定Region大小,Region大小必须是2的整数倍

新生代一开始占堆内存的5%,可通过-XX:G1NewSizePercent设置初始占比,运行过程中会不断增大,但默认最多不超过60%,可通过-XX:G1MaxNewSizePercent设置最大比例

可设置垃圾回收的预期停顿时间,G1做到这点需要跟踪每个Region的回收价值,回收价值即单位时间可回收的内存量,尽可能地保证STW时间在停顿时间范围内

Minor GC:与ParNew类似,不过大对象不是到老年代,而是超过Region大小的50%的对象作为大对象,进行特殊处理,如果对象过大,可以横跨多个Region存储

Full GC:-XX:InitiatingHeapOccupancyPercent默认45%,老年代占堆内存超过45%时触发Full GC。使用三色标记算法,不过在重新标记阶段,追踪黑白新生的连接,然后将白标黑下次再扫描。在并发清理阶段,使用复制算法而非CMS的标记清理算法那样不会产生内存碎片,-XX:G1HeapWastePercent默认是5%,基于复制算法把Region存活的对象放入其他Region,然后清除掉本来的Region。那么当空闲的Region数量达到堆内存的5%,就会立即停止混合回收,-XX:G1MixedGCLiveThresholdPercent默认是85%,要回收的Region存活对象必须少于85%才可以被回收掉,否则复制成本会很高。如果在复制的时候发现没有空闲的Region可以承载存活的对象,那么会触发失败,立马停止系统进程,采用单线程进行标记、清理和压缩整理,空闲出一批Region,这个过程是极慢的

四、三色标记算法 白灰黑

标记:

标记结束后未标到的是垃圾,如果漏标了说明多标了垃圾

三色:

白色:未被标记(若重新标记完成时仍是白色则是垃圾)

灰色:自身被标记,指向的对象存在未标记的

黑色:自身、指向的对象均完成标记

漏标的对策:

CMS:Incremental update 增量更新,黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。重新标记的时候会暂停线程(STW)重新扫描变灰的旧黑对象

G1:SATB(Snapshot At the Beginning) 原始快照,当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色,目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾

五、记忆集与卡表

所有涉及部分区域收集(Partial GC) 行为的垃圾收集器, 典型的如G1、 ZGC和Shenandoah收集器, 都会面临跨代、跨区引用的问题,如果再去跨区域扫描,效率会很低

记忆集(Remember Set):存储某一块非收集区域是否存在指向收集区域的指针

卡表(Card Table):hotspot的记忆集实现,每个元素对应着其标识的内存区域一块特定大小的内存块,称为"卡页",hotSpot使用的卡页是2^9大小,即512字节

六、jvm参数调优

调优:

  • 预调优:根据需求进行jvm规划和优化
  • 优化jvm运行环境(cpu满载,可以用jstask查看线程情况,为了方便排查,开发代码时要定义线程池里每个线程的名称)
  • 解决各种OOM问题,(tcpdump把流量复制一份到测试环境进行真实测试)

-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8

1、标准参数

-开头,任何版本都有

-version

-verbose:gc 查看内存回收情况,与-XX:+PrintGC功能一样,只是这个是稳定版本

-verbose:class 查看类加载情况

-verbose:jni 查看本地方法调用情况

2、非标参数

-X开头,有可能将来会替换

java -X -help 查看所有同类参数-

-Xms:最小内存:注意如果不设置最小内存,默认会设为物理内存的64分之一,建议设成与Xmx一致,避免因所使用的Java堆内存不够导致频繁full gc以及full gc中因动态调节Java堆大小而耗费延长其周期

-Xmx:最大内存

-Xoss:设置本地方法栈大小(HotSpot虚拟机不区分虚拟机栈和本地方法栈,无效)

-Xss:设置虚拟机栈大小(HotSpot中即虚拟机栈和本地方法栈总大小)

-Xmn:新生代

-Xint:禁止编译器运行,强制使用纯解释方式执行字节码,尽量不要用

-Xloggc:../gclogs/gc.log.date:指定 GC log 的路径

-Xverify:none:禁止字节码验证,在代码稳定时可以用

3、-XX

java -XX:+PrintCommandLineFlags -version 查看jvm默认启动参数

-XX:+PrintFlagsFinal 所有-XX参数当前值

-XX:+PrintFlagsInitial 所有-XX参数默认值

-XX:+HeapDumpOnOutOfMemoryError:JVM 就会在发生OOM时抓拍下当时的内存状态,也就是我们想要的堆转储文件。

-XX:+HeapDumpOnCtrlBreak:不想等到发生崩溃性的错误时才获得堆转储文件

-XX:+TraceClassLoading:追踪类加载过程

打印GC:

  • -XX:+PrintGC:打印GC日志
  • -XX:+PrintGCDetails:打印 GC 详情,包括 GC 前/内存等。
  • -XX:+PrintGCDateStamps:打印GC的时间戳(以日期的形式,如 2017-09-04T21:53:59.234+0800)
  • -XX:+PrintTenuringDistribution:打印 GC 发生时的代龄信息。
  • -XX:+PrintGCApplicationStoppedTime:打印 GC 停顿时长
  • -XX:+PrintGCApplicationConcurrentTime:打印 GC 间隔的服务运行时长

垃圾回收器:

  • -XX:+UseSerialGC:使用Serial和Serial Old收集器组合进行内存回收
  • +XX:+UseG1GC:使用G1
  • -XX:+UseParallelGC:使用Parallel Scanvenge和Parallel MarkSweep分别清理新生代和老年代
  • -XX:+UseParNewGC:使用ParNew作为新生代垃圾回收器
  • -XX:+UseConcMarkSweepGC:使用CMS作为老年代垃圾回收器
  • -XX:PretenureSizeThreshold=3145728:设置Serial、ParNew收集器的大对象最小字节数
  • -XX:MaxTenuringThreshold:对象晋升老年代的年龄阈值
  • -XX:SurvivorRatio=8:GC的复制算法中Survivor区域占总区域(2*Survivor+Eden)的1/(1+1+8)
  • -XX:+DisableExplicitGC:屏蔽System.gc()

方法区:

  • -verbose:class -XX:+TraceClassLoading、-XX:+TraceClassUnLoading:查看类加载和卸载信息
  • -XX:PermSize:方法区大小
  • -XX:MaxPermSize:最大方法区大小
  • -XX:MaxMetaspaceSize:1.8元数据区

指针压缩:

不再保存所有引用,而是每隔8个字节保存一个引用,当引用被存入64位的寄存器时,JVM将其左移3位(相当于末尾添加3个0),当从寄存器读出时,JVM又可以右移3位,丢弃末尾的0

  • -XX:+UseCompressedClassPointers
  • -XX:+UseCompressedOops

七、命令行工具

1. jstack

打印堆栈信息

"Thread-1" 是线程的名字,prio 是线程的优先级,tid 是线程id, nid 是本地线程id, waiting to lock 等待去获取的锁,locked 自己拥有的锁。

复制代码
"Thread-1" #11 prio=5 os_prio=0 tid=0x0000000055ff1800 nid=0x1bd4 waiting for monitor entry [0x0000000056e2e000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.itdragon.keyword.ITDragonDeadLock.rightLeft(ITDragonDeadLock.java:37)
        - waiting to lock <0x00000000ecfdf9d0> (a java.lang.Object)
        - locked <0x00000000ecfdf9e0> (a java.lang.Object)
        at com.itdragon.keyword.ITDragonDeadLock$2.run(ITDragonDeadLock.java:54)
        at java.lang.Thread.run(Thread.java:748)

2. jps

jps 进程名

jps -m main方法接受的参数

jps -v jvm参数

3. jinfo

jinfo pid

java进程信息

4. jstack

jstack pid

列出进程里所有的线程

5. jmap

jmap pid

-histo:live 查看堆情况,每个类对应的实例数量和占用的字节数。只要用就会触发Full GC

-dump:format=b,file=heap dump hprof格式的堆内存

-heap 显示堆内存概况

-clstats 类加载器统计信息

6. jcmd

jcmd pid

Thread.print, 打印线程栈信息

GC.class_histogram, 查看系统中类统计信息

GC.heap_dump, 导出堆信息,与jmap -dump功能一样

GC.run_finalization, 触发finalize()

GC.run, 触发Full gc()

VM.uptime, VM启动时间

VM.flags, 获取JVM启动参数

VM.system_properties, 获取系统Properties

VM.command_line, 启动时命令行指定的参数

VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB] 查看堆外内存,需要首先开启-XX参数启用堆外内存追踪,-XX:NativeMemoryTracking=[off | summary | detail],需要注意的是会造成5至10的性能损失

7. jhat

分析jvm heapdump文件,建立一个http服务器方便用户访问

8. javap

分析class文件字节码

javap -verbose Test.class

9. jstat

jstat -gc pid 静态输出统计

jstat -gc pid 1000 每1000毫秒输出统计

sudo -u impala jstat -gcutil $(pgrep -f IMPALAD) 10000

单位:容量KB

  • S0C:第一个幸存区的大小
  • S1C:第二个幸存区的大小
  • S0U:第一个幸存区的使用大小
  • S1U:第二个幸存区的使用大小
  • EC:伊甸园区的大小
  • EU:伊甸园区的使用大小
  • OC:老年代大小
  • OU:老年代使用大小
  • MC:方法区大小
  • MU:方法区使用大小
  • CCSC:压缩类空间大小
  • CCSU:压缩类空间使用大小
  • CCSMN:最小压缩类空间大小
  • CCSMX:最大压缩类空间大小
  • YGC:年轻代垃圾回收次数
  • YGCT:年轻代垃圾回收消耗时间
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间
  • NGCMN:新生代最小容量
  • NGCMX:新生代最大容量
  • NGC:当前新生代容量
  • OGCMN:老年代最小容量
  • OGCMX:老年代最大容量
  • OGC:当前老年代大小
  • OC:当前老年代大小
  • MCMN:最小元数据容量
  • MCMX:最大元数据容量
  • MC:当前元数据空间大小
  • TT:对象在新生代存活的次数
  • MTT:对象在新生代存活的最大次数
  • DSS:期望的幸存区大小
  • LGCC:最近一次GC的原因
  • Compiled:最近编译方法的数量
  • Size:最近编译方法的字节码数量
  • Type:最近编译方法的编译类型。
  • Method:方法名标识。
  • Loaded:加载class的数量
  • Bytes:所占用空间大小
  • Unloaded:未加载数量
  • Bytes:未加载占用空间
  • Time:时间

10. pmap

报告进程的内存映射情况

pmap -x pid | sort -n -k3

相关推荐
暴力袋鼠哥2 小时前
基于springboot与vue的ai多模态数据展示看板
java·spring boot
zhangrelay2 小时前
面向机器人工程的 Linux 发行版:科学选型与深度评测-2026
笔记·学习
半步成诗!2 小时前
【RJ 45连接器】RJ45 网络连接器 3D 模型 3 零件装配体 SolidWorks 源文件 含 STEP/IGS 通用格式
网络·笔记·3d·硬件工程
用户8307196840822 小时前
VS Code Java开发配置与使用经验分享
java·visual studio code
立莹Sir2 小时前
云原生全解析:从概念到实践,Java技术栈如何拥抱云原生时代
java·开发语言·云原生
网络工程小王2 小时前
【Function Calling详解】(学习笔记)
笔记·学习
ohsehun_mek02 小时前
如何为netlify部署网页配置自定义二级域名
笔记
新手小新2 小时前
通信工程师学习笔记3-电信网间互联管理规定和网络安全法
网络·笔记·学习