JVM学习四

本文详细解读了JVM运行时数据区(包括栈、堆、方法区和直接内存)、栈帧的概念,重点介绍了PC、JVM栈、动态链接ing和return address。通过实例分析,探讨了`TestIPulsPlus`程序中指令执行过程。涵盖了本地方法栈、字符串常量区迁移、以及各种指令集和方法调用的深入理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

运行时数据区(Runtime Data Area)

参考jvms(java virtual machine specification)和jls(java language specification)

从一则面试题说起:

public class TestIPulsPlus {
    public static void main(String[] args) {
        int i = 8;
        //此方式输出8
        i = i++;
        //此方式输出9
       // i = ++i;
        System.out.println(i);
    }
}

在这里插入图片描述
在这里插入图片描述
PC(Program Counter 程序计数器):
1、存放指令位置
2、虚拟机的运行,类似于这样的循环:
while(not end) {
取PC中的位置,找到对应位置的指令
执行该指令:
PC++
}
在这里插入图片描述

JVMStack【线程独有的】:
在这里插入图片描述

java运行中写的每个线程对应的一个栈,每个方法对应一个栈帧
native method stacks:指的是本地方法(C/C++),调用的JNI
Direct Memory(直接内存):属于NIO的内容,java 1.4新增的
,为了增加IO的效率。归操作系统管【从JVM里面可以直接访问操作系统
管理的内存的,用户空间直接访问内核空间的内存】,实现了0拷贝

Heap【线程共享的】:
在这里插入图片描述
Method Area【线程共享的】:
存储每一个类的结构
在这里插入图片描述
方法区实现【1.8以前和以后是两个不同的实现】:
1、perm Space(<1.8)永久区
字符串常量位于PermSpace
FGC不会清理
大小启动的时候指定,不能变

2、Meta Space(>=1.8) 元空间
字符串常量位于堆
会触发FGC清理
Runtime Constant Pool:class文件里面的常量池,运行的时候会扔到这里
不设定的话,最大就是物理内存
在这里插入图片描述
如何证明1.7字符串常量位于perm,而1.8位于Heap?
结合GC,一直创建字符串常量,观察堆和MetaSpace的情况,看溢出时的报错也可以。

总结:
VMS:virtual machine stack
nms:native method stack
在这里插入图片描述

栈帧【每个方法对应一个栈帧】

dynamic linking:参照jvms2.6.3
一个线程有自己的线程栈,每个线程栈里面装着一个个的栈帧,每个栈帧里面有个dynamic linking,这个linking指向的是
运行时常量池里面的符号链接,看是否解析了,如果解析了则直接用,如果没解析则动态解析。如a方法调用了b方法,b的内容得去运行时常量池里面去找。
在这里插入图片描述

在这里插入图片描述
return address(返回值地址):a()->b(),方法a调用了方法b,b方法的返回值放什么地方。

回到面试题目:

public class TestIPulsPlus {
    public static void main(String[] args) {
        int i = 8;
        //此方式输出8
        i = i++;
        //此方式输出9
       // i = ++i;
        System.out.println(i);
    }
}

查看它的指令:
首先看局部变量表:里面有两个变量分别是args,和i
在这里插入图片描述

在这里插入图片描述
首先执行bipush 8,将它当成一个int类型扔到栈里面
在这里插入图片描述
istore_1
把栈顶元素出栈,放在下标值为1的局部变量表里面。
至此i=8完成
在这里插入图片描述
iload_1
将局部变量表1位置上的数压栈,就是8压栈
在这里插入图片描述
iinc 1 by 1; 将局部变量表为1的位置上面的数加1,此时变成9
istore_1:将操作数栈中的8拿回去赋值到局部变量表1位置所对应的值,此时将8
赋值回去局部变量表1对应的值修改为8
因此最后的返回结果为8。

指令集补充: 1、基于栈的指令集(JVM)
2、基于寄存器的指令集(汇编涉及)
hotspot的local variable table 类似于寄存器

栈的执行流程

Java虚拟机栈(Java栈)每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的Java方法调用。是线程私有的。
操作数栈:每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈,也可以称为表达式栈。操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈和出栈,操作数栈是为了存计算的临时中间结果的,而局部遍历表一般是固定的,用来放方法的参数和方法内部定义的变量

