JVM垃圾回收
什么是JVM垃圾回收?
我们在使用Java进行开发的时候,我们只会去创建对象,从来没有手动回收过对象,如果我们一直创建对象从不回收的话,那么内存是扛不住的,奇怪的是,我们从来没手动回收过,为什么程序不会崩溃呢?这正是因为JVM有一套完整的垃圾回收机制(Grabage Collection
)也称GC
,它帮我们回收无意义的对象实例,从而保证内存得到释放,这个过程是全自动的,无需开发者关心。
垃圾是怎么来的?
其实就是说对象是怎么来的,因为JVM里的垃圾就是对象的实例。
- 通过
new
关键字创建的对象实例。 - 通过反射
class.newInstance()
创建的对象实例。 - 通过
clone()
方法创建的对象实例。 - 等...
这些对象实例的刚被创建的时候可能是有用处的,当用完之后可能是方法退出了,这些对象就成为了无意义的对象,再也不可能被引用到的对象,这些对象就是"垃圾",要被GC回收掉的对象。
垃圾是怎么没得?
被JVM
的GC
机制给回收掉了。
1.谁是垃圾?
在JVM里,如果一个对象没有了引用,就会成为垃圾。例如
java
public void createUser(){
User user = new User();
user.setUserName("张三");
user.print();
}
在方法执行时,User
对象存在引用信息,当方法执行完毕退出时,User
对象再也不可能被任何对象或类引用到,就会成为垃圾。
2.如何发现垃圾?
目前JVM存在两种垃圾搜索算法,用来找到和标识垃圾对象:
- 引用计数法
- 根搜索算法
引用计数法
什么是引用?
在我们使用一个对象的时候,通常都会创建一个对象,然后通过引用变量进行操作。
如:test就是个引用,持有Test()对象的引用,可以代表Test()对象本身。
javaTest 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;
}
以上代码足以说明一个问题,test1
、test2
被清除掉之后,但是内部还存在一个引用,此时两个对象的引用计数器都还是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错误。
复制交换算法
这种算法需要开辟两块一模一样的内存空间用来存放对象,其实就是新生代的S0
和S1
区。
这种算法效率较高,并且对内存的使用率也很高,不会造成内存碎片过多的问题。
最大的问题就是需要创建两块一模一样的空间,想想,如果你的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:-
这里的-
代表关闭
所以呀,用
Docker
或K8s
部署的时候一定要注意镜像的Java
版本呀,避免遇到版本坑。
2.优化建议
- Java的1.8+版本优先使用
G1
收集器,因为在Java9
的时候,Oracle已经把G1
当做默认的收集器了,因此肯定非常稳定好用。 - 系统上线前可以现将
-Xmx
设置的大一点,假如预估2G
的话,你就设置3G
,上线后跟踪监控日志,按照堆内存峰值进行设置,2-3
倍即可。 - 虚拟机栈空间一般
128K
就足够,如果超过256K
则需要优化代码,为什么方法的调用层级这么深。 G1
收集器一般不设置新生代大小,是根据内存空间动态调整的。G1
的SWT
时间尽可能设置的大一点,根据G1
的特性,如果SWT时间大一些,那么G1
就会尽可能的回收更多的垃圾,从而减少GC
的次数。- 建议设置
200-500
ms
- 建议设置
- 设置附加参数:
-Xloggc:/var/java/gc.log
输出GC日志到文件、-XX:+PrintGCTimeStamps
输出GC耗时、-XX:+PrictGCDetails
输出GC详情 - 添加OOM时自动dump内存的参数
-XX:+HeapDumpOnOutOfMemoryError
、dump文件地址-XX:HeapDumpPath=/tmp/memoryDump.hprof
- 文件名可以动态设置,方式多个应用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内存信息。