【JVM】基础概念之内存结构介绍

通过前一节的内容我们了解了,我们的Java程序都是运行在JVM上面的,那么JVM具体内部是怎样划分的呢?我们来具体了解一下

JVM内存区域可以被划分为这样几个核心部分:运行时数据区、直接内存、执行引擎、本地库接口和垃圾回收系统

  • 运行时数据区:是内存的核心部分,堆、方法区、虚拟机栈等等都在这个区域内,真正存储数据的地方
  • 直接内存:用于NIO的直接缓冲区分配,绕过JVM堆,用作大文件IO、图像处理等
  • 执行引擎:执行字节码指令,将Java代码转换为机器指令,主要包括解释器、即时编译器(JIT)、垃圾回收器、本地方法接口等
  • 本地库接口:提供Java代码调用本地方法(C、C++)的能力
  • 垃圾回收系统:用于回收内存中不再使用的对象,常见垃圾收集器包括CMS、Serial、G1等等

现在我们对这几个核心区域有了一定了解,接下来我们着重介绍一下运行时数据区和垃圾回收系统这两部分

一、运行时数据区

运行时数据区(Runtime Data Areas)分为五个核心部分:堆、方法区、虚拟机栈、本地方法栈、程序计数器

1.1 堆

堆是JVM管理的最大一块内存区域,用于存储所有对象实例和数组。它是垃圾回收的主要区域,所有线程共享堆内存

1.1.1 堆的内存结构

堆采用分代设计,主要分为年轻代和老年代
年轻代

Eden区(伊甸园区):新创建的对象首先分配在Eden区

Survivor0区(From区):存放第一次Minor GC后存活的对象

Survivor1区(To区):存放第二次Minor GC后存活的对象
老年代

存放长期存活的对象,包括:

经历过多次Minor GC仍然存活的对象(默认年龄阈值15次)

大对象直接进入老年代(避免在年轻代频繁复制)

年轻代空间不足时,部分对象会提前晋升到老年代

1.1.2 对象在堆中的内存布局

总结起来包括三部分:

  1. 对象头:包括标记字段(存储哈希码、GC分代年龄、锁状态等)和类型指针(指向方法区中的类元数据)
  2. 实例数据:存储对象的字段值
  3. 对齐填充:保证对象的大小是8字节的倍数
1.1.3 堆的内存分配策略

指针碰撞(Bump the Pointer):内存规整时,通过移动指针分配内存

空闲列表(Free List):内存不规整时,维护空闲内存列表

TLAB(Thread Local Allocation Buffer):每个线程预先分配一小块内存,减少竞争

1.2 方法区

方法区存储被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

1.2.1 方法区存储的具体内容
  1. 类型信息:类的全限定名;类的直接父类的全限定名;类的修饰符(public、abstract、final等);类实现的接口列表
  2. 运行时常量池:字面量(字符串、final常量值等);符号引用(类和接口的全限定名、字段的名称和描述符、方法的名称和描述符)
  3. 字段信息:字段名称;字段类型;字段修饰符
  4. 方法信息:方法名称;方法返回类型;方法参数数量和类型;方法修饰符;方法字节码、操作数栈、局部变量表大小;异常表
  5. 类变量(静态变量):JDK7及以前在方法区,JDK8+在堆中(但概念上仍属于类信息)
  6. 指向类加载器的引用
  7. 指向Class对象的引用
  8. 方法表(虚方法表):提高虚方法调用的效率
1.2.2 JDK版本演变
  • JDK 1.6及以前:
    • 方法区实现为永久代(PermGen)
    • 字符串常量池在永久代
    • 容易发生PermGen Space OOM
  • JDK 1.7:
    • 字符串常量池移到堆中
    • 静态变量移到堆中
  • JDK 1.8+:
    • 永久代被移除,改为元空间(Metaspace)
    • 元空间使用本地内存,不再受JVM堆大小限制
    • 字符串常量池在堆中,类元数据在元空间
1.2.3 元空间特性
  1. 内存位置:使用本地内存
  2. 自动扩展:默认情况下,元空间可动态扩展,直到耗尽系统内存
  3. 压缩:支持类指针压缩,节省内存

1.3 虚拟机栈

虚拟机栈是线程私有的,生命周期与线程相同。每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息

栈帧结构大体长这样

1.3.1 方法调用/返回时的栈帧变化

方法调用时:

  1. 为被调用方法创建新的栈帧
  2. 将参数值复制到新栈帧的局部变量表
  3. 将返回地址压入调用者栈帧的操作数栈
  4. 程序计数器跳转到被调用方法的字节码起始位置

方法返回时:

  1. 将返回值压入调用者栈帧的操作数栈(如果有返回值)
  2. 弹出当前栈帧
  3. 程序计数器跳转到返回地址
1.3.2 栈溢出问题

StackOverflowError:线程请求的栈深度超过JVM允许的最大深度

常见原因:无限递归、递归深度过大

相关JVM参数:-Xss设置每个线程栈大小

1.4 程序计数器

程序计数器是线程私有的小块内存空间,可以看作是当前线程所执行的字节码的行号指示器

它起到一个类似书签的作用,告诉程序现在执行到了哪里,接下来要执行哪里

每个线程都有自己的程序计数器,它是唯一不会发生OOM(OutOfMemoryError)的内存区域

1.5 本地方法栈

本地方法栈为JVM调用native方法服务,为Native方法的执行提供栈空间、为C/C++等本地代码提供函数调用栈、支持JNI调用等等


以上五个部分可以分为两大类别:线程共享区域和线程私有区域

其中堆和方法区是线程共享区域(所有线程共同访问),程序计数器、虚拟机栈、本地方法栈是程序私有区域(每个线程独立拥有)

讲到这里,我们就不得不提到有关内存划分非常重要的一个问题:JVM调优

鉴于JVM调优又是一个比较大的话题,为了保证整篇文章内容的一致性,这里先打个TODO,下一篇再来介绍

相关推荐
Zzzzzxl_2 小时前
互联网大厂Java/Agent面试:Spring Boot、JVM、微服务、RAG与向量检索实战问答
java·jvm·spring boot·kafka·rag·microservices·vectordb
铅笔侠_小龙虾11 小时前
Arthas 命令
java·jvm
上78将1 天前
JVM回收垃圾机制
java·开发语言·jvm
无敌最俊朗@1 天前
C++ 内存管理与编译原理 (面试复习2)
java·开发语言·jvm
酷ku的森1 天前
JVM垃圾回收机制
jvm
Tan_Ying_Y1 天前
垃圾收集机制(在什么时候,对什么,做了什么)
jvm
张人大 Renda Zhang2 天前
Java 虚拟线程 Virtual Thread:让“每请求一线程”在高并发时代复活
java·jvm·后端·spring·架构·web·虚拟线程
杀死那个蝈坦2 天前
Caffeine
java·jvm·spring cloud·tomcat
i***13242 天前
java进阶1——JVM
java·开发语言·jvm