【JVM】Java内存区域详解

本文详细介绍了Java7和8在内存区域上的变化,包括JDK8移除方法区并引入元空间,以及程序计数器、虚拟机栈、本地方法栈和堆的特性和作用。还讨论了栈帧结构、常量池和字符串常量池的存储位置,以及堆和栈的区别,最后提到了直接内存在NIO操作中的角色。
摘要由CSDN通过智能技术生成

运行时数据区域

要知道JDK7和JDK8是不同的,下面画图分别演示。
JDK7:
在这里插入图片描述
JDK8:
在这里插入图片描述
JDK1.8 相比 JDK1.7,移除了方法区,在本地内存中新增了元空间 ,相当于是用元空间来实现方法区,并将运行时常量池放在元空间中。线程共享只有堆以及字符串常量池。
线程私有的:程序计数器,本地方法栈,虚拟机栈
线程共享的:堆,方法区,直接内存

程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
总结,程序计数器两个作用:

  1. 实现代码流程控制,例如循环跳转等等
  2. 记忆位置,在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行位置

注意:
程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡

Java 虚拟机栈

和程序计数器一样,虚拟机栈也是线程私有的,生命周期和线程是相同的,随着线程创建而创建,随着线程销毁而死亡
栈是 JVM 运行时数据区域的一个核心,除了一些 Native 方法调用是通过本地方法栈实现的,其他所有的 Java 方法调用都是通过栈来实现的(也需要和其他运行时数据区域比如程序计数器配合)。
方法调用的数据通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。

栈帧

图解:
在这里插入图片描述

局部变量表

主要存放了编译期可知的各种数据类型(boolean,byte,short,char,int,long,double,float),对象引用。

操作数栈

主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。

动态链接

用于一个方法调用其他方法的场景。Class 文件的常量池里保存有大量的符号引用比如方法引用的符号引用。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接 。

栈可能出现的错误

StackOverFlowError:当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误
OutOfMemoryError:内存溢出

本地方法栈

本地方法栈全称 Native Method Stack,要先理解本地方法栈。并不是我们字面理解的本地,意思是一种原生方法。
本地方法栈和虚拟机栈发挥作用类似,但区别是:

  1. 虚拟机栈为虚拟机执行 Java 方法服务
  2. 本地方法栈为虚拟机使用到的 Native 方法服务

本地方法执行的时候,本地方法栈也会创建栈帧,用于存放本地方法的局部变量表,操作数栈,动态链接,返回地址等信息。
本地方法栈也会出现 StackOverflow 和 OutOfmemoryError 错误。

堆是 Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。(可见堆的重要性不言而喻)
Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。
在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:

  1. 新生代内存(Young Generation)
  2. 老生代(Old Generation)
  3. 永久代(Permanent Generation)

JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 取代,元空间使用的是本地内存。

方法区

方法区是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。方法区会存储已被虚拟机加载的 类信息字段信息方法信息常量静态变量即时编译器编译后的代码缓存等数据
提问:方法区和永久代以及元空间是什么关系呢?
简答:类与接口的关系。方法区就像是定义好的规范(接口),永久代和元空间就是实现接口具体的实现
细答:方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口。这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。

运行时常量池

2024.2.24补充

在介绍运行时常量池之前我们先要了解常量池,常量池就相当于是一张表。虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。我们可以通过javap -v 类名.class命令去查看字节码结构,包含类的基本信息、常量池以及方法定义。我在本地执行命令如下:

在这里插入图片描述

执行命令之后。我们可以看到如下结果(其他信息省略):

在这里插入图片描述

在这里插入图片描述

通过上图我们了解了常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入到运行时常量池,并且把里面的符号地址变为真实地址

什么是符号地址?像上图中的#2、#3、#4 都是符号地址。

字符串常量池

字符串常量池是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa == bb);// true

JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。
JDK 1.7 为什么要将字符串常量池移动到堆中?
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。将字符串常量池放到堆中,能够更高效及时地回收字符串内存。

堆和栈的区别

2024.2.24新增

  1. 栈用来存放局部变量和方法调用,堆用来存储对象和数组。堆会被 GC 垃圾回收,栈帧出栈后直接释放内存
  2. 栈内存是线程私有的,堆是线程共享的

什么是直接内存

2024.2.24新增

首先举例:例如文件拷贝,从 A 位置复制到 B 位置。

常规 IO 操作:

在这里插入图片描述
我们 Java 代码是不能直接操作系统缓存区的,所以需要在堆内存中添加一个 Java 缓冲区,Java 代码操作堆中的缓冲区,从而进行操作系统缓存区,造成了一个不必要的数据复制,那么性能就肯定比较低下

NIO 操作:
在这里插入图片描述
我们可以看到画出了一份直接内存,这个直接内存 Java 代码可以访问,系统内存也可以访问,那么代码就共享了,所以说速度就成倍提升
总的来说,直接内存不由 JVM 管理,是虚拟机的系统内存。直接内存常见于 NIO 操作,用于数据缓冲区,回收成本高,但是读写性能好,不受 JVM 内存回收管理

参考学习链接

https://javaguide.cn/java/jvm/memory-area.html
https://www.pdai.tech/md/interview/x-interview.html
https://www.bilibili.com/video/BV1yT411H7YK?p=121

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值