JVM-Java虚拟机内存模型

简介: Java内存模型在1.8之前和1.8之后略有不同,也就是运行时数据区域,请看如下图:

Java内存模型在1.8之前和1.8之后略有不同,也就是运行时数据区域,请看如下图:

运行时数据区域

Java1.6:

JDK1.8

正如上图所示:Java内存模型可以简要分为两种:
线程私有的:

  • 虚拟机栈
  • 本地方法栈-Native Method Stack
  • 程序计数器-Program Counter Register

线程共享的:

  • 堆-Heap
堆可以是连续空间,也可以不是连续空间,同时也可以固定大小,也可以在运行时扩展;并且虚拟机的实现者可使用任何的垃圾回收算法管理堆,甚至完全不进行垃圾收集也是可以的
  • 方法区-Method Area
  • 直接内存-Direct Memory
程序计数器

程序计数器是一个较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令;分支、循环、跳转、异常处理、线程恢复等功能都需要这个计数器来协助完成。
还有就是为了线程切换后能恢复到正确的位置,每个线程都有自己的独立程序计数器,各个线程之间程序计数器互不影响,独立存储。所以我们称这类内存区域为线程私有的内存。
综上所述:
程序计数器主要有两大作用:

  1. 字节码解析器通过改变程序计数器来依次执行指令,从而实现代码的流程控制,如:顺序执行、选择、跳转、异常处理等等
  2. 在多线程情况先,每个线程拥有自己独立的程序计数器,并由程序计数器记录当前线程执行的位置,从而可以使当前线程切换回来后可以知道上次运行的位置

TIP:
程序计数器是唯一不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的消亡而消亡

Java虚拟机栈

Java虚拟机栈和程序计数器一样也是线程私有的,Java虚拟机栈也可称为栈、Java栈,同样是随着线程的创建二创建随着线程的消亡而消亡。
Java栈可以称得上是JVM运行时数据区域的一个核心。因为除了一些Native方法是通过本地方法栈实现的,其它的所有Java方法都是通过Java栈来实现的。但是也是需要其它的运行时内存区域的配合比如程序计数器。
通过方法调用的数据都需要通过Java栈来进行传递,每一次方法调用都会有一个对应的栈帧压入栈中,每一个方法调用结束后都有一个栈帧弹出。

栈的组成

每一个栈都是由一个个栈帧组成,栈帧里又拥有局部变量表、操作数栈、动态链接、方法返回地址。它的结构和我们学习的数据结构中的栈比较类似,都是先进后出,只支持入栈出栈

局部变量表:主要存放编译器各种可知的各种数据类型(boolean、float、int、double、byte、char、short、long)、对象引用(reference,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其它与此对象相关的位置)
操作数栈:主要是作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果,另外计算过程中产生的临时变量也会存在操作数栈中
动态链接:主要是服务一个方法需要调用其它方法,在Java源文件编译成字节码文件时,所有的方法和变量都作为符号引用 (Symbilic Reference) 保存在Class文件的常量池中,当一个方法调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接就是为了将符号引用转化为调用方法的直接引用。

栈的空间虽然是无限的,但是一般正常情况下调用时没有问题的。不过如果函数陷入了无限循环的话,就会导致被压入太多栈帧而导致占用太多空间,从而导致栈空间过深。如果当前请求的栈深度超过当前Java虚拟机的最大深度的时候就会抛出StackOverFlowError
Tip:
Java方法有两种返回方式:

  • 正常的return返回
  • 抛出异常

以上不管哪种放回方式都会导致栈帧弹出。也就是说栈帧随着方法的调用而创建,随着方法的结束而销毁,无论是正常完成还是异常完成都算方法结束
除了上述提到的StackOverFlowError错误之外,栈还有可能发生OutOfMemoryError 错误,这是因为栈的内存大小可以动态扩展,如果虚拟机在动态扩展时却无法申请到足够的内存空间,则会抛出OutOfMemoryError的异常
综上所述:
栈可能会出现两种错误

  1. StackOverflowError:若栈的内存空间不允许动态扩展,那么当前线程请求栈的深度如果超过Java虚拟机栈的最大深度,则会抛出StackOverflow的错误
  2. OutOfMemoryError:如果栈的内存大小可以动态扩展,如果Java虚拟机栈在动态扩展内存时无法申请到足够的内存空间,则会抛出OutOfMemoryError的错误

