Java 虚拟机 | 内存分配模型|七日打卡

简介: Java 虚拟机 | 内存分配模型|七日打卡

目录

image.png

1. 运行时数据区域


根据《Java虚拟机规范》的规定,Java 虚拟机在执行程序时,会将内存划分为不同的数据区域:


内存区域 线程独占
程序计数寄存器 私有
Java 虚拟机栈 私有
本地方法栈 私有
Java 堆 共享
方法区 共享


image.png

—— 图片引用自网络


1.1 程序计数寄存器(Program Counter Register)


程序计数寄存器记录的是当前线程下一条准备执行执行的字节码行号。当虚拟机在进行顺序执行、分支、循环、函数调用或异常处理时,都会将「下一条字节码指令的行号」存储在程序计数器中。


为什么 Java 虚拟机需要这个程序计数器呢,这是为了保证正确地进行线程切换。操作系统的「时间片轮转机制」会为每个线程分配时间片,当一个线程的时间片用完,或者其他线程提前抢夺 CPU 时间片时,当前线程就会挂起,而将来挂起的线程获得时间片时,就需要通过程序计数器来恢复到正确的指令位置。


程序计数器只在执行 Java 方法时有意义,如果当前线程执行的是 native 方法,程序计数器的值是空(Undefined)。


1.2 虚拟机栈(Java Virtual MachineStack)


虚拟机栈描述的是 Java 方法执行的内存模型。虚拟机在执行方法时会创建一个栈帧(Stack Frame),每个方法从调用到结束的过程,都对应着一个栈帧入栈到出栈的过程。


  • 入栈:创建对应的栈帧,压入虚拟机栈;
  • 出栈:恢复上层方法中的局部变量表和操作数栈,如果有返回值,将返回值入栈,最后调整程序计数器指向方法调用指令的下一条执行。当所有栈帧都出栈后,线程结束。


提示: 栈的默认大小是 1M,可以用虚拟机参数-Xss调整大小。


1.2.1 栈帧


栈帧是支持虚拟机进行方法调用和方法执行的数据结构,每个栈帧都包含四个区域:

  • 1、局部变量表(Local Variable Table)

存放局部变量,当一个变量不再使用时,对应的空间会让出来给其他变量使用,这块区域的大小在编译时确定。

  • 2、操作数栈(Operand Stack)

用于存放字节码指令的操作数,这块区域的大小在编译时确定。在虚拟机的具体实现中,这这块区域可能是寄存器,也可能是栈。所谓 “基于栈的解释执行”,这里的栈指的是操作数栈。

  • 3、动态连接(Dynamic Linking)
  • 4、返回地址

返回地址存放函数调用位置的下一行指令,用于在方法正常返回时返回到上一层方法继续执行。如果是异常返回的话,则是通过异常处理器表来确定。


image.png


—— 引用自 paul.pub/android-dal… 强波(华为)著


1.2.2 栈 vs 寄存器


在《Java虚拟机规范》中,操作数栈是一个栈数据结构,但在虚拟机的具体实现里,也可能是寄存器结构。


  • 基于栈的解释执行 —— Java 虚拟机
  • 基于寄存器的解释执行 —— Android 虚拟机(Dalvik & ART)


易错: 这里的栈和寄存器都是虚拟机的虚拟实现,和 CPU 中的数据寄存器并不是同一个概念。


基于寄存器的虚拟机栈帧中没有操作数栈和局部变量表,取而代之的是虚拟寄存器。与 Java 虚拟机相比,基于寄存器的 Android 虚拟机的指令数明显较少,同时也避免了操作数栈和局部变量表之间的数据移动。


1.2.3 栈的优化技术


  • 编译优化:方法内联

方法内联的就是把目标方法的代码复制到调用位置,避免方法调用的出栈入栈行为。

  • 栈帧数据共享

一般两个栈帧的内存区域是独立的,而在大多数虚拟机实现中,会将两个栈帧中的下层栈帧的「操作数栈」和上层栈帧的「部分局部变量」重叠,这样在方法调用的时候就不用进行额外的参数复制了。

image.png


—— 图片引用自网络


1.3 本地方法栈(Native Method Stacks)


与虚拟机栈类似,区别在于虚拟机栈执行 Java 方法,而本地方法栈执行 native 方法。当虚拟机调用 native 方法时,不会在虚拟机栈中创建栈帧,而是直接动态链接调用 native 方法。


提示: 《Java虚拟机规范》没有强制规定本地方法栈中方法语言、使用方式与数据结构,有的虚拟机(如 HotSpot)直接合并了虚拟机栈和本地方法栈。


1.4 方法区(Method Area)


1.4.1 方法区的数据


方法区主要存放虚拟机加载的类相关数据,包括:


  • 类信息
  • 静态变量
  • 静态常量
  • 运行时常量池
  • 即时编译器生成的代码

「Class 文件常量池」和「运行时常量池」是比较容易混淆的概念。其实它们一个是静态的,一个是动态的。


「Class 文件常量池(Constant Pool Table)」是静态的,指的是编译后存放在 Class 文件常量池中的字面量 & 符号引用,而这些常量会在类加载之后进入运行时常量池。

「运行时常量池」是动态的,Java 不要求常量只能在编译时声明,在运行时同样可以将新的常量加入到常量池中。例如 String#intern()


提示: 所谓字符串常量池属于运行时常量池的一部分。


1.4.2 方法区 ? 永生代 ? 元空间 ?


这三个概念也是比较容易混淆的,简单来说:方法区是虚拟机规定的运行时数据区域,永久代 & 元空间是方法区在不同虚拟机上的具体实现。


