JVM的内存区域划分

《从 0 开始带你成为JVM实战高手》-学习笔记-02

Posted by 瞿广 on Tuesday, December 24, 2019

TOC

一、内存划分

我们的代码运行起来时,是不是需要执行我们写的一个一个的方法?

那么运行方法的时候,方法里面有很多变量之类的东西,是不是需要放在某个内存区域里?

接着如果我们写的代码里创建一些对象,这些对象是不是也需要内存空间来存放?

4、存放类的方法区

JDK 1.8以后,这块区域的名字改了,叫做“Metaspace”,可以认为是“元数据空间”这样的意思。当然这里主要还是存放我们自己写的各种类相关的信息。

5、执行代码指令用的程序计数器

通过编译器,把“.java”后缀的源代码文件编译为“.class”后缀的字节码文件。

这个“.class”后缀的字节码文件里,存放的就是对你写出来的代码编译好的字节码了。

字节码才是计算器可以理解的一种语言,而不是我们写出来的那一堆代码。

public java.lang.String getName();
     descriptor: ()Ljava/lang/String;
     flags: ACC_PUBLIC
     Code:
         stack=1, locals=1, args_size=1
             0: aload_0
             1: get_field    #2
             4: areturn

现在Java代码通过JVM跑起来的第一件事情就明确了, 首先Java代码被编译成字节码指令,然后字节码指令一定会被一条一条执行,这样才能实现我们写好的代码执行的效果。

所以当JVM加载类信息到内存之后,实际就会使用自己的字节码执行引擎,去执行我们写的代码编译出来的代码指令

那么在执行字节码指令的时候,JVM里就需要一个特殊的内存区域了,那就是“程序计数器”

这个程序计数器就是用来记录当前执行的字节码指令的位置的,也就是记录目前执行到了哪一条字节码指令。

大家都知道JVM是支持多个线程的,所以其实你写好的代码可能会开启多个线程并发执行不同的代码,所以就会有多个线程来并发的执行不同的代码指令

因此每个线程都会有自己的一个程序计数器,专门记录当前这个线程目前执行到了哪一条字节码指令了

6、Java虚拟机栈

Java代码在执行的时候,一定是线程来执行某个方法中的代码

哪怕就是下面的代码,也会有一个main线程来执行main()方法里的代码

在main线程执行main()方法的代码指令的时候,就会通过main线程对应的程序计数器记录自己执行的指令位置。

但是在方法里,我们经常会定义一些方法内的局部变量

比如在上面的main()方法里,其实就有一个“replicaManager”局部变量,他是引用一个ReplicaManager实例对象的,关于这个对象我们先别去管他,先来看方法和局部变量。

因此,JVM必须有一块区域是来保存每个方法内的局部变量等数据的,这个区域就是Java虚拟机栈

每个线程都有自己的Java虚拟机栈,比如这里的main线程就会有自己的一个Java虚拟机栈,用来存放自己执行的那些方法的局部变量。

如果线程执行了一个方法,就会对这个方法调用创建对应的一个栈帧

栈帧里就有这个方法的局部变量表 、操作数栈、动态链接、方法出口等东西,这里大家先不用全都理解,我们先关注局部变量。

比如main线程执行了main()方法,那么就会给这个main()方法创建一个栈帧,压入main线程的Java虚拟机栈

同时在main()方法的栈帧里,会存放对应的“replicaManager”局部变量

jvm

7、Java堆内存

现在大家都知道了,main线程执行main()方法的时候,会有自己的程序计数器。

此外,还会依次把main()方法,loadReplicasFromDisk()方法,isLocalDataCorrupt()方法的栈帧压入Java虚拟机栈,存放每个方法的局部变量。

上面的“new ReplicaManager()”这个代码就是创建了一个ReplicaManager类的对象实例,这个对象实例里面会包含一些数据,如下面的代码所示。

这个“ReplicaManager”类里的“replicaCount”就是属于这个对象实例的一个数据。

类似ReplicaManager这样的对象实例,就会存放在Java堆内存里。

Java堆内存区域里会放入类似ReplicaManager的对象,然后我们因为在main方法里创建了ReplicaManager对象的,那么在线程执行main方法代码的时候,就会在main方法对应的栈帧的局部变量表里,让一个引用类型的“replicaManager”局部变量来存放ReplicaManager对象的地址

相当于你可以认为局部变量表里的“replicaManager”指向了Java堆内存里的ReplicaManager对象

jvm

8、核心内存区域的全流程串讲

jvm

首先,你的JVM进程会启动,就会先加载你的Kafka类到内存里。然后有一个main线程,开始执行你的Kafka中的main()方法。

main线程是关联了一个程序计数器的,那么他执行到哪一行指令,就会记录在这里

大家结合上图中的程序计数器来理解一下。

其次,就是main线程在执行main()方法的时候,会在main线程关联的Java虚拟机栈里,压入一个main()方法的栈帧。

接着会发现需要创建一个ReplicaManager类的实例对象,此时会加载ReplicaManager类到内存里来。

然后会创建一个ReplicaManager的对象实例分配在Java堆内存里,并且在main()方法的栈帧里的局部变量表引入一个“replicaManager”变量,让他引用ReplicaManager对象在Java堆内存中的地址。

9、其他内存区域

其实在JDK很多底层API里,比如IO相关的,NIO相关的,网络Socket相关的

如果大家去看他内部的源码,会发现很多地方都不是Java代码了,而是走的native方法去调用本地操作系统里面的一些方法,可能调用的都是c语言写的方法,或者一些底层类库

比如下面这样的:public native int hashCode();

在调用这种native方法的时候,就会有线程对应的本地方法栈,这个里面也是跟Java虚拟机栈类似的,也是存放各种 native 方法的局部变量表之类的信息。

还有一个区域,是不属于JVM的,通过 NIO 中的 allocateDirect 这种API,可以在Java堆外分配内存空间。然后,通过Java虚拟机里的 DirectByteBuffe r来引用和操作堆外内存空间。

其实很多技术都会用这种方式,因为有一些场景下,堆外内存分配可以提升性能。