JVM性能调优与实战基础理论篇-上
Java虚拟机
概述

JVM是一种规范,通过Oracle Java 官方文档找到JVM的规范查阅。Java虚拟机可以看做虚拟出来一台计算机,主体功能字节码指令集(汇编语言)和内存管理(栈、堆、方法区)等
常见的JVM实现
- Hotspot:目前使用的最多的 Java 虚拟机。
- Jrocket:原来属于BEA 公司,曾号称世界上最快的 JVM,后被 Oracle 公司收购,合并于 Hotspot
- J9: IBM 有自己的 java 虚拟机实现,它的名字叫做 J9. 主要是用在 IBM 产品(IBM WebSphere 和 IBM 的 AIX 平台上)
- TaobaoVM: 只有一定体量、一定规模的厂商才会开发自己的虚拟机,比如淘宝有自己的 VM,它实际上是 Hotspot 的定制版,专门为淘宝准备的,阿里、天 猫都是用的这款虚拟机。
- LiquidVM: 它是一个针对硬件的虚拟机,它下面是没有操作系统的(不是 Linux 也不是 windows),下面直接就是硬件,运行效率比较高。
- zing: 它属于 zual 这家公司,非常牛,是一个商业产品,很贵!它的垃圾回收速度非常快(1 毫秒之内),是业界标杆。它的一个垃圾回收的算法后来被 Hotspot 吸收才有了现在的 ZGC。

