几分钟了解下java虚拟机–02


几分钟应该看不完,私密马赛, 俺是标题党
既然来了, 看看吧, 球球你了

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 同理

  1. 在 C 中查找符合名字及描述符的方法。
  2. 如果没有找到,在 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处理异常

异常基本概念

抛出异常

  • 显示抛出 : 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 中常见的有这么三种。

  1. 使用静态方法Class.forName来获取。
  2. 调用对象的getClass()方法。
  3. 直接用类名 +“.class”访问。对于基本类型来说,它们的包装类型(wrapper classes)拥有一个名为“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)。在执行过程中,每当调用 g(y) 的方法句柄,它会在参数列表最前面插入一个 4,再调用指向 f(x, y) 的方法句柄。

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