参考书籍:《深入理解Java虚拟机》-第三版
图片.png

本地方法栈

和虚拟机栈所发挥的作用非常类似,区别是:虚拟机栈为虚拟机执行Java方法服务(也就是字节码服务),而本地方法栈则为虚拟机使用到的Native方法服务在HotSpot虚拟机中和Java虚拟机栈合二为一。
本地方法栈执行的时候,在本地方法栈也会创建一个栈帧,用于存放本地方法的局部变量表,操作数栈、动态链接、方法返回地址。方法执行完毕后相应的栈帧也会弹出并释放内存空间,同时也会出现StackOverflowError和OutOfMemoryError两种错误

Java虚拟机所管理的内存中最大的一块,Java堆使所有线程共享的一块内存区域,在Java虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例和数组都在这里分配内存。
Java世界中几乎所有的对象都在堆中分配,但是随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配标量替换优化技术会导致一些微妙的变化,所有的对象都分配到栈上也没有那么绝对了。从JDK1.7开始就已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
Java堆是垃圾回收器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)。从垃圾回收的角度:由于现在的收集器基本都是采用分代垃圾收集算法,所以Java堆还可细分为:新生代和老年代;再细致一点还有Eden、Survivor、Old等空间,进一步划分的目的是更好的回收内存,或者说是更快的回收内存。
在JDK版本1.7和JDK版本1.7之前堆主要分为:

  • 新生代内存(Young Generation)
  • 老年代(Old Generation)
  • 永久代(Permanent Generation)

具体如图所示(图中的Eden区、两个Survivor区 S0、S1)都属于新生代,中间一层属于老年代,最下面一层属于永久代。

JDK 8 版本后就移除了PermGen(永久)使用MetaSpace(元空间)所替代元空间使用的是直接内存
关于JVM是如何动态计算年龄的大致如下:

Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累计,当累计的某个年龄大小超过Survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升阈年龄值
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
    //survivor_capacity是survivor空间的大小
size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);
size_t total = 0;
uint age = 1;
while (age < table_size) {
total += sizes[age];//sizes数组是每个年龄段对象大小
if (total > desired_survivor_size) break;
age++;
}
uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
    ...
}

堆这里最容易出现的错误是OutOfMemoryError,并且出现这种错误的表现形式还有几种比如:

  • java.lang.OutOfMemoryError: GC Overhead Limit Exceeded,当JVM花费太多时间来执行垃圾回收,并且只能回收很小的堆空间时,就会发生此错误
  • java.lang.OutOfMemoryError: Java heap space假如在创建新对象时,堆的内存空间不足以存放该新对象时,就会发生次错误。(和配置的最大栈内存有关,并且受制于物理内存的大小,最大堆内存可通过参数-Xmx配置,若没有特别配置,则使用默认的配置),这个默认值目前我本人并没有在哪本书籍上看到,或者是我忘记了。可参考文章:默认的堆大小
  • 还有很多类似的表现就不以一举例了
方法区

方法区是JVM运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。
《Java虚拟机规范》只是规定了方法区的概念和它的作用,方法区是如何实现的就要看Java虚拟机它自己的实现了,换句话说就是在不同的Java虚拟机上,方法区的实现方式是有可能不同的。
当虚拟机要使用一个类的时候,它需要读取并解析Class文件获取的相关信息,再将信息存入方法区。方法区会存储已被虚拟机加载的类信息、字段信息、方法信息、常量、静态变量、即使编译器编译后的代码缓存等数据。
那么问题来了方法区和永久代、元空间有什么关系呢?
其实三者之间的关系很像Java类中的接口和实现类,类实现接口;方法区比较像接口,而永久代和元空间更像是方法区的具体实现(这里指的是Hotspot虚拟机对方法区的两种实现方式)。并且永久代是JDK1.8之前的实现方式,JDK1.8之后由元空间所代替。

至于为什么永久代(PermGen)会被元空间(MetaSpace)所替代呢?在《深入理解Java虚拟机中》3版有下面这段话:
图片.png

  • 关于永久代有一个JVM本身设置的固定大小上限,它是无法进行调整的;但是元空间使用的是直接内存,这意味着元空间只受物理内存空间大小的限制,即使它也有可能会出现内存溢出的情况,但是几率要相对小很多
  • 元空间内存溢出会出现如下错误:
