整体介绍
每当我们去面试时,面试官经常会问jvm相关的问题,谈到JVM,首先不得不说jvm内存模型,今天就来深度剖析一下它,jvm内存模型的结构图如下:
对于jvm内存模型为何要如此设计,下面给你解释一下java程序的运行过程你就明白了:
所谓JVM内存模型实际上的意思是java运行时数据区域,它整个过程就是当程序要执行某一段代码时,类加载器加载我们的class字节码文件,把读取的信息翻译成类信息存放到我们的方法区,同时在堆中生成该类的Class对象,当我们程序运行调用方法时,局部变量、对象引用、数组引用会在虚拟机栈中生成,如果在方法调用中需要new对象,则会在堆中根据Class信息生成对象,最后由我们的字节码执行引擎解释执行方法区的代码块,就完成了我们的代码执行。当然这个过程中,涉及到线程的切换,这时程序计数器就派上用场了,它会记录当前线程执行到哪一行代码,下次该线程再次获取cpu执行权时,继续从该行代码继续执行,最后本地方法栈和虚拟机栈的功能几乎一模一样,只不过它是执行的native本地方法,底层是调用的C或C++代码。
这段话其实就是JVM内存模型的整个执行流程,有点干,没事,我后面会做详细分析,读者只需要大概读一下有个整体印象就行,等我分析完了你再回头来看就会豁然开朗!
注意我会从各个区域使用到的先后顺序来分析,以便读者能够把它们串联起来
各个区域详解
方法区
方法区是存放从class文件加载出来的类信息的,包括常量、静态变量、类信息和运行时常量池。这里有个注意点必须给大家提醒一下,那就是该区域并不会存放Class对象,Class对象是存放在堆中的,方法区存放的类信息是一些代码片段(类似于C语言中的结构体),供我们程序调用时字节码执行引擎解释执行的。
对于方法区在JDK8以前的实现是永久代,之后是元空间,这个网上一搜一大把,我就不多废话了。
虚拟机栈
虚拟机栈是我们进行方法调用的地方,每当一个线程执行代码时,jvm就会为该线程开辟一个私有的栈空间,以下面的代码为例:
public class Math {
public static void main(String[] args) {
int a = 8;
add(a);
System.out.println(a);
}
public static int add(int a){
a = a++;
return a;
}
}
你知道输出结果是多少吗?我相信不是回答8就是回答9,那么事实是什么呢?且听我慢慢分析
代码执行时栈的结构如下:
在java程序执行时,每调用一次方法,都会创建一个栈帧,栈帧的结构包括局部变量表,操作数栈,动态链接,方法出口,main方法执行时会创建一个栈帧,然后main方法调用add方法创建另外一个栈帧,那么add方法在执行时底层结构是什么样的呢?这时我们需要用javap -c命令反编译我们的Math.class文件,结果如下:
我们来对照着代码逐一分析这几句字节码指令:
iload_0:加载局部变量表中a的值(为8)到操作数栈,注意这时还没有进行++操作,所以此时操作数栈中a的值为8
iinc:对局部变量表中a进行++操作,所以局部变量表中的a值变为9
istore_0:将操作数中的a值赋给局部变量表中的a值,所以此时局部变量表中的a值又变为8
所以最后的答案是8
至于动态链接和方法返回出口有不懂的可以百度一下,网上很多资料,就不再累述了。
java堆
这个区域是java程序员可以控制最多的地方,我们系统新建的对象大部分都在这里分配内存空间,堆的结构如下:
堆由老年代和新生代组成,新建的对象一般都是在存放在Eden区,当Eden区不够用时,就会就行Minior GC,回收垃圾对象释放内存空间,当垃圾回收后任然存活的对象就会放到S0和S1其中一块区域,当再次发生Minior GC时,会把Eden区和有对象的S区中的存活对象移动到另外一块空的S区,这时先前有对象的S区又变成空的S区,所以S0和S1是相互转化的,当S区对象达到一定的阈值时,新生代的对象就会向老年代转移,当老年代的对象达到一定阈值时,就会触发Full GC
本地方法栈
和虚拟机栈的功能几乎一模一样,由于底层是C或C++实现的,我在这里就不过多介绍了(其实是不会)。
程序计数器
用于记录线程执行到哪一行代码,还记得我们上面反编译后得到的字节码指令吗?读者可以认为是字节码中每行指令前面的行号,也就是地址:
各个区域配置参数
各个区域参数配置图中已标好,记不住时读者可以来这里翻阅查找,我想说的一点是你们知道为啥是-X来设置参数吗?而有些时候却要用-D来设置参数,没错,-X表示参数的调用解析方是C或C++代码,-D表示参数的调用解析方是java代码。
文章首发于我的微信公众号-java时光,欢迎关注!