虚拟机字节码执行引擎(深入理解java虚拟机笔记)
目录
概述
无论是虚拟机还是物理机都会有执行引擎这一概念,执行引擎有代码执行能力。物理机的执行引擎建立在处理器,硬件,指令集,操作系统上,而虚拟机的执行引擎有自己的指令集、虚拟硬件(比如:栈,工作内存等)等等一些与执行引擎有关的结构体系,虚拟机的执行引擎执行java代码(code属性code项中的字节码)方式可以是解析器执行或编译执行(通过即时编译器把code翻译成本地机器码),或两者兼备。
运行时栈帧结构
虚拟机栈中填充的是栈帧元素,栈帧是用于支持虚拟机进行方法调用和执行的数据结构,所以一个方法对应一个栈帧。
方法执行时需要的内存量(包括局部变量表占用、操作数栈占用等)记录在方法表中的code属性里,也即是说栈帧的大小在编译完class文件之后就确定了。
局部变量表
用于存储方法参数和方法内部定义的局部变量。
Code属性的max_locals指明局部变量表的最大容量。
局部变量表以Slot(槽位)为最小单位,Slot的大小随着处理器、操作系统、虚拟机不同而可能不同,若虚拟机明确规定32位的Solt,而实现用64位物理内存去实现,那么要用对齐或补空的措施来让solt使用起来与32位的一致。
若一个Solt可以存放32位以内的数据类型,boolean、int、short、float、char、byte、reference、returnAddress 8种类型32位可以用一个Solt,long、double64位占2个solt。
局部变量表的solt从0开始索引,0位存储指向堆中的this引用,然后才存储参数,接着存储方法内部变量。
局部变量表的Solt可以复用,并且复用对垃圾收集器有影响,举例如下:
gc()之后无影响,日志如下:
设立作用域修改如下:
gc()之后也无影响,日志:
再次修改:
日志:
原代码在gc时,placeholder数组对象的引用还在本方法的作用域内;第一次修改,placeholder引用虽然出了作用域,但是局部变量表再无任何读写操作,没有复用时机;第二次修改有了复用时机。所以编写代码中只要出了作用域我们可以设置引用为null来制造复用时机,从而可以及时地回收内存。
操作数栈
顾名思义,存储需要执行某项操作的操作数。例如add的操作数。
Code属性的max_stacks指明操作数栈的最大深度。
在概念模型中2个栈帧是相互隔离的,但是虚拟机的实现中可以做一些优化,如:
下面栈帧的部分操作数栈与上面栈帧的部*部变量表重叠在一起,这样可以在方法调用时就可以共用一部分数据,无须进行额外的参数赋值传递。
动态链接
指向运行时常量池中该栈帧所属方法的引用,这个引用是为了支持方法调用过程中的动态链接。在使用该方法是才将符号引用转换为直接引用,这成为动态链接。详情参看下文方法调用。
方法返回地址
方法返回有2种方式。一,遇到返回字节码指令,有没有返回值,返回类型由字节码指令决定;二,遇到了异常,无论是athrow字节码指令,还是本方法的异常表中无匹配异常,都会无返回值地返回。
无论哪种返回,都需返回到方法被调用的位置的下一位置,才能继续执行。所以栈帧需要保存一些信息来帮助恢复到调用者的执行状态。栈帧会保存它的调用者的PC计数器的值。
附加信息
虚拟机规范里没有的信息,例如:调试相关的信息。
方法调用
解析
方法调用指令中的目标方法会被解析成常量池中一个符号引用,在类加载阶段会将“编译期可知,运行期不变”的这类方法的符号引用转化为直接引用,这类方法的转化叫解析。
这类方法主要包括:静态方法和私有方法,它们的特点决定了不可能通过继承或其他方式复写其他版本。但静态方法可以有重载方法,故此静态方法也是可以有静态分派的过程。
与方法调用对应的5条字节码调用指令:
invokestatic和invokespecial调用的4中方法称为非虚方法,被final的方法虽然使用invokvirtual指令,但是它也是非虚方法。
分派
解析在编译器完全确定,在类加载的解析阶段即可转化,不会延迟到运行期去转化(动态的过程),这被称为静态的过程。而分派既可以是静态的也可以是动态的,还可以是单分派和多分派,组合有静态单分派,动态单分派,静态多分派,动态多分派四种。
和方法重载有密切关联。
依赖静态类型来定位方法执行版本的分派动作称为静态分派。
/**
* 方法静态分派演示
* @author zzm
*/
public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human guy) {
System.out.println("hello,guy!");
}
public void sayHello(Man guy) {
System.out.println("hello,gentleman!");
}
public void sayHello(Woman guy) {
System.out.println("hello,lady!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);// man的静态类型是Human,实际类型是Man
sr.sayHello(woman);// woman的静态类型是Human,实际类型是Woman
}
}
以上方法重载是静态分派的典型应用场景,他发生在编译阶段。字面量没有显示的静态类型,只能通过语言上的规则去理解和推断。看官可以尝试下面的例子:
public class Overload {
public static void sayHello(Object arg) {
System.out.println("hello Object");
}
public static void sayHello(int arg) {
System.out.println("hello int");
}
public static void sayHello(long arg) {
System.out.println("hello long");
}
public static void sayHello(Character arg) {
System.out.println("hello Character");
}
public static void sayHello(char arg) {
System.out.println("hello char");
}
public static void sayHello(char... arg) {
System.out.println("hello char ...");
}
public static void sayHello(Serializable arg) {
System.out.println("hello Serializable");
}
public static void main(String[] args) {
sayHello('a');
}
}
此例也是在编译期间确定了方法调用的版本。静态方法可以有不同的版本重载,所以静态方法也可以通过静态分派来在编译期间确定版本。上面2个例子字节码的调用指令是invokevirtual,调用的目标方法的符号引用是在编译期确定的。
和方法重写有密切关联。
/**
* 方法动态分派演示
* @author zzm
*/
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
Code为:
0 new #2 <org/fenixsoft/polymorphic/DynamicDispatch$Man>
3 dup
4 invokespecial #3 <org/fenixsoft/polymorphic/DynamicDispatch$Man.<init>>
7 astore_1
8 new #4 <org/fenixsoft/polymorphic/DynamicDispatch$Woman>
11 dup
12 invokespecial #5 <org/fenixsoft/polymorphic/DynamicDispatch$Woman.<init>>
15 astore_2
16 aload_1
17 invokevirtual #6 <org/fenixsoft/polymorphic/DynamicDispatch$Human.sayHello>
20 aload_2
21 invokevirtual #6 <org/fenixsoft/polymorphic/DynamicDispatch$Human.sayHello>
24 new #4 <org/fenixsoft/polymorphic/DynamicDispatch$Woman>
27 dup
28 invokespecial #5 <org/fenixsoft/polymorphic/DynamicDispatch$Woman.<init>>
31 astore_1
32 aload_1
33 invokevirtual #6 <org/fenixsoft/polymorphic/DynamicDispatch$Human.sayHello>
36 return
16行和20行意思是把2个对象的引用压入操作数栈顶,并且分别执行invokevirtual指令。而invokevirtual指令会有一个多态查找的过程(也就是在运行期间确定目标方法的直接引用):
我们把这种在运行期间根据实际类型确定方法执行版本的分派过程称为动态分派。
上述例子中将要执行的sayHello()方法的所有者,称为接受者。方法的接受者和方法的所有参数称为方法的宗量。
单分派是根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行分派。
看例子:
public class Dispatch {
static class QQ {}
static class _360 {}
public static class Father {
public void test(){}
public void hardChoice(QQ arg) {
System.out.println("father choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("father choose 360");
}
}
public static class Son extends Father {
public void hardChoice(QQ arg) {
System.out.println("son choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("son choose 360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}
这个例子既有静态分派过程也有动态分派过程。
先来看在编译期的静态分派过程。
0 new #2 <Dispatch$Father>
3 dup
4 invokespecial #3 <Dispatch$Father.<init>>
7 astore_1
8 new #4 <Dispatch$Son>
11 dup
12 invokespecial #5 <Dispatch$Son.<init>>
15 astore_2
16 aload_1
17 new #6 <Dispatch$_360>
20 dup
21 invokespecial #7 <Dispatch$_360.<init>>
24 invokevirtual #8 <Dispatch$Father.hardChoice>
27 aload_2
28 new #9 <Dispatch$QQ>
31 dup
32 invokespecial #10 <Dispatch$QQ.<init>>
35 invokevirtual #11 <Dispatch$Father.hardChoice>
38 return
21 和24、32 和35分别展示了invokevirtual选择目标方法的依据有2个宗量:一,方法接受者的静态类型都是Father,二,参数的静态类型分别是360和QQ,也就是分别指向了Father. hardChoice(360)和Father. hardChoice(QQ)方法的符号引用,可见java语言的静态分派属于多分派。
再看运行期虚拟机的动态分派过程。
invokevirtual指令在运行时会有一个多态查找的过程,由于压入操作数栈的引用分别指向堆中的Father类型和Son类型,所以一个invokevirtual指令的接受者是Father类型,一个是Son类型,而参数已经在静态分派中确定了符号引用,所以至于参数的类型在此处已经不构成动态分派的影响,所以java语言的动态分派属于单分派。
所以java语言是一门“静态多分派,动态单分派”的语言。但是随着java语言的跟新换代,“静态多分派,动态单分派”不一定亘古不变。
由于动态分派在应用中非常频繁,而且动态分派还需要在类的方法元数据中搜索合适的目标方法,因此有“稳定优化”手段:类的方法区中建立虚方法表(vtable)或者接口方法表(itable),从而使用表中索引来代替元数据查找。
表中存储着各个方法的实际入口地址。没有被重写的方法在父子的表中都有同样的索引号,同样的地址入口。有重写的方法在子表中也有同样的索引号,但有不同的入口地址。
方法表一般在类加载的链接阶段进行初始化,在类变量初始值赋值后进行。
动态类型语言支持
关键特征是它的类型检查的主体过程是在运行期而不是编译期,例如:APL、Clojure、Erlang、Groovy、JavaScript、Jython、Lisp、Lua、PHP、Prolog、Python、Ruby、Smalltalk和Tcl等等。在编译期就进行类型检查过程的语言,如C++和Java等就是最常用的静态类型语言。
一个没有指定语言的语句:obj.println(“hello world”);
若是Java语言,编译后:
invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
这个符号引用包含了此方法定义在哪个具体类型之中、方法的名字以及参数顺序、参数类型和方法返回值等信息,通过这个符号引用,虚拟机就可以翻译出这个方法的直接引用(譬如方法内存地址或者其他实现形式)。
若是ECMAScript,编译后最多只能确定方法名称、参数、返回值这些信息,而不会去确定方法所在的具体类型(方法接收者不固定),这是也是一个关键特征。
iava.lang.invoke包中MethodHandle
从网络略。
基于栈的字节码执行引擎
解释执行
传统编译原理中程序代码到目标代码的生成过程,如下图,解释执行的过程指的是中间那条分支:
Javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。因为这一动作是在Java虚拟机之外进行的,而解释器在虚拟机的内部,所以Java程序的编译是半独立的实现。
基于栈的指令集和基于寄存器的指令集
JVM中的栈和寄存器只能是一种虚拟的概念,JVM不可能要求物理机必须有一套硬件上的栈和寄存器来满足JVM规范。基于寄存器的虚拟机应该是模仿了计算机体系机构中的寄存器部件组的设计,该寄存器是可以映射到物理机上的寄存器的,但是具体怎么样还要看具体的实现。
网上搜索“基于栈的虚拟机 VS 基于寄存器的虚拟机”可以了解到更多内容。
基于栈的虚拟机比基于寄存器的访存要频繁,因此基于寄存器的虚拟机执行速度要快。
基于栈的解释器执行过程举例
例子从书略。
字节码生成与动态代理
public class DynamicProxyTest {
interface IHello{
void sayHello();
}
static class Hello implements IHello {
@Override
public void sayHello() {
System.out.println("hello proxy!");
}
}
static class DynamicProxy implements InvocationHandler{
Object src;
Object bind(Object src){
this.src = src;
return Proxy.newProxyInstance(src.getClass().getClassLoader(),new Class[]{IHello.class},this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("welcome ");
return method.invoke(src,args);
}
}
public static void main(String[] args) throws Throwable {
//把生成的代理类持久化到本地文件 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
IHello hello = (IHello)new DynamicProxy().bind(new Hello());
hello.sayHello();
}
}
跟踪Proxy.newProxyInstance这个方法的源码,可以看到程序进行了验证、优化、缓存、同步、生成字节码、显示类加载等操作,前面的步骤并不是我们关注的重点,而最后它调用了ProxyGenerator.generateProxyClass,该方法用来完成生成字节码的动作,最终生成一个$Proxy0.class文件,使用Fernflower反编译器反编译如下内容:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.ne;
import com.ne.DynamicProxyTest.IHello;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0 extends Proxy implements IHello {
private static Method m3;
private static Method m1;
private static Method m0;
private static Method m2;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final void sayHello() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final boolean equals(Object var1) throws {
try {
return ((Boolean)super.h.invoke(this, m1, new Object[]{var1})).booleanValue();
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final int hashCode() throws {
try {
return ((Integer)super.h.invoke(this, m0, (Object[])null)).intValue();
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m3 = Class.forName("com.ne.DynamicProxyTest$IHello").getMethod("sayHello", new Class[0]);
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
ClassModifier
Class文件修改的工具类2个:
/**
* 修改Class文件,暂时只提供修改常量池常量的功能
* @author zzm
*/
public class ClassModifier {
/**
* Class文件中常量池的起始偏移
*/
private static final int CONSTANT_POOL_COUNT_INDEX = 8;
/**
* CONSTANT_Utf8_info常量的tag标志
*/
private static final int CONSTANT_Utf8_info = 1;
/**
* 常量池中11种常量所占的长度,CONSTANT_Utf8_info型常量除外,因为它不是定长的
*/
private static final int[] CONSTANT_ITEM_LENGTH = { -1, -1, -1, 5, 5, 9, 9, 3, 3, 5, 5, 5, 5 };
private static final int u1 = 1;
private static final int u2 = 2;
private byte[] classByte;
public ClassModifier(byte[] classByte) {
this.classByte = classByte;
}
/**
* 修改常量池中CONSTANT_Utf8_info常量的内容
* @param oldStr 修改前的字符串
* @param newStr 修改后的字符串
* @return 修改结果
*/
public byte[] modifyUTF8Constant(String oldStr, String newStr) {
int cpc = getConstantPoolCount();
int offset = CONSTANT_POOL_COUNT_INDEX + u2;
for (int i = 0; i < cpc; i++) {
int tag = ByteUtils.bytes2Int(classByte, offset, u1);
if (tag == CONSTANT_Utf8_info) {
int len = ByteUtils.bytes2Int(classByte, offset + u1, u2);
offset += (u1 + u2);
String str = ByteUtils.bytes2String(classByte, offset, len);
if (str.equalsIgnoreCase(oldStr)) {
byte[] strBytes = ByteUtils.string2Bytes(newStr);
byte[] strLen = ByteUtils.int2Bytes(newStr.length(), u2);
classByte = ByteUtils.bytesReplace(classByte, offset - u2, u2, strLen);
classByte = ByteUtils.bytesReplace(classByte, offset, len, strBytes);
return classByte;
} else {
offset += len;
}
} else {
offset += CONSTANT_ITEM_LENGTH[tag];
}
}
return classByte;
}
/**
* 获取常量池中常量的数量
* @return 常量池数量
*/
public int getConstantPoolCount() {
return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2);
}
}
/**
* Bytes数组处理工具
* @author
*/
public class ByteUtils {
public static int bytes2Int(byte[] b, int start, int len) {
int sum = 0;
int end = start + len;
for (int i = start; i < end; i++) {
int n = ((int) b[i]) & 0xff;
n <<= (--len) * 8;
sum = n + sum;
}
return sum;
}
public static byte[] int2Bytes(int value, int len) {
byte[] b = new byte[len];
for (int i = 0; i < len; i++) {
b[len - i - 1] = (byte) ((value >> 8 * i) & 0xff);
}
return b;
}
public static String bytes2String(byte[] b, int start, int len) {
return new String(b, start, len);
}
public static byte[] string2Bytes(String str) {
return str.getBytes();
}
public static byte[] bytesReplace(byte[] originalBytes, int offset, int len, byte[] replaceBytes) {
byte[] newBytes = new byte[originalBytes.length + (replaceBytes.length - len)];
System.arraycopy(originalBytes, 0, newBytes, 0, offset);
System.arraycopy(replaceBytes, 0, newBytes, offset, replaceBytes.length);
System.arraycopy(originalBytes, offset + len, newBytes, offset + replaceBytes.length, originalBytes.length - offset - len);
return newBytes;
}
}
例子是把类中使用System.out的输出的日志信息,替换为使用HackSystem.out来输出日志信息。所以还有一个HackSystem类如下:
/**
* 为JavaClass劫持java.lang.System提供支持
* 除了out和err外,其余的都直接转发给System处理
*
* @author zzm
*/
public class HackSystem {
public final static InputStream in = System.in;
private static ByteArrayOutputStream buffer = new ByteArrayOutputStream();
public final static PrintStream out = new PrintStream(buffer);
public final static PrintStream err = out;
public static String getBufferString() {
return buffer.toString();
}
public static void clearBuffer() {
buffer.reset();
}
public static void setSecurityManager(final SecurityManager s) {
System.setSecurityManager(s);
}
public static SecurityManager getSecurityManager() {
return System.getSecurityManager();
}
public static long currentTimeMillis() {
return System.currentTimeMillis();
}
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) {
System.arraycopy(src, srcPos, dest, destPos, length);
}
public static int identityHashCode(Object x) {
return System.identityHashCode(x);
}
// 下面所有的方法都与java.lang.System的名称一样
// 实现都是字节转调System的对应方法
// 因版面原因,省略了其他方法
}
还差一个装载以byte数组形式提供的字节码的类装载器:
/**
* 为了多次载入执行类而加入的加载器<br>
* 把defineClass方法开放出来,只有外部显式调用的时候才会使用到loadByte方法
* 由虚拟机调用时,仍然按照原有的双亲委派规则使用loadClass方法进行类加载
*
* @author zzm
*/
public class HotSwapClassLoader extends ClassLoader {
public HotSwapClassLoader() {
super(HotSwapClassLoader.class.getClassLoader());
}
public Class loadByte(byte[] classByte) {
return defineClass(null, classByte, 0, classByte.length);
}
}
使用这个类修改器来完成这个例子:
使用这个类修改器来完成这个例子:
/**
* JavaClass执行工具
*
* @author zzm
*/
public class JavaClassExecuter {
/**
* 执行外部传过来的代表一个Java类的Byte数组<br>
* 将输入类的byte数组中代表java.lang.System的CONSTANT_Utf8_info常量修改为劫持后的HackSystem类
* 执行方法为该类的static main(String[] args)方法,输出结果为该类向System.out/err输出的信息
* @param classByte 代表一个Java类的Byte数组
* @return 执行结果
*/
public static String execute(byte[] classByte) {
HackSystem.clearBuffer();
ClassModifier cm = new ClassModifier(classByte);
byte[] modiBytes = cm.modifyUTF8Constant("java/lang/System", "org/fenixsoft/classloading/execute/HackSystem");
HotSwapClassLoader loader = new HotSwapClassLoader();
Class clazz = loader.loadByte(modiBytes);
……
return HackSystem.getBufferString();
}
}
上一篇: Java反射机制(带应用)
下一篇: Collections工具类常用方法
推荐阅读
-
荐 【探究JVM四】Java方法执行的线程内存模型——虚拟机栈 字节码指令追踪,万字长文深入探究内部结构
-
《深入理解JAVA虚拟机》第九章 类加载及执行子系统的案例与实战
-
《深入理解java虚拟机》学习笔记--第三章:垃圾收集器与内存分配策略 jvm
-
《深入理解java虚拟机》学习笔记--第四章:虚拟机性能监控与故障处理工具 虚拟机java
-
《深入理解java虚拟机》学习笔记--第四章:虚拟机性能监控与故障处理工具 虚拟机java
-
《深入理解java虚拟机》学习笔记--第三章:垃圾收集器与内存分配策略 jvm
-
读书笔记:深入理解java虚拟机(二)创建对象的时候需要访问哪几块内存
-
读书笔记:深入理解java虚拟机(一)虚拟机的运行时的数据区域
-
垃圾收集器(深入理解 Java 虚拟机笔记)
-
《深入理解Java虚拟机:JVM高级特性与最佳实践》读书笔记