java.lang.OutOfMemoryError: MetaSpace
  • 元空间的大小

    • 关于元空间的大小可以通过参数-XX: MetaSpaceSize 来设置元空间的最大限制,默认是unlimited意味着只受系统内存空间的限制;-XX: MetaSpaceSize参数 定义了元空间的初始大小,如果未指定该参数,则元空间(MetaSpace)则会在运行时的应用程序动态调整大小。
  • 元空间存储的数据

    • 元空间存放的是类的元数据,如果未指定参数 --XX: MetaSpaceSize的大小,那么加载多少类的元数据就不由参数MetaSpaceSize来控制了,就由系统实际可用的内存空间来限制了,其实这样能够加载类的元数据相比较会更多一些。
  • 在JDK 8,合并Hotspot和JRockit的代码时,JRockit压根也没有一个永久代的概念,合并之后就没必要额外的设置一个永久代的地方了,
方法区常用的参数
  • JDK1.8 之前永久代还没有移除的时候通常通过以下参数来进行调解:
# 方法区 永久代的初始大小
-XX:PermSize=N
# 方法区 永久代的最大大小 ,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
-XX:MaxPermSize=N

相对而言垃圾的收集行为在该区域是比较少出现的,但是并不意味着数据进入方法区就永久存在了。
JDK1.8之后,永久代就被移除了(其实在JDK1.7就已经开始了),取而代之的是元空间。

  • 元空间参数调节:
# 设置元空间(MetaSpace)的初始大小(和最小的大小)
-XX:MetaSpaceSize:N
# 设置元空间(MetaSpace)的最大的大小
-XX:MaxMetaSpaceSize:N

元空间与永久代不同就是在于:如果不指定大小的话,随着创建的类越来越多,最后可能后导致系统内存的耗尽。

运行时常量池

Class文件除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量、符号引用的常量池表
字面量在源码中是固定值的表示法,简单来说就是通过字面量我们就知道其值的含义。字面量主要包括整数、浮点数和字符串字面量,符号引用包括类符号引用、字段符号引用、方法符号引用和接口方法符号引用。
常量池会在类加载后存放到方法区的运行时区常量池。
运行时常量池的功能类似于传统编程语言的符号表,尽管它包含了比常规的符号表更宽泛的数据
既然运行时常量池是方法区的一部分,自然而然也收到内存的限制,当常量池无法申请到内存时也会抛出OutOfMemoryError的错误。

字符串常量池

字符串常量池是JVM为了提高性能减少内存消耗专门为字符串(String类)开辟的一块内存区域,主要目的是为了防止字符串的重复创建。

// 在堆中创建字符串对象"paidaxing"
// 将字符串对象"paidaxing"的引用保存在字符串常量池中
String a = "paidaxing";
// 直接返回字符串常量池中字符串对象"paidaxing"的引用
String b = "paidaxing";
System.out.println(a==b);// true

Hotspot常量池的具体实现是:src/hotspot/share/classfile/stringTable.cpp,StringTable实际上就是一个HashSet,容量为StringTableSize。可以通过参数-XX:StringTableSize来设置。
StringTable保存的是字符串对象的引用,字符串的引用指向堆中的字符串的对象。
说到字符串那么就会有一个面试题,字符串是保存在哪里的?先说总结:
在JDK1.7之前字符串常量池是保存在永久代的,JDK1.7及1.7之后字符串常量池和静态变量是保存在Java堆中的。
如图所示:



问题来了为什么JDK1.7要将字符串常量池移到堆中呢?
主要原因就是永久代(PermGen)-【方法区的实现】的GC的效率太低了,只有在整堆收集(也就是Full GC)的时候才会被执行GC,Java通常情况下会有大量被创建的字符串需要被回收,将字符串常量池存放到堆中,能够提高GC的回收效率,及时回收字符串的内存。
比较好的问题:

直接内存

直接内存并不是虚拟机运行时数据区域的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁的使用。也有可能导致OutOfMemoryError的错误。
在Java1.4中新加入的NIO(New Input/Output类),引入了基于通道(Channel)和缓存区(Buffer)的I/O方式。它可以直接使用Native函数直接分配堆外内存,然后通过Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样在一些场景中就能显著的提高性能,避免了Java堆和Native堆之间来回复制数据。
本机直接内存的分配不会受到Java堆的限制,但是既然是内存总会收到系统本机内存以及处理器寻址空间的限制。

