Java 内存区域

对于 C/C++ 开发者而言,内存是一项令人头痛的"权利",开发者有权对每个对象分配不同的内存,只要他们想,但能力越大责任自然也越大,分配了的内存如果不及时收回,如果下一个对象没有空间对其进行分配,导致内存泄漏和内存溢出问题。

而对于 Java 开发者而言,JVM 存在自动内存管理机制,开发者也就不用再去纠结内存的分配和回收,但如果 Java 程序出现内存泄漏和内存溢出的问题而不知道怎么解决就头疼了,所以需要了解 JVM 是如何使用内存,如何分配内存的。

运行时数据区域

当 Java 程序开始运行时,JVM 会将 OS 分配给 Java 的内存空间分成若干个不同的块,而每个块都具有各自的用途

程序计数器(Program Counter Register)

每个线程都有一个私有的程序计数器

程序计数器像是乐队的指挥家,在线程中去管理代码执行,分支,循环,跳转,异常处理,都需要程序计数器来完成,一般而言,程序计数器中不会产生 OutOfMemoryError(内存溢出错误)

原因是因为:

  1. 空间固定且极小:它所占用的内存大小在虚拟机实现时就已经确定(通常就是一个指针的大小),并且不随程序运行而改变。
  2. 存储内容简单:它仅存储一个指向方法区中字节码的地址(或偏移量),不存储复杂的、可动态增长的数据结构。

Java 虚拟机栈(Java VM Stack)

每个方法被执行时,JVM 都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、 方法出口等信息。每一个方法被调用直到执行完毕的过程,就对应着一个栈帧在虚拟机栈从入栈到出栈的过程

局部变量表会在编译时期创建,当方法入栈后会根据局部变量表来分配局部变量槽,当进入一个方法,这个方法需要在栈帧中分配多大的局部变量是完全确定的,所以一般而言运行时一个方法内的局部变量槽的个数是不变的

当线程请求的栈深度大于虚拟机所允许的深度会抛出 StackOverflowError(栈溢出错误)

如果栈的大小是可变的,那么当栈扩展时无法申请到足够的内存就会产生 OOM

局部变量表

局部变量表内存放了编译期可知的各种 JVM 基本数据类型、对象引用类型和 returnAddress类型

  • 基本数据类型: 四类八种,byte、 short、 int、 long、 float、 double、 char、 boolean
  • 对象引用类型: reference 类型,一个指向对象首地址的指针,也可能是指向一个代表对象的句柄或者其他于此对象相关的位置
  • returnAddress: 指向了一条字节码指令的地址,也就是调用方法的返回值返回地址
局部变量槽

数据类型在局部变量表中的存储空间以局部变量槽表示,其中 64 位的 long 和 double 会用到两个局部变量槽

本地方法栈(Native Method Stack)

本地方法栈与虚拟机栈相似,不过虚拟机栈执行的是 Java 方法也就是字节码服务,而本地方法栈则是为虚拟机使用本地方法服务,本地方法服务既是 Java 线程请求 OS 服务的方法服务代码

和虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError 异常

Java 堆(Java Heap)

堆是所有 Java 线程之间共享的一块大的内存区域,所有的对象实例以及数组几乎都在堆上分配。

Java 堆是垃圾收集器管理的内存区域。

从内存分配的角度来看,在 Java 堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB),以提升对象分配时的效率。

将 Java 堆细分只是为了更好地回收内存或者更快地分配内存。

Java 堆的空间大小是可变的。

当堆扩展时申请不到空间会抛出 OutOfMemoryError 异常

方法区(Method Area)

Java 8 开始,废除了永久代,改用元空间

元空间是方法区的一个实现,方法区是元空间的抽象,二者的关系接口与实现的关系,或者说是抽象概念与具体落地之间的关系。

方法区和堆一样是线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

方法区存放的是"设计图纸"(类元信息),堆中存放的是根据图纸造出来的"实物"(对象实例)。

方法区包括以下内容

  1. 类信息(Class Metadata)
    这是最基础的"图纸"信息,JVM 加载一个 .class 文件后,会提取以下信息存入:
    类的全限定名:比如 com.example.Person。
    父类的全限定名:比如 java.lang.Object。
    类的修饰符:是 public?abstract?还是 final?
    接口列表:这个类实现了哪些接口。
  2. 运行时常量池(Runtime Constant Pool)
    这是方法区里非常重要的一部分。
    字面量:代码中写的死数值,比如文本字符串 "Hello World"、整数 100、final 常量等。
    符号引用:这是代码逻辑的"占位符"。比如代码里写了 Date d = new Date(),在编译时,字节码里并不知道 Date 类的真实物理内存地址,只能用"java.util.Date"这个符号字符串来代替。等到运行时,JVM 会去常量池查表,把这个符号引用替换成真实的内存入口地址。
  3. 静态变量(Static Variables)
    被 static 关键字修饰的变量。
    特点:它们不属于某个具体的对象(不属于饼干),而是属于类本身(属于模具的属性,比如模具的生产日期)。
    注:在 JDK 7 及以后的 HotSpot 虚拟机实现中,静态变量和字符串常量池从方法区移到了堆中,但在逻辑规范上,它们仍属于方法区的一部分。
  4. 方法的代码(Method Code)
    这是真正的"操作指南"。
    类中定义的所有方法(包括构造方法、普通方法)的字节码指令都存在这里。
    当你调用 p.sayHello() 时,线程的程序计数器就会指向这里面的地址,开始一行行读取指令执行。

当方法区申请不到空间时会抛出 OutOfMemoryError 异常

直接内存(Direct Memory)

直接内存不是虚拟机运行时数据区的一部分,但是这部分内存也会被频繁地使用。

例如 Java 在使用 NIO 后分配的堆外内存就位于直接内存中,这样子提高了性能

不过直接内存在动态修改时如果申请不到空间会产生 OutOfMemoryError 异常

相关推荐
爱吃山竹的大肚肚2 小时前
文件上传大小超过服务器限制
java·数据库·spring boot·mysql·spring
黄昏恋慕黎明2 小时前
测试模型讲解
java
瑞雪兆丰年兮2 小时前
[从0开始学Java|第十二天]学生管理系统升级
java·开发语言
弹简特2 小时前
【JavaSE-网络部分03】网络原理-泛泛介绍各个层次
java·开发语言·网络
周杰伦的稻香2 小时前
Hexo搭建教程
java·node.js
倔强的石头1062 小时前
飞算JavaAI如何提升重塑Java开发体验
java·飞算javaai·ai开发工具
努力d小白2 小时前
leetcode438.找到字符串中所有字母异位词
java·javascript·算法
短剑重铸之日2 小时前
《设计模式》第九篇:三大类型之结构型模式
java·后端·设计模式·组合模式·代理模式·结构性模式
没有bug.的程序员2 小时前
RocketMQ 与 Kafka 深度对垒:分布式消息引擎内核、事务金融级实战与高可用演进指南
java·分布式·kafka·rocketmq·分布式消息·引擎内核·事务金融