Java是一种解释型语言,首先需要编译成class文件,再交由JVM装载,最终JVM会解释成系统可以直接运行的机器码。性能方面肯定是不如直接编译成机器码的,那么为什么Java不直接编译成机器码呢?因为不同操作系统识别机器码的规则是不同的,而我们写的代码只有一份,JVM来负责转义成不同操作系统下的机器码,带来的好处就是:一次编译,到处运行
上图为JVM加载class到内存中的内存模型,JVM带来的好处除了跨平台外,还有自动内存分配,这也是我们需要了解的重点,c/c++程序员想要使用大量数据时,需要动态申请内存,并在适当的时机手动释放这片内存,一旦忘记释放,造成野指针,那么就会内存泄漏。而JVM帮助我们从申请内存、释放内存的繁琐工作中释放出来
JVM在运行程序时,分成两块数据区,共享数据区和私有数据区
试想以下我们Java代码中哪些代码是固定的?哪些代码是动态的? 固定的可以用文本来表示,比如类名、类中的属性、常量、方法名、代码执行顺序等。其实就是我们写的代码 动态的是文本无法表示的,如变量进行一系列运算得到的值。其实就是需要cpu介入的运算
接下来,看一段代码:
public class Hello {
public int test() {
int a = 3;
int b = 4;
return a + b;
}
public static void main(String[] args) {
Hello h = new Hello();
h.test();
}
}
我们使用javac执行编译后,得到class文件,如果我们运行该程序,执行了main函数,其中对hello对象进行了实例化,那么JVM会加载该class文件,并存储到方法区(元空间)
Hello.class加载.png
此时的class文件已经被转义了,我们可以使用javap命令来反编译查看上面代码在JVM是什么样子的
javap -v Hello.class
不需要细看,后面会详细介绍test方法的内容
Classfile /C:/Users/tyqhc/Documents/javaworkspace/myJava/out/production/myJava/Hello.class
Last modified 2021-9-27; size 525 bytes
MD5 checksum 74b6dc87932a59d4af95208b0eb1edb7
Compiled from "Hello.java"
public class Hello
SourceFile: "Hello.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#25 // java/lang/Object."<init>":()V
#2 = Class #26 // Hello
#3 = Methodref #2.#25 // Hello."<init>":()V
#4 = Methodref #2.#27 // Hello.test:()I
#5 = Class #28 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 LHello;
#13 = Utf8 test
#14 = Utf8 ()I
#15 = Utf8 a
#16 = Utf8 I
#17 = Utf8 b
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 args
#21 = Utf8 [Ljava/lang/String;
#22 = Utf8 h
#23 = Utf8 SourceFile
#24 = Utf8 Hello.java
#25 = NameAndType #6:#7 // "<init>":()V
#26 = Utf8 Hello
#27 = NameAndType #13:#14 // test:()I
#28 = Utf8 java/lang/Object
{
public Hello();
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 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LHello;
public int test();
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: iconst_3
1: istore_1
2: iconst_4
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: ireturn
LineNumberTable:
line 4: 0
line 5: 2
line 7: 4
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this LHello;
2 6 1 a I
4 4 2 b I
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class Hello
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method test:()I
12: pop
13: return
LineNumberTable:
line 11: 0
line 12: 8
line 13: 13
LocalVariableTable:
Start Length Slot Name Signature
0 14 0 args [Ljava/lang/String;
8 6 1 h LHello;
}
Java中每一个类(我们不讨论内部类)对应一个class文件,class文件的信息(类元信息)是存放在方法区的,这是共享数据区,并且同一个类加载器只会加载一份,当我们需要实例化该类的对象时,会从方法区查找,如果以前加载过了,那么直接使用。谁都可以实例化这个类,自然它就是存放在共享数据区了
方法区存放着class信息,而堆中存放了实例化的对象,同一个类的对象可以被实例化多次,对象是可以被其他线程使用的,所以堆也是共享数据区。实例化对象时,对象中有一个对象头,其中有个类型指针会指向方法区的类元信息,下面的图看看就好,不必深究
对象头组成.png
除了方法区和堆,其他的都是私有数据区,前面已经提到了,私有数据区都是方法运行时的数据,需要cpu介入
Java栈也称为虚拟机栈、线程栈,叫什么其实无所谓,重要的是里面的内容:栈帧
栈帧主要分为四个部分组成:
每个方法调用时,都会新建一个栈帧,然后执行方法中的代码,上面test方法中的汇编指令为:
0: iconst_3
1: istore_1
2: iconst_4
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: ireturn
用一张gif图,来表示test方法是如何在栈帧中操作的:
程序计数器:程序计数器就是临时记录方法运行到哪一行了,程序运行实际并不存在并行,而是不同的线程不断的抢占cpu,然后执行一段时间,又重新开始竞争,只是执行时间太短,导致人根本感受不到,当一个方法运行到某一行的时候开始重新竞争了,就需要记录下当前方法运行到哪了,用来下次cpu被该方法抢占时,从上次中断的地方继续执行
本地方法栈:本地方法栈用来运行c/c++代码,结构和Java栈是相同的,区别是c/c++代码动态申请的内存由自己手动管理