在这里插入图片描述
当大于127的时候采用sipush
在这里插入图片描述
注意下图的this,只要不是static方法的情况,这个方法的调用是需要对象的,然后这个对象
被扔进了局部变量表里面了
在这里插入图片描述
如图首先局部变量表0-3位置上对应的变量依次是this,a,b,c。
依次把3压栈
依次吧4压栈
然后再把3+4的结果压栈
然后弹出栈顶元素 7
压在c里面
在这里插入图片描述

public class Hello_02 {
    public static void main(String[] args) {
        Hello_02 h = new Hello_02();
        h.m1();
    }

    public void m1() {
        int i = 200;
    }
}

m1字节码
在这里插入图片描述
main方法字节码:
new完对象之后会把这个对象的地址压栈,对象此时是一个默认值
dup命令是将栈里面的那个地址再复制一份,现在栈里面就有两个元素指向了那个对象了,
再调用invokespecial执行默认构造方法,会将栈顶的元素给弹出来,做计算,这个过程中
对象里面的值就不再是默认值而是初始值了,并且也调用了初始化方法。
此时栈里面只有一个对象地址了。然后执行astore_1,将栈顶元素赋值给本地变量表的1位置上
。aload1是将h入栈,然后调用invokevirtual将这个h弹出栈顶,然后调用m1方法。
注意指令重排是:调用invokespecial和astore_1进行指令重排
在这里插入图片描述
main本地变量表:
在这里插入图片描述
由于main调用了m1方法,当main要调用m1方法的时候,main停止执行,等m1栈帧执行弹出完毕再执行
在这里插入图片描述
递归调用:
递归的阶乘。
在这里插入图片描述
这里我们来分析上图的m方法的字节码:
本地变量表是0位置this,1位置n
1、iload_1 首先将n=3入栈
2、iconst_1,将常量值1压栈
3、if_icmpne 如果不等于1【注意这里是load_1和iconst_1都弹出去才能比较】,
就跳到第七条指令去执行 iload_1又将3压栈,再执行aload_0,将this压栈,iload_1
又将3压栈,iconst_1,将常量1压栈,isub再将两数相减。再调用m方法【注意此时栈中包含元素
为3 this 2 】,执行m方法的时候需要两个参数,第一个参数是n就是栈顶的2,还要把this传给它,因为
调用的时候还需要知道是哪个对象在调用【此时弹出this 和 2】,此时n的值变成2。
直到当n=1的时候则返回,执行m(2)的后续逻辑,最后再执行m3的后续逻辑【imul】执行相乘的逻辑。

小总结

clinit :静态语句块的初始化【没有显示的调用看不出来】
init:构造方法初始化

invoke指令:

  1. InvokeStatic:调用静态方法
public class T01_InvokeStatic {
    public static void main(String[] args) {
        m();
    }

    public static void m() {}
}

  1. InvokeVirtual:自带多态的根据对象来调用方法,看压栈的是哪个对象
public class T02_InvokeVirtual {
    public static void main(String[] args) {
        new T02_InvokeVirtual().m();
    }

    public void m() {}
}

  1. InvokeInterface
    public static void main(String[] args) {
    //作为interface来调用的
        List<String> list = new ArrayList<String>();
        list.add("hello");
		//这个是invokevirtual
        ArrayList<String> list2 = new ArrayList<>();
        list2.add("hello2");
    }
  1. InovkeSpecial
    可以直接定位,不需要多态的方法
    private 方法 , 构造方法
    注意final也是invokevirtual的,不是invokespecial的。
public class T03_InvokeSpecial {
    public static void main(String[] args) {
        T03_InvokeSpecial t = new T03_InvokeSpecial();
        t.m();
        t.n();
    }

    public final void m() {}
    private void n() {}
}

  1. InvokeDynamic
    JVM最难的指令
    lambda表达式或者反射或者其他动态语言scala kotlin,或者CGLib ,ASM,
    运行时动态产生的class,会用到的指令
    类似于一个函数式的指针,C::n指向的时n这个函数
public class T05_InvokeDynamic {
    public static void main(String[] args) {


        I i = C::n;
        I i2 = C::n;
        I i3 = C::n;
        I i4 = () -> {
            C.n();
        };
        System.out.println(i.getClass());
        System.out.println(i2.getClass());
        System.out.println(i3.getClass());

        //for(;;) {I j = C::n;} //MethodArea <1.8 Perm Space (FGC不回收)小于1.8会产生OOM
    }

    @FunctionalInterface
    public interface I {
        void m();
    }

    public static class C {
        static void n() {
            System.out.println("hello");
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值