【👨‍💼面试官:什么是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内存信息。


相关推荐
魔道不误砍柴功12 分钟前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_23412 分钟前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨15 分钟前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
测开小菜鸟2 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity3 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天3 小时前
java的threadlocal为何内存泄漏
java