
几分钟应该看不完,私密马赛, 俺是标题党
既然来了, 看看吧, 球球你了
Java类加载器
类的生命周期和加载过程
- 加载 加载所有的.class文件/jar文件/网络流 →字节流 (JVM 与java.lang.classLoader协作)
存储于Metaspace/Method Area
- 校验 确保 class 文件里的字节流信息符合当前虚拟机的要求,不会危害虚拟机的安全
- 准备 设置变量默认值 分配内存
- 解析 解析常量池符号引用转换为直接引用,主要有以下四种:类或接口的解析、字段解析、类方法解析、接口方法解析 链接相关参数, 方法索引(为后续初始化和运行提供直接引用)
- 初始化 执行类构造器<clinit>方法 初始化
类加载时机
- 启动main方法所在类
- new 类时
- 调用静态方法
- 访问静态对象
- 子类初始化 →父类初始化
- 类的初始化 →接口初始化
- 初次调用Methon 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, 我们已经学过了, invokespecial 指令用来调用构造函数,但也可以用于调用同一个类中的 private 方法, 以及可见的超类方法。
动态调用
- invokevirtual,如果是具体类型的目标对象,invokevirtual用于调用公共,受保护和打包私有方法。
- invokeinterface,当要调用的方法属于某个接口时,将使用 invokeinterface 指令。
JVM方法查询
子类的静态方法会隐藏(注意与重写区分)父类中的同名、同描述符的静态方法 Interface 同理
- 在 C 中查找符合名字及描述符的方法。
- 如果没有找到,在 C 的父类中继续搜索,直至 Object 类。
- 如果没有找到,在 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处理异常
异常基本概念
抛出异常
- 显示抛出 : application层面
- 隐式抛出 : JVM层面
异常捕获
- try 代码块 用来标记需要进行异常监控的代码
- catch 代码块
try 代码块后面可以跟着多个 catch 代码块,来捕获不同类型的异常。Java 虚拟机会从上至下匹配异常处理器
- finally 代码块 用来声明一段必定运行的代码
因为异常总是动态的, 实时的, 所以我们总是new exception()
- Error 它的执行状态已经无法恢复,需要中止线程甚至是中止虚拟机
错误的运行影响到了全局代码
- Exception 涵盖程序可能需要捕获{try – catch}并且处理的异常
- RuntimeException 局部的、可恢复的。通过适当的错误处理,程序可以继续运行
当前版本 Java 编译器的做法(变种1),复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中
如何捕获异常
每个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 中常见的有这么三种。
- 使用静态方法Class.forName来获取。
- 调用对象的getClass()方法。
- 直接用类名 +“.class”访问。对于基本类型来说,它们的包装类型(wrapper classes)拥有一个名为“TYPE”的 final 静态字段,指向该基本类型对应的 Class 对象。int[].class
- 使用newInstance()来生成一个该类的实例。它要求该类中拥有一个无参数的构造器。
- 使用isInstance(Object)来判断一个对象是否该类的实例,语法上等同于instanceof关键字(JIT 优化时会有差别,我会在本专栏的第二部分详细介绍)。
- 使用Array.newInstance(Class,int)来构造该类型的数组。
- 使用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)。在执行过程中,每当调用 g(y) 的方法句柄,它会在参数列表最前面插入一个 4,再调用指向 f(x, y) 的方法句柄。
如果你看完了, 非常感谢你对我付出的认可🥰🥰🥰