【👨‍💼面试官:什么是JVM垃圾回收】👨‍💻我:你打开看看🔎(4K高清大图+调优建议)

JVM垃圾回收

什么是JVM垃圾回收?

我们在使用Java进行开发的时候,我们只会去创建对象,从来没有手动回收过对象,如果我们一直创建对象从不回收的话,那么内存是扛不住的,奇怪的是,我们从来没手动回收过,为什么程序不会崩溃呢?这正是因为JVM有一套完整的垃圾回收机制(Grabage Collection)也称GC,它帮我们回收无意义的对象实例,从而保证内存得到释放,这个过程是全自动的,无需开发者关心。

垃圾是怎么来的?

其实就是说对象是怎么来的,因为JVM里的垃圾就是对象的实例。

  • 通过new关键字创建的对象实例。
  • 通过反射class.newInstance()创建的对象实例。
  • 通过clone()方法创建的对象实例。
  • 等...

这些对象实例的刚被创建的时候可能是有用处的,当用完之后可能是方法退出了,这些对象就成为了无意义的对象,再也不可能被引用到的对象,这些对象就是"垃圾",要被GC回收掉的对象。

垃圾是怎么没得?

JVMGC机制给回收掉了。

1.谁是垃圾?

在JVM里,如果一个对象没有了引用,就会成为垃圾。例如

java 复制代码
public void createUser(){
   User user = new User();
   user.setUserName("张三");
   user.print();
}

在方法执行时,User对象存在引用信息,当方法执行完毕退出时,User对象再也不可能被任何对象或类引用到,就会成为垃圾。

2.如何发现垃圾?

目前JVM存在两种垃圾搜索算法,用来找到和标识垃圾对象:

  • 引用计数法
  • 根搜索算法

引用计数法

什么是引用?

在我们使用一个对象的时候,通常都会创建一个对象,然后通过引用变量进行操作。

如:test就是个引用,持有Test()对象的引用,可以代表Test()对象本身。

java 复制代码
Test test = new Test();

每一个对象都存在一个引用计数器,作用就是每当这个对象被引用时引用计数器就+1,最后通过判断引用此时来决定对象是否回收。

每当引用被设置为null时,或者取消了引用,那么引用计数器就-1。

如果引用计数器为0时,则代表这个对象不可能被引用了,因为这个时候不可能再有别的办法得到这个对象的引用了。这个对象也就没有什么作用了。


从上边的描述里看起来没有什么问题,应该不会出现什么意外,那么意外就来了。

引用计数法最致命的问题就是,不能解决对象的循环引用,什么意思呢,看下边的代码就知道了。

java 复制代码
public static void main(String[] args){
   Test test1 = new Test();
   Test test2 = new Test();
   
   // 赋值引用 test2引用计数器+1
   test1.otherRefrence = test2;
   // 赋值引用 test1引用计数器+1
   test2.otherRefrence = test1;
   
   // 清空
   test1 = null;
   test2 = null;
   
   // otherRefrence取不到了,但是引用还在,而且test1、test2已经game over了
}


public static class Test{
   // 保存其它对象引用
   public Test otherRefrence;
}

以上代码足以说明一个问题,test1test2被清除掉之后,但是内部还存在一个引用,此时两个对象的引用计数器都还是1,而且都还拿不到,还不能清除,GC也不能回收,时间一久最终就会造成了内存泄漏,发生OutOfMemoryError


根搜索算法

也叫可达性分析算法。由一个根节点来判断对象是否存在引用。这个根节点被称为GC Root,根节点有几种类型:

  • 栈帧创建的局部变量
  • 静态变量
  • 运行时常量池
  • JNI指针

这些根节点其实都是指向了堆中的某个对象,层层递进,如果其中一层断掉了,那么就证明这个对象已经可以被回收了,因为已经没有引用可以到自己身上了。

如果栈帧局部变量用完了,或者类静态变量用完了,那么指向堆内存对象实例的指针将销毁。如下图所示

此时的User对象实例以及没有引用了,那么User对象就被标记为是可以被回收的对象,并且User内的Job也会被一同标记。因为Job存在于User内部,User没有引用,那么Job肯定不可能被使用到。

如果是引用计数法则无法删除Job对象,虽然User被清除,但是Job的引用次数依然是1,所以不能被清掉。


3.垃圾回收算法

