【JVM】Java内存区域详解

运行时数据区域

要知道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 及以后方法区的实现变成了元空间。

运行时常量池

这部分有点枯燥,也欢迎评论区提供学习方法。

等学习之后填充此部分,待补充~~~~~~

字符串常量池

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

java 复制代码
// 在堆中创建字符串对象"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。将字符串常量池放到堆中,能够更高效及时地回收字符串内存。

参考学习链接

https://javaguide.cn/java/jvm/memory-area.html
https://www.pdai.tech/md/interview/x-interview.html

相关推荐
weixin_462428474 分钟前
使用 Caffeine 缓存并在业务方法上通过注解实现每3到5秒更新缓存
java·缓存
程序媛小果6 分钟前
基于java+SpringBoot+Vue的桂林旅游景点导游平台设计与实现
java·vue.js·spring boot
骑鱼过海的猫1238 分钟前
【java】java通过s3访问ceph报错
java·ceph·iphone
杨充14 分钟前
13.观察者模式设计思想
java·redis·观察者模式
Lizhihao_16 分钟前
JAVA-队列
java·开发语言
喵叔哟25 分钟前
重构代码之移动字段
java·数据库·重构
喵叔哟25 分钟前
重构代码之取消临时字段
java·前端·重构
fa_lsyk28 分钟前
maven环境搭建
java·maven
Daniel 大东1 小时前
idea 解决缓存损坏问题
java·缓存·intellij-idea
wind瑞1 小时前
IntelliJ IDEA插件开发-代码补全插件入门开发
java·ide·intellij-idea