短时间洞悉Java虚拟机——第二部分

2周前发布 gsjqwyl
12 0 0

短时间洞察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)表。

分离invokinterfaceinvokevirtual的原因

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.findStaticLookup.findVirtualLookup.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)的方法句柄。

如果你看完了,非常感谢你对我付出的认可🥰🥰🥰😘😘😘

© 版权声明

相关文章

暂无评论

暂无评论...