本文主要讲述Java代码在JVM中是如何执行的.
一、概述:
Java代码的一生大致如下图所示,其中,JVM运行时数据区是我们需要重点关注的部分:
1.1 所有线程共享部分
方法区:存储JVM加载的类信息、常量、静态变量、编译后的代码等数据。在虚拟机规范中,对于方法区的定义是一个不严谨的、逻辑上的概念,实际上各虚拟机会有不同的实现。
堆内存:JVM启动时创建该区域,主要用于存储对象的实例,GC垃圾回收主要是处理堆内存。
1.2 各线程独有部分
栈内存:或称为虚拟机栈,由多个栈帧组成,一个JVM线程会执行一个或多个方法,而__一个方法对应一个栈帧__。一个栈帧包括:局部变量表、操作数栈等部分。每个线程的栈内存大小默认是1M,超出则报StackOverflowError。
本地方法栈:顾名思义,是为JVM执行native方法准备的。
程序计数器:由于CPU在单位轮片时间内,只执行一个线程的指令。每个线程的程序计数器记录当前线程执行的字节码位置,存储的是字节码的地址(如果执行的native方法,则计数器数值为空)。这样在线程切换出去再切换回来时,就能找到上次执行的指令位置,然后继续执行。
二、小试牛刀:class字节码文件内容追踪
2.1 准备工作
2.1.1 Demo.java
public class Demo01 {
public static void main(String[] args) {
int a = 200;
int b = 100;
int c = a / b;
int d = 3;
System.err.println(c + d);
}
}
复制代码
2.1.2 编译(javac)并解析(javap)Demo.class
# 编译
javac Demo01.java
# 解析。并将指令内容输出到Demo01.txt中
javap -v Demo01.class > Demo01.txt
复制代码
2.1.3 分析class文件中的指令
解析后的指令内容如下:
Classfile /Users/zephyrlai/IdeaProjects/zephyr/java-arch/src/cn/zephyr/ch1/Demo01.class
Last modified 2020-3-10; size 429 bytes
MD5 checksum 5c8846f77018cc43a36eeba64ff68cbd
Compiled from "Demo01.java"
public class cn.zephyr.ch1.Demo01
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.err:Ljava/io/PrintStream;
#3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#4 = Class #19 // cn/zephyr/ch1/Demo01
#5 = Class #20 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 Demo01.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #21 // java/lang/System
#16 = NameAndType #22:#23 // err:Ljava/io/PrintStream;
#17 = Class #24 // java/io/PrintStream
#18 = NameAndType #25:#26 // println:(I)V
#19 = Utf8 cn/zephyr/ch1/Demo01
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/System
#22 = Utf8 err
#23 = Utf8 Ljava/io/PrintStream;
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (I)V
{
public cn.zephyr.ch1.Demo01();
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 9: 0
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: bipush 200
2: istore_1
3: sipush 100
6: istore_2
7: iload_1
8: iload_2
9: idiv
10: istore_3
11: iconst_3
12: istore 4
14: getstatic #2 // Field java/lang/System.err:Ljava/io/PrintStream;
17: iload_3
18: iload 4
20: iadd
21: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
24: return
LineNumberTable:
line 11: 0
line 12: 3
line 13: 7
line 14: 11
line 15: 14
line 16: 24
}
SourceFile: "Demo01.java"
复制代码
可以看到大致可以分为如下4块
-
前4行:无关代码内容的描述信息
-
5~8行:版本号、访问标志
-
(major-verion)版本号:jdk5、6、7、8分别是49、50、51、52
-
访问标志:
标志名称 标志值 含义 ACC_PUBLIC 0x0001 标记为public类型 ACC_FINAL 0x0010 标记被声明为final(仅类能设置) ACC_SUPER 0x0020 jdk1.2之后为true(是否使用invokespecial字节码指令) ACC_INTERFACE 0x0200 标记为接口 ACC_ABSTRACT 0x0400 标记为abstract类型(对于接口、抽象类则为true) ACC_SYNTHETIC 0x1000 标记这个类不是用户产生的 ACC_ANNOTATION 0x2000 标记为注解类 ACC_ENUM 0x4000 标记为枚举类
-
-
以Constant pool为起点的常量池(区别于String常量池):主要存储类信息中包含的静态常量,即编译当前类之后,所用到的常量。例如类名、方法名等。常见的常量类型如下:
-
以
{}
包裹的业务代码,而这块业务代码包含了2块:- 默认无参构造器
- main方法
- flags:依旧是访问控制
- Code下的
stack=3, locals=5, args_size=1
则分别表示本地变量数量为3、参数数量为5、(方法对应的)栈帧中操作数栈的深度为1 - 剩下的即为业务代码
2.2 程序运行分析
2.2.1 加载class文件
主要加载各种类信息、以及类中用到的常量、静态变量等,存储在方法区。创建类的实例,存储在堆内存中。
2.2.2 JVM创建线程来执行代码
开辟各线程独立的虚拟机栈(包括多个栈帧)、程序计数器等内存空间