本文所有内容基于JDK8
简介
JVM,Java Virtual Machine。
优点:
- 一次编写,到处运行。.java的源码文件编译成.class的字节码文件,这些字节码文件是给JVM看的,不同的操作系统对应不同的JVM,JVM将统一的字节码文件解释给不同的操作系统执行相同的指令。
- 自动内存管理,垃圾回收机制
- 数组下标越界检查
- 多态
目前常用的JVM版本是HotSpot,OpenJDK edition。
内存结构
- 程序计数器
Program Counter Register程序计数器,使用的是CPU的寄存器,用来记住下一条JVM指令的执行地址。
- 线程私有
- 不存在内存溢出问题
- 虚拟机栈
Java Virtual Machine Stacks Java虚拟机栈
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
- 每个线程只能由一个活动栈帧,对应着当前正在执行的那个方法
问题:
1.垃圾回收是否涉及栈内存?
不涉及,当方法调用开始,为每个方法创造栈帧,方法调用结束,栈帧内存收回。
2.栈内存溢出的几种可能性,Exception in thread "main" java.lang.StackOverflowError - 栈帧数量过多:方法的递归调用,没有设置合适的递归结束的条件。
- 栈帧大小过大:方法内使用了占用内存特别多的局部变量。比如创建了100个byte[1024]等。
3.方法内的局部变量线程安全吗? - 如果方法内局部变量没有逃离方法的作用范围,就是线程安全的。
- 如果是局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全。
- 本地方法栈
当虚拟机需要调用本地方法时,为本地方法开辟的内存空间。 - 堆
通过new关键字,创建对象都会使用堆内存。
- 线程共享的,有线程安全问题
- 有垃圾回收机制
- 方法区
It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods used in class and instance initialization and interface initialization.
MethodArea是JVM定的规范,但是具体的实现不同的JVM版本不同。
Jdk1.8中,方法区使用MetaSpace的方式实现的,放在本地内存中。字符串表放在heap中,为了方便进行垃圾回收。
设置方法区大小:-XX:MaxMetaspaceSize=8m
元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
使用反编译技术,可以看到在元空间中存放的类结构信息,即二进制字节码信息。
$ javap -v App.class
package com.wangxiamei;
public class App {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
------------
Classfile /Users/chunfengshili/Downloads/jdk_8/target/classes/com/wangxiamei/App.class
Last modified 2023年1月27日; size 543 bytes
MD5 checksum dfe0ef67933354be530fb36d7579e843
Compiled from "App.java"
public class com.wangxiamei.App
minor version: 0
major version: 49
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // com/wangxiamei/App
super_class: #6 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // Hello World!
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/wangxiamei/App
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/wangxiamei/App;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 App.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 Hello World!
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/wangxiamei/App
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public com.wangxiamei.App();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/wangxiamei/App;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 9: 0
line 10: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "App.java"
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
运行时常量池,常量池就是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实的地址。
StringTable
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象(享元模式,其他基本类型也有此类优化)
- 字符串变量拼接的原理是StringBuilder
- 字符串常量拼接的原理是编译器优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池,将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会吧串池中的对象返回。
- 由于StringTable本身是使用hashMap实现的,可以通过-XX:StringTableSize=1600来设置bucket大小。
直接内存
Direct Memory
- 常见于NIO操作时,用于数据缓冲区
- 分配成本较高,但读写性能高
- 不受JVM内存回收管理。
分配和回收的原理 - ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么会有由ReferenceHandler线程来通过cleaner的clean方法调用freeMemory来释放直接内存。
垃圾回收
如何判断对象可以回收
- 引用计数法
即计算每个对象上的引用个数,只有等于0的时候才需要回收。但是这个算法有个循环引用的问题,两个对象彼此引用,就会造成都无法回收,最终造成内存泄漏。 - 可达性分析
●Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
●扫描堆中的对象,看是否能够沿着GC ROOT对象为起点里的引用链找到该对象,找不到,表示可以回收。
●哪些是GC ROOT对象
○System Class
■java.lang.Class
○JNI Global
■java.lang.String
■java.lang.OutOfMemoryError
○Thread
■java.lang.Thread
- 四种引用类型
●强引用
只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
●软引用(SoftReference)
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用
对象,可以配合引用队列来释放软引用自身
●弱引用(WeakReference)
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象可以配合引用队列来释放弱引用自身
●虚引用(PhantomReference)
必须配合引用队列使用,主要配合 ByteBuffffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
●终结器引用(FinalReference)
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 fifinalize 方法,第二次 GC 时才能回收被引用对象分代垃圾回收
垃圾回收算法
- 标记清除,Mark Sweep
a速度较快
b会造成内存碎片 - 标记整理,Mark Compact
a速度慢
b没有内存碎片 - 复制,Copy
a不会有内存碎片
b需要占用双倍内存空间
分代垃圾回收
●对象首先分配在伊甸园
●新生代空间不足时,触发Minor GC,伊甸园和from存活的对象复制到to中,存活的对象年龄加1并且交换from和to。使用的是复制算法
●Minor GC会引发stop the world (STW),暂停其他用户线程,等垃圾回收结束,用户线程才恢复运行
●当对象寿命超过阈值时,会晋升至老年代,最大寿命15(4bit)
●当老年代空间不足,会先尝试触发Minor GC,如果之后空间不足,那么触发Full GC,STW的时间更长。Full GC 一般是标记整理算法。
●分代回收算法是一个理念概念,具体的垃圾回收器在实现的细节上可能有所不同。
垃圾回收器
- 串行
a单线程
b堆内存小,适合于个人电脑 - 吞吐量优先
a多线程
b堆内存较大,多核CPU
c让单位时间内,STW的时间最短,比如0.2+0.2=0.4。垃圾回收时间占比最低,适用于科学计算型应用 - 响应时间优先
a多线程
b堆内存较大,多核CPU
c尽可能让单次STW的时间最短,0.1+0.1+0.1+0.1+0.1=0.5. 适用于Web应用需要响应优先。
选择一个收集器
JavaSE 8 的Oracle的官方手册有这样一段话
- 如果应用产生的数据较少,最大到100MB,或者应用运行在单核CPU上对停顿时间没有要求,那么使用串行。-XX:+UseSerialGC.
- 如果应用的性能要求较高,对停顿时间没有要求,能接受1s左右甚至更长的停顿,那么可以使用并行收集器。-XX:+UseParallelGC
- 如果应用对响应时间要求比较高,并且停顿时间不能到1s,那么可以使用并发收集器,-XX:+UseConcMarkSweepGC or -XX:+UseG1GC.
- because performance is dependent on the size of the heap, the amount of live data maintained by the application, and the number and speed of available processors。停顿时间和性能更依赖于堆的大小,存活对象的数量,以及可用CPU的数量和速度。
- If the recommended collector does not achieve the desired performance, first attempt to adjust the heap and generation sizes to meet the desired goals. If performance is still inadequate, then try a different collector: use the concurrent collector to reduce pause times and use the parallel collector to increase overall throughput on multiprocessor hardware.如果需要提高垃圾收集器的性能,首先应该考虑的是调整堆大小和各代的大小。调整之后,仍然不足,在多核CPU上才需要考虑使用并行收集器提高吞吐量,使用并发收集器减少暂停时间。
串行
-XX:+UseSerialGC使用该参数打开- Serial+Serialold,新生代和老年代都是串行
吞吐量优先
相关JVM参数:
-XX:+UseParallelGC
-XX:+UseParallelOldGC
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n
响应时间优先
相关参数:
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark
其实就是使用的CMS垃圾回收器,响应时间优先应该使用G1垃圾回收器,视频中这里应该是比较旧信息导致的。
CMS垃圾回收器 Concurrent Mark Sweep
The CMS collector is deprecated. Strongly consider using the Garbage-First collector instead.
- 新生代使用复制算法,并行线程执行
- 老年代使用标记清除算法,所以有比较小的停顿时间。但是如果老年代占堆内存比较到达一定值,就会触发并行标记整理算法,这一次的停顿时间会比较长。
G1
Garbage First,2004 论文发布, 2009 JDK 6u14 体验 ,2012 JDK 7u4 官方支持,2017 JDK 9 默认。那么说明从JDK7 开始就可以使用G1
特点:
- 同时注重吞吐量(ThroughPut)和低延迟(Low latency),默认的暂停目标是200ms
- 超大堆内存,会将堆划分为多个大小相等的region,可以通过单次停顿处理一定数量的region来达到目标的停顿时间,其他的region下次再整理。
- 整体上是标记+整理算法,两个区域之间是复制算法。
参数:
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
垃圾回收
堆内存分Region,每个Region可以属于新生代,存活区,老年代。每个Region的大小可以不同。
分为三个阶段。
Young Collection ->Young Collection+CM->Mixed Collection->Young Collection...
- Young Collection 将属于新生代的Region内存,标记复制到新的存活区region。
- Young Collection+CM
-
- 在Young Collection是会进行GC Root的初始标记
- 老年代占用堆空间达到阈值时,进行并发标记,并不会STW,为了减少STW时间,先进行一次标记,由下面的JVM参数决定。
-XX:InitiatingHeapOccupancyPercent=percent(默认45%)
- Mixed Collection
-
- 会对E S O进行全面垃圾回收
- 最终标记会STW
- 拷贝存活也会STW,
-XX:MaxGCPauseMillis=ms
其他G1的优化
- 字符串去重
-
- 节省大量内存,略微增加新生代CPU时间
- -XX:+UseStringDeduplication
- 将所有新分配的字符串放入一个队列,当新生代回收时,G1并发检查是否有字符串重复。如果他们值一样,让他们引用一个char[]
- 与String.intern()不一样,这个关注的是字符串对象,而字符串去重关注的是char[]
- 并发标记类卸载
-
- 所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用时,则卸载他加载的所有类。-XX:+ClassUnloadingWithConcurrentMark
- 回收巨型对象
-
- 一个对象大于Region的一半时,称为巨型对象,G1不会对巨型对象进行拷贝,回收时优先考虑,特殊引用关注这类对象。
垃圾回收调优
- 查看Full GC前后的内存占用,考虑一下几个问题。
- 数据是不是太多?
resultSet = statement.executeQuery("select * from 大表 limit n")
- 数据表示是否太臃肿?
对象图 ,大对象
对象大小 16 Integer 24 int 4
- 是否存在内存泄漏?
static Map map =
第三方缓存实现
新生代调优
新生代的特点:
- 所有new操作的内存分配非常廉价
- 死亡对象的回收代价为零
- 大部分对象用过即死
- Minor GC的时间远远低于Full GC
-Xmn
Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery). GC is performed in this region more often than in other regions. If the size for the young generation is too small, then a lot of minor garbage collections are performed. If the size is too large, then only full garbage collections are performed, which can take a long time to complete. Oracle recommends that you keep the size for the young generation greater than 25% and less than 50% of the overall heap size.
oracle建议新生代的大小设置为整个堆内存的25%-50%。
- 新生代能容纳所有【并发量 * (请求-响应)】的数据
- 幸存区大到能保留【当前活跃对象+需要晋升对象】
- 晋升阈值配置得当,让长时间存活对象尽快晋升
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistribution
老年代调优
- 先尝试不做调优,如果没有Full GC那么已经很优秀了,否则先尝试调优新生代。
- 观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4-1/3。
-
- -XX:CMSInitiatingOccupancyFraction=percent
案例
- Full GC 和Minor GC 频繁
-
- 新生代内存太小。导致年龄很小的对象就晋升到老年代,进而导致老年代内存空间也紧张,进而频繁触发Full GC。
- 请求高峰期发生Full GC,单次停顿时间特别长(CMS)。
-
- 重新标记阶段,还要标记新生代的对象。
- 解决方法,-XX:+CMSScavengeBeforeRemark,Full GC之前先进性一次Minor GC
- 老年代充裕的情况,发生Full GC。
-
- 1.7以前的永久代中类信息的过多导致Full GC。
- 增加永久代大小。
Oracle在JavaSE11关于G1优化的建议
一般性建议
- 尽量使用G1的默认配置,如果有需要可以设置堆大小和停顿时间。因为G1的目标既不是高吞吐量也不是最低的延迟,而是在高吞吐量下提供相对小的、一致的延迟时间。
Avoid limiting the young generation size to particular values by using options like -Xmn, -XX:NewRatio and others because the young generation size is the main means for G1 to allow it to meet the pause-time. Setting the young generation size to a single value overrides and practically disables pause-time control.
- 尽量避免设置新生代的大小或者新生代的比例,因为G1主要通过新生代大小来控制停顿时间的。如果设置了新生代大小,会覆盖或者使停顿时间的控制失效。
从其他收集器转到G1
start by removing all options that affect garbage collection, and only set the pause-time goal and overall heap size by using -Xmx and optionally -Xms.
删除所有会影响垃圾收集的配置,除了暂停时间和堆大小设置
提升G1性能
使用-Xlog:gc*=debug,这个选项提供了综合性的收集日志,包括垃圾收集中和之后的详情。也包括了各阶段垃圾收集的类型和花费时间。
对于延迟的优化
The VM allocating or giving back memory from the operating system memory may cause unnecessary delays. Avoid the delays by setting minimum and maximum heap sizes to the same value using the options -Xms and -Xmx, and pre-touching all memory using -XX:+AlwaysPreTouch to move this work to the VM startup phase.
新生代优化
正常的新生代的垃圾收集,一般来说,任何新生代的收集所花费的时间大致与年轻一代的大小成正比,或者更具体地说,与需要复制的收集器中的活动对象的数量成正比。如果“清空收集集”阶段(特别是“对象复制”子阶段)花费的时间太长,请减少-XX:G1NewSizePercent。这减少了新生代的最小规模,允许可能更短的暂停。
如果应用程序性能,特别是集合中幸存的对象数量突然发生变化,新生代的规模调整可能会出现另一个问题。这可能会导致垃圾收集暂停时间的峰值。使用-XX:G1MaxNewSizePercent可以减少新生代的最大规模。这限制了新生代的最大大小,从而限制了暂停期间需要处理的对象的数量。
运行期优化
JVM 将执行状态分成了 5 个层次:
0 层,解释执行(Interpreter)
1 层,使用 C1 即时编译器编译执行(不带 profifiling)
2 层,使用 C1 即时编译器编译执行(带基本的 profifiling)
3 层,使用 C1 即时编译器编译执行(带完全的 profifiling)
4 层,使用 C2 即时编译器编译执行
profifiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等
即时编译器(JIT)与解释器的区别
- 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释;
- JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译。
- 解释器是将字节码解释为针对所有平台都通用的机器码
- JIT 会根据平台类型,生成平台特定的机器码,对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。
- 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由来),优化之。
刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析。
总之JVM在运行阶段也会根据一些情况,进行优化,以提高性能。
常见问题
接口响应慢
-
频繁GC
- 堆内存设置太小。
案例:由于项目设置jvm的参数时,Xms和Xmx设置的太小,结果导致项目运行起来频繁GC。
解决方法:增加堆内存大小。
- 线程数用完
案例:有一个业务中调用远程接口返回数据,接口调用频繁,并且接口数据要2s才能返回,结果造成Tomcat线程数用完。没有可用的线程来处理其他的业务。
解决方法:这个业务使用单独的线程池来处理。与其他业务线程区分开。
CPU100%
- top,用top定位哪个进程对cpu的占用过高
- ps H -eo pid,tid,%cpu | grep 进程id,用ps命令进一步定位是哪个线程引起的cpu占用过高
- jstack 进程id,可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号
举例
- top
$ jstack 2335
"Service Thread" #10 daemon prio=9 os_prio=31 tid=0x00007f934c812000 nid=0x4403 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread3" #9 daemon prio=9 os_prio=31 tid=0x00007f9339877800 nid=0x4203 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread2" #8 daemon prio=9 os_prio=31 tid=0x00007f934c02f000 nid=0x4003 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" #7 daemon prio=9 os_prio=31 tid=0x00007f934a02e000 nid=0x3f03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #6 daemon prio=9 os_prio=31 tid=0x00007f934a02d800 nid=0x3e03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Monitor Ctrl-Break" #5 daemon prio=5 os_prio=31 tid=0x00007f9349819800 nid=0x3d03 runnable [0x000070000e60d000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x000000071584e548> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x000000071584e548> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:55)
"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x00007f9339834800 nid=0x4903 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x00007f9349815800 nid=0x3103 in Object.wait() [0x000070000e301000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000715588ed0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x0000000715588ed0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)
"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x00007f934b80b000 nid=0x3003 in Object.wait() [0x000070000e1fe000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000715586bf8> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x0000000715586bf8> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"main" #1 prio=5 os_prio=31 tid=0x00007f934c00c000 nid=0x1003 runnable [0x000070000d7e0000]
java.lang.Thread.State: RUNNABLE
at com.wangxiamei.App.method1(App.java:16)
at com.wangxiamei.App.main(App.java:11)
"VM Thread" os_prio=31 tid=0x00007f934a016000 nid=0x2e03 runnable
"GC task thread#0 (ParallelGC)" os_prio=31 tid=0x00007f934980d000 nid=0x2007 runnable
"GC task thread#1 (ParallelGC)" os_prio=31 tid=0x00007f934980d800 nid=0x1e03 runnable
"GC task thread#2 (ParallelGC)" os_prio=31 tid=0x00007f934c808800 nid=0x1c03 runnable
"GC task thread#3 (ParallelGC)" os_prio=31 tid=0x00007f934980e800 nid=0x2a03 runnable
栈内存溢出
- 栈帧数量过多:方法的递归调用,没有设置合适的递归结束的条件。
- 栈帧大小过大:方法内使用了占用内存特别多的局部变量。比如创建了100个byte[1024]等。
堆内存溢出
- 一个线程内的heap out of memory,并不会影响其他线程。
- jps,查看当前有哪些java进程,jmap -heap pid,进而查看堆内存的使用情况
- jconsole,图像化用户界面,可查看堆内存的使用情况。
- Eclipse Memory Analyzer,Java heap analyzer分析工具。
-
- jps 查看java 进程id
- jmap -dump:format=b,live,file=1.bin pid
- mat 软件打开1.bin这个文件,查看
元空间内存溢出
由于Spring、Mybatis等框架大量使用了动态代理技术,所以会在运行时增加很多class,并加载到jvm中,如果使用不当会造成元空间内存溢出。
案例: SkyWalking组件在收集程序运行数据时,增加了很多agent,最后造成元空间内存溢出。
GC频繁
GC后,堆内存使用依然很高
dump 堆内存的使用情况,查看那个类和对象占用内存最多。
一般是集合、String这类对象,集合size的最大值是Integer的最大值。所以在使用这类对象的时候要注意:集合内对象的个数要可控。String内容的大小要可控。
案例: 使用List来保存请求一个未知公共网站数据的结果,每个结果的大小为2M,请求几百次后数据结果统一处理,造成堆内存溢出。
解决方法:1 获取网站结果后,先解析,只保存有用的关键数据放入集合中2计算完成后,主动释放List的引用,尽早回收。
JVM参数配置
docs.oracle.com/javase/8/do… 官方参数配置手册
常见JVM参数
-Xms4G //堆初始大小
-Xmx8G //堆最大大小
-Xmn //新生代大小
-Xss //线程栈大小,The default value depends on virtual memory.
-XX:+UseG1GC //使用G1垃圾收集器
-XX:G1HeapRegionSize=size //Region大小
-XX:MaxGCPauseMillis=time //最大停顿时间,默认是200ms
-XX:InitialSurvivorRatio=ratio //幸存区初始比例,动态调整
-XX:SurvivorRatio=ratio //幸存区比例
-XX:MaxTenuringThreshold=threshold //晋升阈值
-XX:+PrintTenuringDistribution //晋升详情
-XX:+PrintGCDetails -verbose:gc //GC 详情
-XX:+ScavengeBeforeFullGC //Full GC前先进行MinorGC
-XX:+UseStringDeduplication //字符串去重
-XX:+ClassUnloadingWithConcurrentMark //并发标记类卸载
-XX:InitiatingHeapOccupancyPercent=45%
-XX:+DisableExplicitGC //禁用代码中显示的调用Full GC
JDK8-推荐JVM配置
-Xms10M
-Xmx10M
-XX:+AlwaysPreTouch
-XX:+UseG1GC
-XX:+PrintGCDetails
-verbose:gc
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintHeapAtGC
-XX:+PrintTenuringDistribution
-Xloggc:/logs/myGC.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/logs/
-XX:+ScavengeBeforeFullGC
JDK11 因为从JDK9以后默认的垃圾收集器就是G1
-Xms2G
-Xmx2G
-Xlog:gc*=info,gc+heap=debug,gc+age=trace,safepoint:/logs/gc_%t.log:time,level,tags:filecount=5,filesize=20m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/logs/
附录
《深入理解Java虚拟机》
docs.oracle.com/javase/spec… JVM官方规范文档
docs.oracle.com/en/java/jav… Oracle官方JavaSE指导手册