文章目录
  1. 虚拟机执行系统
    1. Class文件结构与字节码
      1. Class文件结构(详细结构规则请Google)
      2. 字节码(列表请Google)
    2. 类加载
      1. 时机
      2. 类加载过程
      3. 类加载器
      4. 双亲委派
      5. 案例:Tomcat类加载结构
    3. 字节码执行引擎
      1. 运行时栈帧结构
        1. 局部变量表
        2. 操作数栈
        3. 动态连接
        4. 方法返回地址
      2. 方法调用
      3. 基于栈的字节码执行引擎
      4. 案例:远程调用

虚拟机执行系统

Class文件结构与字节码

Class文件结构(详细结构规则请Google)

  • 魔数与类文件版本:文件的头4字节表示Class文件:0XCAFEBABE
  • 常量池
  • 访问标志
  • 类索引、父类索引与接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合

    字节码(列表请Google)

  • 大部分与数据类型有关的字节码都包含了类型信息,如iload、fload, 但底层可能是同样逻辑
  • 大多数对于boolean、short、byte、char等类型的操作,实际使用int类型作为运算类型
  • 运算指令中,只有除0操作抛出ArithmeticException,其他操作不会出现异常
  • 窄化类型转换指令直接丢弃最低N个字节以外的东西,不会有异常
  • 指令类型主要有(加载存储、运算、类型转换、对象创建与访问、操作数栈、控制转移、方法调用与返回、异常处理、同步(管程))

类加载

时机

  1. 遇到new getstatic putstatic invokestatic这4条指令,如果类没有初始化,则进行初始化
  2. 使用reflect包对类方法反射调用,如果没有初始化则初始化
  3. 初始化某类,若父类未初始化,则初始化父类
  4. jvm启动时,用户需要指定一个主类(main方法),jvm首先初始化这个类
  5. 使用JDM1.7的动态语言支持,MethodHandler实例的相关使用时,方法句柄对应类未初始化时
  • 以上5中情况称为对类的主动引用,被动引用时不触发初始化类(创建数组,引用别的类的静态常量,通过子类引用父类的静态变量)
  • 接口的初始化中,不要求父接口初始化,只有需要使用时才进行初始化父接口

类加载过程

  • 类的生命周期:加载->验证->准备->解析->初始化->使用->卸载
  • 加载:类的二进制字节流保存在方法区中,并生成一个Class对象作为方法区这个类数据的入口,对于HotSpot虚拟机来说Class对象存放在方法区中而不是堆中(如果是数组类型,则递归加载去掉一个维度的组件类型,如果组件类型为引用类型,则数组在加载这个组件的类加载器的类空间名称上被标识)
  • 验证:文件格式、元数据、字节码、符号引用
  • 准备:设置类变量(非实例变量)初始值,方法区中进行,如果不是final static, 则首先按照类型赋值0值,初始化用户指定值包装在类构造器<clinit>方法中,在初始化阶段执行,final static则直接赋值为用户指定值
  • 解析:常量池中的符号引用替换为直接引用(类或接口、字段、类方法、接口方法)
  • 初始化:执行类构造器<clinit>()方法,
    • <clinit>()由类变量的赋值动作和static语句块合并产生,static语句块不可以赋值定义在其后的变量
    • jvm保证父类的<clinit>()方法先执行,static语句块也是父类的先执行
    • <clinit>()方法不是必须的,如果既没有类变量的赋值也没有static语句块
    • 接口也会有该方法,但执行时不需要先执行父接口(使用时才执行)中的该方法,接口的实现类初始化不会执行接口的该方法
    • jvm保证执行该方法多线程安全,如果耗时,则造成阻塞

类加载器

  • 类加载器用于获取某个类的二进制字节流
  • 每一个类加载器,都有一个类名称空间,判断两个类是否相等,需要在两个类都为同一个类加载器加载的情况下才有意义,否则必定不等
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class TestUserClassLoder {
    public static void main(String[] args) throws Exception {
    ClassLoader MyClassLoader = new ClassLoader() {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
    try {
    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
    InputStream is = getClass().getResourceAsStream(fileName);
    if (is == null) {
    return super.loadClass(name);
    }
    byte[] b = new byte[is.available()];
    is.read(b);
    return defineClass(name, b, 0, b.length);
    } catch (Exception e) {
    throw new ClassNotFoundException(name);
    }
    }
    };
    Object a = MyClassLoader.loadClass("jvm.classloader.TestUserClassLoder").newInstance();
    Object b = new TestUserClassLoder();
    System.out.println(a instanceof TestUserClassLoder);//false 不是同一个类加载器实例加载 必然不相等 包括instance
    System.out.println(b instanceof TestUserClassLoder);
    }
    }

