3.JVM 内存模型

JVM概述与类的加载机制
JVM 内存模型
对象逃逸分析、JVM 内存分配和回收策略
垃圾回收算法详解、垃圾收集器全解
JVM 调优

3 JVM 内存模型

3.1 jvm 内存模型图

我们先来看下 jvm 内存模型图:

在这里插入图片描述

首先呢,字节码文件(例如,Math.class)由类装载子系统装载到 jvm 运行时数据区,接着执行引擎执行字节码文件内容。

接下来,我们一个一个的介绍 jvm 内存模型里面的这些东西。

3.2 线程栈

3.2.1 初步介绍

这里的栈既可以叫做线程栈(一个线程分配一块栈内存空间),也可以叫做虚拟机栈。这里的栈是每个线程私有的。大家都知道栈里面是存放局部变量的,比如下面的代码:

在这里插入图片描述

当启动运行的时候,comput()方法里面的局部变量 a,b,c 等都是存在栈里面,接下来我们具体介绍下线程栈。

先看下栈的结构图,以上面的 Math 类为例:

### [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ATEKvIcu-1611562467207)(https://uploader.shimo.im/f/FCit5hL19tnwVGFC.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

每一个线程在内存区域都有一块自己的栈内存空间,上图是 mian 线程的栈结构图。

一个线程中,每一个方法都对应该栈中的一个栈帧。如上图中,执行到 main 方法时就有一个当前栈帧 main 的与之对应,压入栈。,执行到 compute 方法时就有一个当前的 compute 的栈帧与之对应,压入栈。

可以发现,后入栈的 compute 栈帧先执行完出栈,先入栈的 main 栈帧后执行完出栈。这完全符和我们数据结构中栈的先进后出(FILO, first in Last out)。

3.2.2 栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟 机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。

3.2.2.1 局部变量表

在栈帧中会存在一个局部变量表,其容量以一个 slot(变量槽,Variable Slot)为单位,非 static 方法的话,索引 0 对传递方法所属对象实例的引用,即关键字 this 的引用对象,方法内局部变量默认从 1 开始索引。如果是 static 方法的话,那么该方法的局部变量就从 0 开始索引。

3.2.2.2 操作数栈

操作数栈也叫做操作栈,具体栈的特性先进后出。在方法运行刚开始这个栈是空的,可以当作一个临时的内存空间。顾名思义,就是一些操作数临时存到这里,比如:执行相加运算的时候,栈顶的两个元素就是两个加数,这两个加数类型要一致(在类加载阶段会做校验的)。过程是,先让栈顶的两个元素出栈进行相加后把结果再入栈。

3.2.2.3 动态链接

见 2.1 类加载过程。

3.2.2.4 返回地址

方法返回一般有两种类型。

正常调用完成(Normal Method Invocation Completion),指方法在执行过程中遇到 return 语句(该方法的返回类型不为 void),或者,方法代码都执行完了(方法返回类型为 void)返回。

异常调用完成(Abrupt Method Invocation Completion),指方法在执行过程中遇到了异常,在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法异常退出。

不管是那种方法返回,最终都是要回到主调方法(调用该方法的方法)。正常返回时,主调方法的程序计数器可能会记录了这个返回地址;异常返回时,返回地址是要通过异常处理器表确定的。

一个方法返回时要把当前方法对应得栈帧出栈,恢复主调方法的局部变量表,操作数栈,如如果有返回值的话就把该返回值压入主调方法栈顶,程序计数器的值指向方法调用指令的下一条指令。

3.2.3 反汇编代码解读

