copyright 个人网站,福州模板做网站,wordpress手机登录设置方法,无锡网站制作平台目录
初识JVM
JVM是什么#xff1f;
JVM的功能
解释、即时编译和运行
内存管理
常见的JVM
JVM虚拟机规范
HotSpot的发展历程
JVM的组成
字节码文件详解
应用场景
以正确姿势打开字节码文件
编辑字节码文件的组成
基本信息 Magic魔数
主副版本号
常量池
接口…目录
初识JVM
JVM是什么
JVM的功能
解释、即时编译和运行
内存管理
常见的JVM
JVM虚拟机规范
HotSpot的发展历程
JVM的组成
字节码文件详解
应用场景
以正确姿势打开字节码文件
编辑字节码文件的组成
基本信息 Magic魔数
主副版本号
常量池
接口
字段
方法
案例分析
案例分析
案例分析
属性
常用工具
javap -v命令
IDEA插件-jclasslib
阿里Arthas
入门使用
监控面板
查看字节码信息
案例分析
类的生命周期重点
应用场景
生命周期概述
加载阶段
查看内存中的对象
连接阶段
验证阶段
版本号的检测
准备阶段
静态变量分配内存初始值
解析阶段
初始化阶段
何时初始化
clinit指令
初始化的要点
案例分析
案例分析
案例分析
案例分析
常量折叠 初识JVM
JVM是什么
JVMJava Virtual Machine中文名Java虚拟机
JVM就是一款软件用来运行Java字节码文件
将字节码文件【解释】配合JIT、AOT编译器成【机器码】计算机能直接运行机器码 机器码是计算机能够直接运行的指令集机器码由二进制数字组成通常以0和1的形式表示。这些指令被处理器CPU解码和执行 关于JIT和AOT可以看我另一篇博客【Java基础面试题003】Java的JIT | AOT是什么_java jit面试-CSDN博客 JVM的功能
JVM 包含内存管理、解释执行虚拟机指令、即时编译三大功能
解释、即时编译和运行 将字节码文件解释即时编译成机器码交给计算机执行即时编译对热点代码进行优化提升执行效率 无论JVM到底是编译字节码还是解释字节码最终都是生成机器码 由于JVM需要实时解释虚拟机指令所以Java语言如果不做任何优化性能不如C、C等语言 JVM提供了即时编译JIT进行性能的优化尤其是C2编译器最终能勉强接近C、C的运行性能接近还是勉强~~在特性场景下有机会实现超越 Java需要实时解释主要是为了支持跨平台特性 内存管理 自动给对象、方法等分配内存自动的垃圾回收机制回收不再使用的对象 常见的JVM
常见的JVM有HotSpot、GraalVM、OpenJ9等另外DragonWell龙井JDK也 提供了一款功能增强版的JVM。其中使用最广泛的是HotSpot虚拟机。 看得出Java的生态真的很好不过我用的JDK是这一家的
Adoptium Eclipse TemurinHome | Adoptium JVM虚拟机规范
官网地址Java SE Specifications (oracle.com)
《Java虚拟机规范》由Oracle制定内容主要包含了Java虚拟机在设计和实现时需要遵守的规范主 要包含class字节码文件的定义、类和接口的加载和初始化、指令集等内容。
《Java虚拟机规范》是对虚拟机设计的要求而不是对Java设计的要求也就是说虚拟机可以运行在 其他的语言比如Groovy、Scala生成的class字节码文件之上。 HotSpot的发展历程 JVM的组成 字节码文件详解
应用场景
面试回答
学习字节码文件的组成可以更深入的理解Java代码说点实际的最起码面试的时候有用
比如下列面试题 如果从Java语法层面回答这个题面试官可能会觉得你的道行不深如果从字节码文件中的字节码指令来回答这个题面试官可能会觉得你对JVM的了解还不错 这里给个例子复习一下后自增
i先返回值用来赋值操作后自增但是自增的值丢失了没有被保存下来可以理解为自增操作被赋值操作覆盖了 int i 0;i i;System.out.println(i); // 0int j 0;j j;System.out.println(j); // 1 解决版本冲突 系统升级 以正确姿势打开字节码文件
一般的记事本包括高级记事本打开字节码文件会部分乱码毕竟.java文件编译成.class文件用的编码格式有很多不单纯是单一的UTF8啥的 为了更直观查看.class文件可以用下面这一款工具【jclasslib】
官网GitHub - ingokegel/jclasslib: jclasslib bytecode editor is a tool that visualizes all aspects of compiled Java class files and the contained bytecode.
版本号v6.0.5 字节码文件的组成 下面围绕这段代码进行分析字节码文件的组成
public interface Test {void printOneLine(int age);
}
public class MyTest implements Test{// 常量private static final String str1 黄小桃;private static final String str2 黄小桃;private int age;public MyTest(){}public MyTest(int age){this.age age;}public int getAge(){return this.age;}Overridepublic void printOneLine(int age){System.out.println(str1 那年 age 岁);}public static void main(String[] args) {MyTest myTest new MyTest(19);myTest.printOneLine(myTest.getAge());}
} 基本信息 Magic魔数
文件是无法通过文件扩展名来确定文件类型的文件扩展名可以随意修改不影响文件的内容软件使用文件的头几个字节文件头去校验文件的类型如果软件不支持该种类型就会出错Java字节码文件中将文件头称为Magic魔数 魔数是指文件的前四个字节用于标识该文件确实是一个有效的Java字节码文件。这个魔数的值是固定的。魔数帮助系统快速识别文件不需要检查文件内容 下面给一个魔数的例子 0000: CAFEBABE (魔数)
0004: 0000 0034 (次版本号、主版本号)
...主副版本号
就是编译字节码文件的JDK版本号
主版本号用来标识大版本号JDK1.0 - 1.1使用了45.0 - 45.3JDK1.2是46之后没升级一个大版本就加1副版本号是当主版本号相同时作为区分不同版本的标识一般只需要关心主版本号
版本号的作用主要是判断当前字节码的版本和运行时的JDK是否兼容 案例主版本号不兼容错误 两种解决方案
升级JDK版本容易引发其他的兼容性问题需要大量的测试将第三方依赖的版本号降低或者更换依赖以满足JDK版本的需求建议采用 常量池
避免重复定义相同的内容节省空间
常量池中的数据都有一个编号编号从1开始。在字段或者字节码指令中通过编号可以快速的找到对应的数据字节码指令中通过编号引用到常量池的过程称之为符号引用 查看源码有两个常量的内容一致 查看字段发现有三个字段常量值索引为#21 根据索引查看常量内容 补充说明
如果常量名与常量内容相同那么加载到常量池中为了节省空间将直接指向内容而非先指向类型再指向内容 接口 字段 方法
字节码中的方法区是存放字节码指令的核心位置字节码指令的内容存放在方法的Code属性中 方法部分换一个代码案例
public class MyTest {public static void main(String[] args) {int i 0;int j i 1;}
} 这些指令是什么意思呢可以查看源文档
左键点击指令查看规范 字面意思就是将这个指令放入操作栈至于操作栈下面我会说明 局部变量表是根据源代码从上到下、从右到左执行的顺序加载到局部变量表的
案例分析
初始环境
先根据顺序加载所有局部变量到局部变量数组中 这个就是局部变量表 iconst_0
对于int i 0;先将常量池的常量0加载到操作数栈 istore_1
对于int i 0;再将操作数栈中的0存入局部变量数组下标为1的局部变量i中 iload_1 iconst_1
对于int j i 1;将局部变量数组下标为1的局部变量i的值(0)加载到操作数栈, 并且将常量1加载到操作数栈 iadd
对于int j i 1;对操作数栈中的常量们进行加法运算 istore_2
对于int j i 1;将操作数栈中的常量值存入局部变量数组[2]的局部变量j中 return
结束方法main方法
JVM清空操作数栈局部变量数组被销毁内存空间释放 现在再来分析一个案例
案例分析 public class MyTest {public static void main(String[] args) {int i 0;i i;}
}初始环境 iconst_0
对于int i 0;先加载常量值0到操作数栈中 istore_1
对于int i 0;再将常量值0弹入到局部变量i iload_1
对于i i;从语法上看是先执行i将局部变量i的值加载到操作数栈中 iinc 1 by 1
查看源文档 所以是用常数1增加局部变量i
注意看细节这次的加法是在局部变量中运行的这也是回答面试的关键点 istore_1
将操作数栈的常量值存入局部变量i也就是将0替换了1导致最终结果还是0 return
JVM清空操作数栈局部变量数组被销毁内存被释放 最后回答这种从字节码指令层面的回复会比Java语法层面的先增后增回复好一些
Java语法层面说 n i是先返回值给n再自增对应到字节码指令层面就是先iload_ 到操作数栈n获取的是操作数栈中的数再 iinc 1 by 1自增 如果是i 案例分析 public class MyTest {public static void main(String[] args) {int i 0, j 0, k 0;i;j j 1;k 1;}
} 这么看的话性能排序是 (i) (k1) (j j 1) 属性 常用工具
javap -v命令
javap是JDK自带的反编译工具可以通过控制台查看字节码文件的内容。适合在服务器上查看字节码文件内 容直接输入javap查看所有参数。输入javap -v 字节码文件名称 查看具体的字节码信息。如果jar包需要先使用 jar –xvf命令解压 IDEA插件-jclasslib
这款软件也有idea插件版本建议开发时使用IDEA插件版本 阿里Arthas
Java应用诊断器一款宝藏工具这里我必须吹一波阿里云真的很棒下文会频繁的使用这个工具
简介 | arthas (aliyun.com) 链接https://arthas.aliyun.com/arthas-boot.jar 还有很多中下载安装方式自己查看官网 入门使用
public class MyTest {public static void main(String[] args) throws Exception{while (true){Thread.sleep(1000);}}
}
这个代码死循环睡眠模拟程序持续运行
运行jar包 会显示所有Java相关进程我们要用第4个
输入4回车 看到当前程序的进程是2388然后下面的所有命令都只对2388这个进程生效
监控面板
arthas的功能很多本章节先学习监控面板查看字节码信息这两个功能其余的后面可能会出现 查看官方文档 dashboard -i 2000 -n 3 查看字节码信息
dump命令 dump -d [存放路径] [目标类的全限定名] jad命令
反编译命令 案例分析 类的生命周期重点
类的生命周期描述了一个类加载、使用、卸载的整个过程
类的生命周期是高频面试、笔试点也是后续大量知识点的基础部分很重要
比如下列的输出结果是 public class MyTest {public static void main(String[] args) {System.out.print(A);new MyTest();new MyTest();}public MyTest(){System.out.print(B);}{System.out.print(C);}static{System.out.print(D);}
} public class MyTest {public static void main(String[] args) {new BO2();System.out.println(BO2.a);}
}class AO2{static int a 0;static {a 1;}
}class BO2 extends AO2{static {a 2;}
} 应用场景 生命周期概述 加载阶段
1.加载Loading阶段的第一步是类加载器根据类的全限定名通过不同渠道以二进制流的方式获取字节码信息。
开发者可以使用Java代码拓展不同的渠道 2.类加载器在加载完类之后Java虚拟机会将字节码中的信息保存到方法区中 3.字节码信息加载到方法区中之后JVM在方法区生成一个InstanceKlass对象保存类的所有信息里面还包含实现特定功能比如多态的信息 4.同时JVM还会在堆中生成一份与方法区中数据类似的java.lang.Class对象
作用是在Java代码中去获取类的信息存储静态字段的数据JDK8及之后 对于开发者来说只需要访问堆中的Class对象而不需要访问方法区中所有信息这样JVM就能很好地控制开发者访问数据的范围 查看内存中的对象
推荐使用JDK自带的hsdb工具查看JVM内存信息。
在JDK/lib目录下 进到lib目录启动命令
java -cp sa-jdi.jar sun.jvm.hotspot.HSDB 输入java进程PID连接查看虚拟机内存信息 连接阶段 验证阶段
1.连接Linking阶段的第一个环节是验证验证的主要目的是检测Java字节码文件是否遵守了《Java虚拟机规范》中的约束。这个阶段一般不需要开发者参与
2.主要包含如下四个部分具体详见《Java虚拟机规范》
文件格式验证比如文件是否以0xCAFEBABE开头也就是魔数主次版本号是否满足当前Java虚拟机版本要求元信息验证例如类必须有父类则super不能为空验证程序执行指令的语义比如方法内的指令执行中跳转到不正确的位置符号引用验证例如是否访问了其他类中private的方法等
版本号的检测
Hotspot JDK8中虚拟机源码对版本号检测的代码如下你能读懂它的含义吗 准备阶段
静态变量分配内存初始值
准备阶段为静态变量static分配内存并设置初始值注意本章涉及到的内存结构只讨论JDK8及以后的版本准备阶段只会给静态变量赋初始值而每一种基本数据类型和引用数据类型都有其初始值final修饰的基本数据类型的静态变量准备阶段直接会将代码中的值进行赋值而没有初始化阶段 解析阶段
解析阶段主要是常量池中的符号引用替换为内存地址直接引用
符号引用就是在字节码文件中使用编号来访问常量池中的内容 直接引用不再使用编号而是使用内存中地址进行访问具体的数据 初始化阶段
准备阶段为静态变量static分配内存设置初始值
初始化阶段为静态变量static赋值执行静态代码块中的代码
注意两者的区别途中value的最终值应该是1而不是0 初始化阶段会执行字节码文件中clinit部分的字节码指令。 public class MyTest {public static int value 1;static{value 2;}public static void main(String[] args) {}
} 重点来了 何时初始化
访问一个类的静态变量或者静态方法注意变量是final修饰的并且等号右边是常量不会触发初始化。 1.调用Class.forName(String className)。 2.new一个该类的对象时。 3.执行Main方法的当前类。 4.直接引用静态变量或静态方法 5.使用反射机制访问类的构造器例如 Constructor? constructor Class.forName(A).getDeclaredConstructor(); 6.静态导入比如 import static ... 7.如果有一个枚举类型并且在其中使用了静态代码块访问任何枚举值时也会导致整个枚举类被加载这样其静态代码块会被执行。 clinit指令
clinit指令在特定情况下不会出现比如如下几种情况是不会进行初始化指令执行的。 1.无静态代码块且无静态变量赋值语句。 2.有静态变量的声明但是没有赋值语句。 3.静态变量的定义使用final关键字这类变量会在准备阶段直接进行初始化。 初始化的要点 数组的创建不会导致数组中元素的类进行初始化
final修饰的变量如果赋值的内容需要执行指令才能得出结果会执行clinit方法进行初始化 案例分析
面试题一
public class MyTest {public static void main(String[] args) {System.out.print(A);new MyTest();new MyTest();}public MyTest(){System.out.print(B);}{System.out.print(C);}static{System.out.print(D);}
} 先执行clinit部分也就是static代码块 先搞清楚新指令的作用查看文档
getstatic
从字节码中获取静态字段值载入操作栈顶中 ldc
将常量池中的值载入操作数栈中 invokevirtual
处理栈顶操作 OK现在分析字节码指令 第一行通过 getstatic 获取 System.out 的 PrintStream 对象并将其放入操作数栈顶。 也就是定义中的获取静态字段(System.out)的值(PrintStream对象) 这个PrintStream对象就是用来打印的 第二行从常量池取出字符串D将常量存入常量池就编译源代码的时候做的并不需要什么显式的字节码指令这里可以直接从常量池获取取出后加载到操作数栈上 第三行invokevirtual调用printStream类的print方法打印栈顶的字符串D 第四行return结束静态代码块 执行main方法前首先初始化类的static代码块
输出结果D
现在开始执行main方法 前三行
输出结果A 第四行new创建MyTest类的实例是对象的一个引用此时对象还没有初始化也就是所谓的this还将此引用推送到操作数栈中 第五行dup复制一份对象的引用放在操作数栈顶上每次调用对象都会dup一次应该是为了每个引用互不影响 第六行invokespecial调用构造方法开始初始化刚刚new出来的空对象 看3 - 5行打印字符串C 看6 - 8行打印字符串B 书接上回 第七行pop弹出栈顶的对象引用对象引用处理完毕 8 - 11行重复一次上述操作 输出结果CBCB 案例分析
public class MyTest {public static void main(String[] args) {System.out.println(BO2.a);}
}class AO2{static int a 0;static {a 1;}
}class BO2 extends AO2{static {a 2;}
} 分析初始化的条件有三个上面我记的有 所以B02.a这一行就先获取的是父类A02的静态变量a执行了父类A02的static代码块将a赋值为1 此时字节码执行return结束了静态代码块的初始化操作 这时不再执行子类B02的static代码块理由是初始化操作通过return指令已经结束如果要执行子类B02的static代码块需要重新初始化比如可以加一行new B02(); new的操作会再次访问B02的父类A02发现AO2的静态代码块已经加载过了不会再触发了于是开始加载子类的所以会赋值a为2 案例分析 这个好理解new的数据分配了10个空间每个元素都在准备阶段设置了初始值A类的默认值是null所以这个数组的元素全是null
再来复习一下初始化的条件 案例分析 public class MyTest {public static void main(String[] args) {System.out.println(A.a);}
}class A{// public static final int a 1;public static final int a Integer.valueOf(1);static {System.out.println(A类的静态代码块初始化);}
} 常量折叠
引入一个概念编译时常量A类的public static final int a 1;
JVM做了优化在编译时编译器直接将a的值嵌入到代码中不需要运行时初始化了直接跳过了初始化阶段
public class MyTest {public static void main(String[] args) {System.out.println(A.a);}
}class A{public static final int a 1;
// public static final int a Integer.valueOf(1);static {System.out.println(A类的静态代码块初始化);}
} 仔细对比字节码指令发现第二种压根没有getstatic A的clinit初始化操作而是iconst_1直接从常量池把常量1拿到了操作数栈中初始化操作直接跳过了
下一章【JVM】JVM基础教程二-CSDN博客