前言
上篇文章我们一起了解了jvm虚拟机类的加载机制,而且是以一种纯大白话进行的一场闲聊,相信小伙伴们应该印象深刻,感兴趣的小伙伴可以重温一下上一篇文章大白话谈JVM的类加载机制。
当jvm加载了类后,会把需要使用的对象放入到内存当中,那么jvm的内存模型是什么样的呢?
今天我们就来探索一下jvm的内存模型。由于有小伙伴反映想加些图更容易理解,王子接下来的文章打算用更多的图例来讲解。
方法区
很多小伙伴之前也了解过jvm的内存模型,知道有方法区这个东西,但可能了解的不是很详细。
其实方法区是在JDK1.8以前的版本里存在的一块内存区域,主要就是存放从class文件里加载进来的类的,而且常量池也是在这块区域内的。
但是在JDK1.8之后,这块区域摇身一变,换了名字,叫做“Metaspace”,翻译过来就是“元数据空间”的意思。当然它只是改了个名,实现的功能是没变的。
程序计数器
假设我们的代码是这样的:
public class Main { public static void main(String[] args) { SysUser sysUser = new SysUser(); sysUser.setAvatar("1"); } }
这个是我们的java代码,是面向我们开发者的,然后会编译成class字节码文件,在class字节码文件中存放的是一条条的字节码命令,他对应了一条条的机器指令,计算机只有读到机器指令才知道它要干什么。
所以当JVM加载类信息后,实际上就是使用字节码执行引擎去执行我们的代码编译出来的一条条字节码指令,如下图。
那么在执行字节码指令的时候,jvm是怎么知道该执行哪条指令了呢?这时候程序计数器就出现了。
它就是用来记录当前执行的字节码指令位置的。
另外,小伙伴们都知道,JVM是支持多线程的,所以如果我们开启了多线程,就会有多个线程在执行不同的字节码指令,为了他们之间的字节码指令不会混在一起,所以每个线程都会有自己的程序计数器,用来记录每个线程自己的指令现在执行到哪一条了,如下图:
JAVA虚拟机栈
我们现在知道,jvm执行class中指令时是通过程序计数器来锁定执行的指令位置的,但是在我们执行的方法里,会有很多的局部变量等数据,虚拟机栈就是用来保存方法的局部变量的,而且每个线程都会有自己的虚拟机栈,比如我们之前的代码:
public class Main { public static void main(String[] args) { SysUser sysUser = new SysUser(); sysUser.setAvatar("1"); } }
这个代码会启动一个main线程,并把局部变量sysUser保存到栈中。
如果线程执行了一个方法,就会对这个方法调用创建一个栈帧,然后就是所谓的压栈操作(先进后出),如下:
然后我们代码继续执行,调用了setAvatar方法,那么就会继续创建栈帧,如下:
当setAcatar方法执行完毕,就会对方法的栈帧执行出栈操作。
以上就是JAVA虚拟机栈这一部分的作用,简单概括就是:调用方法就创建栈帧,压栈,方法执行完就执行出栈操作。
JAVA堆内存
说完了java虚拟机栈,那我们再来说一个很重要的内存区域java堆内存,它是用来存放我们代码中创建的各种对象的。
还是以刚才的代码为例,当我们执行new SysUser()的时候,就创建了一个SysUser实例对象,而这个对象本身又会有很多的属性和方法,这样的实例化对象的数据就是存放在堆内存中的。
而这个时候我们在栈中存储的局部变量实际上存的就是这个对象的内存地址,也可以理解为一个引用地址。如下图:
到这里JVM的内存区域已经和小伙伴们介绍完了,给大家来一张整体的内存区域图,以便理解:
其他内存区域
除了前文我们介绍的内存区域,jdk的api中(io、nio、socket)相关,其实它们的内部已经不是java代码了,而是调用了native方法调用了本地操作系统的一些方法,可能是c语言编写的或者是一些底层类库。
在调用native方法的时候,线程就会对应本地方法栈,这个是于java虚拟机栈类似的东东,放的就是native方法的各种局部变量表。
除此之外还有一个区域,是不属于JVM的,通过NIO的allocateDirect的api,可以在堆外分配内存空间,从而直接操作堆外的内存空间数据。
有些场景下,堆外内存空间会提升性能,这个问题我们之后再逐步探索,今天就不说这个了。
总结
本文到这里就结束啦,王子采纳了一些小伙伴的意见,画了很多图有助于小伙伴们更好的理解,希望能够帮到大家。
那我们下篇文章见。
往期文章推荐:
大白话谈JVM的类加载机制