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、方法区主要存放虚拟机加载的类相关数据。
目录
相关文章
|
26天前
|
存储 缓存 安全
Java内存模型深度解析:从理论到实践####
【10月更文挑战第21天】 本文深入探讨了Java内存模型(JMM)的核心概念与底层机制,通过剖析其设计原理、内存可见性问题及其解决方案,结合具体代码示例,帮助读者构建对JMM的全面理解。不同于传统的摘要概述,我们将直接以故事化手法引入,让读者在轻松的情境中领略JMM的精髓。 ####
33 6
|
27天前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
211 1
|
16天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
21 0
|
1月前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
7天前
|
人工智能 物联网 C语言
SVDQuant:MIT 推出的扩散模型后训练的量化技术,能够将模型的权重和激活值量化至4位,减少内存占用并加速推理过程
SVDQuant是由MIT研究团队推出的扩散模型后训练量化技术,通过将模型的权重和激活值量化至4位,显著减少了内存占用并加速了推理过程。该技术引入了高精度的低秩分支来吸收量化过程中的异常值,支持多种架构,并能无缝集成低秩适配器(LoRAs),为资源受限设备上的大型扩散模型部署提供了有效的解决方案。
33 5
SVDQuant:MIT 推出的扩散模型后训练的量化技术,能够将模型的权重和激活值量化至4位,减少内存占用并加速推理过程
|
16天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
19天前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
|
25天前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
26天前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
21 3
|
29天前
|
Java
Java内存模型
JMM(Java内存模型 )屏蔽了各种硬件和操作系统的内存访问差异,实现让Java程序在各平台下都能达到一致的内存访问效果,它定义了JVM如何将程序中的变量在主存中读取 具体定义为:所有变量都存在主存中,主存是线程共享区域;每个线程都有自己独有的工作内存,线程想要操作变量必须从主从中copy变量到自己的工作区,每个线程的工作内存是相互隔离的 由于主存与工作内存之间有读写延迟,且读写不是原子性操作,所以会有线程安全问题
下一篇
DataWorks