Java是一种解释型语言,首先需要编译成class文件,再交由JVM装载,最终JVM会解释成系统可以直接运行的机器码。性能方面肯定是不如直接编译成机器码的,那么为什么Java不直接编译成机器码呢?因为不同操作系统识别机器码的规则是不同的,而我们写的代码只有一份,JVM来负责转义成不同操作系统下的机器码,带来的好处就是:一次编译,到处运行
上图为 JVM加载class到内存中的 内存模型,JVM带来的好处除了跨平台外,还有 自动内存分配,这也是我们需要了解的 重点,c/c++程序员想要使用大量数据时,需要动态申请内存,并在适当的时机手动释放这片内存,一旦忘记释放,造成野指针,那么就会内存泄漏。而 JVM帮助我们从申请内存、释放内存的繁琐工作中释放出来 二、线程共享与私有
JVM在运行程序时,分成两块数据区,共享数据区和私有数据区
1.共享数据区试想以下我们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();
}
}
1.1方法区
我们使用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."":()V #2 = Class #26 // Hello #3 = Methodref #2.#25 // Hello." ":()V #4 = Methodref #2.#27 // Hello.test:()I #5 = Class #28 // java/lang/Object #6 = Utf8 #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 // " ":()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." ":()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 " ":()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文件的信息(类元信息)是存放在方法区的,这是共享数据区,并且同一个类加载器只会加载一份,当我们需要实例化该类的对象时,会从方法区查找,如果以前加载过了,那么直接使用。谁都可以实例化这个类,自然它就是存放在共享数据区了
1.2堆方法区存放着class,而堆中存放了实例化的对象,同一个类的对象可以被实例化多次,对象是可以被其他线程使用的,所以堆也是共享数据区。实例化对象时,对象中有一个对象头,其中有个类型指针会指向方法区的类元信息,下面的图看看就好,不必深究
对象头组成.png 2.私有数据区除了方法区和堆,其他的都是私有数据区,前面已经提到了,私有数据区都是方法运行时的数据,需要cpu介入
2.1栈帧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方法是如何在栈帧中操作的:
2.2程序计数器与本地方法栈程序计数器:程序计数器就是临时记录方法运行到哪一行了,程序运行实际并不存在并行,而是不同的线程不断的抢占cpu,然后执行一段时间,又重新开始竞争,只是执行时间太短,导致人根本感受不到,当一个方法运行到某一行的时候开始重新竞争了,就需要记录下当前方法运行到哪了,用来下次cpu被该方法抢占时,从上次中断的地方继续执行
本地方法栈:本地方法栈用来运行c/c++代码,结构和Java栈是相同的,区别是c/c++代码动态申请的内存由自己手动管理
对于内存模型,只要了解下就行了,对于移动端开发,我们做内存优化,针对的是堆中的内存,下一篇将介绍堆中的内存结构