JVM中一共有3种垃圾回收算法:

  • Mark-Sweep 标记清除算法
  • Coping 复制交换算法
  • Mark-Compact 标记压缩算法

标记清除算法

这种算法非常简单粗暴,就是将堆中的对象实例进行标记,然后进行删除。

看似实现简单实际上有很多的问题:

  • 会造成堆空间的内存不连续,中间出现很多空隙。内存过于碎片化。
  • 会造成内存使用率低,如果一个大对象需要3块内存空间存放,但是没有任何一段连续的空间能存放下这个大对象,就会造成OOM错误。

复制交换算法

这种算法需要开辟两块一模一样的内存空间用来存放对象,其实就是新生代的S0S1区。

这种算法效率较高,并且对内存的使用率也很高,不会造成内存碎片过多的问题。

最大的问题就是需要创建两块一模一样的空间,想想,如果你的JVM堆设置的是2G,如果都用这种算法,那么JVM就要申请4G空间才能正常运行,问题就是太浪费内存资源,不适合大内存区域使用。

新生代的Minor GC就在使用。

标记压缩算法

这种算法和复制交换算法一样,都不会产生内存碎片,并且对内存空间的占用也要比复制交换节省一半的空间。

缺点就是效率较慢,需要对对象进行排序整理后才能完成垃圾回收。目前被用于Full GC


4.垃圾收集器

在JVM(<=1.8)里共存在6中内置的垃圾收集器:

  • Serial GC(串行垃圾收集器) :它是单线程执行垃圾回收的,因此效率较低,如果是用在内存小的JVM里,比如几十mb,那用它是个不错的选择,否则会造成应用程序STW时间过长,影响使用。
  • SerialOld GC(串行垃圾收集器) :它和Serial GC的区别是回收的区域不同,主要用于老年代的回收,并且采用了复制整理算法,效率更低。
  • Parallel Scavenge GC(并行垃圾收集器) :它是并行多线程的垃圾收集器,他可以多个线程同时执行垃圾回收,SWT时间长,GC次数少,多线程GC,效率很高。
  • ParNew GC(并行垃圾收集器) :它也是并行收集垃圾的,它和Parallel Scavenge GC最主要的区别就是SWT时间短,GC次数多,适合和用户交互的应用程序。
  • Parallel Old(并行垃圾收集器) :它是和ParNew GC的区别是用作于老年代的垃圾收集,并且采用复制整理算法,效率不如ParNew GC,但是效率依然比SerialOld GC强N倍
  • Concurrent Mark Sweep GC(CMS垃圾收集器) :它是与用户线程并行执行的垃圾收集器,超短的SWT,适合超大内存使用,缺点是会产生浮动垃圾标记失败,会降低系统CPU效率,并且会产生大量内存碎片,因为主要使用标记清除算法。

这几种垃圾收集器都各有千秋,真实场景要结合应用程序实际情况来决定如何搭配使用。

垃圾收集器的组合,以及设置参数

新生代垃圾收集器 老年代垃圾收集器 JVM参数
Serial GC Serial Old GC -XX:+UseSerialGC
ParNew GC CMS GC -XX:+UseParNewGC
Parallel Scavenge GC Parallel Old GC -XX:+UseParallelGC
G1 Young Generation G1 Old Generation -XX:+UseG1GC
ZGC ZGC -XX:+UseZGC
Shenandoah Shenandoah -XX:+UseShenandoahGC
Epsilon Epsilon -XX:+UseEpsilonGC
Serial GC CMS GC -XX:+UseSerialGC -XX:+UseConcMarkSweepGC
ParNew GC Serial Old GC -XX:+UseParNewGC -XX:+UseSerialOldGC
Parallel Scavenge GC CMS GC -XX:+UseParallelGC -XX:+UseConcMarkSweepGC

JVM调优建议

大多数情况下JVM调优主要考虑3个方面:

  • 最大堆和最小堆大小
  • GC垃圾收集器选择
  • 新生代大小

调优需要依靠全面的监控数据,没有监控数据谈何调优!


1.JVM参数说明

  • -version 只有一个-的参数,是标准选项,所有JVM都支持

  • -Xms10 像这种-X开头的参数是非标准选项,部分版本JVM才会识别,但是主流JVM都是支持的

  • -XX:+PrintGCDetails 这种-XX+开头的,是不稳定参数,不同的JVM可能会有差异,随时可能会被移除。

    • -XX:+ 这里的+代表开启
    • -XX:- 这里的-代表关闭

