文章目录
- 1 类文件结构
- 2 字节码指令
- 2.1 编译执行流程分析
- 2.2 多态原理
- 2.3 异常处理
- 2.4 synchronized
- 3 编译器处理
- 4 类加载阶段
- 5 类加载器
- 6 运行期优化
1 类文件结构
执行 javac -parameters -d . HellowWorld.java编译为 HelloWorld.class文件,根据 JVM 规范,类文件结构如下
ClassFile {u4 magic;//魔数u2 minor_version;//版本u2 major_version;u2 constant_pool_count;//常量池cp_info constant_pool[constant_pool_count-1];u2 access_flags;//访问标识与继承信息u2 this_class;u2 super_class;u2 interfaces_count;u2 interfaces[interfaces_count];u2 fields_count;//Field 信息field_info fields[fields_count];u2 methods_count;//Method 信息method_info methods[methods_count];u2 attributes_count;//附加属性attribute_info attributes[attributes_count];}
2 字节码指令
2.1 编译执行流程分析
原始代码如下:
package cn.itcast.jvm.t3.bytecode;/*** 演示 字节码指令 和 操作数栈、常量池的关系*/public class Demo3_1 {public static void main(String[] args) {int a = 10;int b = Short.MAX_VALUE + 1;int c = a + b;System.out.println(c);}}
字节码文件自己分析嫌慢,可以执行指令javap -v filepath反编译命令,直接获取字节码指令更直观
[root@localhost ~]# javap -v Demo3_1.classClassfile /root/Demo3_1.classLast modified Jul 7, 2019; size 665 bytesMD5 checksum a2c29a22421e218d4924d31e6990cfc5Compiled from "Demo3_1.java"public class cn.itcast.jvm.t3.bytecode.Demo3_1minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPERConstant pool:#1 = Methodref #7.#26 // java/lang/Object."":()V#2 = Class #27 // java/lang/Short#3 = Integer 32768#4 = Fieldref #28.#29 //java/lang/System.out:Ljava/io/PrintStream;#5 = Methodref #30.#31 // java/io/PrintStream.println:(I)V#6 = Class #32 // cn/itcast/jvm/t3/bytecode/Demo3_1#7 = Class #33 // java/lang/Object#8 = Utf8 <init>#9 = Utf8 ()V#10 = Utf8 Code#11 = Utf8 LineNumberTable#12 = Utf8 LocalVariableTable#13 = Utf8 this#14 = Utf8 Lcn/itcast/jvm/t3/bytecode/Demo3_1;#15 = Utf8 main#16 = Utf8 ([Ljava/lang/String;)V#17 = Utf8 args#18 = Utf8 [Ljava/lang/String;#19 = Utf8 a#20 = Utf8 I#21 = Utf8 b#22 = Utf8 c#23 = Utf8 MethodParameters#24 = Utf8 SourceFile#25 = Utf8 Demo3_1.java#26 = NameAndType #8:#9 // "":()V#27 = Utf8 java/lang/Short#28 = Class #34 // java/lang/System#29 = NameAndType #35:#36 // out:Ljava/io/PrintStream;#30 = Class #37 // java/io/PrintStream#31 = NameAndType #38:#39 // println:(I)V#32 = Utf8 cn/itcast/jvm/t3/bytecode/Demo3_1#33 = Utf8 java/lang/Object#34 = Utf8 java/lang/System#35 = Utf8 out#36 = Utf8 Ljava/io/PrintStream;#37 = Utf8 java/io/PrintStream#38 = Utf8 println#39 = Utf8 (I)V{public cn.itcast.jvm.t3.bytecode.Demo3_1();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 6: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcn/itcast/jvm/t3/bytecode/Demo3_1;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=4, args_size=10: bipush 102: istore_13: ldc #3 // int 327685: istore_26: iload_17: iload_28: iadd9: istore_310: getstatic #4 // Fieldjava/lang/System.out:Ljava/io/PrintStream;13: iload_314: invokevirtual #5 // Methodjava/io/PrintStream.println:(I)V17: returnLineNumberTable:line 8: 0line 9: 33)常量池载入运行时常量池4)方法字节码载入方法区5)main 线程开始运行,分配栈帧内存(stack=2,locals=4)line 10: 6line 11: 10line 12: 17LocalVariableTable:Start Length Slot Name Signature0 18 0 args [Ljava/lang/String;3 15 1 a I6 12 2 b I10 8 3 c IMethodParameters:Name Flagsargs}
问题:方法如何被执行的呢?
ANS:原始代码编译成字节码文件->常量池载入运行时常量池->方法字节码载入方法区->main线程开始运行,分配栈帧内存->执行引擎开始执行字节码->最终在内存结构上表现如下图
常用字节码指令参照:
指令码 | 操作码 | 描述(栈指操作数栈) |
---|---|---|
0x03 | iconst_0 | 0(int)值入栈 |
0x10 | bipush | valuebyte值带符号扩展成int值入栈 |
0x11 | sipush | 将一个 short 值入栈 |
0x12 | ldc | 常量池中的常量值入栈 |
0x2a | aload_0 | 加载 slot 0 的局部变量 |
0x4b | astroe_0 | 将栈顶值保存到 slot 0 的局部变量中 |
0x57 | pop | 从栈顶弹出一个字长的数据。 |
0x59 | dup | 复制栈顶一个字长的数据,将复制后的数据压栈。 |
0x60 | iadd | 将栈顶两int类型数相加,结果入栈。 |
0x84 | iinc | 直接在局部变量 slot 上进行运算 |
0x9c | ifge | 若栈顶int类型值大于等于0则跳转。 |
0xa7 | goto | 无条件跳转到指定位置。 |
0xbb | new | 创建新的对象实例。 |
0xb4 | getfield | 获取对象字段的值。 |
0xb2 | getstatic | 获取静态字段的值。 |
0xb7 | invokespecial | 预备调用构造方法 |
0xb6 | invokevirtual | 预备调用成员方法 |
0xb8 | invokestatic | 预备调用静态方法 |
0xb9 | invokeinterface | 预备调用方法 |
0xb0 | areturn | 返回引用类型值。 |
0xb1 | return | void函数返回。 |
0xc2 | monitorenter | 进入并获得对象监视器。(线程同步) |
0xc3 | monitorexit | 释放并退出对象监视器。(线程同步) |
2.2 多态原理
借助工具分析
①jps 获取进程 id
②运行 HSDB 工具,进入 JDK 安装目录,执行java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB,进入图形界面 attach 进程 id
③查找对象,打开 Tools -> Find Object By Query,输入 select d from cn.itcast.jvm.t3.bytecode.Dog d 点击 Execute 执行
④点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是MarkWord,后 8 字节就是对象的 Class 指针
⑤通过 Windows -> Console 进入命令行模式,执行mem ③中的对象头地址 2
⑥查看类的 vtable,Alt+R 进入 Inspector 工具,输入刚才的⑤得到的 Class 内存地址,得到vtable长度为n
⑦ ⑤得到的 Class 内存地址偏移 0x1b8 就是 vtable 的起始地址,通过 Windows -> Console 进入命令行模式,执行 mem vtable起始地址 6,就得到了 6 个虚方法的入口地址
⑧通过 Tools -> Class Browser 查看每个类的方法定义,比较可知,方法属于那个类,以判断是否多态调用
结论
当执行 invokevirtual 指令时,
先通过栈帧中的对象引用找到对象
分析对象头,找到对象的实际 Class
Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
查表得到方法的具体地址
执行方法的字节码
2.3 异常处理
原始代码:
public class Demo3_11_4 {public static void main(String[] args) {int i = 0;try {i = 10;} catch (Exception e) {i = 20;} finally {i = 30;}}}
字节码指令:
public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=1, locals=4, args_size=10: iconst_01: istore_1 // 0 -> i2: bipush 10 // try --------------------------------------4: istore_1 // 10 -> i |5: bipush 30 // finally |7: istore_1 // 30 -> i |8: goto 27 // return -----------------------------------11: astore_2 // catch Exceptin -> e ----------------------12: bipush 20 // |14: istore_1 // 20 -> i |15: bipush 30 // finally |17: istore_1 // 30 -> i |18: goto 27 // return -----------------------------------21: astore_3 // catch any -> slot 3 ----------------------22: bipush 30 // finally |24: istore_1 // 30 -> i |25: aload_3 // <- slot 3 |26: athrow // throw ------------------------------------27: returnException table:from to target type2 5 11 Class java/lang/Exception2 5 21 any // 剩余的异常类型,比如 Error11 15 21 any // 剩余的异常类型,比如 ErrorLineNumberTable: ...LocalVariableTable:Start Length Slot Name Signature12 3 2 e Ljava/lang/Exception;0 28 0 args [Ljava/lang/String;2 26 1 i IStackMapTable: ...MethodParameters: ...
总结:
Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
11 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置
可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
2.4 synchronized
原始代码:
public class Demo3_13 {public static void main(String[] args) {Object lock = new Object();synchronized (lock) {System.out.println("ok");}}}
字节码:
public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=4, args_size=10: new #2 // new Object3: dup4: invokespecial #1 // invokespecial :()V7: astore_1 // lock引用 -> lock8: aload_1 // <- lock (synchronized开始)9: dup10: astore_2 // lock引用 -> slot 211: monitorenter // monitorenter(lock引用)12: getstatic #3 // <- System.out15: ldc #4 // <- "ok"17: invokevirtual #5 // invokevirtual println:(Ljava/lang/String;)V20: aload_2 // <- slot 2(lock引用)21: monitorexit // monitorexit(lock引用)22: goto 3025: astore_3 // any -> slot 326: aload_2 // <- slot 2(lock引用)27: monitorexit // monitorexit(lock引用)28: aload_329: athrow30: returnException table:from to target type12 22 25 any25 28 25 anyLineNumberTable: ...LocalVariableTable:Start Length Slot Name Signature0 31 0 args [Ljava/lang/String;8 23 1 lock Ljava/lang/Object;StackMapTable: ...MethodParameters: ...
总结:
①加载对象然后上锁或者解锁
②异常表的作用是保证上锁的代码块出现异常时,对象锁也能正常释放掉
③方法级别的 synchronized 不会在字节码指令中有所体现
3 编译器处理
语法糖,即.java文件编译为.class字节码文件过程中的代码转换,例如
语法糖 | 转换 |
---|---|
默认构造器 | 无参构造,方法内调用父类无参构造 |
自动拆装箱 | Integer.valueOf / 整型值x.intValue |
泛型集合取值 | ((Integer)list.get(0)).intValue() |
可变参数 | 其实是一个数组 |
foreach 循环 | 数组转为下标循环,集合则准换为迭代器 |
switch 字符串 | 两层switch,第一层先匹配hashcode提高效率, 再匹配内容 |
switch 枚举 | 和上面类似,先在静态代码块中将类元素做映射,再两层switch |
try-with-resources | 接口实现了 AutoCloseable ,使用 try-withresources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码 |
方法重写 | 子类返回值可以是父类返回值的子类,子类中定义了桥接方法 |
匿名内部类 | 额外生成类,且如果引用了局部变量,会在新类的有参构造中对该变量赋值 |
4 类加载阶段
分为三个阶段:加载、链接、初始化
阶段 | 主要内容 |
---|---|
加载 | 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类 重要 field 有_java_mirror 即 java 的类镜像(存储在堆中),例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用 有父类先加载父类 加载和链接可能交替运行 |
链接 | ①验证:验证类是否符合 JVM规范,安全性检查 ②准备:为 static 变量分配空间,设置默认值,赋值有两个可能,如果变量为final修饰的基本类型以及字符串常量,准备阶段就能赋值,否则只能初始化再赋值 ③解析:将常量池中的符号引用解析为直接引用 |
初始化 | 调用 ()V ,虚拟机会保证这个类的『构造方法』的线程安全 |
类初始化发生的时机:
会初始化(懒惰的) | 不会初始化 |
---|---|
main 方法所在的类,总会被首先初始化 | 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化 |
首次访问这个类的静态变量或静态方法时 | 类对象.class 不会触发初始化 |
子类初始化,如果父类还没初始化,会引发 | 创建该类的数组不会触发初始化 |
子类访问父类的静态变量,只会触发父类的初始化 | 类加载器的 loadClass 方法 |
Class.forName | Class.forName 的参数 2 为 false 时 |
new 会导致初始化 |
5 类加载器
名称 | 加载哪的类 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问,显示为null |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap |
Application ClassLoader | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
一、如何指定类加载器加载指定类” />protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 1. 检查该类是否已经加载Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {// 2. 有上级的话,委派上级 loadClassc = parent.loadClass(name, false);} else {// 3. 如果没有上级了(ExtClassLoader),则委派BootstrapClassLoaderc = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {}if (c == null) {long t1 = System.nanoTime();// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载c = findClass(name);// 5. 记录耗时sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 – t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}
执行流程为:
sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有
sun.misc.LauncherAppClassLoader//2处,==委派上级==sun.misc.LauncherAppClassLoader // 2 处,==委派上级==sun.misc.Launcher AppClassLoader//2处,==委派上级==sun.misc.LauncherExtClassLoader.loadClass()
sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader 查找
BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有
sun.misc.LauncherExtClassLoader//4处,调用自己的findClass方法,是在JAV A HOME/jre/lib/ext下找H这个类,显然没有,回到sun.misc.LauncherExtClassLoader // 4 处,调用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到 sun.misc.Launcher ExtClassLoader//4处,调用自己的findClass方法,是在JAVAHOME/jre/lib/ext下找H这个类,显然没有,回到sun.misc.LauncherAppClassLoader 的 // 2 处
继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在classpath 下查找,找到了
三、以Driver驱动类为例来分析说明线程上下文类加载器?
我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写
Class.forName("com.mysql.jdbc.Driver")
也是可以让 com.mysql.jdbc.Driver 正确加载的,看源码:
public class DriverManager {// 注册驱动的集合private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers= new CopyOnWriteArrayList<>();// 初始化驱动static {loadInitialDrivers();println("JDBC DriverManager initialized");}
DriverManager类加载器是 Bootstrap ClassLoader,即该类存在于核心类库, 但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?
看 loadInitialDrivers() 方法:
private static void loadInitialDrivers() {String drivers;try {drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {public String run() {return System.getProperty("jdbc.drivers");}});} catch (Exception ex) {drivers = null;}// 1)使用 ServiceLoader 机制加载驱动,即 SPIAccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {ServiceLoader<Driver> loadedDrivers =ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();try{while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {// Do nothing}return null;}});println("DriverManager.initialize: jdbc.drivers = " + drivers);// 2)使用 jdbc.drivers 定义的驱动名加载驱动if (drivers == null || drivers.equals("")) {return;}String[] driversList = drivers.split(":");println("number of Drivers:" + driversList.length);for (String aDriver : driversList) {try {println("DriverManager.Initialize: loading " + aDriver);// 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());} catch (Exception ex) {println("DriverManager.Initialize: load failed: " + ex);}}}
先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载
再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称
再看 1)中ServiceLoader.load 方法可看到底层使用线程上下文类加载器器,默认就是应用程序类加载器,它内部又是由Class.forName 调用了线程上下文类加载器完成类加载
四、自定义类加载器” />class MyClassLoader extends ClassLoader {@Override // name 就是类名称protected Class<?> findClass(String name) throws ClassNotFoundException {String path = “e:\\myclasspath\\” + name + “.class”;try {ByteArrayOutputStream os = new ByteArrayOutputStream();Files.copy(Paths.get(path), os);// 得到字节数组byte[] bytes = os.toByteArray();// byte[] -> *.classreturn defineClass(name, bytes, 0, bytes.length);} catch (IOException e) {e.printStackTrace();throw new ClassNotFoundException(“类文件未找到”, e);}}}
6 运行期优化
由JVM内存结构可知 (回顾:JVM内存结构) ,字节码需由解释器逐行解释为机器码再执行,而即时编译器(JIT)不仅能实现这一功能,还能进一步优化,对比如下:
引擎 | 作用/特点 | 优势 |
---|---|---|
解释器 | 将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释 | 将字节码解释为针对所有平台都通用的机器码 |
即时编译器 | 根据平台类型,生成平台特定的机器码 | 将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译 缺点显然是耗费时间和资源,因此针对的是热点代码 |
根据JIT不同参与程度又将JVM执行状态分为5个层次:
0 层,解释执行(Interpreter)
1 层,使用 C1 即时编译器编译执行(不带 profiling)
2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
4 层,使用 C2 即时编译器编译执行
profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等
JIT相关优化的经典应用场景为:
应用 | 说明 |
---|---|
逃逸分析 | 观察新建的对象是否逃逸 |
方法内联 | 把热点方法内代码拷贝、粘贴到调用者的位置 |
常量折叠 | 9 * 9 替换为81 |
字段优化 | 方法外的字段首次读取会缓存起来,以减少访问次数 |
反射优化 | invoke的调用,使用的是MethodAccessor 的 NativeMethodAccessorImpl 实现(本地实现), 当调用次数达到膨胀阈值时,使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右 |