Understand JVM Part II
虚拟机执行系统
Class文件结构与字节码
Class文件结构(详细结构规则请Google)
- 魔数与类文件版本:文件的头4字节表示Class文件:0XCAFEBABE
- 常量池
- 访问标志
- 类索引、父类索引与接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
字节码(列表请Google)
- 大部分与数据类型有关的字节码都包含了类型信息,如iload、fload, 但底层可能是同样逻辑
- 大多数对于boolean、short、byte、char等类型的操作,实际使用int类型作为运算类型
- 运算指令中,只有除0操作抛出ArithmeticException,其他操作不会出现异常
- 窄化类型转换指令直接丢弃最低N个字节以外的东西,不会有异常
- 指令类型主要有(加载存储、运算、类型转换、对象创建与访问、操作数栈、控制转移、方法调用与返回、异常处理、同步(管程))
类加载
时机
- 遇到new getstatic putstatic invokestatic这4条指令,如果类没有初始化,则进行初始化
- 使用reflect包对类方法反射调用,如果没有初始化则初始化
- 初始化某类,若父类未初始化,则初始化父类
- jvm启动时,用户需要指定一个主类(main方法),jvm首先初始化这个类
- 使用JDM1.7的动态语言支持,MethodHandler实例的相关使用时,方法句柄对应类未初始化时
- 以上5中情况称为对类的主动引用,被动引用时不触发初始化类(创建数组,引用别的类的静态常量,通过子类引用父类的静态变量)
- 接口的初始化中,不要求父接口初始化,只有需要使用时才进行初始化父接口
类加载过程
- 类的生命周期:加载->验证->准备->解析->初始化->使用->卸载
- 加载:类的二进制字节流保存在方法区中,并生成一个Class对象作为方法区这个类数据的入口,对于HotSpot虚拟机来说Class对象存放在方法区中而不是堆中(如果是数组类型,则递归加载去掉一个维度的组件类型,如果组件类型为引用类型,则数组在加载这个组件的类加载器的类空间名称上被标识)
- 验证:文件格式、元数据、字节码、符号引用
- 准备:设置类变量(非实例变量)初始值,方法区中进行,如果不是final static, 则首先按照类型赋值0值,初始化用户指定值包装在类构造器
<clinit>
方法中,在初始化阶段执行,final static则直接赋值为用户指定值 - 解析:常量池中的符号引用替换为直接引用(类或接口、字段、类方法、接口方法)
- 初始化:执行类构造器
<clinit>()
方法,<clinit>()
由类变量的赋值动作和static语句块合并产生,static语句块不可以赋值定义在其后的变量- jvm保证父类的
<clinit>()
方法先执行,static语句块也是父类的先执行 <clinit>()
方法不是必须的,如果既没有类变量的赋值也没有static语句块- 接口也会有该方法,但执行时不需要先执行父接口(使用时才执行)中的该方法,接口的实现类初始化不会执行接口的该方法
- jvm保证执行该方法多线程安全,如果耗时,则造成阻塞
类加载器
- 类加载器用于获取某个类的二进制字节流
- 每一个类加载器,都有一个类名称空间,判断两个类是否相等,需要在两个类都为同一个类加载器加载的情况下才有意义,否则必定不等 1234567891011121314151617181920212223242526272829public class TestUserClassLoder {public static void main(String[] args) throws Exception {ClassLoader MyClassLoader = new ClassLoader() {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 不是同一个类加载器实例加载 必然不相等 包括instanceSystem.out.println(b instanceof TestUserClassLoder);}}
双亲委派
对于JVM来说,只存在两种不同的类加载器:一种是启动类加载器(jvm一部分,c++实现,用户获取不到),另一种由Java实现,独立于JVM
从开发角度,主要有三种类加载器:
- 启动类加载器:加载JAVA_HOME/lib目录下面的复合条件的类(jar包)
- 扩展类加载器:加载JAVA_HOME/lib/ext目录下面的所有类库
- 应用程序类加载器:用于加载用户类路径上的类库,程序中默认的类加载器
类加载器之间通过双亲委派规则进行配合,如图
双亲委派模型中:
- 除了顶层启动类加载器,其他类加载器都应当有自己的父类加载器(一般通过组合实现)
- 类加载器接收到类加载请求,首先将请求委派给父类加载器完成,只有父类加载器反馈无法加载才尝试自己加载
- Java类随着类加载器具备了优先级的层次关系(Object类存在于rt.jar中,用户定义的Object即使全类名一致也不会加载,因为启动类加载器已经加载了该类,所有任何环境中Object类都是同一个)
双亲委派模型的破坏,JVM团队为了解决某些问题引入了线程上下文加载器,破坏了双亲委派请求链路,直接使用下层类加载器加载目标类(如Spring框架可以放在共享的jar目录中,则Spring类的加载由共享类加载器加载,而此时Spring需要管理用户的类,加载Spring类的共享类加载器无法加载用户的类,此时Spring则使用线程上下文加载器,即谁调用Spring去管理Bean,谁就要提供给Spring类加载器)
案例:Tomcat类加载结构
要解决的问题
- 部署在同一个服务器上的多个应用Java类库实现相互隔离
- 部署在同一个服务器上的应用间Java类库可以共享(例如公共的类库)
- 服务器需要保证自身的安全不受应用影响,服务器所使用的类库与应用使用的类库应该隔离
- 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行为会与解释执行有不同,不会有问题
1234567891011public 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的SlotSystem.gc();}}局部变量初始化才可以使用,不存在准备阶段(赋初值,参考类变量)
操作数栈
- 编译程序时,需要使用深的操作数栈已经确定,不受代码运行期间影响
- 操作数栈的数据类型必须与字节码指令序列严格匹配,编译器保证
- Java虚拟机的解释执行引擎称为基于栈的执行引擎,栈即指操作数栈
- 一般的虚拟机会对操作数栈进行优化,让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠,省去了额外的参数复制
动态连接
- 指向运行时常量池中该栈帧方法的引用,用户支持调用过程中的动态连接(Dynamic Linking)
方法返回地址
- 保存方法被调用的位置
- 方法可以正常返回或异常返回,异常返回没有返回值
- 方法退出操作大致:恢复上层方法的局部变量表和操作数栈 -> 返回值压入调用者操作数栈 -> PC计数器指向下一条地址
方法调用
- 确定被调用方法的版本过程而不是执行过程
- 类加载阶段的解析阶段,将一部分方法转换为直接引用,这些方法在执行前可确定版本,且运行期不可变(见下)
Java关于方法调用的字节码:
- invokestatic:调用静态方法 (执行前可确定版本)
- invokespecial:调用实例构造器方法,私有方法和父类方法 (执行前可确定版本)
- invokevirtual:调用虚方法 (final方法也是该字节码调,但是其不是虚方法)
- invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
- invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
方法版本的确定-分派:
- 静态分派:根据依赖静态类型选择方法版本(方法重载)
- 动态分派:运行期根据实际类型确定方法执行版本
- Java: 静态多分派(根据静态类型与参数列表选择方法,编译期确定),动态单分派(此时只需要确定具体对象类型的方法版本,不同参数列表导致的方法版本确定已经在编译期完成)
动态分派实现方式:虚方法表,保存各个方法的实际入口,没有重写父类方法就指向父类实现入口(还有其他方法,如内联缓存Inline Cache、基于继承关系分析CHA的守护内联Guard Inlining等非稳定手段)
Java7之后提供了MethodHandle机制实现动态方法调用
基于栈的字节码执行引擎
- Java 编译器输出的指令流,基本上是一种基于栈的指令集架构,大部分都是零地址指令。
- 基于栈的指令集主要的优点就是可移植,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。
- 栈架构指令集的主要缺点是执行速度相对来说会稍慢一些。
案例:远程调用
|
|