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、方法区主要存放虚拟机加载的类相关数据。
目录
相关文章
|
4月前
|
安全 Java 应用服务中间件
Spring Boot + Java 21:内存减少 60%,启动速度提高 30% — 零代码
通过调整三个JVM和Spring Boot配置开关,无需重写代码即可显著优化Java应用性能:内存减少60%,启动速度提升30%。适用于所有在JVM上运行API的生产团队,低成本实现高效能。
456 3
|
5月前
|
存储 缓存 Java
Java数组全解析:一维、多维与内存模型
本文深入解析Java数组的内存布局与操作技巧,涵盖一维及多维数组的声明、初始化、内存模型,以及数组常见陷阱和性能优化。通过图文结合的方式帮助开发者彻底理解数组本质,并提供Arrays工具类的实用方法与面试高频问题解析,助你掌握数组核心知识,避免常见错误。
|
3月前
|
Java 大数据 Go
从混沌到秩序:Java共享内存模型如何通过显式约束驯服并发?
并发编程旨在混乱中建立秩序。本文对比Java共享内存模型与Golang消息传递模型,剖析显式同步与隐式因果的哲学差异,揭示happens-before等机制如何保障内存可见性与数据一致性,展现两大范式的深层分野。(238字)
108 4
|
3月前
|
存储 缓存 Java
我们来说一说 JVM 的内存模型
我是小假 期待与你的下一次相遇 ~
314 5
|
3月前
|
存储 缓存 Java
【深入浅出】揭秘Java内存模型(JMM):并发编程的基石
本文深入解析Java内存模型(JMM),揭示synchronized与volatile的底层原理,剖析主内存与工作内存、可见性、有序性等核心概念,助你理解并发编程三大难题及Happens-Before、内存屏障等解决方案,掌握多线程编程基石。
|
3月前
|
存储 缓存 算法
深入理解JVM《JVM内存区域详解 - 世界的基石》
Java代码从编译到执行需经javac编译为.class字节码,再由JVM加载运行。JVM内存分为线程私有(程序计数器、虚拟机栈、本地方法栈)和线程共享(堆、方法区)区域,其中堆是GC主战场,方法区在JDK 8+演变为使用本地内存的元空间,直接内存则用于提升NIO性能,但可能引发OOM。
|
4月前
|
缓存 监控 Kubernetes
Java虚拟机内存溢出(Java Heap Space)问题处理方案
综上所述, 解决Java Heap Space溢出需从多角度综合施策; 包括但不限于配置调整、代码审查与优化以及系统设计层面改进; 同样也不能忽视运行期监控与预警设置之重要性; 及早发现潜在风险点并采取相应补救手段至关重要.
648 17
|
5月前
|
监控 Kubernetes Java
最新技术栈驱动的 Java 绿色计算与性能优化实操指南涵盖内存优化与能效提升实战技巧
本文介绍了基于Java 24+技术栈的绿色计算与性能优化实操指南。主要内容包括:1)JVM调优,如分代ZGC配置和结构化并发优化;2)代码级优化,包括向量API加速数据处理和零拷贝I/O;3)容器化环境优化,如K8s资源匹配和节能模式配置;4)监控分析工具使用。通过实践表明,这些优化能显著提升性能(响应时间降低40-60%)同时降低资源消耗(内存减少30-50%,CPU降低20-40%)和能耗(服务器功耗减少15-35%)。建议采用渐进式优化策略。
257 1
|
5月前
|
人工智能 边缘计算 自然语言处理
普通电脑也能跑AI:10个8GB内存的小型本地LLM模型推荐
随着模型量化技术的发展,大语言模型(LLM)如今可在低配置设备上高效运行。本文介绍本地部署LLM的核心技术、主流工具及十大轻量级模型,探讨如何在8GB内存环境下实现高性能AI推理,涵盖数据隐私、成本控制与部署灵活性等优势。
2707 0
普通电脑也能跑AI:10个8GB内存的小型本地LLM模型推荐
|
6月前
|
SQL 缓存 安全
深度理解 Java 内存模型:从并发基石到实践应用
本文深入解析 Java 内存模型(JMM),涵盖其在并发编程中的核心作用与实践应用。内容包括 JMM 解决的可见性、原子性和有序性问题,线程与内存的交互机制,volatile、synchronized 和 happens-before 等关键机制的使用,以及在单例模式、线程通信等场景中的实战案例。同时,还介绍了常见并发 Bug 的排查与解决方案,帮助开发者写出高效、线程安全的 Java 程序。
346 0