1 运行时数据区 Run-time data areas
- Java虚拟机执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域

1.1 程序计数器 Program Counter
- 每个线程一个
- 用于存放指令的位置
- 虚拟机的运行类似如下循环
while(not end){
取PC中的位置,找到对应位置的指令;
执行该指令;
PC++;
}
1.2 堆 Heap
1.3 本地方法栈 native method stacks
本地方法所使用的栈
1.4 直接内存 Direct Memory
- NIO使用,可以直接访问操作系统内存
- 老版本Java读取网络资源时,先将网络资源读到内核空间,当jvm使用时,从内核拷贝到jvm内存中,效率低
- NIO可以使用直接内存,省略了拷贝的步骤,直接访问内核空间,也就是所谓的0拷贝(zero copy)
1.5 方法区 method area
- 存放运行时常量池run-time constant pool
- 编译时产生的constant pool table,到运行时,就放在这里
- 存放class文件中的符号引用,以及这些符号引用转为的直接引用
- 字符串常量1.7在method area,1.8以后在堆中
- 永久代(permspace)(1.7)/ 元数据区(Metaspace)(1.8) ,是hotspot的不同jdk版本,对method area的具体实现
- 永久代启动时必须指定大小限制 ,元数据可以设置,也可以不设置,仅受限于物理内存
- fgc无法对永久代进行回收,但可以对metaspace进行回收
- 注意Class对象存放于堆中,Class文件的字节码,存放于method area
1.6 栈 JVM stacks
- 每个线程对应一个栈,每个方法对应一个栈针(frame),栈中存放着一个个栈帧
- 栈帧:存放数据,包含以下内容
- 局部变量表(local variable table):存放当前栈帧中所用到的局部变量,栈帧弹出后,局部变量表消失,局部变量的名,记录在constant_pool
- 对于非static方法,局部变量表中第一个局部变量是this,这也是为什么我们可以直接使用this关键字
- 对于main方法,局部变量表中第一个局部变量是args

- 操作数栈(operand stacks):理解为存放操作数的栈,方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作
- 动态连接(dynamic linking):字节码中的方法调用指令,会以常量池指向方法的符号引用作为参数,这些符号引用一部分在类加载时,转为直接引用,称为静态连接,另一部分运行时被转化为直接引用,称为动态链接
- 方法返回地址(return address):方法a调用方法b,b方法的return address,记录b方法返回值放在哪,以及b方法执行完后,应返回a方法的什么位置继续执行
- 局部变量表(local variable table):存放当前栈帧中所用到的局部变量,栈帧弹出后,局部变量表消失,局部变量的名,记录在constant_pool
2 JVM指令集
2.1 ++i与i++
- ++i
public class TestIPulsPlus {
public static void main(String[] args) {
int i = 8;
// i = i++;
i = ++i;
System.out.println(i);
}
}

//第一列表示指令序号,第二列为指令
//使用jclasslib时,指令具体含义可以直接单击指令,会进入oracle网页的官方介绍,也可以在jvm虚拟机规范文档中查到,jvms13中6.5节
//放入一个byte值自动扩展为int,然后压栈
0 bipush 8
//将栈顶的int数出栈,放到局部变量表下标为1的局部变量中,也就是i
2 istore_1
//将局部变量表中,下标为1的int类型的局部变量,增加1,此时局部变量表中,i值为9,操作数栈中无元素
3 iinc 1 by 1
//将局部变量表中,下标为1的局部变量压栈
6 iload_1
7 istore_1
8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #3 <java/io/PrintStream.println>
15 return
- i++

