JVM(Java Virtual Machine)又被分为三大子系统,类加载子系统,运行时数据区,执行引擎。在这里我们主要讲解一下JVM的运行时数据区,也就是我们常说的JVM存储数据的内存模型。在这里提一点,平常我们常说内存模型,其实在Java中存在两大内存模型,一个是JVM的内存模型,也就是堆,栈之类的。还有一个是Java线程工作的内存模型,java工作的内存模型指的是主内存,工作内存,两个是不同的概念
JVM的结构图
这里重点讲一下JVM的运行时数据区,也就是我们所说的JVM内存模型。其实像JVM结构中的类加载子系统,也能够讲出很多东西出来。
首先,JVM的内存模型分为线程私有,线程共享的区域
线程私有部分
图中所画的部分程序计数器,虚拟机栈,本地方法栈是每个线程私有的部分,大概讲述一下每个部分所起到的作用
程序计数器:线程在运行期间,由于会出发CPU时间片资源抢夺的情况,假如线程A执行到if判断,循环,异常处理等这些字节码指令,时间片突然被抢占,出现阻塞,程序计数器会帮助记录每个线程所执行到下一条字节码指令,等线程A再次拿到时间片继续执行。
虚拟机栈:每个线程在执行每个方法时,都会创建一个栈帧,栈帧里面又会包括一些局部变量表,操作数栈,方法出口等信息,每个方法从执行到执行完成,也就是完成我们的入栈和出栈过程
首先看一段代码,来讲解栈帧中每个部分的作用
public class Test {
public static void test() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
}
public static void main(String[] args) {
test();
}
}
以上代码对应的虚拟栈图
局部变量表:在我们程序中,方法中定义的一些局部变量都会存放在我们的局部变量表,也就是我们代码中的a,b,c这种局部变量
将上述代码用javap -c Test命令可以查看到程序的字节码指令
public class com.ezhiyang.crm.config.Test {
public com.ezhiyang.crm.config.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void test();
Code:
0: iconst_1 // 将一个常量1加载到操作数栈
1: istore_0 // 将一个数值(也就是常量1)从操作数栈存储到局部变量 表,也就是我们代码中的int a = 1
2: iconst_2
3: istore_1
4: iload_0
5: iload_1
6: iadd // 执行加法操作
7: bipush // 将10入操作数栈 10
9: imul // 执行乘法操作
10: istore_2 // 再将乘法出来的结果从操作数栈存储到局部变量 表,也就是我们的int c = (a + b) * 10
11: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #2 // Method test:()V
3: return
}
操作数栈:像我们的程序后面都会被编译成这种字节码指令,而操作数栈将变量之间的运算入栈,然后存储计算结果,再出栈赋值给局部变量表
方法出口:代码中main()方法调用了test()方法,这时test()执行完成后,需要继续执行main()方法,方法出口记录了test()方法执行完成后的一个出口,也就是回到main()
本地方法栈:在Java代码中我们有时可以看到用native修饰的方法,而这些方法并不由Java语言去实现,而是由Java去调用底层的C++语言实现的,跟我们的虚拟机栈有点类似,只不过是执行native方法的线程栈帧
线程共享部分
线程共享的部分就是我们图中所画的堆,方法区(又叫做永久代,JDK1.8后被改为元空间)
看一张方法区内存分布图
方法区:方法区主要存储的是虚拟机加载的类信息(版本,字段,方法,接口等信息),成员常量,静态变量等数据,其中又包括一部分是运行时常量池,运行时常量池存放的是编译期间生成的符号引用,后面经过解析出来的直接引用也会储存在常量池中。
当虚拟机new指令时,首先会根据这个指令的参数在常量池中定位到一个类的符号引用,如果定位不到,则需要重新对这个类进行加载,解析和初始化了,这里就涉及了Class文件加载的7大过程。如果能找到符号引用,证明这个类已经加载过,并会给该对象分配内存空间,调用构造方法进行初始化。也就是我们new一个对象发生了那些事
JDK1.8后,方法区被叫做元空间,并且内存大小限制不限JVM的内存大小限制,而是直接使用计算机的直接内存,受计算机的内存大小限制
堆:堆是JVM中最大的一块内存区域了,主要存放我们程序中new出来的对象,提一点并非所有的对象都是在堆上进行分配,随着线程逃逸,标量替换等技术的发展,对象也有可能在栈上进行分配。下面通过一段代码进行讲解,对象不一定在堆上进行分配
public class Test {
public static void test() {
A a = new A();
}
public static void main(String[] args) {
test();
}
}
class A {
int a = 0;
}
像这段代码,执行test()方法时,new了一个A对象,但是A对象又只有一个基本类型变量a,A对象也没有出现线程逃逸现象,因为JVM存在一些指令优化功能,并会把A对象直接进行标量替换成int a = 0,这时就会出现对象在栈上进行分配。
堆也是GC进行回收最主要的区域,容易出现OutOfMemoryError异常
堆中又分为young(新生代),old(老年代),分配的内存比例1:2。young新生代中的对象具有"朝生夕死"的特点,也就是对象刚创建不久,并会被回收掉。是Minor回收的主要区域。而老年代中的对象具有的特点则是不容易被回收掉,一些大对象比如数组并会存放在老年代中。是Full GC回收的主要区域。后面写一篇JVM GC垃圾回收的文章做介绍,这里不做过多的讲解了
young新生代中又分为eden区,两个survivor区,分配的内存比例8:1:1。我们程序中所new出的对象首先会在eden区进行分配,然后经过Minor GC,eden区没有被回收的对象又会被放到survivor Form区,survivor Form区经过GC后的对象没有被回收掉,又会转移到survivor To区,这两个区的对象是可以双向转移的。新生代采用GC的算法是复制算法,两个survivor区正是复制算法所要预留出来的区域