所以呀,用DockerK8s部署的时候一定要注意镜像的Java版本呀,避免遇到版本坑。


2.优化建议

  1. Java的1.8+版本优先使用G1收集器,因为在Java9的时候,Oracle已经把G1当做默认的收集器了,因此肯定非常稳定好用。
  2. 系统上线前可以现将-Xmx设置的大一点,假如预估2G的话,你就设置3G,上线后跟踪监控日志,按照堆内存峰值进行设置,2-3倍即可。
  3. 虚拟机栈空间一般128K就足够,如果超过256K则需要优化代码,为什么方法的调用层级这么深。
  4. G1收集器一般不设置新生代大小,是根据内存空间动态调整的。
  5. G1SWT时间尽可能设置的大一点,根据G1的特性,如果SWT时间大一些,那么G1就会尽可能的回收更多的垃圾,从而减少GC的次数。
    1. 建议设置200-500ms
  6. 设置附加参数:-Xloggc:/var/java/gc.log 输出GC日志到文件、-XX:+PrintGCTimeStamps 输出GC耗时、-XX:+PrictGCDetails 输出GC详情
  7. 添加OOM时自动dump内存的参数-XX:+HeapDumpOnOutOfMemoryError、dump文件地址-XX:HeapDumpPath=/tmp/memoryDump.hprof
    1. 文件名可以动态设置,方式多个应用OOM时文件被替换的风险。

3.JVM监控命令

jps

使用jps命令可以快速查看系统上的所有Java进程以及PID

shell 复制代码
jps

# 输出
12457 jar
12333 Jps
12332 Bootstrap

jinfo

使用jinfo快速查看Java进程的参数,以下是我使用IDEA启动的Java程序

tex 复制代码
Attaching to process ID 20088, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.381-b09
Java System Properties:

jboss.modules.system.pkgs = com.intellij.rt
java.vendor = Oracle Corporation
sun.java.launcher = SUN_STANDARD
catalina.base = C:\Users\28678\AppData\Local\Temp\tomcat.8080.3815623927401760070
sun.management.compiler = HotSpot 64-Bit Tiered Compilers
catalina.useNaming = false
spring.output.ansi.enabled = always
os.name = Windows 11
sun.boot.class.path = ...
sun.desktop = windows
spring.application.admin.enabled = true
com.sun.management.jmxremote =
java.vm.specification.vendor = Oracle Corporation
java.runtime.version = 1.8.0_381-b09
spring.liveBeansView.mbeanDomain =
user.name = 28678
spring.jmx.enabled = true
user.language = zh
sun.boot.library.path = E:\Env\jdk8-orcale\jre\bin
CONSOLE_LOG_CHARSET = UTF-8
PID = 20088
java.version = 1.8.0_381
user.timezone = Asia/Shanghai
sun.arch.data.model = 64
java.endorsed.dirs = E:\Env\jdk8-orcale\jre\lib\endorsed
java.rmi.server.randomIDs = true
sun.cpu.isalist = amd64
sun.jnu.encoding = GBK
file.encoding.pkg = sun.io
file.separator = \
java.specification.name = Java Platform API Specification
java.class.version = 52.0
user.country = CN
java.home = E:\Env\jdk8-orcale\jre
java.vm.info = mixed mode
os.version = 10.0
path.separator = ;
java.vm.version = 25.381-b09
user.variant =
java.awt.printerjob = sun.awt.windows.WPrinterJob
sun.io.unicode.encoding = UnicodeLittle
management.endpoints.jmx.exposure.include = *
java.specification.maintenance.version = 5
awt.toolkit = sun.awt.windows.WToolkit
user.script =
user.home = C:\Users\28678
java.specification.vendor = Oracle Corporation
java.library.path = ...
java.vendor.url = http://java.oracle.com/
spring.beaninfo.ignore = true
java.vm.vendor = Oracle Corporation
java.runtime.name = Java(TM) SE Runtime Environment
sun.java.command = cn.yufire.yinta.translation.YintaTranslationApplication
java.class.path = ..
java.vm.specification.name = Java Virtual Machine Specification
java.vm.specification.version = 1.8
catalina.home = C:\Users\28678\AppData\Local\Temp\tomcat.8080.3815623927401760070
sun.cpu.endian = little
sun.os.patch.level =
java.awt.headless = true
java.io.tmpdir = C:\Users\28678\AppData\Local\Temp\
FILE_LOG_CHARSET = UTF-8
java.vendor.url.bug = http://bugreport.sun.com/bugreport/
os.arch = amd64
java.awt.graphicsenv = sun.awt.Win32GraphicsEnvironment
java.ext.dirs = E:\Env\jdk8-orcale\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
user.dir = D:\WorkSpace\Project\My\yinta-translation
line.separator =

