短时间洞察Java虚拟机——第二部分
Java类加载器
类的生命周期与加载进程
- 加载:将所有的.class文件、jar文件或网络流中的内容转化为字节流,这一过程由JVM与java.lang.ClassLoader协同完成,最终存储于Metaspace/Method Area区域。
- 校验:对class文件的字节流信息进行检查,确保其符合当前虚拟机的要求,保障虚拟机的安全。
- 准备:为类中的变量设置默认值,并分配相应的内存空间。
- 解析:把常量池中的符号引用转换为直接引用,主要涵盖类或接口的解析、字段解析、类方法解析以及接口方法解析等,为后续的初始化和运行提供直接引用依据。
- 初始化:执行类构造器
<clinit>
方法来完成类的初始化操作。
类加载的时机
📖
* 启动包含main方法的类时。
* 创建类的实例对象时。
* 调用类的静态方法时。
* 访问类的静态成员时。
* 子类进行初始化时会触发父类的初始化。
* 类进行初始化时会引发接口的初始化。
* 初次调用Method Handle方法时。
📖
* 引用父类的静态字段,不会触发子类的初始化。
* 定义对象数组,不会触发对象的初始化。
* 调用常量,不会引发类的初始化。
* ClassLoader.loadClass方法仅执行类的加载操作,不会进行初始化。
类加载机制
⚙
* 启动类加载器(bootstrap class loader):用于加载Java的核心类(由C++编写),是JVM自带的加载器。例如,java.lang.String类就是由启动类加载器加载的,所以String.class.getClassLoader()会返回null。
* 扩展类加载器(extensions class loader):负责加载JRE的扩展目录下的类,由启动类加载器进行加载。
* 应用类加载器(app class loader):通过ClassLoader.getSystemClassLoader()方法获取,若未特别指定自定义类加载器,用户自定义的类通常由该加载器进行加载。
📖
* 双亲委托:加载所需类时采用懒加载策略,将加载相关类的任务委托给父类加载器来负责依赖关系的处理。
* 负责依赖:在加载所需类时,会同时加载相关的依赖类和接口。
* 缓存加载:类被加载后会被缓存起来。
JVM方法调用
方法在JVM中的构成包括类名、方法名以及方法描述符(由参数类型和返回参数类型组成)。
静态方法
invokestatic
指令,用于调用某个类的静态方法,是方法调用指令中执行速度较快的一种。invokespecial
指令,我们已经了解过,它可以用来调用构造函数,也能用于调用同一个类中的private方法以及可见的超类方法。
动态调用
invokevirtual
指令,当目标对象是具体类型时,该指令用于调用公共、受保护和打包私有的方法。invokeinterface
指令,当要调用的方法属于某个接口时,会使用该指令。
JVM方法查询
子类的静态方法会隐藏(需注意与重写区分)父类中同名且同描述符的静态方法,接口也同理
📖
1. 在C类中查找符合名字及描述符的方法。
2. 如果在C类中未找到,就在C类的父类中继续搜索,直至Object类。
3. 如果还未找到,就在C类直接实现或间接实现的接口中进行搜索,搜索到的目标方法必须是非私有且非静态的。并且,如果目标方法在间接实现的接口中,需满足C类与该接口之间不存在其他符合条件的目标方法。若存在多个符合条件的目标方法,任意返回其中一个即可。
虚方法调用
- 虚拟方法表:在链接阶段会建立class的虚方法(非static、final)表。
分离invokinterface
与invokevirtual
的原因
class A
1: method1
2: method2
class B extends A
1: method1
2: method2
3: method3
class B extends A implements X
1: method1
2: method2
3: method3
4: methodX
class C implements X
1: methodC
2: methodX
- 内联缓存:
* -只是缓存并非内联(嵌入内部)
在执行过程中,如果遇到已缓存的类型,内联缓存会直接调用该类型对应的目标方法。
💡
* 单态内联:缓存了一种动态类型及其对应的目标方法。
* 多态内联:缓存多种…,常用于热门方法的前期调用。
- 劣化为超多态状态
💡
当类型切换过于频繁(超多态)时,缓存的维护成本(写开销)超过收益,JVM会选择放弃缓存。
JVM处理异常
异常基本概念
💡
* 抛出异常:包括应用层面的显示抛出和JVM层面的隐式抛出。
* 异常捕获:
* try
代码块用于标记需要进行异常监控的代码。
* catch
代码块用于捕获异常。
try
代码块后可跟随多个catch
代码块,用于捕获不同类型的异常,Java虚拟机会从上至下匹配异常处理器。
* finally
代码块用于声明一段必定会执行的代码。
因为异常具有动态性和实时性,所以通常使用new exception()
来创建异常对象。
💡
* Error:其执行状态已无法恢复,需要中止线程甚至是中止虚拟机,错误的运行会影响到全局代码。
* Exception:涵盖程序可能需要捕获(通过try – catch)并处理的异常。
* RuntimeException:属于局部的、可恢复的异常,通过适当的错误处理,程序可以继续运行。
如何捕获异常
每个method都会维护一张Exception table
Exception table:
from to target type
0 3 6 Class java/lang/Exception
⚙
当程序触发异常时,会自上到下遍历异常表中的条目,若命中相应条目,将PC指向target,否则照常抛出异常。
Java 7 的 Suppressed 异常以及语法糖
📌
语法糖具有以下特点:
* 可读性强:使代码更接近人类语言,便于理解。
* 非必需:去掉语法糖后,语言仍能实现相同功能,只是写法更复杂。
* 编译器/解释器处理:语法糖通常在编译或解释时被转换为更基础的代码。
📖
Java 7专门构造了try-with-resources语法糖,在字节码层面自动使用Suppressed异常来解决代码繁琐的问题,示例如下:
try {
in0 = new FileInputStream(new File("in0.txt"));
...
try {
in1 = new FileInputStream(new File("in1.txt"));
...
try {
in2 = new FileInputStream(new File("in2.txt"));
...
} finally {
if (in2 != null) in2.close();
}
} finally {
if (in1 != null) in1.close();
}
} finally {
if (in0 != null) in0.close();
try (Foo foo0 = new Foo("Foo0"); // try-with-resources语法糖优化后
Foo foo1 = new Foo("Foo1"); // 该语法糖下自动使用suppressed异常
Foo foo2 = new Foo("Foo2")) {
throw new RuntimeException("Initial");
}
**suppressed异常**允许将一个异常附于另一个异常之上,因此,抛出的异常可以附带多个异常的信息。
JVM实现反射机制
依赖于JVM的类加载器和运行时数据结构(如方法表、字段表)
怎么用
📖
通常使用反射API的第一步是获取Class对象,在Java中常见的有以下三种方式。
1. 使用静态方法Class.forName
来获取。
2. 调用对象的getClass()
方法。
3. 直接通过类名+“.class”进行访问。对于基本类型来说,它们的包装类型拥有一个名为“TYPE”的final静态字段,指向该基本类型对应的Class对象,例如int[].class。
📌
1. 使用newInstance()
来生成该类的一个实例,这要求该类中存在一个无参数的构造器。
2. 使用isInstance(Object)
来判断一个对象是否为该类的实例,语法上等同于instanceof
关键字(JIT优化时会有差异,会在本专栏第二部分详细介绍)。
3. 使用Array.newInstance(Class,int)
来构造该类型的数组。
4. 使用getFields()/getConstructors()/getMethods()
来访问该类的成员。除了这三个方法外,Class类还提供了许多其他方法,详情可参考[4]。需要注意的是,方法名中带Declared的不会返回父类的成员,但会返回私有成员;而不带Declared的则相反。
📌
当获取到类成员后,可进行以下操作:
* 使用Constructor/Field/Method.setAccessible(true)
来绕过Java语言的访问限制。
* 使用Constructor.newInstance(Object[])
来生成该类的实例。
* 使用Field.get/set(Object)
来访问字段的值。
* 使用Method.invoke(Object, Object[])
来调用方法。
应用
- IDE:每当敲入点号时,IDE会根据点号前的内容,动态展示可访问的字段或方法。
- Java调试器:在调试过程中可以枚举某一对象所有字段的值。
- Spring framework:用于IOC(控制反转)。
Method.invoke
本地实现、委派实现、动态实现(均由MethodAccessor
抽象)
getMethod会形成一份class内方法的拷贝 -避免在热点代码中使用getMethod
取消委派实现,关闭检查时目标方法的权限可以小幅度提升性能
public final class Method extends Executable {
...
public Object invoke(Object obj, Object... args) throws ... {
... // 权限检查
MethodAccessor ma = methodAccessor;
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
}
-
本地实现
// v0 版本
import java.lang.reflect.Method;public class Test {
public static void target(int i) {
new Exception(“#” + i).printStackTrace();
}public static void main(String[] args) throws Exception {
Class<?> klass = Class.forName(“Test”);
Method method = klass.getMethod(“target”, int.class);
method.invoke(null, 0);
}
}不同版本的输出略有不同,这里使用了Java 10。
$ java Test
java.lang.Exception: #0
at Test.target(Test.java:5)
本地实现 at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke0(Native Method)
↑ at java.base/jdk.internal.reflect.NativeMethodAccessorImpl. .invoke(NativeMethodAccessorImpl.java:62)
委派实现 at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.i .invoke(DelegatingMethodAccessorImpl.java:43)
↑
invoke at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at Test.main(Test.java:131 -
委派实现:作为invoke实现的中间件,用于选择method invoke本地实现还是动态实现。
-
动态实现(纯java字节码,无需重新从java→c++→java):优势在于避免了JNI(java native interface)的切换开销,但缺点是生成字节码耗时。
// 动态实现的伪代码,这里仅列举关键调用逻辑,实际还包括调用者检测、参数检测的字节码。
package jdk.internal.reflect;public class GeneratedMethodAccessor1 extends … {
@Overrides
public Object invoke(Object obj, Object[] args) throws … {
Test.target((int) args[0]);
return null;
}
}
💡
当本地实现某一方法的次数大于Dsun.reflect.inflationThreshold
时,JVM虚拟机会开始对反射method以java字节码形成动态实现。
Method method1 = Test.class.getMethod("target", int.class);
Method method2 = Test.class.getMethod("target", int.class);
📖
每次get都会创建一个新的method实例,即使访问的方法相同,method1也不等于method2。
JVM实现invokedynamic
方法句柄(Method Handle)
💡
* 通过MethodHandles.Lookup
类完成,它提供了多个API,既可以使用反射API中的Method来查找,也能根据类、方法名以及方法调用类型来查找。
* Lookup.findStatic
、Lookup.findVirtual
、Lookup.findSpecial
* 方法句柄的类型(MethodType)仅由Method的参数类型和返回类型决定。
class Foo {
private static void bar(Object o) {
..
}
public static Lookup lookup() {
return MethodHandles.lookup();
}
}
// 获取方法句柄的不同方式
MethodHandles.Lookup l = Foo.lookup(); // 具备Foo类的访问权限
Method m = Foo.class.getDeclaredMethod("bar", Object.class);
MethodHandle mh0 = l.unreflect(m);
MethodType t = MethodType.methodType(void.class, Object.class);
MethodHandle mh1 = l.findStatic(Foo.class, "bar", t);
⚙
* 方法句柄同样会检查权限,但相比于反射,其权限检查在创建阶段完成,无需每次使用时重复检查。
* 方法句柄的访问权限不取决于方法句柄的创建位置,而是取决于Lookup对象的创建位置。
方法句柄的操作
📌
严格匹配传入参数类型的invokeExact
,只接受相同类型(Object)String→Object,不允许String→Object。
@PolymorphicSignatur
实现签名多态性,可根据传入参数不同自动创建不同的方法句柄。
📌
如果需要自动适配参数类型,可选取方法句柄的第二种调用方式invoke,它同样是一个签名多态性的方法。invoke会调用MethodHandle.asType
方法,生成一个适配器方法句柄,对传入的参数进行适配,再调用原方法句柄。调用原方法句柄的返回值也会先进行适配,然后再返回给调用者。
📖
方法句柄还支持增删改参数的操作,这些操作通过生成另一个方法句柄来实现。其中,改操作是通过MethodHandle.asType
方法实现的;删操作是将传入的部分参数就地抛弃,对应的API是MethodHandles.dropArguments
方法;增操作则是往传入的参数中插入额外的参数,对应的API是MethodHandle.bindTo
方法。Java 8中捕获类型的Lambda表达式就是用这种操作来实现的,下一篇会详细解释。增操作还可用于实现方法的柯里化[3],例如有一个指向f(x, y)的方法句柄,将x绑定为4,可生成另一个方法句柄g(y) = f(4, y),执行时会在参数列表最前面插入一个4,再调用指向f(x, y)的方法句柄。
如果你看完了,非常感谢你对我付出的认可🥰🥰🥰😘😘😘