//注意修改java文件后,需要重新执行一次,class才变,在jclasslib中,也要刷新,指令才会变
0 bipush 8
2 istore_1
3 iload_1
//只有此处变换了位置,这就是为什么i++当时指令时,值不变,下一条指令时,其值才变化
4 iinc 1 by 1
7 istore_1
8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #3 <java/io/PrintStream.println>
15 return
2.2 设计指令集的方式
- 基于栈的指令集:JVM采用的就是这种,不断压栈、出栈
- 基于寄存器的指令集:例如汇编语言,复杂,但快
- 硬件层面最后都是基于寄存器的
2.3 Hello_03
- 代码
public class Hello_03 {
public static void main(String[] args) {
Hello_03 h = new Hello_03();
int i = h.m1();
}
public int m1() {
return 100;
}
}
- 线程栈中情况
- 只有一个线程,是主线程
- 主线程栈中存在两个栈帧,一个是main方法对应的栈帧,另一个是m1方法对应的栈帧
- 执行到h.m1时,m1方法的栈帧才进入主线程栈中
- m1方法执行完后,其栈帧会弹出,此时main方法才能继续执行
- 线程的栈存满了无法再放入栈帧时,报错stack over flow
- 指令集
//1. main方法
//创建对象,并赋默认值,然后将该对象地址压栈
0 new #2 <Hello_03>
//将操作数栈中栈顶的元素复制一个并压栈,此时相当于操作数栈中,存放了两份Hello_03对象的地址
3 dup
//栈顶元素出栈,并调用其inti方法,也就是构造函数,这也是为什么dup需要复制一份对象地址的原因,因为调用构造方法时,会用掉一个
4 invokespecial #3 <Hello_03.<init>>
7 astore_1
8 aload_1
//#4表示指向常量池中的第四个常量,这个常量是一个符号引用,表示要执行的方法m1
9 invokevirtual #4 <Hello_03.m1>
//2. m1方法。由于调用了m1方法,此时m1栈帧进入线程栈
0 bipush 100
2 ireturn
12 istore_2
13 return
2.4 Hello_04
- 代码
public class Hello_04 {
public static void main(String[] args) {
Hello_04 h = new Hello_04();
int i = h.m(3);
}
public int m(int n) {
if(n == 1) return 1;
return n * m(n-1);
}
}
- 指令集
//main方法
0 new #2 <Hello_04>
3 dup
4 invokespecial #3 <Hello_04.<init>>
7 astore_1
8 aload_1
//将3这个常量值压栈
9 iconst_3
10 invokevirtual #4 <Hello_04.m>
//2. m方法
//每次递归都创建一个新的栈帧,每个栈帧都叫m,只不过参数不同
0 iload_1
1 iconst_1
//cmp:compare,ne:not equal如果栈顶的两个int元素不相等,跳转至第7号指令,否则跳转到5号指令
//指令码编号跨越,是因为某个指令,占的字节数过大,将后面的编号的字节也占用了
2 if_icmpne 7 (+5)
5 iconst_1
6 ireturn
7 iload_1
8 aload_0
9 iload_1
10 iconst_1
//两个int元素出栈,相减后,结果入栈
11 isub
12 invokevirtual #4 <Hello_04.m>
//3. m方法,第二次调用
0 iload_1
1 iconst_1
......
//两个int元素出栈,相乘后,结果入栈
15 imul
16 ireturn
13 istore_2
14 return
2.5 invoke指令
- invokestatic:调用了一个static方法
- invokevirtual:调用普通方法,自带多态功能,就是说栈中存的是哪个对象的地址,就会调用哪个对象的方法,所以父类引用指向子类,最后会调用子类的方法
- invokeinterface:接口的引用,调用方法
- invokespecial:调用可以直接定位的方法
- 可以直接定位:虚拟机不需要看这个该引用所指向的对象,直接看该引用本身是什么类型,就知道调用哪个方法,不会有多态的问题
- 例如:private方法,构造方法,注意final方法不是invokespecial,是invokevirtual
- invokedynamic:lambda表达式、反射,或其他动态语言
- 动态语言:可以动态产生class的语言,例如:scala、kotlin、cglib、asm
- lambda表达式调用时,相当于产生了匿名内部类,这个匿名内部类的class文件也会被加载到内存中
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();
};
I i5 = new I(){
@Override
public void m() {
C.n();
}
};
System.out.println(i.getClass());
System.out.println(i2.getClass());
System.out.println(i3.getClass());
System.out.println(i5.getClass());
//由于每次使用lambda表达式,都相当于创建了一个匿名内部类,这个匿名内部类的字节码1.7以前,都存在于Perm Space,无法被回收
//循环使用,导致永久代内存增加,甚至内存溢出
//for(;;) {I j = C::n;} //MethodArea <1.8 Perm Space (FGC不回收)
}
@FunctionalInterface
public interface I {
void m();
}
public static class C {
static void n() {
System.out.println("hello");
}
}
}
2.6 其他

- iadd:连续出栈两个int类型的数,将相加后的结果压栈
- <clinit>:class initialize,静态的语句块,没有显示的调用
本文详细解析Java虚拟机(JVM)的运行时数据区,包括程序计数器、堆、本地方法栈、直接内存、方法区及栈的运作原理。通过实例说明JVM指令集的执行过程,探讨栈与寄存器指令集的区别,以及Hello系列示例中的JVM行为。
1万+

被折叠的 条评论
为什么被折叠?