java.vm.name = Java HotSpot(TM) 64-Bit Server VM
file.encoding = UTF-8
java.specification.version = 1.8
intellij.debug.agent = true

VM Flags:
Non-default VM flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:CICompilerCount=4 -XX:InitialHeapSize=534773760 -XX:+ManagementServer -XX:MaxHeapSize=8539602944 -XX:MaxNewSize=2846359552 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=178257920 -XX:OldSize=356515840 -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
Command line:  -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:49478,suspend=y,server=n -XX:TieredStopAtLevel=1 -Xverify:none -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -javaagent:C:\Users\28678\AppData\Local\JetBrains\IntelliJIdea2023.2\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8

可以看到Java进程的所有信息。


jstat

使用jstat可以看到JVM的GC情况,格式:jstat 查看方式 pid 刷新间隔 输出次数

  • -gcutil 可以查看对应空间的百分百信息
  • -gc 可以查看详细的占用空间信息
shell 复制代码
jstat -gcutil 20088 1000 10

  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00   0.00  86.99   6.15  95.38  91.63      1    0.011     1    0.026    0.038
  0.00   0.00  86.99   6.15  95.38  91.63      1    0.011     1    0.026    0.038
  0.00   0.00  86.99   6.15  95.38  91.63      1    0.011     1    0.026    0.038
  0.00   0.00  86.99   6.15  95.38  91.63      1    0.011     1    0.026    0.038
  0.00   0.00  86.99   6.15  95.38  91.63      1    0.011     1    0.026    0.038
  0.00   0.00  86.99   6.15  95.38  91.63      1    0.011     1    0.026    0.038
  0.00   0.00  86.99   6.15  95.38  91.63      1    0.011     1    0.026    0.038
  0.00   0.00  86.99   6.15  95.38  91.63      1    0.011     1    0.026    0.038
  0.00   0.00  86.99   6.15  95.38  91.63      1    0.011     1    0.026    0.038
  0.00   0.00  86.99   6.15  95.38  91.63      1    0.011     1    0.026    0.038

分别可以看到各个JVM空间的内存使用情况。以及GC的信息

参数名 说明
S0 幸存区0的使用百分比
S1 幸存区1的使用百分比
E 伊甸园区的使用百分比
O 老年代的使用百分比
M 元空间的使用百分比
CCS 压缩类空间的使用百分比
YGC 新生代GC次数
YGCT 新生代GC耗时
FGC Full GC次数
FGCT Full GC耗时
GCT 总GC耗时

jstack

用于查看各个线程调用的堆栈情况。可以查看每个线程调用经过了哪些方法。

生产环境一般不会使用,生产线程通常会很多,使用这个命令分析会很头疼因为会很多。


jmap

用于dump内存信息。


相关推荐
不会Hello World的小苗10 分钟前
Java——列表(List)
java·python·list
二十七剑1 小时前
jvm中各个参数的理解
java·jvm
东阳马生架构2 小时前
JUC并发—9.并发安全集合四
java·juc并发·并发安全的集合
计算机小白一个3 小时前
蓝桥杯 Java B 组之岛屿数量、二叉树路径和(区分DFS与回溯)
java·数据结构·算法·蓝桥杯
孤雪心殇3 小时前
简单易懂,解析Go语言中的Map
开发语言·数据结构·后端·golang·go
菠菠萝宝3 小时前
【Java八股文】10-数据结构与算法面试篇
java·开发语言·面试·红黑树·跳表·排序·lru
不会Hello World的小苗3 小时前
Java——链表(LinkedList)
java·开发语言·链表
Allen Bright4 小时前
【Java基础-46.3】Java泛型通配符详解:解锁类型安全的灵活编程
java·开发语言
柃歌4 小时前
【UCB CS 61B SP24】Lecture 7 - Lists 4: Arrays and Lists学习笔记
java·数据结构·笔记·学习·算法
柃歌4 小时前
【UCB CS 61B SP24】Lecture 4 - Lists 2: SLLists学习笔记
java·数据结构·笔记·学习·算法