JVM--内存模型
安卓高级工程师想要做性能优化、NDK、设计架构时的健全性等工作时,必须是对JVM有一定的了解。技术的路越往上走,就越需要对底层的理解。计算机原理、c/c++语言、JVM原理、数据结构与算法等知识缺一不可。在学习的过程中,一开始觉得知识是线性的,就像一个数组,只需要不断往里面填充数据,然而后面越学习,越发现知识是广度的,到达一定程度后,我们想学习一个知识,往往需要学习一连串的其他知识,其中不乏需要使用其他编程语言,也渐渐明白了全栈工程师的由来,技术的路上没有尽头,只有不断的学习
JVM的内容偏向概念,不像代码执行就能够看到结果,总结起来也比较麻烦,有条件的还是看下JVM的相关书籍比较好,总结不到位的地方也可以相互探讨
一.JVM内存模型
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文件,并存储到方法区(元空间)
此时的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文件的信息(类元信息)是存放在方法区的,这是共享数据区,并且同一个类加载器只会加载一份,当我们需要实例化该类的对象时,会从方法区查找,如果以前加载过了,那么直接使用。谁都可以实例化这个类,自然它就是存放在共享数据区了
1.2堆
方法区存放着class信息,而堆中存放了实例化的对象,同一个类的对象可以被实例化多次,对象是可以被其他线程使用的,所以堆也是共享数据区。实例化对象时,对象中有一个对象头,其中有个类型指针会指向方法区的类元信息,下面的图看看就好,不必深究
对象头组成.png2.私有数据区
除了方法区和堆,其他的都是私有数据区,前面已经提到了,私有数据区都是方法运行时的数据,需要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++代码动态申请的内存由自己手动管理