双亲委派

  • 对于JVM来说,只存在两种不同的类加载器:一种是启动类加载器(jvm一部分,c++实现,用户获取不到),另一种由Java实现,独立于JVM

  • 从开发角度,主要有三种类加载器:

    1. 启动类加载器:加载JAVA_HOME/lib目录下面的复合条件的类(jar包)
    2. 扩展类加载器:加载JAVA_HOME/lib/ext目录下面的所有类库
    3. 应用程序类加载器:用于加载用户类路径上的类库,程序中默认的类加载器
  • 类加载器之间通过双亲委派规则进行配合,如图

  • 双亲委派模型中:

    1. 除了顶层启动类加载器,其他类加载器都应当有自己的父类加载器(一般通过组合实现)
    2. 类加载器接收到类加载请求,首先将请求委派给父类加载器完成,只有父类加载器反馈无法加载才尝试自己加载
    3. Java类随着类加载器具备了优先级的层次关系(Object类存在于rt.jar中,用户定义的Object即使全类名一致也不会加载,因为启动类加载器已经加载了该类,所有任何环境中Object类都是同一个)
  • 双亲委派模型的破坏,JVM团队为了解决某些问题引入了线程上下文加载器,破坏了双亲委派请求链路,直接使用下层类加载器加载目标类(如Spring框架可以放在共享的jar目录中,则Spring类的加载由共享类加载器加载,而此时Spring需要管理用户的类,加载Spring类的共享类加载器无法加载用户的类,此时Spring则使用线程上下文加载器,即谁调用Spring去管理Bean,谁就要提供给Spring类加载器)