上面提到了线程栈中的每一个方法会有一个栈帧,接下来我们详细讲下这个栈帧,同样以我们的 Math 类为例,代码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0L721N27-1611562467209)(https://uploader.shimo.im/f/msP5FcmvZ3Otkptw.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

我们看下虚拟机在运行这段代码时是怎么执行的,看 Maht.java 看不出来,看 Maht.class 内容的话,我们又看不懂,那么我们用 javap 命令堆 Math.class 进行反汇编(前提是 Maht 类运行过了,存在 Math.class 文件)和上面 2.1 部分的相似把反汇编结果保存到 math.txt 文件中,这里我们用:javap -c Math.class > math.txt,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9auEi0xd-1611562467210)(https://uploader.shimo.im/f/ZgD7as52kyHBXDMg.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

接着打开这个 math.txt 文件(和 Maht.class 在同以目录下)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gMiDAk0s-1611562467212)(https://uploader.shimo.im/f/UtLfO6ItEvdxru8E.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

我们对照着 Math 类源码和这个反汇编文件 math.txt 一起看。这里我们要借助 jvm 指令手册,可在https://blog.csdn.net/weixin_41968788/article/details/105517552该博客中查找。
在这里插入图片描述

先来看 compute 方法,从上图的 13 行开始,进入 compute 方法,继续从 Code 块向下看。一行代码一行代码的去分析。


0:iconst_1

上面的代码 0 是改行代码的类似下标的东西从 0 开始,在指令手册中查找“iconst_1”,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uNLuDHRv-1611562467212)(https://uploader.shimo.im/f/Oh0LgjEXJgx1HIuX.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]
将 int 类型常量 1 压入操作数栈。我们再来看看这个时候的栈结构图,如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gNoptaxJ-1611562467213)(https://uploader.shimo.im/f/aKcjzGWnfudmRAvN.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

1: istore_1

去指令手册查找“istore_1”,释义为:将 int 类型值存入局部变量 1,此时在我们的当前栈帧里面的局部变量表。

局部变量表:在栈帧中会存在一个局部变量表,其容量以一个 slot(变量槽,Variable Slot)为单位,非 static 方法的话,索引 0 对传递方法所属对象实例的引用,即关键字 this 的引用对象,方法内局部变量默认从 1 开始索引。如果是 static 方法的话,那么该方法的局部变量就从 0 开始索引。

我们参照源码的 compute 方法,该方法依次声明了 3 个局部变量 a,b,c,是非 static 方法,则这 3 个变量的索引依次为 1,2,3。

istore_1 就是把操作数栈顶的 1(上一步压入操作数栈的)出栈存到索引为 1 的局部变量 a,结构图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BhFtqn8Z-1611562467213)(https://uploader.shimo.im/f/kfrQczxLGCrASJnu.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

2: iconst_2

去指令手册查找“iconst_2”,释义为:将 int 类型常量 2 压入操作数栈,结构图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dRp3aEnd-1611562467214)(https://uploader.shimo.im/f/GCrz0D4PtIm0573s.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

3: istore_2

去指令手册查找“istore_2”,释义为:将 int 类型值存入局部变量 2。结构图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uiJCJ6iQ-1611562467214)(https://uploader.shimo.im/f/RmL49PnLUBPTrmWE.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

4: iload_1

指令手册查找“iload_1”,释义为:从局部变量 1 中装载 int 类型值,即,把变量 a 的值 1 压入操作数栈。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3jTeQ4aJ-1611562467215)(https://uploader.shimo.im/f/p6sI1dGgOwesVCld.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

5: iload_2

指令手册查找“iload_2”,释义为:从局部变量 1 中装载 int 类型值,即,把变量 b 的值 2

压入操作数栈。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IxXCyWKH-1611562467215)(https://uploader.shimo.im/f/FxVrcoRPyRHX1LMH.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

6: iadd

指令手册查找“iadd”,释义为:执行 int 类型的加法,即把操作数栈顶的两个元素相加,执行 1 + 2 操作,结果为 3 存入操作数栈。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xgtkkB0K-1611562467216)(https://uploader.shimo.im/f/5pjjOkAsfXxa9UFh.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

7: bipush 10

指令手册查找“bipush”,释义为:将一个 8 位带符号整数压入栈。即,把 10 压入操作数栈,结构图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b6QnntXd-1611562467216)(https://uploader.shimo.im/f/hiAk4fwzmAdDNcFS.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

9: imul

指令手册查找“imul”,释义为:执行 int 类型的乘法。即,把操作数栈顶的两个元素出栈相乘,执行 3 * 10,结果为 30 压入栈。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VpM9BeRH-1611562467217)(https://uploader.shimo.im/f/5hXl6GXQHRW2jo6A.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

10: istore_3

指令手册查找“istore_3”,释义为:将 int 类型值存入局部变量 3。即,操作数栈顶元素出栈存入索引为 3 的变量 c。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z55w3ag4-1611562467218)(https://uploader.shimo.im/f/OC461x5I9Grf2y90.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

11: iload_3

指令手册查找“iload_3”,释义为:从局部变量 3 中装载 int 类型值 。即,把局部变量表里面索引为 3 的变量 c 的值压入操作数栈。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WRsU335b-1611562467218)(https://uploader.shimo.im/f/W8O3xWLFPV0eboNq.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

12: ireturn

指令手册查找“iload_3”,释义为: 从方法中返回 int 类型的数据 。即,把操作数栈顶元素返回。

3.2.4 栈内存大小

在《虚拟机规范》中,这个区域规定了两类异常:

如果线程请求的栈的深度大于虚拟机允许的栈的最大深度,会抛出 StackOverflowError 异常;

如果虚拟机容量可以动态扩展,当栈扩展时无法申请到足够的内存,会抛 OutOfMemoryError

来看下这段代码:

/**
 * 设置栈内存大小 -Xss 大小,-Xss 默认 1M,比如:-Xss128k
 */
public class StackOverflowTest {
    static int count = 0;
    
    static void redo(){
        count++;
        redo();
    }
    public static void main(String[] args) {
        try {
            redo();
        } catch (Throwable t) {
            System.out.println("当前 count 值:" + count);
            t.printStackTrace();
        }
    }
}

在上面代码中,我们无限递归调用 redo()方法,控制台打印如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EPIUIjXC-1611562467219)(https://uploader.shimo.im/f/Oc8WYhG8eTXaBNEw.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

可以发现递归了 21528 次,创建了 21528 个栈帧,发生了 StackOverflowError 异常。

-Xss 用来设置栈内存容量。例如:-Xss128k

还是上面的代码,在 idea 中,我们设置 jvm 参数,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iAAgEL9l-1611562467219)(https://uploader.shimo.im/f/QlxcGIcZOXszINgE.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

点击 ok 后,再次点击运行,控制台打印如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M8F7pTW4-1611562467219)(https://uploader.shimo.im/f/Mmu8kf375GplDRSZ.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

可以看到我们把每个线程栈的大小设置为 128kb 时,能递归调用 redo()方法 1078 次,能创建 1078 个栈帧。

注:

JDK5.0以后每个线程栈大小为1M,之前每个线程栈大小为256K。应当根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。需要注意的是:当这个值被设置的较大(例如>2MB)时将会在很大程度上降低系统的性能。

对于不同版本的 Java 虚拟机和不同的操作系统,栈容量最小值可能会有所限制,这主要取决于操 作系统内存分页大小。譬如上述方法中的参数-Xss128k 可以正常用于 32 位 Windows 系统下的 JDK 6,但 是如果用于 64 位 Windows 系统下的 JDK 11,则会提示栈容量最小不能低于 180K,而在 Linux 下这个值则 可能是 228K,如果低于这个最小限制,HotSpot 虚拟器启动时会给出如下提示:

“The Java thread stack size specified is too small. Specify at least 228k”

对于“OutOfMemoryError”情况的异常我们这里不做演示,java 虚拟机线程会映射到操作系统的内核线程中, 在模拟多线程导致栈内存 OOM 异常,会造成操作系统假死,抛出异常后,系统会很慢。这里给出模拟思路,循环创建线程去调用某个方法(可以是上面代码中的 redo 方法)。

3.3 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

程序计数器是线程私有的。在多线程并发时,某一时刻肯定是一个线程在执行的(从微观角度来看,多线程其实是多个线程走走停停,谁获得了时间片谁执行的,在单个时间片内是一个线程执行的)。每个线程都需要一个程序计数器,记录该线程执行到的字节码指令位置,下次执行时从该位置的下一条指令开始执行。

3.4 方法区

方法区,是各个线程共享的内存区域,存储一些类元信息,静态变量,常量,即时编译器编译的代码缓存等数据。

可能接触 java 虚拟机比较早的一些人,习惯把方法区称为“永久代”。从严格意义上来讲,这两者是不能等同的。在早期,虚拟机 HotSpot 设计团队把收集器的分代设计扩展到了方法区,或者说是用永久代实现的方法区。这种设计更容易发生内存溢出问题(永久代有

-XX:MaxPermSize 的上限)。到了后来 Oracle 的 HotSpot 团队放弃了永久代,方法区内容存储在本地内存中。jdk1.8 起,把 jdk7 总永久代剩余内容(只要是类型信息)全部移到元空间中,本地内存实现了元空间。

看到这里大家是否对方法区,永久代,元空间这 3 个概念理解不清呢?

方法区《Java 虚拟机规范》中的一个逻辑概念。在 java 官方的 HotSpot 虚拟机,Java8 之前是用永久代实现的方法区,数据存储在虚拟的空间中。而之后是用元空间实现的方法区,数据存储在本地服务器的内存中。可以这样理解,永久代和元空间都是实现方法区的一种技术手段。

《Java 虚拟机规范》中对方法区管理和堆一样很宽松。存储数据不需要连续的空间,可以选择固定大小或者扩展,甚至还可以选择不实现垃圾收集。垃圾收集动作发生在这个区域的情况非常少,但并不是说数据存到这个区域就“永久”了。这个区域的内存回收主要针对常量池和类型的卸载。根据《Java 虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时将抛出 OOM 异常。

3.5 直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导 OutOfMemoryError 异常出现。

在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,则肯定还是会受到

本机总内存(包括物理内存、SWAP 分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx 等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。

3.6 本地方法栈

本地方法栈(Native Method Stacks),是为虚拟机用到的本地(native)方法服务的。

这里的本地方法有时候也称为原生方法,在刚开始的时候代码是用 C 语言编写的,后来使用 Java 语言的时候为了兼容 C 语言,C 语言编写的部分被打包成类似.dll 的文件,也可以理解为一个 jar 包,而 Java 源码中一些 native 修饰的本地方法就是去调用这些用 C 语言编写的代码。

3.7 堆

对于 Java 应用程序来说,Java 堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。Java 堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作“GC 堆”。

3.7.1 堆内存分代划分

堆内存的分代划分情况如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UfmBQP9b-1611562467220)(https://uploader.shimo.im/f/fUM3e82qo1zR389s.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

3.7.2 堆内存 GC 动态演示

系统在运行过程中,不断有对象产生和销毁。假如当系统刚启动后,下面的产生的对象都用 o1、o2·······o9999 表示(假设都是小对象)。

(1).初始情况,有对象 o1 产生了先放在 eden 区:

![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ybiASsap-1611562467221)(https://uploader.shimo.im/f/BHDJBp1x8oEXC37F.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]](https://img-blog.csdnimg.cn/20210125163325350.png

(2). 接着又产生了一些对象,放在了 Eden 区,慢慢的 eden 区放满了。这时侯会进行一次 Minor GC/Young GC。把 gc 后的对象 o1、02 都移到了 from 区。
在这里插入图片描述

(3).接着又产生了一些对象,慢慢的 eden 区又放满了。这时侯又会进行一次 Minor GC/Young GC。Eden 区、from 区 gc 后还存活的对象移动到 to 区。

![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VKHxqFwx-1611562467221)(https://uploader.shimo.im/f/bHN0th7tbZT7UIiQ.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]](https://img-blog.csdnimg.cn/20210125163335383.png

(4).接着又产生了一些对象,慢慢的 eden 区又放满了。这时侯又会进行一次 Minor GC/Young GC。Eden 区、to 区 gc 后还存活的对象移动到 from 区。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ug6BgfgK-1611562467222)(https://uploader.shimo.im/f/FzNnyyXi1MNMPsk8.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

(5).每 Eden 区满一次(也有可能是 from 或 to 区满)进行一次 minor GC,也是 young gc,后面就只说一个了。gc 后还存活的对象年龄就加 1(对象初始年龄为 0)。对象在 from 区和 to 区来回移动,如此往复,直到年龄满 15 次(默认为 15,可配置)之后。该对象就会被直接移动到老年代。下图是 from 区直接移动到老年代的情况,也有可能从 to 区直接移动到老年代。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rE6cxO3E-1611562467222)(https://uploader.shimo.im/f/htm2WwKEEr7U8IXy.png!thumbnail?fileGuid=Wttdh3K6tTttJwV8)]

(5).越来越多的对象被移动到老年代了,有一天老年代也放满了。这时候会 stw(stop the world),即停止执行。开始对老年代,年轻代甚至方法区进行垃圾回收。这样一次 gc,称之为 Major/Full GC。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值