class文件内容
class文件内容包含java程序执行的字节码,数据严格按照格式紧凑排列在class文件中的二进制流,中间无任何分隔符;文件开头有一个0xcafebabe(16进制)特殊的一个标志.
JVM运行时数据区
java程序最终执行是由JVM执行引擎和本地库来做的一些适配,所以在linux和windos上的执行引擎和本地库都会有所不同,目的是为了一次编写,随处运行.如果有想对于JVM有很深入的研究可以看一下C代码,比如openjdk
线程独占:每个线程都会有它独立的空间,随线程的生命周期而创建和销毁
线程共享:所有线程能访问这块内存数据,随虚拟机或者GC而创建和销毁
方法区
jvm用来存储加载的类信息、常量、静态变量、编译后的代码等数据,虚拟机规范中这是一个逻辑区域。具体实现根据不同虚拟机实现。如oracle的HotSpot在java7中方法区放在永久代,java8放在元数据空间并且通过GC机制对这个区域进行管理。比如IBM的虚拟机实现就不同了
堆
堆内存还可以细分为:老年代、新生代(Eden,From Survivor,To Survivor).jvm启动时创建,存放对象的实例。垃圾回收器主要就是管理堆内存。如果满了,就会出现OutOfMemoryError
虚拟机栈
每个线程都在这个空间有一个私有的空间。程序栈由多个栈帧(Stack Frame)组成。一个线程会执行一个或多个方法,一个方法对应一个栈帧。
栈帧内容包含:局部变量表、操作数栈、动态链接、方法返回地址、附加信息等。
栈内存默认最大是1M,超出则抛出StackOverflowError
本地方法栈
和虚拟机栈功能类似,虚拟机栈是为虚拟机执行java方法而准备的,本地方法栈是为虚拟机使用native本地方法而准备的,虚拟机栈规范没有规定具体的实现,由不同的虚拟机厂商去实现.
HotSpot虚拟机中虚拟机栈和本地方法栈的实现是一样的。同样超出大小以后也会跑出StackOverflowError
程序计数器
记录当前线程执行字节码的位置,存储的是字节码指令地址,如果执行Native方法,则计数器值为空.
每个线程都在这个空间有一个私有的空间,占用内存很少。
CPU同一时间,只会执行一条线程中的指令。JVM多线程会轮流切换并分配CPU执行时间的方式。为了线程切换后知道具体执行的位置,需要通过程序计数器,来恢复正确的执行位置
实操分析
java文件示例
public class Demo {
public static void main(String[] args) {
int a = 1000;
int b = 100;
int c = a / b;
int d = 13;
System.out.println(c + d);
}
}
javac Demo.java //将java文件编译成class文件
javap -v Demo.class > Demo.txt //将class文件输出到txt文件
分析指令码文件
public class com.Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#25 // java/lang/Object."<init>":()V
#2 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #28.#29 // java/io/PrintStream.println:(I)V
#4 = Class #30 // com/Demo
#5 = Class #31 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lcom/Demo;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 a
#18 = Utf8 I
#19 = Utf8 b
#20 = Utf8 c
#21 = Utf8 d
#22 = Utf8 MethodParameters
#23 = Utf8 SourceFile
#24 = Utf8 Demo.java
#25 = NameAndType #6:#7 // "<init>":()V
#26 = Class #32 // java/lang/System
#27 = NameAndType #33:#34 // out:Ljava/io/PrintStream;
#28 = Class #35 // java/io/PrintStream
#29 = NameAndType #36:#37 // println:(I)V
#30 = Utf8 com/Demo
#31 = Utf8 java/lang/Object
#32 = Utf8 java/lang/System
#33 = Utf8 out
#34 = Utf8 Ljava/io/PrintStream;
#35 = Utf8 java/io/PrintStream
#36 = Utf8 println
#37 = Utf8 (I)V
{
public com.Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/Demo;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: sipush 1000
3: istore_1
4: bipush 100
6: istore_2
7: iload_1
8: iload_2
9: idiv
10: istore_3
11: bipush 13
13: istore 4
15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_3
19: iload 4
21: iadd
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: return
LineNumberTable:
line 11: 0
line 12: 4
line 13: 7
line 14: 11
line 15: 15
line 16: 25
LocalVariableTable:
Start Length Slot Name Signature
0 26 0 args [Ljava/lang/String;
4 22 1 a I
7 19 2 b I
11 15 3 c I
15 11 4 d I
MethodParameters:
Name Flags
args
}
SourceFile: "Demo.java"
版本访问标志说明
minor version: 0 // 次版本号
major version: 52 // 主版本号 Java版本 6,7,8,9 依次对应的版本号为 49,50,51,52 > 咋们这里为java8
flags: ACC_PUBLIC, ACC_SUPER // 访问标志
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令,JDK1.2之后编译出来的类这个标志位true |
ACC_INTERFACE | 0X0200 | 标志这是一个接口 |
ACC_ABSTRACT | 0X0400 | 是否为abstract类型,对于接口和抽象类来说此标记为true,其他的为false |
ACC_SYNTHETIC | 0X1000 | 这个类并非由用户产生的 |
ACC_ANNOTATION | 0X2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
静态池说明
此常量池和String这个常量池有一定的区别,这里的静态池存的是类里面包含的一些静态常量,比如说 类的名字,方法的名字,变量声明的名字等这些信息。通过编译这个类以后就能确认。
Constant pool:
#1 = Methodref #5.#25 // java/lang/Object."<init>":()V
#2 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #28.#29 // java/io/PrintStream.println:(I)V
#4 = Class #30 // com/Demo
#5 = Class #31 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lcom/Demo;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 a
#18 = Utf8 I
#19 = Utf8 b
#20 = Utf8 c
#21 = Utf8 d
#22 = Utf8 MethodParameters
#23 = Utf8 SourceFile
#24 = Utf8 Demo.java
#25 = NameAndType #6:#7 // "<init>":()V
#26 = Class #32 // java/lang/System
#27 = NameAndType #33:#34 // out:Ljava/io/PrintStream;
#28 = Class #35 // java/io/PrintStream
#29 = NameAndType #36:#37 // println:(I)V
#30 = Utf8 com/Demo
#31 = Utf8 java/lang/Object
#32 = Utf8 java/lang/System
#33 = Utf8 out
#34 = Utf8 Ljava/io/PrintStream;
#35 = Utf8 java/io/PrintStream
#36 = Utf8 println
#37 = Utf8 (I)V
常量名 | 含义 |
---|---|
CONSTANT_utf8_info | UFU8编码的字符串 |
CONSTANT_Integer_info | 整形字面量 |
CONSTANT_Float_info | 浮点型字面量 |
CONSTANT_Long_info | 长整型字面量 |
CONSTANT_Double_info | 双精度浮点型字面量 |
CONSTANT_Class_info | 类或接口的符号引用 |
CONSTANT_String_info | 字符串类型字面量 |
CONSTANT_Fielderf_info | 字段的符号引用 |
CONSTANT_Methodref_info | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 字段或方法的符号引用 |
CONSTANT_MothodType_info | 标志方法类型 |
CONSTANT_MothodHandle_info | 表示方法句柄 |
CONSTANT_InvokeDynamic_info | 表示一个动态方法调用点 |
默认的构造函数
public com.Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
由此可见,没有定义构造函数时,会有隐式的公开的无参构造函数
程序入口main方法
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC // 此方法的访问控制
Code:
stack=3, locals=5, args_size=1 // 1方法对应栈帧中操作数栈的深度 2本地变量数量 3参数数量
0: sipush 1000 //将1000的值压到操作数栈的栈顶
3: istore_1 //将操作数栈栈顶中的值1000出栈挪到本地变量表且赋值istore_1
4: bipush 100 //将100的值压到操作数栈的栈顶
6: istore_2 //将操作数栈栈顶中的值100出栈挪到本地变量表且赋值istore_2
7: iload_1 //将本地变量表1000的值压入操作数栈顶
8: iload_2 //将本地变量表100的值压入操作数栈顶 1000的值被压到了栈底
9: idiv //将操作数栈中的值出栈进行整除1000/100=10,结果留在栈顶
10: istore_3 //将操作数栈栈顶中的结果值10出栈挪到本地变量表且赋值istore_3
11: bipush 13 //将13值压到操作数栈的栈顶
13: istore 4 //将操作数栈栈顶中的值13栈挪到本地变量表且赋值istore
15: getstatic #2 //静态获取PrintStream打印流对象
18: iload_3 //将本地变量表10的值压入操作数栈栈顶
19: iload 4 //将本地变量表13的值压入操作数栈栈顶 10的值被压到了栈底
21: iadd //将操作数栈栈中的值出栈进行相加10+13=23,结果留在栈顶
22: invokevirtual #3 //调用PrintStream.println(V)将栈顶结果传入.进入新的栈帧。程序计数 器归零,新的本地变量表,新的操作数栈
25: return //程序执行结束
JVM执行引擎去执行这些源码编译过后的指令码,javap翻译出来的是操作符,class存储的是指令码。前面的数字,是偏移量(字节),jvm根据这个去区分不同的指令。详情可查看JVM指令码表
总结
将JVM运行核心逻辑进行了详细梳理
运行时数据区、栈和栈帧的关系 、本地变量表和操作数栈的执行逻辑 、相关指令码的介绍
JVM运行原理中更底层实现,针对不同的操作系统或者处理器,会有不同的实现。这也是java能够实现 ‘一次编写,随处运行’的原因。