案例:Tomcat类加载结构

  • 要解决的问题

    1. 部署在同一个服务器上的多个应用Java类库实现相互隔离
    2. 部署在同一个服务器上的应用间Java类库可以共享(例如公共的类库)
    3. 服务器需要保证自身的安全不受应用影响,服务器所使用的类库与应用使用的类库应该隔离
    4. JSP服务器需要支持HotSwap(JSP文件修改,JSP对应的生成类需要热替换)
  • Tomcat的类文件目录结构

    • /common/*:放在这个下面的类库,可以被Tomcat和多有的Web应用程序共同使用。
    • /server/*:类库可以被Tomcat使用,对所有的Web应用程序都不可见。
    • /shared/*:类库可被所有web应用程序共同使用,但对Tomcat自己不可见
    • /WEB-INF/*:类库仅仅可以被此Web应用程序使用,对Tomcat和其他web程序都不可见。
  • Tomcat设计的类加载器结构,不同的类加载器加载不同的部分,实现了隔离

  • JasperLoader的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class, 它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。

  • 对于 Tomcat 的 6.x 版本,只有指定了 tomcat/conf/catalina.properties配置文件的 server.loader 和 share.loader 项后才会真正建立 CatalinaClassLoader 和 SharedClassLoader 的实例,否则会用到这两个类加载器的地方都会用 CommonClassLoader 的实例代替,而默认没有设置,所以 Tomcat 6.x 顺理成章地把/common、/server 和/shared 三个目录默认合并到一起变成一个/lib 目录。这个目录里的类库相当于以前/common目录中类库的作用。这是Tomcat设计团队为了简化部署所做的一项改进。

字节码执行引擎

运行时栈帧结构

  • 栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
  • 每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
  • 每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。
  • 对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧。

局部变量表

  • 存放方法参数和方法内部的局部变量
  • 局部变量表容量以变量槽为最小单位(Slot),每个Slot可以存放32位以内的数据,64存放在两个Slot中(long double)
  • 编译程序时,需要使用多少局部变量表已经确定,不受代码运行期间影响
  • 方法执行时,使用局部变量表完成传参,如果是实例方法,第0位Slot存放对象的实例引用(this指针),其他参数从1开始传入,之后是内部的局部变量
  • Slot会复用,会对垃圾回收产生影响,但编译成JIT代码后枚举GC Roots行为会与解释执行有不同,不会有问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Test{
    public static void main(String[] args){
    {
    byte[] bytes=new byte[64 * 1024 * 1024];
    }
    //此处不一定回收内存,回收bytes本质是要看Slot中是否存在对bytes数据对象的引用,GC Roots包含局部变量表
    //此时占用的Slot没有被其他变量使用,栈帧中的存放bytes的Slot中仍然存在对数据的引用
    // int i=0; 此处加本行则会回收,i会复用bytes的Slot
    System.gc();
    }
    }
  • 局部变量初始化才可以使用,不存在准备阶段(赋初值,参考类变量)

操作数栈

  • 编译程序时,需要使用深的操作数栈已经确定,不受代码运行期间影响
  • 操作数栈的数据类型必须与字节码指令序列严格匹配,编译器保证
  • Java虚拟机的解释执行引擎称为基于栈的执行引擎,栈即指操作数栈
  • 一般的虚拟机会对操作数栈进行优化,让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠,省去了额外的参数复制

动态连接

  • 指向运行时常量池中该栈帧方法的引用,用户支持调用过程中的动态连接(Dynamic Linking)

方法返回地址

  • 保存方法被调用的位置
  • 方法可以正常返回或异常返回,异常返回没有返回值
  • 方法退出操作大致:恢复上层方法的局部变量表和操作数栈 -> 返回值压入调用者操作数栈 -> PC计数器指向下一条地址

方法调用

  • 确定被调用方法的版本过程而不是执行过程
  • 类加载阶段的解析阶段,将一部分方法转换为直接引用,这些方法在执行前可确定版本,且运行期不可变(见下)
  • Java关于方法调用的字节码:

    1. invokestatic:调用静态方法 (执行前可确定版本)
    2. invokespecial:调用实例构造器方法,私有方法和父类方法 (执行前可确定版本)
    3. invokevirtual:调用虚方法 (final方法也是该字节码调,但是其不是虚方法)
    4. invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
    5. invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
  • 方法版本的确定-分派:

    • 静态分派:根据依赖静态类型选择方法版本(方法重载)
    • 动态分派:运行期根据实际类型确定方法执行版本
  • Java: 静态多分派(根据静态类型与参数列表选择方法,编译期确定),动态单分派(此时只需要确定具体对象类型的方法版本,不同参数列表导致的方法版本确定已经在编译期完成)
  • 动态分派实现方式:虚方法表,保存各个方法的实际入口,没有重写父类方法就指向父类实现入口(还有其他方法,如内联缓存Inline Cache、基于继承关系分析CHA的守护内联Guard Inlining等非稳定手段)

  • Java7之后提供了MethodHandle机制实现动态方法调用

基于栈的字节码执行引擎

  • Java 编译器输出的指令流,基本上是一种基于栈的指令集架构,大部分都是零地址指令。
  • 基于栈的指令集主要的优点就是可移植,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。
  • 栈架构指令集的主要缺点是执行速度相对来说会稍慢一些。

案例:远程调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ClassExecutor {
static class MyClassLoader extends ClassLoader {
public MyClassLoader() {
super(MyClassLoader.class.getClassLoader());
}
public Class loadBytes(byte[] bytes) {
return defineClass(null, bytes, 0, bytes.length);
}
}
/**
* 执行外部传过来的代表一个Java类的Byte数组<br>
*
* @param classByte
* 代表一个Java类的Byte数组
* @param methodName
* 方法名称
* @return 执行结果
*/
public static Object execute(byte[] classByte, String methodName) {
Class clazz = new MyClassLoader().loadBytes(classByte);
try {
Object obj=clazz.newInstance();
Method method = clazz.getMethod(methodName);
return method.invoke(obj);
} catch (Throwable e) {
e.printStackTrace();
return "Err" + e.getMessage();
}
}
}
文章目录
  1. 虚拟机执行系统
    1. Class文件结构与字节码
      1. Class文件结构(详细结构规则请Google)
      2. 字节码(列表请Google)
    2. 类加载
      1. 时机
      2. 类加载过程
      3. 类加载器
      4. 双亲委派
      5. 案例:Tomcat类加载结构
    3. 字节码执行引擎
      1. 运行时栈帧结构
        1. 局部变量表
        2. 操作数栈
        3. 动态连接
        4. 方法返回地址
      2. 方法调用
      3. 基于栈的字节码执行引擎
      4. 案例:远程调用