二、类加载机制与JDK调优命令
上节课程回顾:
执行引擎执行每个class文件有两种方式:
- JIT编译器:即时编译
- 字节码解释器:
执行引擎可以通过这两种方式在运行的时候执行一些class文件。还有一些类在一开始的时候就把一些类编译好。
java是一门既可以编译、又可以解释的语言。
java运行时编译源码(.java)成字节码文件,由JRE生成。JRE由java虚拟机实现。JVM分析字节码后,解释并执行。
类的生命周期
1.加载
将.class文件从磁盘读到内存
通过类的全限定名(写出类的完整路径)。
2.连接
2.1 验证
验证字节码文件的正确性
2.2 准备
给类的静态变量分配内存,并赋予默认值
2.3 解析
类装载器装入类所引用的其它所有类
3.初始化
为类的静态变量赋予正确的初始值,上述的准备阶段为静态变量赋予的是虚拟机默认的初始值,此处赋予的才是程序编写者为变量分配的真正的初始值,执行静态代码块
4.使用
5.卸载
类加载器的种类
启动类加载器(Bootstrap ClassLoader)
负责加载JRE的核心类库,如JRE目标下的rt.jar,charsets.jar等
扩展类加载器(Extension ClassLoader)
负责加载JRE扩展目录ext中jar类包
系统类加载器(Application ClassLoader)
负责加载ClassPath路径下的类包
用户自定义加载器(User ClassLoader)
负责加载用户自定义路径下的类包
类加载机制
全盘负责委托机制
当一个ClassLoader加载一个类的时候,除非显示的使用另一个ClassLoader,该类所依赖和引用的类也由这个ClassLoader载入
双亲委派机制
指先委托父类加载器寻找目标类,在找不到的情况下载自己的路径中查找并载入目标类
双亲委派模式的优势
- 沙箱安全机制:比如自己写的String.class类不会被加载,这样可以防止核心库被随意篡改
- 避免类的重复加载:当父ClassLoader已经加载了该类的时候,就不需要子ClassLoader再加载一次
这里我们自己看看类加载:
import com.sun.crypto.provider.DESKeyFactory;
public class ClassLoaderDemo {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(DESKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(ClassLoaderDemo.class.getClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());
}
}
发现String类的加载器没有打印出来:因为BootStrap类加载器是使用C写的,所以不直接体现出来,打印出来就是null。即Null表示的是BootStrap类加载器。
这里我们自己写一个String类:
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("testString");
}
}
这时候根据类加载的双亲委派机制,会去java的类库中找String,而不是直接加载我们定义的String,然后在java的String类中没有找到main方法,所以报错了。
这里的父加载器并不是说加载器之间是父类关系!!!所有的加载器都实现了的是ClassLoader。
如何打破双亲委派机制: 3种。
-
jdk1.2之前的动态加载;
-
STR:Service Provider Interface:java提供的一种规范,具体的实现有厂商自己去实现;
mysql的这种驱动的加载方式就是打破了双亲委派机制。
DriverManager 是在rt.jar包下,而现在是“DriverManager.getConnection(url)”是调用当前类加载器去加载,此时不知道是哪个厂商的驱动(可以是mysql、sql Server等)
为什么要打破双亲委派机制
查看类加载信息的JVM参数:
-verbose class
可以看出来,JVM类加载是按需加载的。
JDK性能调优监控工具
Jinfo
查看正在运行的Java程序的扩展参数
查看JVM的参数
使用“jinfo -flags 对应java进程运行数"即可。
查看java系统属性
等同于System.getProperties()
Jstat
jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令格式:
jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]
Jmap
可以用来查看内存信息
堆的对象统计
jmap -histo 7824 > xxx.txt
如图:
- Num:序号
- Instances:实例数量
- Bytes:占用空间大小
- Class Name:类名
这样打印出来的是全部的对象。可以看活的对象:
jmap -histo:live 运行数 > 输出文件.txt
堆信息
堆内存dump
jmap -dump:format=b,file=temp.hprof
也可以在设置内存溢出的时候自动导出dump文件(项目内存很大的时候,可能会导不出来)
1.-XX:+HeapDumpOnOutOfMemoryError
2.-XX:HeapDumpPath=输出路径
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:\oomdump.dump
package com.luban;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class OutOfMemortTest {
/**
* 设置JVM参数
* -Xms10m
* -Xmx10m
* -XX:+PrintGCDetails
* -XX:+HeapDumpOnOutOfMemoryError
* -XX:HeapDumpPath=./ (路径)
*/
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
long i = 0;
while (true) {
list.add(new User(i ++, UUID.randomUUID().toString()));
}
}
}
可以使用jvisualvm命令工具导入文件分析
我们可以将生成的dump文件装入到vm中,然后重点查看类这一项。
这里发现char数组实例很多,原因是String底层是使用final char[]来存储的。
此时可以根据实例较多的类进行分析,判断OOM问题出现的原因。
Jstack
jstack用于生成java虚拟机当前时刻的线程快照。
调优
JVM调优主要就是调整下面两个指标
停顿时间:垃圾收集器做垃圾回收中断应用执行的时间。-XX:MaxGCPauseMillis
吞吐量:垃圾收集的时间和总时间的占比:1/(1+n),吞吐量为1-1/(1+n)。-XX:GCTimeRatio=n
死锁程序:
package com.luban;
public class DeadLock {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread1 start");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread1 end");
}
}
}).start();
new Thread(() -> {
synchronized (lock2) {
try {
System.out.println("Thread2 start");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Thread2 end");
}
}
}).start();
System.out.println("The End");
}
}
此时会形成死锁,进程不会结束。
随后我们生成该程序的线程快照:
jstack 39204 > deadlock.txt
2020-05-29 08:32:18
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.201-b09 mixed mode):
"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x00000000027b4800 nid=0x6ad0 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001b8d2000 nid=0x94f4 waiting for monitor entry [0x000000001c2af000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.luban.DeadLock.lambda$main$1(DeadLock.java:33)
- waiting to lock <0x0000000780a4e260> (a java.lang.Object)
- locked <0x0000000780a4e270> (a java.lang.Object)
at com.luban.DeadLock$$Lambda$2/664223387.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"Thread-0" #11 prio=5 os_prio=0 tid=0x000000001b8cf800 nid=0x94cc waiting for monitor entry [0x000000001c1af000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.luban.DeadLock.lambda$main$0(DeadLock.java:17)
- waiting to lock <0x0000000780a4e270> (a java.lang.Object)
- locked <0x0000000780a4e260> (a java.lang.Object)
at com.luban.DeadLock$$Lambda$1/885284298.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"Service Thread" #10 daemon prio=9 os_prio=0 tid=0x000000001ae8c800 nid=0x9e20 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000000001ae43800 nid=0x777c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000000001adc2000 nid=0x94ec waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000000001adc1000 nid=0x9640 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000000001adbf800 nid=0x8d84 runnable [0x000000001b3ae000]
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 <0x0000000780b21b20> (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 <0x0000000780b21b20> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)
"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001acf3800 nid=0x5f70 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x0000000019962800 nid=0x2ea8 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x00000000028ad000 nid=0x85e4 in Object.wait() [0x000000001acaf000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000780808ed0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x0000000780808ed0> (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=2 tid=0x00000000028a4000 nid=0x82a4 in Object.wait() [0x000000001abae000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000780806bf8> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x0000000780806bf8> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Thread" os_prio=2 tid=0x0000000019917800 nid=0x9bb0 runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00000000027ca000 nid=0x8588 runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00000000027cb800 nid=0x6bd4 runnable
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00000000027cd000 nid=0x92ac runnable
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00000000027ce800 nid=0x968c runnable
"VM Periodic Task Thread" os_prio=2 tid=0x000000001ae98800 nid=0x8300 waiting on condition
JNI global references: 317
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00000000028a9e08 (object 0x0000000780a4e260, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00000000028aa018 (object 0x0000000780a4e270, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.luban.DeadLock.lambda$main$1(DeadLock.java:33)
- waiting to lock <0x0000000780a4e260> (a java.lang.Object)
- locked <0x0000000780a4e270> (a java.lang.Object)
at com.luban.DeadLock$$Lambda$2/664223387.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at com.luban.DeadLock.lambda$main$0(DeadLock.java:17)
- waiting to lock <0x0000000780a4e270> (a java.lang.Object)
- locked <0x0000000780a4e260> (a java.lang.Object)
at com.luban.DeadLock$$Lambda$1/885284298.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
随后我们可以在最后或者前面分析发现死锁情况的发生。
GC调优步骤
1.打印GC日志
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log
Tomcat可以直接加载JAVA_OPTS变量里
2.分析日志得到关键性指标
3.分析GC原因,调优JVM参数
1.Parallel Scavenge收集器(默认)
分析parallel-gc.log
第一次调优,设置Metaspace大小:增大元空间大小-XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64M
第二次调优,增大年轻代动态扩容增量(默认是20%),可以减少YGC:-XX:YoungGenerationSizeIncrement=30
比较下几次调优效果:
吞吐量 | 最大停顿 | 平均停顿 | YGC | FGC |
---|---|---|---|---|
97.467% | 370 ms | 50.0 ms | 16 | 2 |
98.9% | 110 ms | 32.5 ms | 12 | 0 |
2.配置CMS收集器
-XX:+UseConcMarkSweepGC
分析gc-cms.log
3.配置G1收集器
-XX:+UseG1GC
分析gc-g1.log
young GC:[GC pause (G1 Evacuation Pause)(young)
initial-mark:[GC pause (Metadata GC Threshold)(young)(initial-mark) (参数:InitiatingHeapOccupancyPercent)
mixed GC:[GC pause (G1 Evacuation Pause)(Mixed) (参数:G1HeapWastePercent)
full GC:[Full GC (Allocation Failure)(无可用region)
(G1内部,前面提到的混合GC是非常重要的释放内存机制,它避免了G1出现Region没有可用的情况,否则就会触发 FullGC事件。CMS、Parallel、Serial GC都需要通过Full GC去压缩老年代并在这个过程中扫描整个老年代。G1的Full GC算法和Serial GC收集器完全一致。当一个Full GC发生时,整个Java堆执行一个完整的压缩,这样确保了最大的空余内存可用。G1的Full GC是一个单线程,它可能引起一个长时间的停顿时间,G1的设计目标是减少Full GC,满足应用性能目标。)
查看发生MixedGC的阈值:jinfo -flag InitiatingHeapOccupancyPercent 进程ID
调优:
第一次调优,设置Metaspace大小:增大元空间大小-XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64M
第二次调优,添加吞吐量和停顿时间参数:-XX:GCTimeRatio=99 -XX:MaxGCPauseMillis=10
GC常用参数
堆栈设置
-Xss:每个线程的栈大小
-Xms:初始堆大小,默认物理内存的1/64
-Xmx:最大堆大小,默认物理内存的1/4
-Xmn:新生代大小
-XX:NewSize:设置新生代初始大小
-XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3。
-XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
-XX:MetaspaceSize:设置元空间大小
-XX:MaxMetaspaceSize:设置元空间最大允许大小,默认不受限制,JVM Metaspace会进行动态扩展。
垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
收集器设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParallelOldGC:老年代使用并行回收收集器
-XX:+UseParNewGC:在新生代使用并行收集器
-XX:+UseParalledlOldGC:设置并行老年代收集器
-XX:+UseConcMarkSweepGC:设置CMS并发收集器
-XX:+UseG1GC:设置G1收集器
-XX:ParallelGCThreads:设置用于垃圾回收的线程数
并行收集器设置
-XX:ParallelGCThreads:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis:设置并行收集最大暂停时间
-XX:GCTimeRatio:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
CMS收集器设置
-XX:+UseConcMarkSweepGC:设置CMS并发收集器
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads:设置并发收集器新生代收集方式为并行收集时,使用的CPU数。并行收集线程数。
-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩
-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况
-XX:ParallelCMSThreads:设定CMS的线程数量
-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理
G1收集器设置
-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区
-XX:GCTimeRatio:吞吐量大小,0-100的整数(默认9),值为n则系统将花费不超过1/(1+n)的时间用于垃圾收集
-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)
-XX:G1MaxNewSizePercent:新生代内存最大空间
-XX:TargetSurvivorRatio:Survivor填充容量(默认50%)
-XX:MaxTenuringThreshold:最大任期阈值(默认15)
-XX:InitiatingHeapOccupancyPercen:老年代占用空间超过整堆比IHOP阈值(默认45%),超过则执行混合收集
-XX:G1HeapWastePercent:堆废物百分比(默认5%)
-XX:G1MixedGCCountTarget:参数混合周期的最大总次数(默认8)
lection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理
G1收集器设置
-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区
-XX:GCTimeRatio:吞吐量大小,0-100的整数(默认9),值为n则系统将花费不超过1/(1+n)的时间用于垃圾收集
-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)
-XX:G1MaxNewSizePercent:新生代内存最大空间
-XX:TargetSurvivorRatio:Survivor填充容量(默认50%)
-XX:MaxTenuringThreshold:最大任期阈值(默认15)
-XX:InitiatingHeapOccupancyPercen:老年代占用空间超过整堆比IHOP阈值(默认45%),超过则执行混合收集
-XX:G1HeapWastePercent:堆废物百分比(默认5%)
-XX:G1MixedGCCountTarget:参数混合周期的最大总次数(默认8)