文章目录

  • 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线程开始运行,分配栈帧内存->执行引擎开始执行字节码->最终在内存结构上表现如下图

常用字节码指令参照:

指令码操作码描述(栈指操作数栈)
0x03iconst_00(int)值入栈
0x10bipushvaluebyte值带符号扩展成int值入栈
0x11sipush将一个 short 值入栈
0x12ldc常量池中的常量值入栈
0x2aaload_0加载 slot 0 的局部变量
0x4bastroe_0将栈顶值保存到 slot 0 的局部变量中
0x57pop从栈顶弹出一个字长的数据。
0x59dup复制栈顶一个字长的数据,将复制后的数据压栈。
0x60iadd将栈顶两int类型数相加,结果入栈。
0x84iinc直接在局部变量 slot 上进行运算
0x9cifge若栈顶int类型值大于等于0则跳转。
0xa7goto无条件跳转到指定位置。
0xbbnew创建新的对象实例。
0xb4getfield获取对象字段的值。
0xb2getstatic获取静态字段的值。
0xb7invokespecial预备调用构造方法
0xb6invokevirtual预备调用成员方法
0xb8invokestatic预备调用静态方法
0xb9invokeinterface预备调用方法
0xb0areturn返回引用类型值。
0xb1returnvoid函数返回。
0xc2monitorenter进入并获得对象监视器。(线程同步)
0xc3monitorexit释放并退出对象监视器。(线程同步)

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 指令时,

  1. 先通过栈帧中的对象引用找到对象

  2. 分析对象头,找到对象的实际 Class

  3. Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了

  4. 查表得到方法的具体地址

  5. 执行方法的字节码

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.forNameClass.forName 的参数 2 为 false 时
new 会导致初始化

5 类加载器

名称加载哪的类说明
Bootstrap ClassLoaderJAVA_HOME/jre/lib无法直接访问,显示为null
Extension ClassLoaderJAVA_HOME/jre/lib/ext上级为 Bootstrap
Application ClassLoaderclasspath上级为 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;}}

执行流程为:

  1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有

  2. sun.misc.LauncherAppClassLoader//2处,==委派上级==sun.misc.LauncherAppClassLoader // 2 处,==委派上级==sun.misc.Launcher AppClassLoader//2处,==委派上级==sun.misc.LauncherExtClassLoader.loadClass()

  3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有

  4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader 查找

  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有

  6. 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 处

  7. 继续执行到 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 倍左右