体系结构
我们通常所说的JDK,其实是指Java开发包,里面包含Java开发用到的工具集。
- JDK体系结构
- Java运行环境(JRE)和开发工具(编译器,调试器,javadoc等)。
- JRE:由JVM,Java运行时类库,动态链接库等组成;它为Java提供了运行环境,其中重要的一环就是通过JVM将字节码解释成可执行的机器码。
- JDK的编译器Javac[.exe],会将Java代码编译成字节码(.class文件)。编译出的字节码在任何平台上都一样的内容,所以我们说Java语言是门跨平台语言;Writeonce, run anywhere。
- Java运行环境(JRE)和开发工具(编译器,调试器,javadoc等)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2vmjOtk2-1644747481396)(http://www.itxiaoshen.com:3001/assets/1644552030402KQTxi6TP.png)]****
- JVM和操作系统关系
- 引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
- 在运行时环境,JVM会将Java字节码解释成机器码。机器码和平台相关的(不同硬件环境、不同操作系统,产生的机器码不同),所以JVM在不同平台有不同的实现。目前JDK默认使用的实现是Hotspot VM。

- JVM架构:Java虚拟机主要分为五大模块:类装载器子系统、运行时数据区、执行引擎、本地方法接口和垃圾收集模块。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ut6GY2lN-1644747481400)(http://www.itxiaoshen.com:3001/assets/16445520369994chXCWt4.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2TtOAZ9M-1644747481409)(http://www.itxiaoshen.com:3001/assets/1644641759500Hs1z7mMd.png)]
逃逸分析(Escape Analysis)
概述
- 逃逸分析的是一个对象的动态作用域,2种情况
- 方法逃逸:对象通过参数传递传给了另一个方法。
- 线程逃特性逸:对象有另外的线程访问。
- 逃逸分析的目的是确认一个对象是否只可能当前线程能访问。
- 逃逸分析可以带来一定程度上的性能优化。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
- JIT(Just In Time Compiler)即时编译技术,在即时编译过程中JVM可能会对我们的代码做一些优化如逃逸分析等。
优化策略
逃逸分析的优化策略主要有三个:对象可能分配在栈上、分离对象或标量替换、消除同步锁。

- 对象可能分配在栈上:JVM通过逃逸分析,分析出新对象的使用范围,就可能将对象在栈上进行分配。栈分配可以快速地在栈帧上创建和销毁对象,不用再将对象分配到堆空间,可以有效地减少 JVM 垃圾回收的压力。
- 分离对象或标量替换:当JVM通过逃逸分析,确定要将对象分配到栈上时,即时编译可以将对象打散,将对象替换为一个个很小的局部变量,我们将这个打散的过程叫做标量替换。将对象替换为一个个局部变量后,就可以非常方便的在栈上进行分配了。所谓标量就是不能再分割的变量,如Java基本数据类型。
- 同步锁消除:如果JVM通过逃逸分析,发现一个对象只能从一个线程被访问到,则访问这个对象时,可以不加同步锁。如果程序中使用了synchronized锁,则JVM会将synchronized锁消除。
逃逸分析开关
逃逸分析其实并不是新概念,早在1999年就有论文提出了该技术。但在Java中算是新颖而前言的优化技术,从 JDK1.6才开始引入该技术,JDK1.7开始默认开启逃逸分析,也可通过开关控制
- -XX:+DoEscapeAnalysis开启逃逸分析
- -XX:-DoEscapeAnalysis 关闭逃逸分析
- -XX:+EliminateAllocations开启标量替换
- -XX:-EliminateAllocations 关闭标量替换
- -XX:+EliminateLocks开启锁消除
- -XX:-EliminateLocks 关闭锁消除
- 开启标量替换或锁消除 必须打开逃逸分析开关
常见面试问题
是不是所有的对象和数组都会在堆内存分配空间?不一定,只要掌握了逃逸分析的原理,那就很清楚知道Java中的对象不一定是在堆上分配的,因为JVM通过逃逸分析,能够分析出一个新对象的使用范围,并以此确定是否要将这个对象分配到堆上。
加了锁的代码锁就一定会生效吗?不一定
JVM运行模式
- 解释模式(Interpreted Mode):只使用解释器(-Xint 强制JVM使用解释模式),执行一行JVM字节码就编译一行为机器码
- 编译模式(Compiled Mode):只使用编译器(-Xcomp JVM使用编译模式),先将所有的JVM字节码一次编译为机器码,然后一次性执行所有机器码
- 混合模式(Mixed Mode):(-Xmixed 设置JVM使用混合模式)依然使用解释模式执行代码,但是对于一些“热点”代码采取编译器模式执行,这些热点代码对应的机器码会被缓存起来,下次执行无需再编译。JVM一般采用混合模式执行代码
| JVM运行模式 | 优点 | 适用场景 |
|---|---|---|
| 解释模式 | 启动快 | 只需要执行部分代码,且大多数代码只会执行一次的情况 |
| 编译模式 | 启动慢,但是后期执行速度快,比较占用内存1 | 适合代码可能会被反复执行的场景 |
| 混合模式 | 一般JVM所默认的模式 |
Java启动参数分类
- 标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
- 非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;jvm参数调优重点
- 非Stable参数(-XX),此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;jvm参数调优重点。可以通过java -XX:+PrintFlagsFinal -version 获取支持参数选项。
类加载
类加载过程
类加载:类加载器将class文件加载到虚拟机的内存
类的7个生命周期:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
加载:在硬盘上查找并通过IO读入字节码文件
连接:执行验证、准备、解析步骤
- 验证:校验字节码文件的正确性,不是必要的,可以关闭校验提高虚拟机加载类的速度
- 准备:给类的静态变量分配内存,并赋予默认值
- 解析:将符号引用替换为直接引用;通过javap -v将字节码文件反汇编为更可读的文件,像类,方法等一切的字面量都转换为符号引用#1...N。包括类解析,字段解析,方法解析,接口解析。即将字节码的静态字面关联(字符串,静态)转换为JVM内存中的动态指针关联(指针,动态)。

初始化:对类的静态变量初始化为指定的值,执行静态代码块

- 使用:new出对象程序中使用
- 卸载:执行垃圾回收
类加载器
概述

- 启动类加载器:C语言开发,加载Java核心类库。基于沙箱机制,只加载java、javax、sun包开头的类。
- 扩展类加载器:Java语言编写,由sun.misc.Launcher$ExtClassLoader实现,上级加载器为启动类加载器,负责加载jre/lib/ext扩展目录中的jar类包。
- 应用程序类加载器:Java语言编写,由sun.misc.Launcher$AppClassLoader实现,上级加载器为扩展类加载器,是默认的类加载器。负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类。
- 自定义类加载器:负责加载用户自定义路径下的类包
- 使用场景:字节码二进制流来自于网络,字节码文件不在指定的lib、ext、classpath路径下,需要对二进制流加工后才能得到字节码。
- 不同的类加载器加载同一个Class字节码文件后,在JVM中产生的类对象是不同的。同一个类加载器,Class实例在JVM中才是全局唯一。
- 在Java中,一个类用其
全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
类加载器获取示例
JVM预定义的类加载器为启动类加载器、扩展类加载器、应用程序类加载器。
package cn.itxs.classloader;
import cn.itxs.entity.User;
public class ClassLoaderMain {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESedeKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(User.class.getClassLoader().getClass().getName());
System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());
}
}

