1. JVM简述
JVM是Java Virtual Machine的缩写。它是一种基于计算设备的规范,是一台虚拟机,即虚构的计算机。
JVM屏蔽了具体操作系统平台的信息(显然,就像是我们在电脑上开了个虚拟机一样),当然,JVM执行字节码时实际上还是要解释成具体操作平台的机器指令的。
如类加载机制、运行时数据区、垃圾回收机制等;
2. 字节码
2.1 字节码由来
Java 所有的指令有 200 个左右,一个字节( 8 位)可以存储 256 种不同的指令信息,一个这样的字节称为字节码( Bytecode )
JVM 将字节码解释执行,屏蔽对底层操作系统的依赖,JVM也可以将字节码编译执行,如果是热点代码,会通过JIT 动态地编译为机器码,提高执行效率
JVM在字节码上也设计了一套操作码助记符,使用特殊单词来标记这些数字。
加载或存储指令 、运算指令、类型转换指令、对象创建与访问指令、操作栈管理指令、方法调用与返回指令。
2.2 源码转化成字节码
词法解析是通过空格分隔出单词 、操作符、控制符等信息 , 将其形成 token 信息流 ,传递给语法解析器。
在语法解析时,把词法解析得到的 token 信息流按照 Java 语法规则组装成一棵语法树 , 如图 4-2 虚线框所示。
在语义分析阶段 , 需要检查关键字的使用是否合理、类型是否匹配、作用域是否正确等。
当语义分析完成之后,即可生成字节码。
2.2 执行三种模式
字节码必须通过类加载过程加载到 JVM 环境后,才可以执行。执行有三种模式:第一,解释执行;第二,JIT 编译执行;第三, JIT 编译与解释混合执行(主流 JVM默认执行模式)。混合执行模式的优势在于解释器在启动时先解释执行,省去编译时间。随着时间推进 , JVM 通过热点代码统计分析 , 识别高频的方法调用、循环体、公共模块等,基于强大的 JlT 动态编译技术,将热点代码转换成机器码,直接交给 CPU执行。 JIT 的作用是将 Java 字节码动态地编译成可以直接发送给处理器指令执行的机器码。简要流程如图 4-3 所示。
注意解释执行与编译执行在线上环境微妙的辩证关系。机器在热机状态可以承受的负载要大于冷机状态(刚启动时 ),如果以热机状态时的流量进行切流 , 可能使处于冷机状态的服务器因无法承载流量而假死。 刚启动的 JVM 均是解释执行,还没有进行热点代码统计和 JIT 动态编译。
3. 类加载过程
3.1 类加载流程
1、Load加载(读取二进制流转特定数据结构)
2、Link链接(
验证-详细校验如类型、常量、静态变量等。
准备-为静态变量分配内存。
解析-确保类与类之间的相互引用正确性,完成内存结构布局)
3、Init初始化(执行类构造器完成类的初始化)
4、解析执行直接给CPU、JIT动态编译机器码再给CPU
类加载是一个将 . class 字节码文件实例化成 Class 对象并进行相关初始化的过程。
全小写的 class 是关键字,用来定义类,而首字母大写的 Class ,它是所有 class 的类。** 问题:**为什么这个抽象还是另外一个类 Class 的对象? 示例代码如下:
/**
* @author cmy
* @version 1.0
* @date 2022/6/29 0029 13:43
* @description 类加载过程测试
*/
public class ClassTest {
//数组类型有一个魔法属性:length来获取数组长度
private static int[] array=new int[3];
private static int length=array.length;
//任何小写class定义的类,也有一个魔法属性:class,来获取此类的大写Class类对象
private static Class<One> one=One.class;
private static Class<Another> another=Another.class;
public static void main(String[] args) throws Exception {
//通过newInstance方法创建One和 Another的类对象(第1处)
One oneObject=one.newInstance();
oneObject.call();
Another anotherObject=another.newInstance();
anotherObject.speak();
//通过one这个大写的Class对象,反射方式获取私有成员属性对象Field(第2处)
Field privateFieldInOne = one.getDeclaredField("inner");
//设置私有对象可以访问和修改(第3处)
privateFieldInOne.setAccessible(true);
privateFieldInOne.set(oneObject,"world changed");
//成功修改类的私有属性inner变量值为world changed.
System.out.println(oneObject.getInner());
}
}
class One{
private String inner="time files.";
public void call(){
System.out.println("hello world");
}
public String getInner(){
return inner;
}
}
class Another{
public void speak(){
System.out.println("easy coding");
}
}
第1处说明
new 是强类型校验 , 可以调用任何构造方法 , 在使用new 操作的时候,这个类可以没有被加载过。
Class 类下的 newInstance是弱类型,只能调用无参数构造方法,如果没有默认构造方法,就抛出InstantiationException 异常,如果此构造方法没有权限访问,则抛出IllegalAccessException 异常。
第2处说明
使用类似的方式获取其他声明 , 如注解、方法等,如图4.5所示。
第3处说明
问题:private 成员在类外是否可以修改?通过 setAccessible(true) 操作,即可使用大写 C lass 类的 set 方法修改其值。
3.2 类加载器实现机制
/**
* 查看本地类加载器的方式如下
*/
//正在使用得类加载器sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader c = One.class.getClassLoader();
//AppClassLoader的父类加载器是ExtensionClassLoader JDK1.9之前
ClassLoader c1 = c.getParent();
//ExtClassLoader父类加载器(最高一层)Bootstrap C++实现 返回null
ClassLoader c2 = c1.getParent();
System.out.println(c);
System.out.println(c1);
System.out.println(c2);
低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。 如图 4-6 所示,左侧绿色箭头向上逐级询问是否已加载此类,直至 Bootstrap ClassLoader ,然后向下逐级尝试是否能够加载此类,如果都加载不了,则通知发起加载请求的当前类加载器 ,准予加载。
通过如下代码可以查看 Bootstrap 所有已经加载的类库:
/**
* 通过如下代码可以查看BootstrapClassLoader所有已经加载的类库:
*/
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (java.net.URL url:urLs) {
System.out.println(url.toExternalForm());
}
在JVM中增加如下启动参数 , 则能通过 Class.forName 正常读取到指定类 , 说明此参数可以增加Bootstrap 的类加载路径:
- Xbootclasspath/a:/Users/yangguanbao/book/easyCoding/byJdk11/src
如果想在启动时观察加载了哪个jar包中的哪个类 ,可以增加-XX:+TraceClassLoading参数
- XX:+TraceClassLoading
3.3 自定义类加载器
学习类加载器的实现机制后 , 双亲委派模型并非强制模型, 用户可以自定义类加载器 。
问题:在什么情况下需要自定义类加载器呢? **
** 隔离加载类、修改类加载方式、扩展加载源、防止源码泄露。
实现自定义类加载器的步骤,继承ClassLoader,重写 findClass()方法,调用defineClass()方法。一个简单的类加载器实现的示例代码如下:
/**
* @author cmy
* @version 1.0
* @date 2022/6/29 0029 15:34
* @description 自定义类加载器
*
* 自定义类加载器步骤:
* 继承ClassLoader
* 重写findClass()方法
* 调用defineClass()方法
*/
public class CustomClassLoader extends ClassLoader {
private String classpath;
public CustomClassLoader(String classpath) {
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = getClassFromCustomPath(name);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
private byte[] getClassFromCustomPath(String name) throws IOException {
//从自定义路径中加载指定类
FileInputStream fis = new FileInputStream(classpath + File.separator + name.replace(".", File.separator).concat(".class"));
byte[] bytes = new byte[fis.available()];
fis.read(bytes);
fis.close();
return bytes;
}
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader("F:\\IDEA2022\\workmenu\\SoftwareTools\\src\\main\\java");
try {
Class<?> clazz = customClassLoader.loadClass("Test");
//调用的静态方法
clazz.getDeclaredMethod("say").invoke(clazz);
Object o = clazz.newInstance();
Method print = clazz.getDeclaredMethod("print", String.class);
print.invoke(o, "调用的对象方法");
//自定义类加载器
System.out.println(clazz.getClassLoader());
//AppClassLoader
System.out.println(clazz.getClassLoader().getParent());
//ExtClassLoader
System.out.println(clazz.getClassLoader().getParent().getParent());
//BootstrapLoader
System.out.println(clazz.getClassLoader().getParent().getParent().getParent());
}catch (Exception e){
e.printStackTrace();
}
}
}
//
//javac编译后,记得删除Test.java比较路径寻找和Test.class冲突
public class Test {
public static void say() {
System.out.println("this is a static method!");
}
public void print(String s) {
System.out.println("printing:"+s);
}
}
按某种规则 jar 包的版本被统←指定 ,导致某些类存在包路径、类名相同 的情况 , 就会引起类冲突 ,导致应用程序出现异常。主流的容器类框架都会自定义类加载器,实现不同中间件之间的类隔离 , 有效避免了类冲突。
4. 内存布局(运行时数据区域)
前面回顾连接
在类加载过程第二步Link链接的解析阶段,解析类和方法确保类与类之间的相互引用正确性,完成内存
结构布局。
4.1 内存布局简介
Java 程序在运行时,会为 JVM 单独划出一块内存区域,而这块内存区域又可以再次划分出一块运行时数据区,运行时数据区域大致可以分为五个部分:
4.2 Heap堆区
**Heap堆区作用:**Heap 是 OOM 故障最主要的发源地 , 它存储着几乎所有的实例对象 , 堆由垃圾收集器自动回收 , 堆区由各子线程共享使用。 堆的内存空间既可以固定大小 , 也可以在运行时动态地调整。
比如 -Xms256M -Xmxl024M ,其中 -X 表示它是 JVM 运行参数, ms 是 memory start 的简 称, mx 是 memory max 的简称,分别代表最小堆容量和最大堆容量。 一般最大和最小一样。避免在GC后调整堆大小时带来的额外压力。
堆分成两大块:新生代和老年代。新生代= 1 个 Eden 区+ 2 个Survivor 区。
当 Eden区装填满的时候 , 会触发 Young Garbage Collection , 即 YGC。垃圾回收的时候 , 在 Eden 区实现清除策略 , 没有被引用的对象则直接回收。依然存活的对象会被移送到 Survivor 区。
**问题:**Survivor 区分为 S0和 S1两块内存空间 , 送到哪块空间呢?
每次 YGC 的时候, 它们将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除 , 交换两块空间的使用状态。
**问题:**如何防止对象没有进取心?
每个对象都有一个计数器,每次 YGC 都会加1。 -XX:MaxTenuringThreshold 参数能配置计数器的值到达某个阀值的时候 , 对象从新生代晋升至老年代。
对象分配与简要GC流程图如图4-9所示。
图 的 中,如果 Survivor 区无法放下,或者超大对象的闹值超过上限,则尝试在老年代中进行分配 ; 如果老年代也无法放下,则会触发 Full Garbage Collection , 即FGC。如果依然无法放下, 则抛出 OOM。堆内存出现 OOM 的概率是所有内存耗尽异常中最高的。出错时的堆内信息对解决问题非常有帮助 ,
所以给JVM设置运行参数 -XX:+HeapDumpOnOutOfMemoryError,让JVM遇到OOM 异常时能输出堆内信息,特别是对相隔数月才出现的 OOM 异常尤为重要。
4.3 Metaspace (元空间)
元空间作用:存储常量池、方法元信息、类元信息。字符串常量String存在堆内存。
元空间发展:源码解析和示例代码基本采用 JDK11版本, JVM则为 Hotspot。在 JDK7 及之前的版本中,只有 Hotspot才有 Perm 区,译为永久代 , 它在启动时固定大小,很难进行调优。动态加载类过多,容易产生 Perm 区的 OOM。为了解决Perm 区的 OOM, 需要设定运行参数 -XX:MaxPermSize= 1280m。
永久代在垃圾回收过程中还存在诸多问题。 JDK8 使用元空间替换永久代。在 JDK8 及以上版本中,设定 MaxPermSize 参数, JVM在启动时并不会报错。
区别于永久代 , 元空间在本地内存中分配。在 JDK8 里, Perm 区 中的所有内容
中字符串常量移至堆内存,其他内容包括类元信息、字段、静态属性、方法、常量等
都移动至无空间内。
图 4-10 中显示在常量池中的 String, 其实际对象是被保存在堆内存中的。
4.4 JVM Stack (虚拟机栈)
**虚拟机栈作用:描述JAVA方法执行的内存区域。**方法调用到执行完成~入栈到出栈的过程。栈顶的帧称为栈帧,正在执行的方法称为当前方法,栈帧是方法运行的基本结构。StackOverflowError表示请求的栈溢出,内存耗尽。
栈帧在整个 JVM 体系中的地位颇高, 包括局部变量表、操作栈、动态连接、方法返回地址等。
1 局部变量表
2 操作栈
public int simpleMethod(){
//将常量13压入操作栈、保存到局部变量表的slot_1中
int x=13;
//将常量14压入操作栈、保存到局部变量表的slot_2中
int y=14;
//将slot_1元素压入操作栈,将slot_2元素压入操作栈,
//再取出来到CPU中加法,并压回操作栈,把栈顶结果保存到局部变量表的slot_3中
int z= x+y;
//返回栈顶元素值
return z;
}
** 3 动态链接**
每个枝帧中包含一个在常量池中对当前方法的引用 , 目的是支持方法调用过程的动态连接。
4 方法返回地址
4.5 Native Method Stacks (本地方法栈)
本地方法栈作用:虚拟机栈“主内 ”, 而本地方法栈“主外”。本地方法栈为 Native 方法服务。
本地方法可以通过 JNI ( Java Native Int rface )来访问虚拟机运行时的数据区 ,甚至可以调用寄存器,具有和 JVM 相同的能力和权限。对于内存 不足的情况 本地方法枝还是会抛出 native heap OutOfMemory。
4.6 Program Counter Register(程序计数寄存器)
程序计数寄存器作用:CPU 只有把数据装载到寄存器才能够运行。保证在多线程并发执行过程中,保证分毫无差。
**问题:**由于CPU时间片轮限制,众多线程在并发执行过程中,导致经常中断或恢复,如何保证分毫无差呢?
每个线程在创建后,都会产生 自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等, 线程执行或恢复都要依赖程序计数器。程序计数器在各个线程之间互不影响,此区域 也不会发生内存溢出异常。
4.7 运行时数据区域总结
1 Heap堆区作用:Heap 是 OOM 故障最主要的发源地 ,它存储着几乎所有的实例对象。
堆由垃圾收集器自动回收 , 堆区由各子线程共享使用。 堆的内存空间既可以固定大小 , 也可以在运行时动态地调整。
2 元空间作用:存储常量池、方法元信息、类元信息。字符串常量String存在堆内存。
3 虚拟机栈作用:描述JAVA方法执行的内存区域。方法调用到执行完成~入栈到出栈的过程。
栈顶的帧称为栈帧,正在执行的方法称为当前方法,栈帧是方法运行的基本结构。StackOverflowError表示请求的栈溢出,内存耗尽。
4 本地方法栈作用:虚拟机栈“主内 ”, 而本地方法栈“主外”。本地方法栈为 Native 方法服务。
本地方法可以通过 JNI ( Java Native Int rface )来访问虚拟机运行时的数据区 ,甚至可以调用寄存器。
5 程序计数寄存器作用:CPU 只有把数据装载到寄存器才能够运行。保证在多线程并发执行过程中,保证分毫无差。
每个线程在创建后,都会产生 自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等。
5. 对象实例化
Java 是面向对象的静态强类型语言,根据某个类声明一个引用变量指向被创建的对象。
问题:在实例化对象的过程中,JVM会发生什么化学反应?
5.1 从字节码的进行分析
1、NEW类加载
2、DUP栈顶复制引用变量
3、INVOKESPECIAL初始化
5.2 从执行步骤的角度分析
1、确认类元信息是否存在
2、分配对象内存
3、设定默认值
4、设置对象头
5、执行init方法
6. 垃圾回收
垃圾回收( Garbage Collection, GC )。垃圾回收的主要目的是清除不再使用的对象,自动释放内存。
6.1 对象是否存活的标准
问题:GC 是如何判断对象是否可以被回收的呢?
为了判断对象是否存活 , JVM 引人了GC Roots。某个失去任何引用的对象,或者两个互相环岛状循环引用的对象等,可以直接回收。
问题:什么对象可以作为 GC Roots 呢?
比如类静态属性中引用的对象、常量引用的对象、虚拟机栈中寻引用的对象、本地方法栈中引用的对象等。
6.2 垃圾回收的相关算法
“标记-清除算法”。
该算法从GC Roots出发,依次标记有引用关系的对象,将没有被标记的对象清除。此算法会带来大量的空间碎片,要分配较大连续空间容易出现FGC。
“标记-整理算法”。
该算法从GC Roots出现标记存活对象,然后将存活对象整理导内存空间的一端,形成已使用的连续空间,最后把已使用空间外的部分全部清理掉。
“Mark-Copy算法”(主流YGC算法新生代垃圾回收)。也称标记-复制
为了并行地标记和整理,将空间分为两块,每次只激活一块。垃圾回收时只需要把存活对象复制到另一块未激活空间,将未激活空间标记为已激活,将已激活空间标记为未激活,然后清除原空间对象,如此反复置换清除。每次只使用堆区一块Eden区和Survior区,减少了内存空间的浪费。
6.2 垃圾回收器
垃圾回收器( Garbage Collector )是实现垃圾回收算法并应用在 JVM 环境中的内存管理模块 。
Serial 回收器
Serial 回收器是一个主要应用于 YGC 的垃圾回收器,采用串行单线程的方式完成 GC 任务。其中"Stop The World"简称STW,即垃圾回收的某个阶段会暂停整个应用程序的执行。标记-复制算法YGC
CMS 回收器
CMS 回收器 (Concurrent Mark Sweep Collector) 是回收停顿时间比较短、目前比较常用的垃圾回收器,采用"标记-清除算法"。
通过初始标记(Initial Mark) 、并发标记(Concurrent Mark )、重新标记(Remark)、并发清除(Concurrent Sweep)四个步骤完成垃圾回收工作。
G1回收器
Hotspot 在 JDK7 中 推出了新一代 G1 ( Garbage-First Garbage Collector ) 垃圾回收 ,通过去-XX:+UseG1GC参数启用。G1采用"Mark-Copy算法"。
GI 将 Java 堆空间分割成了若干相同大小的 区域,即 region ,包括 Eden 、Survivor 、 Old 、 Humongous 四种类型。
7. JVM监控工具
7.1 jconsole
Jconsole(Java Monitoring and Management Console)是从 java5 开始,在 JD K中自带的 java 监控和管理控制台,用于对 JVM 中内存,线程和类等的监控,是一个基于 JMX(java management extensions)的 GUI 性能监测工具。
7.2 VisualVM
VisualVM(All-in-One Java Troubleshooting Tool)是功能最强大的运行监视和故障处理程序之一,曾经在很长一段时间内是 Oracle 官方主力发展的虚拟机故障处理工具。
8. JVM 调优选择
8.1 选择合适的垃圾回收器
- CPU 单核:那么毫无疑问 Serial 垃圾收集器是你唯一的选择;
- CPU 多核:关注吞吐量 ,那么选择 PS+PO 组合;JDK8默认
- CPU 多核:关注用户停顿时间,JDK 版本 1.6 或者 1.7,那么选择 CMS;
- CPU 多核:关注用户停顿时间,JDK1.8 及以上,JVM 可用内存 6G 以上,那么选择 G1。
参数配置:
//设置Serial垃圾收集器(新生代)
开启:-XX:+UseSerialGC
//设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
开启 -XX:+UseParallelOldGC
//CMS垃圾收集器(老年代)
开启 -XX:+UseConcMarkSweepGC
//设置G1垃圾收集器
开启 -XX:+UseG1GC
8.2 调整内存大小
现象:垃圾收集频率非常频繁。
原因:如果内存太小,就会导致频繁的需要进行垃圾收集才能释放出足够的空间来创建新的对象,所以增加堆内存大小的效果是非常显而易见的。
注意:如果垃圾收集次数非常频繁,但是每次能回收的对象非常少,那么这个时候并非内存太小,而可能是内存泄露导致对象无法回收,从而造成频繁 GC。
参数配置:
//设置堆初始值
指令1:-Xms2g
指令2:-XX:InitialHeapSize=2048m
//设置堆区最大值
指令1:`-Xmx2g`
指令2: -XX:MaxHeapSize=2048m
//新生代内存配置
指令1:-Xmn512m
指令2:-XX:MaxNewSize=512m
8.3 设置符合预期的停顿时间
现象:程序间接性的卡顿
原因:如果没有确切的停顿时间设定,垃圾收集器以吞吐量为主,那么垃圾收集时间就会不稳定。
注意:不要设置不切实际的停顿时间,单次时间越短也意味着需要更多的 GC 次数才能回收完原有数量的垃圾.
参数配置:
//GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间
-XX:MaxGCPauseMillis
8.4 调整内存区域大小比率
现象:某一个区域的GC频繁,其他都正常。
原因:如果对应区域空间不足,导致需要频繁GC来释放空间,在JVM堆内存无法增加的情况下,可以调整对应区域的大小比率。
注意:也许并非空间不足,而是因为内存泄造成内存无法回收,从而导致 GC 频繁。
参数配置:
//survivor区和Eden区大小比率
指令:-XX:SurvivorRatio=6 //S区和Eden区占新生代比率为1:6,两个S区2:6
//新生代和老年代的占比
-XX:NewRatio=4 //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2
8.5 调整对象升老年代的年龄
现象:老年代频繁 GC,每次回收的对象很多。
原因:如果升代年龄小,新生代的对象很快就进入老年代了,导致老年代对象变多,而这些对象其实在随后的很短时间内就可以回收,这时候可以调整对象的升级代年龄,让对象不那么容易进入老年代解决老年代空间不足频繁 GC 问题。
注意:增加了年龄之后,这些对象在新生代的时间会变长可能导致新生代的 GC 频率增加,并且频繁复制这些对象新生的 GC 时间也可能变长。
配置参数:
//进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,默认值7
-XX:InitialTenuringThreshol=7
8.6 调整大对象的标准
现象:老年代频繁 GC,每次回收的对象很多,而且单个对象的体积都比较大。
原因:如果大量的大对象直接分配到老年代,导致老年代容易被填满而造成频繁 GC,可设置对象直接进入老年代的标准。
注意:这些大对象进入新生代后可能会使新生代的 GC 频率和时间增加。
配置参数:
//新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。
-XX:PretenureSizeThreshold=1000000
8.7 调整GC的触发时机
现象:CMS,G1 经常 Full GC,程序卡顿严重。
原因:G1 和 CMS 部分 GC 阶段是并发进行的,业务线程和垃圾收集线程一起工作,也就说明垃圾收集的过程中业务线程会生成新的对象,所以在 GC 的时候需要预留一部分内存空间来容纳新产生的对象,如果这个时候内存空间不足以容纳新产生的对象,那么JVM就会停止并发收集暂停所有业务线程(STW)来保证垃圾收集的正常运行。这个时候可以调整GC触发的时机(比如在老年代占用 60% 就触发 GC),这样就可以预留足够的空间来让业务线程创建的对象有足够的空间分配。
注意:提早触发 GC 会增加老年代 GC 的频率。
配置参数:
//使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小
-XX:CMSInitiatingOccupancyFraction
//G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%
-XX:G1MixedGCLiveThresholdPercent=65
8.8 调整 JVM本地内存大小
现象:GC 的次数、时间和回收的对象都正常,堆内存空间充足,但是报 OOM
原因:JVM 除了堆内存之外还有一块堆外内存,这片内存也叫本地内存,可是这块内存区域不足了并不会主动触发 GC,只有在堆内存区域触发的时候顺带会把本地内存回收了,而一旦本地内存分配不足就会直接报 OOM 异常。
注意:本地内存异常的时候除了上面的现象之外,异常信息可能是 OutOfMemoryError:Direct buffer memory。解决方式除了调整本地内存大小之外,也可以在出现此异常时进行捕获,手动触发 GC(System.gc())。
配置参数:
XX:MaxDirectMemorySize
9. JVM 调试实战
为什么要调整JVM
JVM调优背景
生产环境中的问题
生产环境发生了内存溢出该如何处理?
生产环境应该给服务器分配多少内存合适?
如何对垃圾回收器的性能进行调优?
生产环境 CPU 负载飙高该如何处理?
生产环境应该给应用分配多少线程合适?
不加 log,如何确定请求是否执行了某一行代码?
不加 log,如何实时查看某个方法的入参与返回值?
为什么要调优
- 防止出现 OOM
- 解决 OOM
- 减少 Full GC 出现的频率
不同阶段的考虑
- 上线前
- 项目运行阶段
- 线上出现 OOM
JVM调优方案
监控的依据
- 运行日志
- 异常堆栈
- GC 日志
- 线程快照
- 堆转储快照
调优的大方向
- 合理地编写代码
- 充分并合理的使用硬件资源
- 合理地进行 JVM 调优
JVM性能优化的步骤
第 1 步:性能监控
GC 频繁
cpu load 过高
OOM
内存泄露
死锁
程序响应时间较长
第 2 步:性能分析
打印 GC 日志,通过 GCviewer 或者 Universal JVM GC analyzer - Java Garbage collection log analysis made easy 来分析异常信息
灵活运用命令行工具、jstack、jmap、jinfo 等
dump 出堆文件,使用内存分析工具分析文件
使用阿里 Arthas、jconsole、JVisualVM 来实时查看 JVM 状态
jstack 查看堆栈信息
第 3 步:性能调优
- 适当增加内存,根据业务背景选择垃圾回收器
- 优化代码,控制内存使用
- 增加机器,分散节点压力
- 合理设置线程池线程数量
- 使用中间件提高程序效率,比如缓存、消息队列等
- 其他……
性能评价/测试指标
**1 停顿时间(或响应时间):**提交请求和返回该请求的响应之间使用的时间,一般比较关注平均响应时间。常用操作的响应时间列表:
2 垃圾回收环节:
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
- -XX:MaxGCPauseMillis 表示每次GC最大的停顿毫秒
3 吞吐量
- 对单位时间内完成的工作量(请求)的量度
- 在 GC 中:运行用户代码的事件占总运行时间的比例(总运行时间:程序的运行时间+内存回收的时间)
- 吞吐量为 1-1/(1+n),其中-XX::GCTimeRatio=n
4 并发数
- 同一时刻,对服务器有实际交互的请求数
5 内存占用
- Java 堆区所占的内存大小
6 相互间的关系
以高速公路通行状况为例
- 吞吐量:每天通过高速公路收费站的车辆的数据
- 并发数:高速公路上正在行驶的车辆的数目
- 响应时间:车速
9.1 网站流量浏览量暴增后,网站反应页面响很慢
1、问题推测:在测试环境测速度比较快,但是一到生产就变慢,所以推测可能是因为垃圾收集导致的业务线程停顿。
2、定位:为了确认推测的正确性,在线上通过 jstat -gc 指令 看到 JVM 进行 GC 次数频率非常高,GC 所占用的时间非常长,所以基本推断就是因为 GC 频率非常高,所以导致业务线程经常停顿,从而造成网页反应很慢。
3、解决方案:因为网页访问量很高,所以对象创建速度非常快,导致堆内存容易填满从而频繁 GC,所以这里问题在于新生代内存太小,所以这里可以增加 JVM 内存就行了,所以初步从原来的 2G 内存增加到 16G 内存。
4、第二个问题:增加内存后的确平常的请求比较快了,但是又出现了另外一个问题,就是不定期的会间断性的卡顿,而且单次卡顿的时间要比之前要长很多。
5、问题推测:之前的优化加大了内存,所以推测可能是因为内存加大了,从而导致单次 GC 的时间变长从而导致间接性的卡顿。
6、定位:还是通过 jstat -gc 指令 查看到 的确 FGC 次数并不是很高,但是花费在 FGC 上的时间是非常高的,根据 GC 日志 查看到单次 FGC 的时间有达到几十秒的。
7、解决方案:因为 JVM 默认使用的是 PS+PO 的组合,PS+PO 垃圾标记和收集阶段都是 STW,所以内存加大了之后,需要进行垃圾回收的时间就变长了,所以这里要想避免单次 GC 时间过长,所以需要更换并发类的收集器,因为当前的 JDK 版本为 1.7,所以最后选择 CMS 垃圾收集器,根据之前垃圾收集情况设置了一个预期的停顿的时间,上线后网站再也没有了卡顿问题。
9.2 后台导出数据引发的 OOM
问题描述:公司的后台系统,偶发性的引发 OOM 异常,堆内存溢出。
1、因为是偶发性的,所以第一次简单的认为就是堆内存不足导致,所以单方面的加大了堆内存从 4G 调整到 8G。
2、但是问题依然没有解决,只能从堆内存信息下手,通过开启了 -XX:+HeapDumpOnOutOfMemoryError 参数 获得堆内存的 dump 文件。
3、VisualVM 对堆 dump 文件进行分析,通过 VisualVM 查看到占用内存最大的对象是 String 对象,本来想跟踪着 String 对象找到其引用的地方,但 dump 文件太大,跟踪进去的时候总是卡死,而 String 对象占用比较多也比较正常,最开始也没有认定就是这里的问题,于是就从线程信息里面找突破点。
4、通过线程进行分析,先找到了几个正在运行的业务线程,然后逐一跟进业务线程看了下代码,发现有个引起我注意的方法,导出订单信息。
5、因为订单信息导出这个方法可能会有几万的数据量,首先要从数据库里面查询出来订单信息,然后把订单信息生成 excel,这个过程会产生大量的 String 对象。
6、为了验证自己的猜想,于是准备登录后台去测试下,结果在测试的过程中发现到处订单的按钮前端居然没有做点击后按钮置灰交互事件,结果按钮可以一直点,因为导出订单数据本来就非常慢,使用的人员可能发现点击后很久后页面都没反应,结果就一直点,结果就大量的请求进入到后台,堆内存产生了大量的订单对象和 EXCEL 对象,而且方法执行非常慢,导致这一段时间内这些对象都无法被回收,所以最终导致内存溢出。
7、知道了问题就容易解决了,最终没有调整任何 JVM 参数,只是在前端的导出订单按钮上加上了置灰状态,等后端响应之后按钮才可以进行点击,然后减少了查询订单信息的非必要字段来减少生成对象的体积,然后问题就解决了。
9.3 Window JVM调优
查询JDK所用虚拟机
java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)
//支持 Java 8, 使用的是 Oracle 的64位HotSpot虚拟机。
//HotSpot VM 是 OracleJDK / SunJDK 以及 OpenJDK 里的 JVM 实现。使用最广泛,JDK默认安装的。
Java问题诊断和排查工具(查看JVM参数、内存使用情况及分析等)
常用查询命令
JPS (打印Java进程信息)
使用场景 : 查看当前机器的所有Java进程信息(可追踪到应用进程ID 、启动类名、文件路径。)。
jps 显示当前所有java进程pid的命令
jps -v 输出传递给JVM的参数
jstack(JVM线程信息监控)
使用场景: 查看JVM线程信息 和生成线程快照。
jstack pid 主要用于生成指定进程当前时刻的线程快照,线程快照是当前java虚拟机每一条线程正在执行的方法堆栈的集合。分析线程栈
Jmap(JVM内存占用信息和快照)
使用场景: 监控堆内存使用情况和对象占用情况, 生成堆内存快照文件,查看堆内存区域配置信息。
**jmap **打印指定java进程的共享对象内存映射或堆内存细节。堆Dump是反映堆使用情况的内存镜像,其中主要包括系统信息、虚拟机属性、完整的线程Dump、所有类和对象的状态等。
**jmap pid **共享对象的起始地址、映射大小、共享对象路径的全程。
jmap -heap pid:查看堆使用情况
jmap -histo pid:查看堆中对象数量和大小
Jstat (JVM内存信息统计)
使用场景 :用于查看各个功能和区域的统计信息(如:类加载、编译相关信息统计,各个内存区域GC概况和统计)
jstat-gc pid: 统计垃圾回收堆的行为
Jinfo(JVM参数查看和修改)
使用场景: 查看和调整JVM启动和运行参数。
Jinfo pid 查看JVM整个系统参数信息
jinfo -flag [参数名] pid 查看某个具体参数
jinfo -flag启动某个配置
java查询JVM配置参数
查询JVM配置参数
java -XX:+PrintCommandLineFlags
C:\Program Files\Java\jdk1.8.0_91\bin>java -XX:+PrintCommandLineFlags
-XX:InitialHeapSize=199690240 //初始堆大小bytes 这里23M
-XX:MaxHeapSize=3195043840 //最大堆大小bytes 这里380M
-XX:+PrintCommandLineFlags //PrintCommandLineFlags 是打印那些被新值覆盖的项
-XX:+UseCompressedClassPointers //UseCompressedClassPointers:类指针压缩
-XX:+UseCompressedOops //UseCompressedOops:普通对象指针压缩
-XX:-UseLargePagesIndividualAllocation //关闭减少处理器 TLB 缓存压力的技术
-XX:+UseParallelGC //设置并行收集器 “Parallel Scavenge” + "Parallel Old"组合
查询JVM配置参数
java -XX:+PrintFlagsFinal -version |FINDSTR /i “:”
C:\Program Files\Java\jdk1.8.0_91\bin>java -XX:+PrintFlagsFinal -version |FINDSTR /i ":"
intx CICompilerCount := 3 {product}
uintx InitialHeapSize := 201326592 //初始堆大小bytes {product}
uintx MaxHeapSize := 3196059648 //最大堆大小bytes {product}
uintx MaxNewSize := 1065353216 //新生代分配内存最大上限,小于-Xmx的值; {product}
uintx MinHeapDeltaBytes := 524288 //要扩容或者缩容最小扩/缩多少 {product}
uintx NewSize := 67108864 //新生代初始内存的大小,应该小于-Xms的值; {product}
uintx OldSize := 134217728 //老年代的默认大小 {product}
bool PrintFlagsFinal := true //打印所有的默认参数设置 {product}
bool UseCompressedClassPointers := true {lp64_product}
bool UseCompressedOops := true {lp64_product}
bool UseLargePagesIndividualAllocation := false {pd product}
bool UseParallelGC := true {product}
java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)
配置Windows JVM参数
1、系统环境中配置(推荐)
虚拟机内存的大小除了在web容器中设置,我们还可以通过系统环境变量来设置,下面看看设置步骤:
打开windows系统环境变量,在系统变量中,新建变量JAVA_OPTS,值设置为:
8G物理内存JVM虚拟机配置
idea优化命令
-XX:+UseG1GC -XX:+UseStringDeduplication -XX:MaxGCPauseMillis=95 -Xms5120m -Xmx5120m -Xmn1024m -Xss128k -XX:MaxTenuringThreshold=0
jdk8 使用G1垃圾回收器
-XX:-UseParallelGC
-Xms3550m
-Xmx3550m
-Xmn1024m
-Xss128k
-XX:+UseG1GC
-XX:+UnlockExperimentalVMOptions
-XX:+UseStringDeduplication
-XX:MaxGCPauseMillis=95
-XX:NewRatio=4
-XX:SurvivorRatio=4
-XX:MatespaceSize=6
优化前启动
优化前jmeter测试
优化后启动
优化后jmeter测试
jvm中常用的参数含义:
每个对象都有一个计数器,每次YGC 都会加1,配置计数器的值到达某个阐值的时候, 对象从
新生代晋升至老年。
-XX:MaxTenuringThreshold
为功能点比较多,在运行过程中,要不断动态加载很多的类,经常出现致命错误。为了解决该问题, 需要设定运行参数
-XX: MaxPermSize= 1280m ,
以给NM 设置运行参数让JVM 遇到OOM 异常时能输出堆内信息。
-XX:+HeapDumpOnOutOfMemoryError
1: -Xmx
指定 jvm 的最大内存大小 , 如 :-Xmx=2048M(根据设备物理内存以及实际情况设定,建议为物理内存的80%)
2: -Xms
指定 jvm 的初始内存大小 , 如 :-Xms=2048M, 高并发应用, 建议和-Xmx一样, 防止因为内存收缩/突然增大带来的性能影响.
3: -Xmn
指定 jvm 中 New Generation (堆空间的新生代空间)的大小 , 如 :-Xmn=256m。 这个参数很影响性能, 如果你的程序需要比较多的临时内存, 建议设置到512M, 如果用的少, 尽量降低这个数值, 一般来说128/256足以使用了。
4: -XX:PermSize (java7,java8移除)
指定 jvm 中 Perm Generation (永久存储区)的最小值 , 如 :-XX:PermSize=32m。 这个参数需要看你的实际情况。可以通过jmap 命令看看到底需要多少。
5: -XX:MaxPermSize(java7,java8移除)
指定 Perm Generation 的最大值 , 如 :-XX:MaxPermSize=64m
6: -Xss
指定线程桟大小 , 如 :-Xss=128k, 一般来说,webx框架下的应用需要256K。 如果程序中有大规模的递归行为,请考虑设置到512K/1M。 这个需要全面的测试才能知道。 不过,256K已经很大了。 这个参数对性能的影响比较大的。
7:-XX:MatespaceSize(java8)和-XX:MatespaceSize(java8)
JVM加载类的时候,需要记录类的元数据,这些数据会保存在一个单独的内存区域内,在Java 7里,这个空间被称为永久代(Permgen),在Java 8里,使用元空间(Metaspace)代替了永久代。由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为512M。
2、使用命令配置
java命令配置
8G物理内存JVM虚拟机配置
java
-XX:+UseG1GC
-Xms3550m
-Xmx3550m
-Xmn1024m
-Xss128k
-XX:NewRatio=4
-XX:SurvivorRatio=4
-XX:MatespaceSize=512m
-XX:MaxMetaspaceSize=512m
-XX:MaxTenuringThreshold=0
使用 -Xms 设置堆的初始空间大小
java -Xms20m -Xmx30m GCDemo
JVM 提供了参数 -Xmn 来设置年轻代内存的大小
java -Xms20m -Xmn10M GCDemo
使用 -XX:SurvivorRatio 这个参数,该参数设置 eden / from 空间的比例关系
-XX:SurvivorRatio = eden/from = eden/to
java -Xms20m -Xmn10M -XX:SurvivorRatio=2 -XX:+PrintGCDetails GCDemo
永久代(JDK1.7)所加载的类信息都放在永久代中。用 -XX:PermSize 设置永久代初始大小,用 -XX:MaxPermSize 设置永久代最大大小。
java -XX:PermSize=10m -XX:MaxPermSize=50m -XX:+PrintGCDetails GCDemo
元空间(JDK1.8)在 JDK1.8 之时,永久代被移除,取而代之的是元空间
java -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=50m -XX:+PrintGCDetails GCDemo
栈空间是每个线程各自有的一块区域,如果栈空间太小,也会导致 StackOverFlow 异常。而要设置栈空间大小,只需要使用 -Xss 参数就可以。
java -Xss2m GCDemo
在 JVM 中还有一块内存,它独立于 JVM 的堆内存,它就是:直接内存。
java -XX:MaxDirectMemorySize=50m GCDemo
实时修改JVM参数:jinfo -flag name = value PID
如果要对参数进行实时调整:则需要看到参数后面有manageable的才能被实时调整
9.4 Linux JVM调优
基本同windows,只是命令上大同小异。
使用命令配置
8G物理内存JVM虚拟机配置
java -XX:+UseG1GC-Xms3550m -Xmx3550m -Xmn1024m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MatespaceSize=512m -XX:MaxMetaspaceSize=512m -XX:MaxTenuringThreshold=0