运行时栈帧
栈帧( 局部变量表(基本数据类型,对象引用,returnAddress类型 ), 操作数栈 , 动态链接 , 方法返回地址 )
局部变量表
以Slot为最小的单位。用来存放32位以内的数据类型。像long,double需要连续的两个Slot
局部变量从0位开始会每一个index对应一个变量的值0一般是this
Slot可以改变,当程序计数器指针超出Slot的作用域(用{}指定)的时候则这个Slot就能够复制给其他人了。
垃圾回收的时候Slot会影响GC的行为,比如:
public class TestSlot { public static void main(String[] args) { { byte[] placeholder = new byte[64*1024]; } System.gc(); } }
输出:
[Full GC 345K->212K(15872K), 0.0051430 secs]
改为:
public static void main(String[] args) { { byte[] placeholder = new byte[64*1024]; } int a = 1; System.gc(); }
输出:
[Full GC 345K->148K(15872K), 0.0067385 secs]
或者改为:
public static void main(String[] args) { { byte[] placeholder = new byte[64*1024]; placeholder = null; } System.gc(); }
输出跟上面一样。
这个是因为,加入了int a = 1;之后 placeholder占用的Slot被a用了,就能被回收了。
null也是因为Slot为空了。但是null这种编程办法并不提倡,因JIT的时候这行代码会被去掉。通常我们提倡使用作用于({})来控制Slot以促进回收。
操作数栈
后入先出栈,方法执行开始的时候为空,当需要的时候再入栈出栈,比如做数学运算,或者方法调用的传参都是用操作数栈进行的。
动态链接
栈帧持有一个指向运行时常量池中该帧所属方法的引用,Class文件常量池中有大量的符号引用,一部分在类加载的时候转化为直接引用。另外一部分就是在运行的时候通过该帧的这个引用来转化为直接引用。也就是动态链接了。
方法返回地址
方法退出可能因为return 或者是异常,退出实际上是把当前的栈帧出栈,然后根据这个返回地址找到被调用的位置,还会把返回值压入栈帧等。
方法调用
解析
编译器可知,运行期不可变的方法的调用成为解析。 有static的方法和private的方法。 在编译器就会转为直接引用
分派
1. 静态分派
Human man = new Man(). 其中 Man继承于Human, Human就成为静态变量, Man就是实际变量, 静态变量编译器确定,实际变量运行期确定。
重载就是使用了静态分派的方式,根据静态变量来判断的。
另外看下面这段代码:
public class OverLoad { public static void main(String[] args) { OverLoad.sayHello('a'); } private static void sayHello(char c) { System.out.println("Char"); } private static void sayHello(int c) { System.out.println("Int"); } private static void sayHello(long c) { System.out.println("long"); } private static void sayHello(Character c){ System.out.println("Character"); } private static void sayHello(char c, String... a) { System.out.println("Char..."); } }
打印什么呢。从sayHello char往下开始一直注释,分别会打印 Char, Int, long,Character, Char...
为什么呢,肯定不是因为代码顺序。 是因为重载的机制,是用的静态分派,分派规则是先找自身,然后JVM强转的,然后是自动封包的,最后是动态参数的
2. 动态分派
如下代码:
abstract class Human{ abstract void sayHello(); } class Man extends Human{ @Override void sayHello() { System.out.println("Man"); } } class Woman extends Human{ @Override void sayHello() { System.out.println("Woman"); } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); man.sayHello(); woman.sayHello(); } }
man.sayHello()和woman.sayHello()的调用部分的字节码是一样的,都是invoke Human.sayHello() 大体的格式,实际上不是这样的。
主要在于new 的时候会把实际变量的引用压栈,然后调用的时候会从栈顶找到根当前符号变量一致的直接引用进行与符号变量的一致性验证,如果通过就是用这个直接引用,没通过会继续对这个符号变量的父类进行验证,如果都没通过则抛出异常
基于栈的执行
基于栈的可以避免硬件差异,能够更紧凑,还能够根据语言特点进行优化,灵活性也更高,但是会比寄存器的执行速度慢一些,因为会有入栈和出栈的过程。
代码:
public int add(){ int a = 100; int b = 200; return a + b; }
使用javap -c来查看字节码:
public int add(); Code: 0: bipush 100 2: istore_1 3: sipush 200 6: istore_2 7: iload_1 8: iload_2 9: iadd 10: ireturn }
那么这个方法的执行过程就应该是:
计数器 从0开始执行到10
0的时候100入操作数栈,this入局部变量表
1把100出栈放入局部变量表
7,8两部把100,200放入操作数栈顶
9把100,200从操作数站定出栈,进行计算放入操作数栈顶。
10. 返回调用点,把栈顶的300压入调用栈。
字节码生成技术及动态代理
javac, JDK动态代理, CGLIB等等
interface IHello{ void sayHello(); } class Hello implements IHello{ @Override public void sayHello() { System.out.println("hello world"); } } public class DynamicProxyTest { static class DynamicProxy implements InvocationHandler{ private IHello hello; public IHello bind(IHello hello){ this.hello = hello; return (IHello) Proxy.newProxyInstance(hello.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(hello, args); } } public static void main(String[] args) { IHello hello = new Hello(); IHello helloP = new DynamicProxy().bind(hello); helloP.sayHello(); } }
打印的是:
welcome hello world
主要在于Proxy.newProxyInstance这个方法,其进行了验证,生成字节码等操作。
另外是能够通过设置参数来把这个生成的动态类放到硬盘上的可以反编译查看。