执行 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];
}
原始代码如下:
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.class
Classfile /root/Demo3_1.class
Last modified Jul 7, 2019; size 665 bytes
MD5 checksum a2c29a22421e218d4924d31e6990cfc5
Compiled from "Demo3_1.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant 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: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t3/bytecode/Demo3_1;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field
java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method
java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 8: 0
line 9: 3
3)常量池载入运行时常量池
4)方法字节码载入方法区
5)main 线程开始运行,分配栈帧内存
(stack=2,locals=4)
line 10: 6
line 11: 10
line 12: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I
MethodParameters:
Name Flags
args
}
问题:方法如何被执行的呢?
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 | 释放并退出对象监视器。(线程同步) |
借助工具分析
①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,它在类加载的链接阶段就已经根据方法的重写规则生成好了
查表得到方法的具体地址
执行方法的字节码
原始代码:
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;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1 // 0 -> i
2: 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: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any // 剩余的异常类型,比如 Error
11 15 21 any // 剩余的异常类型,比如 Error
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
12 3 2 e Ljava/lang/Exception;
0 28 0 args [Ljava/lang/String;
2 26 1 i I
StackMapTable: ...
MethodParameters: ...
总结:
Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
11 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置
可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
原始代码:
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;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #2 // new Object
3: dup
4: invokespecial #1 // invokespecial :()V
7: astore_1 // lock引用 -> lock
8: aload_1 // <- lock (synchronized开始)
9: dup
10: astore_2 // lock引用 -> slot 2
11: monitorenter // monitorenter(lock引用)
12: getstatic #3 // <- System.out
15: ldc #4 // <- "ok"
17: invokevirtual #5 // invokevirtual println:
(Ljava/lang/String;)V
20: aload_2 // <- slot 2(lock引用)
21: monitorexit // monitorexit(lock引用)
22: goto 30
25: astore_3 // any -> slot 3
26: aload_2 // <- slot 2(lock引用)
27: monitorexit // monitorexit(lock引用)
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
StackMapTable: ...
MethodParameters: ...
总结:
①加载对象然后上锁或者解锁
②异常表的作用是保证上锁的代码块出现异常时,对象锁也能正常释放掉
③方法级别的 synchronized 不会在字节码指令中有所体现
语法糖,即.java文件编译为.class字节码文件过程中的代码转换,例如
语法糖 | 转换 |
---|---|
默认构造器 | 无参构造,方法内调用父类无参构造 |
自动拆装箱 | Integer.valueOf / 整型值x.intValue |
泛型集合取值 | ((Integer)list.get(0)).intValue() |
可变参数 | 其实是一个数组 |
foreach 循环 | 数组转为下标循环,集合则准换为迭代器 |
switch 字符串 | 两层switch,第一层先匹配hashcode提高效率, 再匹配内容 |
switch 枚举 | 和上面类似,先在静态代码块中将类元素做映射,再两层switch |
try-with-resources | 接口实现了 AutoCloseable ,使用 try-withresources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码 |
方法重写 | 子类返回值可以是父类返回值的子类,子类中定义了桥接方法 |
匿名内部类 | 额外生成类,且如果引用了局部变量,会在新类的有参构造中对该变量赋值 |
分为三个阶段:加载、链接、初始化
阶段 | 主要内容 |
---|---|
加载 | 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类 重要 field 有_java_mirror 即 java 的类镜像(存储在堆中),例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用 有父类先加载父类 加载和链接可能交替运行 |
链接 | ①验证:验证类是否符合 JVM规范,安全性检查 ②准备:为 static 变量分配空间,设置默认值,赋值有两个可能,如果变量为final修饰的基本类型以及字符串常量,准备阶段就能赋值,否则只能初始化再赋值 ③解析:将常量池中的符号引用解析为直接引用 |
初始化 | 调用 < cinit >()V ,虚拟机会保证这个类的『构造方法』的线程安全 |
类初始化发生的时机:
会初始化(懒惰的) | 不会初始化 |
---|---|
main 方法所在的类,总会被首先初始化 | 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化 |
首次访问这个类的静态变量或静态方法时 | 类对象.class 不会触发初始化 |
子类初始化,如果父类还没初始化,会引发 | 创建该类的数组不会触发初始化 |
子类访问父类的静态变量,只会触发父类的初始化 | 类加载器的 loadClass 方法 |
Class.forName | Class.forName 的参数 2 为 false 时 |
new 会导致初始化 |
名称 | 加载哪的类 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问,显示为null |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap |
Application ClassLoader | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
一、如何指定类加载器加载指定类?
①启动类加载器:
java -Xbootclasspath/a:. 类路径
其中,可以用下面参数来替换核心类
java -Xbootclasspath: 新路径
java -Xbootclasspath/a: 追加路径(后追加)
java -Xbootclasspath/p: 追加路径(前追加,用于替换核心类)
②扩展类加载器:
jar -cvf my.jar 类路径 打个jar包,拷贝到 JAVA_HOME/jre/lib/ext
二、双亲委派模式怎么理解?
类加载器的 loadClass 方法说明了查找类的规则,如下:
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. 有上级的话,委派上级 loadClass
c = parent.loadClass(name, false);
} else {
// 3. 如果没有上级了(ExtClassLoader),则委派
BootstrapClassLoader
c = 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.Launcher A p p C l a s s L o a d e r / / 2 处, = = 委派上级 = = s u n . m i s c . L a u n c h e r AppClassLoader // 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.Launcher E x t C l a s s L o a d e r / / 4 处,调用自己的 f i n d C l a s s 方法,是在 J A V A H O M E / j r e / l i b / e x t 下找 H 这个类,显然没有,回到 s u n . m i s c . L a u n c h e r ExtClassLoader // 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 机制加载驱动,即 SPI
AccessController.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 调用了线程上下文类加载器完成类加载
四、自定义类加载器?
步骤:
继承 ClassLoader 父类
要遵从双亲委派机制,重写 findClass 方法注意不是重写 loadClass 方法,否则不会走双亲委派机制
读取类文件的字节码
调用父类的 defineClass 方法来加载类
使用者调用该类加载器的 loadClass 方法
示例代码:
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[] -> *.class
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类文件未找到", e);
}
}
}
由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 倍左右 |