性能优化专题共计四个部分,分别是:
本节是性能优化专题第二部分 —— JVM 性能优化篇,共计六个小节,分别是:
通过这六节的学习,你将学到:
➢ 了解JVM内存模型以及每个分区详解。
➢ 熟悉运行时数据区,特别是堆内存结构和特点。
➢ 熟悉GC三种收集方法的原理和特点。
➢ 熟练使用GC调优工具,快速诊断线上问题。
➢ 生产环境CPU负载升高怎么处理?
➢ 生产环境给应用分配多少线程合适?
➢ JVM字节码是什么东西?
工欲善其事,必先利其器!在Linux上调优之前,我们看看需要掌握哪些常见的JVM相关的工具以及命令的使用呢?
经过前面的各种分析学习,我们知道了关于JVM很多的知识,比如版本信息,类加载,堆,方法区,垃圾回
收等,但是总觉得心里不踏实,原因是没看到实际的一些东西。
所以这一章节,咱们就好好来聊一聊关于怎么将这些内容进行直观地展示在我们面前,包括怎么进行相应的
一些设置。OK,let’s go!
-version
-help
-server
-cp
非标准参数,也就是在JDK各个版本中可能会变动
-Xint
#解释执行
-Xcomp
#第一次使用就编译成本地代码
-Xmixed
#混合模式,JVM自己来决定
使用得最多的参数类型
非标准化参数,相对不稳定,主要用于JVM调优和Debug
格式:-XX:[+-]
+或-表示启用或者禁用name属性
比如:-XX:+UseConcMarkSweepGC
表示启用CMS类型的垃圾回收器
-XX:+UseG1GC
表示启用G1类型的垃圾回收器
格式:-XX
表示name属性的值是value
比如:-XX:MaxGCPauseMillis=500
想要设置的话,得先知道默认JVM中参数相关的信息,下面这个针对的是java这个进程
java -XX:+PrintFlagsFinal -version > flags.txt
如果要查看一个运行中的JVM相关参数的信息,可以使用jinfo,不过要先知道Java进程的ID。
比如启动一个tomcat,它的PID为2908,如下图过程所示:
开发工具中设置比如IDEA,eclipse
运行jar包的时候:java -XX:+UseG1GC xxx.jar
web容器比如tomcat,可以在脚本中的进行设置
通过jinfo实时调整某个java进程的参数(参数只有被标记为manageable的flags可以被实时修改)
-Xms1000
# 等价于-XX:InitialHeapSize=1000
-Xmx1000
# 等价于-XX:MaxHeapSize=1000
-Xss100
# 等价于-XX:ThreadStackSize=100
所以这块也相当于是-XX类型的参数
一般要设置参数,可以先查看一下当前参数是什么,值得注意的是"=“表示默认值,”:="表示被用户或JVM修改后的值。
官网
The jps command lists the instrumented Java HotSpot VMs on the target system. The command is limited to reporting information on JVMs for which it has the access permissions.
实时查看和调整JVM配置参数
The jinfo command prints Java configuration information for a specified Java process or core file or a remote debug server. The configuration information includes Java system properties and Java Virtual Machine (JVM) command-line flags.
jinfo命令显示指定的Java进程或核心文件或远程调试服务器的Java配置信息。配置信息包括Java系统属性和Java虚拟机(JVM)命令行标志。
查看用法:
jinfo -flag name PID
# 查看某个java进程的name属性的值
比如:
jinfo -flag MaxHeapSize PID
jinfo -flag UseG1GC PID
调整用法:
参数只有被标记为manageable的flags可以被实时修改
jinfo -flag [+|-] PID jinfo -flag <name>=<value> PID
查看曾经赋过值的一些参数:
jinfo -flags PID
查看虚拟机性能统计信息
The jstat command displays performance statistics for an instrumented Java HotSpot VM. The target JVM is identified by its virtual machine identifier, or vmid option.
jstat命令显示已检测的Java HotSpot VM的性能统计信息。目标JVM由其虚拟机标识符或vmid选项标识。
查看类装载信息
jstat -class PID 1000 10
# 查看某个java进程的类装载信息,每1000毫秒输出一次,共输出10次
比如:
jstat -class PID 1000 10
jstat -gc PID 1000 10
查看线程堆栈信息
The jstack command prints Java stack traces of Java threads for a specified Java process, core file, or remote debug server.
jstack命令为指定的Java进程,核心文件或远程调试服务器打印Java线程的Java堆栈跟踪。
用法:
jstack PID
生成堆转储快照
The jmap command prints shared object memory maps or heap memory details of a specified process, core file, or remote debug server.
jmap命令打印指定进程,核心文件或远程调试服务器的共享对象内存映射或堆内存详细信息。
打印出堆内存相关信息
jmap -heap PID
dump出堆内存相关信息:
jmap -dump:format=b,file=heap.hprof PID
关于dump下来的文件
一般dump下来的文件直接看有些费力,可以结合MAT工具来分析。
一般在开发中,JVM参数可以加上下面两句,这样内存溢出时,会自动dump出该文件
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=heapdump.hprof
JConsole工具是JDK自带的可视化监控工具。查看java应用程序的运行概况、监控堆信息、永久区使用情况、类加
载情况等。
可以监控本地的java进程的CPU,类,线程等
https://help.eclipse.org/2020-12/index.jsp?topic=/org.eclipse.mat.ui.help/welcome.html
要想分析日志的信息,得先拿到GC日志文件才行,所以得先配置一下,根据前面参数的学习,下面的配置很
容易看懂
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-Xloggc:$CATALINA_HOME/logs/gc.log
GCViewer
通过前面的知识铺垫,我们基本了解了JVM整体的工作流程。现在我们整理一下整体架构:
除此之外,从Class文件往前推的话,肯定是Java源码文件,经过编译之后生成Class文件。
所以接下来咱们就从Java源码文件开始慢慢聊。
public class Person{
private String name="Jack";
private int age;
private final double salary=100;
private static String address;
private final static String hobby="Programming";
private Object obj=new Object();
public void say(){
System.out.println("person say...");
}
public static int calc(int op1,int op2){
op1=3;
int result=op1+op2;
Object o=obj;
return result;
}
public static void main(String[] args){
System.out.println(calc(1,2));
}
}
此时你需要一个能够看懂反编译指令的宝典
首先我们获取到字节码文件:
javac Person.java
得到Person.class,因为都是二进制的数字嘛,我们当然可以根据Oracle官方文档自己去破译字节码文件,这里我们则使用JVM自带工具,javap进行反编译:
javap -v Person.class
Compiled from "Person.java"
public class com.testjvm.domain.Person
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #13.#39 // java/lang/Object."":()V
#2 = String #40 // Jack
#3 = Fieldref #14.#41 // com/testjvm/domain/Person.name:Ljava/lang/String;
#4 = Double 100.0d
#6 = Fieldref #14.#42 // com/testjvm/domain/Person.salary:D
#7 = Fieldref #43.#44 // java/lang/System.out:Ljava/io/PrintStream;
#8 = String #45 // person say...
#9 = Methodref #46.#47 // java/io/PrintStream.println:(Ljava/lang/String;)V
#10 = Fieldref #14.#48 // com/testjvm/domain/Person.obj:Ljava/lang/Object;
#11 = Methodref #14.#49 // com/testjvm/domain/Person.calc:(II)I
#12 = Methodref #46.#50 // java/io/PrintStream.println:(I)V
#13 = Class #51 // java/lang/Object
#14 = Class #52 // com/testjvm/domain/Person
#15 = Utf8 name
#16 = Utf8 Ljava/lang/String;
#17 = Utf8 age
#18 = Utf8 I
#19 = Utf8 salary
#20 = Utf8 D
#21 = Utf8 ConstantValue
#22 = Utf8 address
#23 = Utf8 hobby
#24 = String #53 // Programming
#25 = Utf8 obj
#26 = Utf8 Ljava/lang/Object;
#27 = Utf8
#28 = Utf8 ()V
{
public com.testjvm.domain.Person();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: ldc #2 // String Jack
7: putfield #3 // Field name:Ljava/lang/String;
10: aload_0
11: ldc2_w #4 // double 100.0d
14: putfield #6 // Field salary:D
17: return
LineNumberTable:
line 3: 0
line 5: 4
line 9: 10
public void say();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #8 // String person say...
5: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 19: 0
line 21: 8
public static int calc(int, int);
descriptor: (II)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=2
0: iconst_3
//将int类型常量3压入操作数栈
1: istore_0 //将int类型值存入局部变量0
2: iload_0 //从局部变量0中装载int类型值
3: iload_1 //从局部变量1中装载int类型值
4: iadd //执行int类型的加法
5: istore_2 //将int类型值存入局部变量2
6: iload_2 //从局部变量2中装载int类型值
7: ireturn //从方法中返回int类型的数据
LineNumberTable:
line 23: 0
line 24: 2
line 25: 6
line 26: 10
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iconst_1
4: iconst_2
5: invokestatic #11 // Method calc:(II)I
8: invokevirtual #12 // Method java/io/PrintStream.println:(I)V
11: return
值得探讨的两个方向:(1)类的装载方式有哪些?(2)类装载到底做了什么?
类的装载方式有哪些?
(1)本地系统加载
(2)网络下载.class文件
(3)从zip,jar等归档文件中加载.class文件
(4)从数据库中提取.class文件
(5)由java源文件动态编译成.class文件
(6)Class.forName()加载
(7)ClassLoader.loadClass()加载
类装载到底做了什么?
(1)通过一个类的全限定名获取定义此类的二进制字节流
这个阶段是可控性比较强的阶段,既可以用系统提供的类加载器进行加载,又可以自定义类加载器进行加
载。
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
类信息:类的版本、字段、方法、构造方法、接口定义等
(3)类加载的最终产品是位于堆区中的Class对象。
Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口
Java对象实例以及数组都在堆上分配
public final class Class<T> implements java.io.Serializable, GenericDeclaration, Type, AnnotatedElement {
保证被加载类的正确性
验证阶段很重要,但不是必须的。若所引用的类经过反复验证没问题,可以使用-Xverifynone参数关闭大部
分类验证措施,从而缩短虚拟机类加载的时间。
为类的静态变量分配内存,并将其初始化为默认值
在方法区中,为类变量分配内容并设置初始值
把类中的符号引用转换为直接引用
Run-Time Constant Pool:Class文件中除了有类的版本、字段、方法、接口等描述 信息外,还有一项信息就是常量池,用于存放编译时 期生成的各种字面量和符号引用,这部分内容将在类加载后进 入方法区的运行时常量池中存放。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。 符号引用就是一组符号来描述目标,可以是任何字面量。 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
执行类构造器,为类的静态变量赋予正确的初始值,有两种方式
类的初始化步骤
(1)如果这个类还没有被加载和链接,那先进行加载和链接
(2)假如这个类存在直接父类,并且这个类还没有被初始化(在一个类加载器中,类只能初始化一次),那就初始化直接的 父类(不适用于接口) (3)假如类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
类什么时候才会被初始化?
(1)创建类的实例
(2)访问某个类或接口的静态变量,或者对该静态变量进行赋值
(3)调用类的静态方法
(4)反射[Class.forName(“com.XXX”)]
(5)初始化一个类的子类(因为会先初始化父类)
(6)JVM启动时表明的启动类
@RestController
public class HeapController {
List<Person> list=new ArrayList<Person>();
/**
* -Xmx32M -Xms32M
* @return
*/
@GetMapping("/heap")
public String heap(){
int i = 0;
while(true){
list.add(new Person(i++, UUID.randomUUID().toString()));
}
}
}
VM参数配置:
-Xmx32M -Xms32M
访问->http://localhost:8080/heap
得到OOM报错:
Exception in thread "http-nio-8080-exec-2" java.lang.OutOfMemoryError: GC overhead limit exceeded
# jmap手动导
jmap -dump:format=b,file=heap.hprof PID
比如向方法区中添加Class的信息
<dependency>
<groupId>asmgroupId>
<artifactId>asmartifactId>
<version>3.3.1version>
dependency>
@RestController
public class NonHeapController {
List<Class<?>> list=new ArrayList<Class<?>>();
@GetMapping("/nonheap")
public String nonheap(){
while(true){
list.addAll(MetaspaceUtil.createClasses());
}
}
}
public class MetaspaceUtil extends ClassLoader {
public static List<Class<?>> createClasses() {
List<Class<?>> classes = new ArrayList<Class<?>>();
for (int i = 0; i < 10000000; ++i) {
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null,
"java/lang/Object", null);
MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "" ,
"()V", null, null);
mw.visitVarInsn(Opcodes.ALOAD, 0);
mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
"" , "()V");
mw.visitInsn(Opcodes.RETURN);
mw.visitMaxs(1, 1);
mw.visitEnd();
MetaspaceUtil test = new MetaspaceUtil();
byte[] code = cw.toByteArray();
Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);
classes.add(exampleClass);
}
return classes;
}
}
VM参数配置:
# 设置Metaspace的大小
-XX:MetaspaceSize=50M
-XX:MaxMetaspaceSize=50M
访问->http://localhost:8080/nonheap
打印如下:
java.lang.OutOfMemoryError: Metaspace at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0_191] at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ~[na:1.8.0_191]
public class StackDemo {
public static long count=0;
public static void method(long i){
System.out.println(count++);
method(i);
}
public static void main(String[] args) {
method(1);
}
}
7252
7253
7254
7255
Exception in thread "main" java.lang.StackOverflowError
at java.lang.Long.toString(Long.java:396)
at java.lang.String.valueOf(String.java:3113)
Stack Space用来做方法的递归调用时压入Stack Frame(栈帧)。所以当递归调用太深的时候,就有可能耗尽Stack Space,爆出StackOverflow的错误。 -Xss128k:设置每个线程的堆栈大小。JDK 5以后每个线程堆 栈大小为1M,以前每个线程堆栈大小为256K。根据应用的 线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一 个进程内的线程数还 是有限制的,不能无限生成,经验值在3000~5000左右。 线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性 更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。
//运行主类
public class DeadLockDemo {
public static void main(String[] args) {
DeadLock d1=new DeadLock(true);
DeadLock d2=new DeadLock(false);
Thread t1=new Thread(d1);
Thread t2=new Thread(d2);
t1.start();
t2.start();
}
}
//定义锁对象
class MyLock{
public static Object obj1= new Object();
public static Object obj2= new Object();
}
//死锁代码
class DeadLock implements Runnable{
private boolean flag;
DeadLock(boolean flag){
this.flag=flag;
}
public void run() {
if(flag) {
while(true) {
synchronized(MyLock.obj1) {
System.out.println(Thread.currentThread().getName()+"----if获得obj1锁");
synchronized(MyLock.obj2) {
System.out.println(Thread.currentThread().getName()+"----if获得obj2锁");
}
}
}
} else {
while(true){
synchronized(MyLock.obj2) {
System.out.println(Thread.currentThread().getName()+"----否则获得obj2锁");
synchronized(MyLock.obj1) {
System.out.println(Thread.currentThread().getName()+"----否则获得obj1锁");
}
}
}
}
}
}
将线程信息dump出来
内存被使用了之后,难免会有不够用或者达到设定值的时候,就需要对内存空间进行垃圾回收。
GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。 当然,我们可以手动进行垃圾回收,比如调用System.gc()方法通知JVM进行一次垃圾回收,但是具体什么时刻运行也无 法控制。也就是说System.gc()只是通知要回收,什么时候回收由JVM决定。 但是不建议手动调用该方法,因为消耗的资源比较大。
虽然垃圾回收的时机是不确定的,但是可以结合之前一个对象的一辈子案例,文字图解再次梳理一下堆内存
回收的流程。
一个对象的一辈子
我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长 时间。 有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了, 有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该 去社会上闯闯了。 于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了 20年(每次GC加一岁),然后被回收。
我的本地机器使用的是jdk1.8和tomcat8.5
回顾升华一下垃圾收集器图
要想分析日志的信息,得先拿到GC日志文件才行,所以得先配置一下,之前也看过这些参数。
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-Xloggc:$CATALINA_HOME/logs/gc.log
比如打开windows中的catalina.bat,在第一行加上
-Xms300M -Xmx300M
set JAVA_OPTS=%JAVA_OPTS% -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps - Xloggc:gc.log
这样使用startup.bat启动tomcat的时候就能够在当前目录下拿到gc.log文件
可以看到默认使用的是ParallelGC
吞吐量优先
T23:21:53.305+0800: 1.303: [GC (Allocation Failure) [PSYoungGen: 65536K[Young区回 收前]->10748KYoung区回收后] 65536K[整个堆回收前]->15039K[整个堆回收后] (251392K[整个堆总大小]), 0.0113277 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
停顿时间优先
VM参数配置:
-XX:+UseConcMarkSweepGC
重启tomcat获取gc日志,这里的日志格式和上面差不多,不作分析。
停顿时间优先
-XX:+UseG1GC
是否选用G1垃圾收集器的判断依据
(1)50%以上的堆被存活对象占用
(2)对象分配和晋升的速度变化非常大
(3)垃圾回收时间比较长
(1)使用G1GC垃圾收集器: -XX:+UseG1GC
修改配置参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间
(2)调整内存大小再获取gc日志分析
-XX:MetaspaceSize=100M -Xms300M -Xmx300M
比如设置堆内存的大小,获取到gc日志,使用GCViewer分析吞吐量和响应时间
(3)调整最大停顿时间
-XX:MaxGCPauseMillis=200 设置最大GC停顿时间指标
比如设置最大停顿时间,获取到gc日志,使用GCViewer分析吞吐量和响应时间
(4)启动并发GC时堆内存占用百分比
-XX:InitiatingHeapOccupancyPercent=45
# G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的 使用比例。值为 0 则表示“一直执行GC循环)'. 默认值为 45 (例如, 全部的 45% 或者使用了45%).
比如设置该百分比参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间
(1)不要手动设置年轻代的大小-Xmn,不然G1的默认行为会被干扰
G1在垃圾收集时将不再关心暂停时间指标。 所以从本质上说,设置年轻代的大小将禁用暂停时间目标.
G1在必要时也不能够增加或者缩小年轻代的空间, 因为大小是固定的,所以对更改大小无能为力.
(2)暂停时间不要使用平均响应时间
暂停时间只是一个目标,并不能总是得到满足。
(3)增加对内存大小
(4)使用-XX:ConcGCThreads=n来增加标记线程的数量
(5)其他参数设置
-XX:InitiatingHeapOccupancyPercent
-XX:G1MixedGCLiveThresholdPercent -XX:G1HeapWastePercent
-XX:G1MixedGCCountTarget -XX:G1OldGCSetRegionThresholdPercent
本节代码下载地址为:https://github.com/harrypottry/jvmDemo
更多架构知识,欢迎关注本套系列文章:Java架构师成长之路