运行时栈帧结构
局部变量表
操作数栈
动态连接
方法返回地址
附加信息
java虚拟机的运行时数据区域:
之前我们已经了解了有这么个区域,而且已经知道有栈帧这么一个结构,那我们就从方法执行的角度来剖析栈帧。
栈帧也叫过程活动记录,是编译器用来进行方法调用和方法执行的一种数据结构,它是虚拟机运行时数据区域中的虚拟机栈的栈元素。栈帧中包括了局部变量表,操作数栈,动态链接和方法返回地址以及额外的一些附加信息,在编译过程中,局部变量表的大小已经确定,操作数栈深度也是已经确定,因此栈帧在运行的过程中需要分配多大的内存时固定的,不受运行时影响。对于没有逃逸的对象也会在栈上分配内存,对象的大小其实在运行时也是确定的,因此即便出现了栈上内存分配,也不会导致栈帧改变大小。
一个线程中,可能调用链会很长,很多方法都同时处于执行状态。对于执行引擎来讲,活动线程中,只有栈顶的栈帧是最有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的字节码指令仅对当前栈帧进行操作。
局部变量表
局部变量表是一组变量池的存储空间,用来存储方法参数和方法内部定义的局部变量;
类变量、常量、静态变量存放到方法区中
public class Demo{
// 成员变量在类加载时会赋值一次默认值,
// 在初始化阶段会赋值一次自定义的值
private int a ;
private int b ;
public int add1(){
return a+b;
}
public int add2(){
// 局部变量不会被初始化,所以这里会报错
int a;
int b;
return a+b;
}
}
局部变量表最小单位:slot(变量槽)
8种基本数据类型
byte boolean short char int float double long
引用数据类型
reference returnAddress
虚拟机规范中没有指定slot的大小,一般32位java虚拟机slot就是32位
对于double、long占用62位,所以需要2个slot空间来存放,java虚拟机栈是线程独占的,所以用两个slot空间存储也不会存在线程安全性问题
若为64位系统,则一个slot就可以存储
并发安全性问题出现条件
- 多线程操作
- 要有共享资源
- 对共享资源进行非原子性操作
有的时候slot大小和定义的变量个数并不相同,是因为slot可以复用的原因
slot复用
当一个变量的pc寄存器的值大于slot的作用域的时候,slot是可以复用的
public class GCDemo{
public static void main(String[] args){
byte[] buff = new byte[60 * 1024 * 1024];
System.gc();
}
}
加上gc日志 -verbose:gc
运行结果:
gc的时候buff作用域还没有结束,所以不会被回收
对代码进行修改后
public class GCDemo{
public static void main(String[] args){
{
byte[] buff = new byte[60 * 1024 * 1024];
}
System.gc();
}
}
依然没有被回收
再次修改
public class GCDemo{
public static void main(String[] args){
{
byte[] buff = new byte[60 * 1024 * 1024];
}
int a = 10;
System.gc();
}
}
被回收,上面虽然已经在buff的作用域外gc,但是gcroot中还有引用,所以不会被回收。定义了一个变量int a = 10;后其实就是对局部变量表进行了读写,更新了gcroot,buff的引用清除了。
所以在方法中,不再使用的对象最好手动指定为null,便于垃圾回收
操作数栈
java虚拟机的解释执行引擎被称为“基于栈的执行引擎”,其中所指的栈就是指-操作数栈。
操作数栈也常被称为操作栈。和局部变量表一样,操作数栈也是被组织成一个以字节长为单位的数组。但是和前者不同的是,它不是通过索引来访问的,而是通过标准的栈操作-压栈和出栈来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。
虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如int、long、float、double、reference的存储。对于byte、short、char类型的值在压入到操作数栈之前,也会被转换为int.
虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中,看看下面的示例,它演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的:
iload_0 //
iload_1 //
iadd //
istore_2 //
在这个字节码序列里,前两个指令iload_0和iload_1将存储在局部变量中索引为0和1的整数压入操作数栈中,其后iadd指令从操作数栈中弹出那两个整数相加,再将结果压入操作数栈。第四条指令istore_2则从操作数栈中弹出结果,并把它存储到局部变量表索引为2的位置。下面详细描述了这个过程中局部变量和操作数栈的状态变化,途中没有使用的局部变量表和操作数栈区域以空白表示
理论上栈之间是完全独立的,但在具体java虚拟机实现中有可能会有部分重叠,为了减少变量的传递
动态连接
静态连接
动态连接:指向栈帧中的方法引用
方法返回地址
方法调用时通过一个指向方法的指针指向方法的地址,方法返回时将回归到调用处,那个地方是返回地址。
正常返回时通过pc寄存器值找到返回地址即可
当抛出异常时通过异常处理器表来确定方法返回地址。
附加信息
虚拟机规范中允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中。这部分信息完全取决于虚拟机的实现。
方法的调用
方法调用并不等同于方法的执行,方法调用阶段的唯一任务就是确定被调用方法的版本
解析调用
解析调用:编译期就能确定调用的方法
比如静态方法,构造器方法,私有方法,final修饰的方法
方法执行的字节码指令:
invokestatic
invokespecial
invokevirtual
invokeinterface
invokedynamic
可以被解析调用的肯定会解析成invokestatic、invokespecial这两种指令
分派调用
分派调用:需要在运行期间才能确定调用的方法版本
静态分派调用
静态分派主要针对的是方法的重载
public class Demo {
static class Parent {}
static class Child1 extends Parent {}
static class Child2 extends Parent {}
public void say(Child1 c){
System.out.println("c1 is call");
}
public void say(Child2 c){
System.out.println("c2 is call");
}
public void say(Parent p){
System.out.println("p is call");
}
public static void main(String[] args) {
// 多态 父类引用指向子类实例 Parent称为变量的静态类型 Child1为实际类型
Parent c1 = new Child1();
Parent c2 = new Child2();
/** 输出
p is call
p is call
*/
// 方法通过静态类型来确定调用的版本,这种则为 静态分派调用
// 静态分派在编译阶段就能确定
Demo demo = new Demo();
demo.say(c1);
demo.say(c2);
// 输出 c2 is call
// 静态类型发生改变
demo.say((Child2)c2);
}
}
静态分派并不是一定固定的,方法调用过程中会优先匹配最合适的方法调用,如果没有最优则会进行类型转换再匹配
示例:
public class Demo2 {
// 1
public void say(short p){
System.out.println("short is call");
}
// 2
public void say(int p){
System.out.println("int is call");
}
// 3
public void say(long p){
System.out.println("long is call");
}
// 4
public void say(char p){
System.out.println("char is call");
}
// 5
public void say(Object p){
System.out.println("Object is call");
}
// 6
public void say(char ... p){
System.out.println("char... is call");
}
public static void main(String[] args) {
Demo2 demo = new Demo2();
demo.say('a');
}
}
/**
直接执行输出:char is call
将4注释掉输出:int is call
将2注释掉输出:long is call
将3注释掉输出:Object is call
将5注释掉输出:char... is call
由此可以知道静态分派并不是固定的,当不能确定是哪个方法时,jvm会选择最接近的
*/
动态分派调用
动态分派调用主要针对的是方法的重写
- 找到操作数栈顶的第一个元素所指向的对象的实际类型
- 如果在实际类型中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则直接返回这个方法的直接引用,查找过程结束,如果不通过,抛出异常
- 按照继承关系从下往上依次对实际类型的各父类进行搜索与验证
- 如果始终没有找到,则抛出AbstractMethodError
public class Demo3 {
static class Parent{
public void say(){
System.out.println("parent");
}
}
static class Child1 extends Parent{
@Override
public void say(){
System.out.println("child1");
}
}
static class Child2 extends Parent{
@Override
public void say(){
System.out.println("child2");
}
}
static class Child11 extends Child1{
@Override
public void say(){
System.out.println("child11");
}
}
public static void main(String[] args) {
Parent p = new Parent();
Parent p1 = new Child1();
Parent p2 = new Child2();
Parent p3 = new Child11();
p.say();
p1.say();
p2.say();
p3.say();
}
}
输出:
parent
child1
child2
child11
这里不再是根据静态类型来调用方法,而是根据实际类型来调用方法
动态类型语言支持
- 静态类型的语言在非运行阶段,变量的类型是可以确定的,也就是说变量是有类型的。比如java
- 动态类型语言在非运行阶段,变量的类型是无法确定的,也就是变量是没有类型的,但是值是有类型的,也就是运行期间可以确定变量的值的类型。比如javascript
jdk1.6开始增加了对javascript的支持
public class JavaScriptDemo {
public static void main(String[] args) throws ScriptException {
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine se = sem.getEngineByName("JavaScript");
Object eval = se.eval("function add(a,b){return a+b;} add(1,2)");
System.out.println(eval);
}
}
字节码执行引擎小结
代码执行时涉及的内存结构
如何找到正确的执行方法
如何执行方法的字节码
总结
- java的技术体系
- java程序设计语言
- java虚拟机
- class文件的格式
- JavaApi
- 第三方类库
java技术体系,认识java虚拟机
内存管理
运行时内存区域
对象的创建,内存布局以及访问定位
垃圾回收
判断对象是否存活
垃圾收集算法
垃圾收集器
内存的分配
java的监控工具
java虚拟机的运行
class文件格式
class字节码指令
类加载
字节码的执行引擎