1, 架构师面对JVM调优, 能做什么?
JVM调优目的: 让系统运行更快, 更稳定.
背景问题: 1, 高并发; 2, 高吞吐;
2, 再次认识Java
-
Java技术体系
JDK: Java程序设计语言; Java虚拟机; Java类库. 是支持Java程序开发的最小环境.
JRE: JavaSE API(类库)和Java虚拟机统称为JRE, 是支持Java程序运行的标准环境.
-
Java发展历史
1995正是发布Java, Write Once, Run Anywhere的特点;
1996JDK1.0
1999HotSpot虚拟机诞生;
2004JDK5
2014JDK8: 支持Lambda, 移除了HotSpot永久代
2017起, JDK每年3月和9月发布一个大版本, 每六个大版本画出一个长期支持版LTS, 三年的支持和更新
2018JDK8 - LTS版本
-
JVM虚拟机种类
HotSpot: OracleJDK和OpenJDK中默认的Java虚拟机, 也是使用最广泛的Java虚拟机, 热点代码探测技术. Oracle收购Sun之后, 将JRockit的优秀特性融合到HotSpot之中.
JRockit: 来自BEA, 定位于专门为服务器硬件和服务端应用场景, 做了专门优化, 不关注程序启动速度, 因此JRockit内部不包含解释器实现, 全部代码都靠即时编译器编译后执行.号称世界上速度最快的Java虚拟机.
IBM J9: IBM开源, 捐献给了Eclipse基金会管理.
3,JVM虚拟机内存管理
为什么要了解JVM的内存管理?
对于Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄漏和内存溢出问题,看起来由虚拟机管理内存一切都很美好。
不过,也正是因为Java程序员把控制内存的权力交给了Java虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那排查错误、修正问题将会成为一项异常艰难的工作。
3.1 整体架构
JVM主要组成: 类加载系统, 运行时数据区, 执行引擎, 本地方法库与本地库接口
3.1.1 类加载系统
作用: 加载class文件, 形成可以被虚拟机直接使用的Java类型
1, 将Class文件加载到内存;
2, 数据校验, 转换解析和初始化;
3.1.2 运行时数据区
作用: JVM管理的内存, 划分为若干不同的数据区域, 用于存储Java程序的数据.
3.1.3 执行引擎
作用:
1, 用于执行JVM字节码指令(解释执行和编译执行)
2, 垃圾回收器自动管理运行数据区的内存, 将无用的内存占用进行清除, 释放内存资源.
3.1.4 本地方法库与本地库接口
作用: JDK底层用于调用系统本地方法的方法或者接口
3.2 运行时数据区
运行时数据区是jvm中最为重要的部分,也是我们在调优时需要重点关注的区域
3.2.1 程序计数器
作用: 当前线程执行字节码指令的指示器. 指向下一条需要执行的字节码指令. 分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存.
代码展示:
public class Demo1_ProgramCounter {
public void show(){
System.out.println("Method is running");
for (int i = 0; i < 3; i++) {
System.out.println("Hello JVM "+i);
}
System.out.println("Love JVM");
}
public static void main(String[] args) {
Demo1_ProgramCounter counter = new Demo1_ProgramCounter();
counter.show();
}
}
反编译该Java代码的字节码文件:
命令行中, 打开字节码class文件所在位置, 执行: javap -c Demo1_ProgramCounter.class > ProgramCounter.txt
Compiled from "Demo1_ProgramCounter.java"
public class jvm.stu.Demo1_ProgramCounter {
public jvm.stu.Demo1_ProgramCounter();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void show();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Method is running
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: iconst_0
9: istore_1
10: iload_1
11: iconst_3
12: if_icmpge 46
15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
18: new #5 // class java/lang/StringBuilder
21: dup
22: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
25: ldc #7 // String Hello JVM
27: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
30: iload_1
31: invokevirtual #9 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
34: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
37: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
40: iinc 1, 1
43: goto 10
46: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
49: ldc #11 // String Love JVM
51: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
54: return
public static void main(java.lang.String[]);
Code:
0: new #12 // class jvm/stu/Demo1_ProgramCounter
3: dup
4: invokespecial #13 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #14 // Method show:()V
12: return
}
可以看到将class文件中字节码进行反汇编,得到上面的代码,其中code所对应的编号就可以理解为计数器中所记录的执行编号
3.2.2 Java虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。Java虚拟机栈描述的是**`Java方法`**执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个**栈帧**,用于存储`局部变量表`、`操作数栈`、`动态连接`、`方法出口`等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 局部变量表
- 操作数栈
- 动态连接
- 方法出口
3.2.3 本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
3.2.4 Java虚拟机堆
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的**`唯一目的就是存放对象实例`**,Java世界里“几乎”所有的对象实例都在这里分配内存。需要注意的是,《Java虚拟机规范》并没有对堆进行细致的划分,所以对于堆的讲解要基于具体的虚拟机,我们以使用最多的HotSpot虚拟机为例进行讲解。
Java堆是垃圾收集器管理的内存区域,因此它也被称作“GC堆”,这就是我们做JVM调优的重点区域部分。
Young 年轻区(代)
Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。
Tenured 年老区
Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。
Perm 永久区
(注意JDK8中永久代使用元数据空间进行了替换, 也就是永久区使用了机器内存而不是jvm内存)
Perm代主要保存class,method,filed对象,这部份的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以解决问题。
Virtual区
最大内存和初始内存的差值,就是Virtual区。
3.2.5 方法区
- 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
- 《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。
- JDK8之前将HotSpot虚拟机把收集器的分代设计扩展至方法区,所以可以将永久代看做是方法区,JDK8之后废弃永久代,用元空间来代替。
4, 虚拟机性能相关工具
4.1 JVM的运行参数
-
标准参数
jvm的标准参数,一般都是很稳定的,在未来的JVM版本中不会改变,可以使用java -help检索出所有的标准参数。
-
-X参数(非标准参数)
jvm的-X参数是非标准参数,在不同版本的jvm中,参数可能会有所不同,可以通过java -X查看非标准参数。
-
-XX参数(使用率较高)
4.2 参数: -X
jvm的-X参数是非标准参数,在不同版本的jvm中,参数可能会有所不同,可以通过java -X查看非标准参数。
D:\>java -X
-Xmixed 混合模式执行(默认)
-Xint 仅解释模式执行
-Xbootclasspath:<用 ; 分隔的目录和 zip/jar 文件>
设置引导类和资源的搜索路径
-Xbootclasspath/a:<用 ; 分隔的目录和 zip/jar 文件>
附加在引导类路径末尾
-Xbootclasspath/p:<用 ; 分隔的目录和 zip/jar 文件>
置于引导类路径之前
-Xdiag 显示附加诊断消息
-Xnoclassgc 禁用类垃圾收集
-Xincgc 启用增量垃圾收集
-Xloggc:<file> 将 GC 状态记录在文件中(带时间戳)
-Xbatch 禁用后台编译
-Xms<size> 设置初始 Java 堆大小
-Xmx<size> 设置最大 Java 堆大小
-Xss<size> 设置 Java 线程堆栈大小
-Xprof 输出 cpu 分析数据
-Xfuture 启用最严格的检查,预计会成为将来的默认值
-Xrs 减少 Java/VM 对操作系统信号的使用(请参阅文档)
-Xcheck:jni 对 JNI 函数执行其他检查
-Xshare:off 不尝试使用共享类数据
-Xshare:auto 在可能的情况下使用共享类数据(默认)
-Xshare:on 要求使用共享类数据,否则将失败。
-XshowSettings 显示所有设置并继续
-XshowSettings:system
(仅限 Linux)显示系统或容器
配置并继续
-XshowSettings:all
显示所有设置并继续
-XshowSettings:vm 显示所有与 vm 相关的设置并继续
-XshowSettings:properties
显示所有属性设置并继续
-XshowSettings:locale
显示所有与区域设置相关的设置并继续
-X 选项是非标准选项。如有更改,恕不另行通知。
4.3 参数: -XX
-XX参数也是非标准参数,主要用于jvm的调优和debug操作。
-XX参数的使用有2种方式,一种是boolean类型,一种是非boolean类型
使用方法:
-
boolean类型使用: 格式:-XX:[±] 表示启用或禁用属性
如:-XX:+DisableExplicitGC 表示禁用手动调用gc操作,也就是说调用System.gc()无效
-
非boolean类型使用: 格式:-XX:= 表示属性的值为
如:-XX:NewRatio=4 表示新生代和老年代的比值为1:4
4.4 参数: -Xms与-Xmx
-Xms与-Xmx分别是设置jvm的堆内存的初始大小和最大大小。
-Xmx2048m:等价于-XX:MaxHeapSize,设置JVM最大堆内存为2048M。
-Xms512m:等价于-XX:InitialHeapSize,设置JVM初始堆内存为512M。
适当的调整jvm的内存大小,可以充分利用服务器资源,让程序跑的更快
4.5 查看正在运行的jvm参数
1, 启动一个java程序;
在IDE或者通过命令行启动一个带时延的程序,
public class Demo2_JVM_args {
public static void main(String[] args) {
System.out.println("启动一个java程序.");
try {
System.out.println("下面时延500秒, 用于在命令行中查看java进程.");
Thread.sleep(500000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("程序运行结束, 命令行中看不到进程.");
}
}
启动该程序…
2, 查找该应用进程编号;
作用: 找到进程编号, 供第3步查看参数使用
操作: 命令行中执行jps -l
命令
D:\>jps -l
16404 jvm.stu.Demo2_JVM_args
19236 org.jetbrains.jps.cmdline.Launcher
10936
12968 sun.tools.jps.Jps
3, 查看运行参数
D:\>jinfo -flags 16404
Attaching to process ID 16404, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.271-b09
Non-default VM flags: -XX:CICompilerCount=4 -XX:InitialHeapSize=199229440 -XX:MaxHeapSize=3183476736 -XX:MaxNewSize=1061158912 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=66060288 -XX:OldSize=133169152 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
Command line: -javaagent:D:\engineering\install\JetBrains\IntelliJ IDEA 2020.3\lib\idea_rt.jar=8095:D:\engineering\install\JetBrains\IntelliJ IDEA 2020.3\bin -Dfile.encoding=UTF-8
D:\>jinfo -flag MaxHeapSize 16404
-XX:MaxHeapSize=3183476736
4.6 jstat
jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下:
jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]
继续启动上面程序
1.查看class加载统计
D:\>jstat -class 16404
Loaded Bytes Unloaded Bytes Time
625 1258.8 0 0.0 0.13
2.查看编译统计
D:\>jstat -compiler 16404
Compiled Failed Invalid Time FailedType FailedMethod
70 0 0 0.03 0
3.垃圾回收统计
D:\>jstat -gc 16404
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
7680.0 7680.0 0.0 0.0 49152.0 4915.4 130048.0 0.0 4480.0 776.5 384.0 76.6 0 0.000 0 0.000 0.000
设置每500毫秒打印1次, 一共打印5次
D:\>jstat -gc 16404 500 5
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
7680.0 7680.0 0.0 0.0 49152.0 4915.4 130048.0 0.0 4480.0 776.5 384.0 76.6 0 0.000 0 0.000 0.000
7680.0 7680.0 0.0 0.0 49152.0 4915.4 130048.0 0.0 4480.0 776.5 384.0 76.6 0 0.000 0 0.000 0.000
7680.0 7680.0 0.0 0.0 49152.0 4915.4 130048.0 0.0 4480.0 776.5 384.0 76.6 0 0.000 0 0.000 0.000
7680.0 7680.0 0.0 0.0 49152.0 4915.4 130048.0 0.0 4480.0 776.5 384.0 76.6 0 0.000 0 0.000 0.000
7680.0 7680.0 0.0 0.0 49152.0 4915.4 130048.0 0.0 4480.0 776.5 384.0 76.6 0 0.000 0 0.000 0.000
字段解释如下
S0C:第一个Survivor区的大小(KB)
S1C:第二个Survivor区的大小(KB)
S0U:第一个Survivor区的使用大小(KB)
S1U:第二个Survivor区的使用大小(KB)
EC:Eden区的大小(KB)
EU:Eden区的使用大小(KB)
OC:Old区大小(KB)
OU:Old使用大小(KB)
MC:方法区大小(KB)
MU:方法区使用大小(KB)
CCSC:压缩类空间大小(KB)
CCSU:压缩类空间使用大小(KB)
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间