相关文章
|
5月前
|
安全 Java 应用服务中间件
Spring Boot + Java 21:内存减少 60%,启动速度提高 30% — 零代码
通过调整三个JVM和Spring Boot配置开关,无需重写代码即可显著优化Java应用性能:内存减少60%,启动速度提升30%。适用于所有在JVM上运行API的生产团队,低成本实现高效能。
592 3
|
6月前
|
存储 缓存 Java
Java数组全解析:一维、多维与内存模型
本文深入解析Java数组的内存布局与操作技巧,涵盖一维及多维数组的声明、初始化、内存模型,以及数组常见陷阱和性能优化。通过图文结合的方式帮助开发者彻底理解数组本质,并提供Arrays工具类的实用方法与面试高频问题解析,助你掌握数组核心知识,避免常见错误。
|
4月前
|
Java 大数据 Go
从混沌到秩序:Java共享内存模型如何通过显式约束驯服并发?
并发编程旨在混乱中建立秩序。本文对比Java共享内存模型与Golang消息传递模型,剖析显式同步与隐式因果的哲学差异,揭示happens-before等机制如何保障内存可见性与数据一致性,展现两大范式的深层分野。(238字)
123 4
|
4月前
|
存储 缓存 Java
我们来说一说 JVM 的内存模型
我是小假 期待与你的下一次相遇 ~
371 5
|
4月前
|
存储 缓存 Java
【深入浅出】揭秘Java内存模型(JMM):并发编程的基石
本文深入解析Java内存模型(JMM),揭示synchronized与volatile的底层原理,剖析主内存与工作内存、可见性、有序性等核心概念,助你理解并发编程三大难题及Happens-Before、内存屏障等解决方案,掌握多线程编程基石。
|
5月前
|
缓存 监控 Kubernetes
Java虚拟机内存溢出(Java Heap Space)问题处理方案
综上所述, 解决Java Heap Space溢出需从多角度综合施策; 包括但不限于配置调整、代码审查与优化以及系统设计层面改进; 同样也不能忽视运行期监控与预警设置之重要性; 及早发现潜在风险点并采取相应补救手段至关重要.
751 17
|
6月前
|
监控 Kubernetes Java
最新技术栈驱动的 Java 绿色计算与性能优化实操指南涵盖内存优化与能效提升实战技巧
本文介绍了基于Java 24+技术栈的绿色计算与性能优化实操指南。主要内容包括:1)JVM调优,如分代ZGC配置和结构化并发优化;2)代码级优化,包括向量API加速数据处理和零拷贝I/O;3)容器化环境优化,如K8s资源匹配和节能模式配置;4)监控分析工具使用。通过实践表明,这些优化能显著提升性能(响应时间降低40-60%)同时降低资源消耗(内存减少30-50%,CPU降低20-40%)和能耗(服务器功耗减少15-35%)。建议采用渐进式优化策略。
283 1
|
7月前
|
SQL 缓存 安全
深度理解 Java 内存模型:从并发基石到实践应用
本文深入解析 Java 内存模型(JMM),涵盖其在并发编程中的核心作用与实践应用。内容包括 JMM 解决的可见性、原子性和有序性问题,线程与内存的交互机制,volatile、synchronized 和 happens-before 等关键机制的使用,以及在单例模式、线程通信等场景中的实战案例。同时,还介绍了常见并发 Bug 的排查与解决方案,帮助开发者写出高效、线程安全的 Java 程序。
403 0
|
6月前
|
存储 监控 算法
Java垃圾回收机制(GC)与内存模型
本文主要讲述JVM的内存模型和基本调优机制。
|
8月前
|
Java 物联网 数据处理
Java Solon v3.2.0 史上最强性能优化版本发布 并发能力提升 700% 内存占用节省 50%
Java Solon v3.2.0 是一款性能卓越的后端开发框架,新版本并发性能提升700%,内存占用节省50%。本文将从核心特性(如事件驱动模型与内存优化)、技术方案示例(Web应用搭建与数据库集成)到实际应用案例(电商平台与物联网平台)全面解析其优势与使用方法。通过简单代码示例和真实场景展示,帮助开发者快速掌握并应用于项目中,大幅提升系统性能与资源利用率。
236 6
Java Solon v3.2.0 史上最强性能优化版本发布 并发能力提升 700% 内存占用节省 50%