以 HotSpot 虚拟机为例:


在 JDK 1.7 之前,HotSpot 虚拟机使用永久代(Permanent Generation)来实现方法区,永久代中存储的都是生命周期较长的数据。永久代可以跟堆一起执行垃圾回收。不过在永久代执行垃圾回收的 “性价比” 并没有新生代高,一般新生代垃圾回收可以回收 70% ~ 95% 的空间,而永久代的垃圾回收率就远低于此。


从 JDK 1.8 开始,HotSpot 虚拟机使用元空间(Metadata)来实现方法区,并且使用了本地内存存储,扩展了方法区的内存上限。


为什么 HotSpot 要使用元空间来代替永久代呢?

因为永久代空间有限,经常出现不够用或者内存溢出异常。而使用本地内存就可以方便扩展方法区的大小。当然,元空间也不是完美的,因为机器总内存是有限的,使用大量的本地内存的话就会挤压堆内存的上限。


1.5 Java 堆(Java Heap)


堆是虚拟机上最大的一块内存,绝大多数对象都是存储在堆上的(Class 对象存储在方法区,满足逃逸分析的对象在栈上分配)。垃圾回收机制操作的主要区域也是堆。


堆和方法区都是线程共享的,为什么 Java 区分出两块区域呢?

这体现的是 动静分离 的思想,堆中存放的是生命周期比较短,经常需要进行垃圾回收的数据;而方法区中存放的是生命周期比较长的数据。将两种数据分开存储,有利于更高效地进行内存管理。


2. 直接内存 / 堆外内存 / 本地内存


这三个概念其实是相同的,直接内存(Direct Memory)不属于 Java 虚拟机规定的运行时数据区域。不受制于 Java 堆大小限制,但是受制于机器总内存。


在 JDK 1.4 NIO 中引入了基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,可以直接通过 native 方法分配一块直接内存,并且创建一个 Java 层 DirectByteBuffer 对象供应用层访问。


3. 内存溢出


3.1 程序计数器


在《Java虚拟机规范》中,程序计数器是 JVM 中唯一不会发生 OOM 的区域。


3.2 栈溢出


虚拟机栈和本地方法栈类似,都可能抛出的两种异常:

  • StackOverflowError 异常: 线程的栈帧深度大于虚拟机允许的最大深度;
  • OutOfMemoryError 异常: 无法申请到足够内存时;


3.3 堆溢出


申请内存空间超出最大堆内存空间时发生堆溢出。应检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少冗余 / 不必要的内存消耗。


3.4 方法区溢出


有两种情况会导致方法区内存溢出:

  • 1、运行时常量池溢出
  • 2、加载的类信息溢出


3.5 直接内存溢出


与堆一样,申请的直接内存超过直接内存容量时,也会发生内存溢出。


ByteBuffer.allocateDirect(128*1024*1024)
复制代码
java.lang.OutofMemoryError : Direct buffer memory
复制代码


4. 总结


  • 1、程序计数器是线程私有,描述的是当前线程下一条需要执行的字节码指令行号;
  • 2、虚拟机栈描述的是 Java 方法执行的内存模型;
  • 3、本地方法栈与虚拟机栈类似,区别在于虚拟机栈执行 Java 方法,而本地方法栈执行 native 方法;
  • 4、堆是虚拟机上最大的一块内存,绝大多数对象都是存储在堆上的,垃圾回收机制操作的主要区域也是堆;
  • 5、方法区主要存放虚拟机加载的类相关数据。
目录
相关文章
|
23天前
|
存储 人工智能 编解码
TripoSF:3D建模内存暴降80%!VAST AI新一代模型细节狂飙82%
TripoSF 是 VAST AI 推出的新一代 3D 基础模型,采用创新的 SparseFlex 表示方法,支持 1024³ 高分辨率建模,内存占用降低 82%,在细节捕捉和复杂结构处理上表现优异。
71 10
TripoSF:3D建模内存暴降80%!VAST AI新一代模型细节狂飙82%
|
1月前
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
164 29
JVM简介—1.Java内存区域
|
3月前
|
存储 设计模式 监控
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
723 166
|
1月前
|
消息中间件 Java 应用服务中间件
JVM实战—2.JVM内存设置与对象分配流转
本文详细介绍了JVM内存管理的相关知识,包括:JVM内存划分原理、对象分配与流转、线上系统JVM内存设置、JVM参数优化、问题汇总。
JVM实战—2.JVM内存设置与对象分配流转
|
5月前
|
存储 缓存 安全
Java内存模型深度解析:从理论到实践####
【10月更文挑战第21天】 本文深入探讨了Java内存模型(JMM)的核心概念与底层机制,通过剖析其设计原理、内存可见性问题及其解决方案,结合具体代码示例,帮助读者构建对JMM的全面理解。不同于传统的摘要概述,我们将直接以故事化手法引入,让读者在轻松的情境中领略JMM的精髓。 ####
85 6
|
1月前
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
4月前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
71 0
|
1月前
|
存储 设计模式 监控
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
|
2月前
|
安全 Linux 开发工具
【Azure 环境】Azure 虚拟机上部署 DeepSeek R1 模型教程(1.5B参数)【失败】
遇见错误一:operator torchvision::nms does not exist 遇见错误二:RuntimeError: Failed to infer device type
320 22
|
2月前
|
存储 算法 Java
JVM: 内存、类与垃圾
分代收集算法将内存分为新生代和老年代,分别使用不同的垃圾回收算法。新生代对象使用复制算法,老年代对象使用标记-清除或标记-整理算法。
38 6