自定义类加载器示例
除了启动类加载器,所有类加载器都是ClassLoader的子类,所以我们可以通过继承ClassLoader来实现自己的类加载器。
package cn.itxs.entity;
public class Goods {
public void sayHello(){
System.out.println("hello goods!");
}
}
生成字节码文件Goods.class后剪切(不是拷贝,删掉类路径下的字节码文件)到D:\temp\cn\itxs\entity目录下
package cn.itxs.classloader;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
public class ItxsClassLoader extends ClassLoader{
private String classPath;
public ItxsClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadBytes(String name) throws IOException {
name = name.replaceAll("\\.","/");
FileInputStream fis = new FileInputStream(classPath+"/"+name+".class");
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);
while (true){
int i = fc.read(by);
if (i == 0 || i == -1){
break;
}
by.flip();
wbc.write(by);
by.clear();
}
fis.close();
return baos.toByteArray();
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = loadBytes(name);
return defineClass(name,bytes,0, bytes.length);
}catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
public static void main(String[] args) throws Exception {
ItxsClassLoader itxsClassLoader = new ItxsClassLoader("D:/temp");
Class clazz = itxsClassLoader.loadClass("cn.itxs.entity.Goods");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sayHello", null);
method.invoke(obj,null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}

双亲委派机制
- 双亲委派机制:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
- 当进行类加载的时候,虽然用户自定义类不会由Bootstrap ClassLoader或是Extension ClassLoader加载(由类加载器的加载范围决定),但是代码实现还是会一直委托到Bootstrap ClassLoader, 上层无法加载,再由下层是否可以加载,如果都无法加载,就会触发findClass,抛出ClassNotFoundException。
- 类加载器之间的层级关系并不是以继承的方式存在的,而是以组合的方式处理的。

- 双亲委派机制是在ClassLoader里的loadClass方法里实现的,如果想自己实现类加载器的话,可以继承ClassLoader后重写findClass方法,加载对应的类。源码实现流程简单如下:
- 首先判断该类是否已经被加载。
- 该类未被加载,如果父类不为空,交给父类加载。
- 如果父类为空,交给Bootstrap Classloader 加载。
- 如果类还是无法被加载到,则触发findClass,抛出ClassNotFoundException。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派机制作用
- 可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
- 保证了安全性。因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,这样可以有效的防止核心Java API被篡改。
打破双亲委派
- 双亲委派机制也非必然。比如Tomcat web容器里面部署了很多的应用程序,这些应用程序对于第三方类库的依赖版本却不一样,且通常这些第三方类库的路径又是一样的,如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。所以Tomcat破坏双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。
- Tomcat的类加载机制:为了实现隔离性,优先加载 Web 应用自己定义的类,不遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载。


**本人博客网站 **IT小神 www.itxiaoshen.com
JVM性能调优与实战基础理论篇-上的更多相关文章
- JVM性能调优与实战进阶篇-上
ZGC 诞生原因 Java生态非常强大,但还不够,有些场景仍处于劣势,而ZGC的出现可以让Java语言抢占其他语言的某些特定领域市场.比如 谷歌主导的Android手机系统显示卡顿. 证券交易市场,实 ...
- JVM性能调优与实战基础理论篇-中
JVM内存模型 概述 我们所说的JVM内存模型是指运行时数据区,用New出来的对象放在堆中,如每个线程中局部变量放在栈或叫虚拟机栈中,下图左边区域部分为栈内存的结构.如main线程包含程序炯酸器.线程 ...
- JVM性能调优与实战基础理论篇-下
JVM内存管理 JVM内存分配与回收策略 对象优先在Eden分配,如果Eden内存空间不足,就会发生Minor GC.虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在 ...
- JVM 性能调优实战之:使用阿里开源工具 TProfiler 在海量业务代码中精确定位性能代码
本文是<JVM 性能调优实战之:一次系统性能瓶颈的寻找过程> 的后续篇,该篇介绍了如何使用 JDK 自身提供的工具进行 JVM 调优将 TPS 由 2.5 提升到 20 (提升了 7 倍) ...
- JVM 性能调优实战之:一次系统性能瓶颈的寻找过程
玩过性能优化的朋友都清楚,性能优化的关键并不在于怎么进行优化,而在于怎么找到当前系统的性能瓶颈.性能优化分为好几个层次,比如系统层次.算法层次.代码层次…JVM 的性能优化被认为是底层优化,门槛较高, ...
- JVM性能调优实践——JVM篇
前言 在遇到实际性能问题时,除了关注系统性能指标.还要结合应用程序的系统的日志.堆栈信息.GClog.threaddump等数据进行问题分析和定位.关于性能指标分析可以参考前一篇JVM性能调优实践-- ...
- 使用阿里开源工具 TProfiler 在海量业务代码中精确定位性能代码 (jvm性能调优)
技术交流群:233513714 本文是<JVM 性能调优实战之:一次系统性能瓶颈的寻找过程> 的后续篇,该篇介绍了如何使用 JDK 自身提供的工具进行 JVM 调优将 TPS 由 2.5 ...
- JVM性能调优(3) —— 内存分配和垃圾回收调优
前序文章: JVM性能调优(1) -- JVM内存模型和类加载运行机制 JVM性能调优(2) -- 垃圾回收器和回收策略 一.内存调优的目标 新生代的垃圾回收是比较简单的,Eden区满了无法分配新对象 ...
- JVM性能调优(4) —— 性能调优工具
前序文章: JVM性能调优(1) -- JVM内存模型和类加载运行机制 JVM性能调优(2) -- 垃圾回收器和回收策略 JVM性能调优(3) -- 内存分配和垃圾回收调优 一.JDK工具 先来看看有 ...
随机推荐
- 字符串的展开expand
A. 字符串的展开(expand.cpp) 内存限制:64 MiB 时间限制:1000 ms 标准输入输出 题目类型:传统 评测方式:文本比较 题目描述 在初赛普及组的"阅读程序写结果&qu ...
- linux(CentOS7) 之 克隆虚拟机并配置网络(固定ip)
克隆机器 原机关机状态下,克隆. 下一步 选择当前状态,下一步 选择创建完整克隆,下一步 设置虚拟机名称(完成后可以修改).克隆机安装位置,下一步 等待克隆完成 克隆完成 配置网络 添加网卡(因为物理 ...
- unittest_测试报告(6)
用例执行完成后,执行结果默认是输出在屏幕上,其实我们可以把结果输出到一个文件中,形成测试报告. unittest自带的测试报告是文本形式的,如下代码: import unittest if __nam ...
- Python 国内镜像源
让 python pip 使用国内镜像源 国内镜像源: 清华:https://pypi.tuna.tsinghua.edu.cn/simple 阿里云:http://mirrors.aliyun.co ...
- js对cookie的操作:读、写、删
js读写cookie //JS操作cookies方法!//写cookiesfunction setCookie(name,value){var Days = 30;var exp = new Date ...
- 正则验证&模态框
在日常生活中,凡是需要表单验证的都会用到正则验证.下面拿一个简单的带有模态框的正则验证的小demo看一下 <style> /* 遮罩层 */ .ma ...
- Mysql实训任务书
注:图片如果损坏,点击文章链接:https://www.toutiao.com/i6635189537079296526/ 什么是数据库:数据库(Database)是按照数据结构来组织.存储和管理数据 ...
- vue-json-editor可视化编辑器的介绍与应用
vue-json-editor可视化编辑器 最近项目中有用到json编辑器,我选用了这款vue的编辑器,看起来也是比较简洁,接下来就具体介绍一下它,以及内部属性. 一.vue-json-editor的 ...
- JS调用堆栈
调用栈 JavaScript 是一门单线程的语言,这意味着它只有一个调用栈,因此,它同一时间只能做一件事.如果我们运行到一个函数,它就会将其放置到栈顶.当从这个函数返回的时候,就会将这个函数从栈顶弹出 ...
- 微服务架构 | 12.1 使用 Apache Dubbo 实现远程通信
目录 前言 1. Dubbo 基础知识 1.1 Dubbo 是什么 1.2 Dubbo 的架构图 1.3 Spring Cloud 与 Dubbo 的区别 1.4 Dubbo 的特点 1.5 Dubb ...