JVM8内存模型

JVM内存模型包括堆、元空间、栈、本地方法栈和程序计数器。堆和元空间是线程共享,栈、本地方法栈、程序计数器是线程私有。JDK8用元空间替代了之前的永久代,元空间直接在本地内存分配,可通过参数设置其大小。堆是对象和数组的主要存储区域,分为新生代和老年代,新生代采用复制算法,当对象无法放入任一Survivor区时会进入老年代。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JVM内存模型

JVM内存模型分为堆(heap)、元空间、栈、本地方法栈、程序计数器。
JDK8的内存模型如下图:
JDK8的内存模型
       堆和元空间是线程共享的,在Java虚拟机中只有一个堆、一个元空间,并在JVM启动的时候就创建,JVM停止才销毁。
       栈、本地方法栈、程序计数器是每个线程私有的,随着线程的创建而创建,随着线程的结束而死亡。
在这里插入图片描述

1. 本地方法栈

提供虚拟机使用到的本地Native方法服务。

2. 程序计数器(Program Counter Register)

       每个线程在创建后,都会产生自己的程序计数器和栈。程序计数器是一块较小的内存空间。由于CPU时间片轮限制,众多线程在并发执行过程中,处理器只会执行某个线程中的一条指令,这样必然涉及线程的切换。
       程序计数器用来存放下一个执行指令的行号。线程恢复要依赖程序计数器。此区域不会发生内存溢出异常。

3. 线程栈(Stack)

       JVM中的线程栈是描述Java方法执行的内存区域,它是线程私有的。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

栈帧存储着局部变量、操作数栈、动态链接、方法出口等信息,局部变量存储方法内的局部变量,操作数栈用来计算时临时存放变量,动态链接存放方法的元信息等,方法出口记录如哪个方法的本方法等。通过javap -c XX.class查看字节码文件。
在这里插入图片描述
       如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出stackoverflowError通常出现在递归方法中;
       如果虚拟机可以动态扩展,但是无法申请到足够的内存时,就会抛出outOfMemoryError异常。

4. 方法区(Method Area)

       方法区中存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。是JDK7之前存在的一个概念,这里仅做简单回顾,模型图如下:
在这里插入图片描述

       方法区与堆(Java Heap)一样,是各个线程共享的内存区域。

  • 运行时常量池
           运行时常量池是方法区的一部分,存放着class文件元信息描述,编译后的代码数据,引用类型数据,类文件常量池。
           JDK1.7之前运行时常量池是方法区的一部分,JDK1.7及之后版本已经将运行时常量池从方法区中移了出来,开辟了一块区域Metaspace(元空间)存放运行时常量池,注意字符串常量池移至堆中。

  • PermGen(永久代)
           绝大部分 Java 程序员应该都见过 “java.lang.OutOfMemoryError: PermGen space “这个异常。这里的 “PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别,前者是 JVM 内存回收的规范,而后者则是 JVM 规范的一种实现,使用永久代来实现方法区而已。这样HotSpot的垃圾收集器就能像管理Java堆一样管理这部分内存。简单点说就是HotSpot虚拟机中内存模型的分代,其中新生代和老生代在堆中,永久代使用方法区实现。
           类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出,容易产生Perm区的OOM。比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多的类,经常出现致命错误:Exception in thread ‘dubbo client x.x connector' java.lang.OutOfMemoryError: PermGenspac,为解决该问题,需要设定运行参数-XX:MaxPermSize= l280m,如果部署到新机器上,往往会因为JVM参数没有修改导致故障再现。不熟悉此应用的人排查问题时往往苦不堪言。除此之外,永久代会为 GC 带来不必要的复杂度,并且回收效率偏低;字符串存在永久代中,容易出现性能问题和内存溢出。

5. 元空间(Metaspace)

       JDK8使用元空间替换永久代,元空间的本质和永久代类似,都是对JVM规范中方法区的实现。之前永久代的内容:类元信息、字段、静态属性、方法、常量,还有运行时常量池等都移动至元空间,但是字符串常量移至堆内存。
       元空间在本地内存中分配,它并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,它直接从操作系统中分配,因此不受Java堆大小的限制,但是会受到本机总内存的大小及处理器寻址空间的限制,因此它也可能导致OutOfMemoryError异常出现。“元空间”的大小可以动态调整,通过以下参数来指定元空间大小:
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
元空间详解

6. 堆

       Heap存储着几乎所有的对象及数组,JVM8中把静态变量(字符串常量池)也移到堆区进行存储。

       堆是OOM故障最主要的发源地,也是是垃圾回收的主要区域,所以也被称为GC堆。通常情况下,它占用的空间是所有内存区域中最大的,但如果无节制地创建大量对象,也容易消耗完所有的空间。堆的内存空间既可以固定大小,也可运行时动态地调整,通过如下参数设定初始值和最大值,比如-Xms256M. -Xmx1024M。其中-X表示它是JVM运行参数,ms是memorystart初始堆容量的简称 ,mx是memory max最大堆容量的简称。但是在通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,势必形成不必要的系统压力,所以在线上生产环境中,JVM的Xms和Xmx设置成一样大小,避免在GC后调整堆大小时带来的额外压力。

       堆分成两大块:新生代和老年代,对象产生之初在新生代,步入暮年时进入老年代。
       新生代又分为1个Eden区+ 2个Survivor区,8:1:1的比例。
       绝大部分对象在Eden(意为伊甸园)区生成,当Eden区装填满的时候,会触发Young GC。垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到Survivor(幸存者)区,这个区真是名副其实的存在。Survivor 区分为S0和S1两块内存空间,送到哪块空间呢?每次Young GC的时候,将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。
       如果Young GC要移送的对象大于Survivor区容量上限,或者超大对象的阈值超过eden分配担保设置值的上限,则直接移交给老年代.如果老年代也无法放下,则会触发Full Garbage Collection(Full GC),如果依然无法放下,则抛OOM.。
       假如一些没有进取心的对象以为可以一直在新生代的Survivor区交换来交换去,那就错了。每个对象都有一个计数器,每次Young GC都会加1。-XX:MaxTenuringThreshold参数能配置计数器的值到达某个阈值的时候,对象从新生代晋升至老年代。默认值是15,可以在Survivor 区交换14次之后,晋升至老年代。

在这里插入图片描述

       堆出现OOM的概率是所有内存耗尽异常中最高的。出错时的堆内信息对解决问题非常有帮助,所以给JVM设置运行参数-XX:+HeapDumpOnOutOfMemoryError,让JVM遇到OOM异常时能输出堆内信息,使用-XX:HeapDumpPath参数指定dump路径。利用JVM参数-XX:OnOutOfMemoryError可以在发生OOM异常时,运行一个本机的脚本或指令。

jVM内存模型
内存回收

<think>好的,用户现在想详细了解JDK8JVM内存模型结构和工作原理。我需要结合之前提供的资料和用户的引用内容来组织回答。首先,用户之前已经问过JVM内存模型,我回答过,现在用户进一步聚焦在JDK8版本上。 首先,用户给出的引用有四个,其中引用[1]提到了JDK8内存模型结构,包括堆、方法区(元空间)、虚拟机栈等。引用[3]和[4]详细说明了JDK8中方法区的变化,即永久代被元空间替代,以及运行时常量池的位置。引用[2]和[4]讨论了虚拟机栈的结构和异常情况。 需要确保回答的结构清晰,按照用户之前示例的分块方式,比如结构划分、工作原理、核心机制等。同时,用户可能希望了解JDK8相较于之前版本的变化,特别是元空间的部分,这部分在引用[3]中提到了元空间使用直接内存,不受JVM参数限制,但仍有OOM的可能。 接下来,要检查是否需要覆盖所有内存区域,包括堆、方法区、虚拟机栈、本地方法栈和程序计数器。每个部分需要简要说明其作用、特点以及JDK8中的变化。例如,方法区在JDK8中由元空间实现,而元空间位于直接内存,这样调整的好处是避免永久代的OOM问题,但需要说明元空间仍然可能抛出OOM,当物理内存不足时。 然后,工作原理部分需要涉及内存分配流程、垃圾回收机制,特别是分代收集策略。需要提到新生代和老年代的划分,以及对应的GC算法,比如新生代的复制算法,老年代的标记-整理或清除算法。这部分可以结合引用[3]中提到的TLAB来优化并发分配,引用[3]和用户之前的回答都提到了TLAB的作用,需要解释清楚。 另外,用户可能对参数配置感兴趣,比如如何调整元空间大小(-XX:MetaspaceSize),或者堆内存的各个部分参数(-Xmx, -Xms),以及栈大小的设置(-Xss)。这些参数在引用[1]和[2]中都有提及,需要整合到回答中。 还需要注意用户的问题中提到的“工作原理”,所以需要描述对象创建、内存分配、垃圾回收的整体流程。例如,对象优先在Eden区分配,大对象直接进入老年代,以及GC触发条件等。 最后,检查是否遗漏了用户引用中的关键点,比如运行时常量池的位置变化,在JDK8中,运行时常量池是否还在方法区?根据引用[3],运行时常量池是方法区的一部分,但方法区由元空间实现,所以运行时常量池现在位于元空间。而字符串常量池在JDK7时已经移到堆中,这点需要明确区分,避免混淆。 可能还需要提到直接内存,虽然不属于JVM内存模型的一部分,但元空间使用直接内存,所以需要简要说明。此外,GC策略方面,可以提到G1垃圾回收器在JDK8中的使用情况,但用户的问题可能更关注内存模型结构,所以GC部分可以简要带过。 在组织内容时,要确保使用正确的格式,行内公式用$...$,独立公式用$$...$$,代码块用```,引用标记正确。例如,引用[3]和[4]关于元空间和虚拟机栈的部分需要正确标注。 最后生成相关问题,要基于用户之前的问题和当前内容,可能涉及分代的原因、参数调整、TLAB机制、元空间优化、OOM场景等。确保问题覆盖回答中的关键点,帮助用户深入理解。</think>### JDK8 JVM内存模型深度解析 结合JVM规范与JDK8实现特性,其内存模型在保留经典架构基础上进行了重大优化,以下从结构划分、核心机制、工作原理三个维度展开说明: --- #### 一、内存结构划分(JDK8核心组件) $$ \begin{array}{|c|c|c|c|} \hline \text{区域名称} & \text{线程属性} & \text{存储内容} & \text{关键参数} \\ \hline 堆(Heap) & 共享 & 对象实例、数组 & -Xmx/-Xms \\ \hline 元空间(Metaspace) & 共享 & 类信息、运行时常量池 & -XX:MetaspaceSize \\ \hline 虚拟机栈(VM Stack) & 私有 & 栈帧(局部变量表/操作数栈) & -Xss \\ \hline 本地方法栈 & 私有 & Native方法调用 & - \\ \hline 程序计数器 & 私有 & 执行指令地址 & - \\ \hline \end{array} $$ 1. **堆(Heap)** - **分区结构**: - 新生代(Young Generation):Eden区(80%)+ Survivor区(From/To各10%) - 老年代(Old Generation):长期存活对象(默认15次GC存活) - **分配策略**: - 新对象优先在Eden区通过$指针碰撞$分配(内存规整时) - 大对象(如长数组)直接进入老年代(避免复制开销) 2. **元空间(Metaspace)** - **核心变化**:取代JDK7的永久代,使用本地内存(Native Memory)存储类元数据[^3] - **优势**: - 自动扩展空间,避免永久代的$OutOfMemoryError: PermGen$ - 内存上限受物理内存限制,但仍可能触发$OutOfMemoryError$ 3. **虚拟机栈(VM Stack)** - **栈帧结构**: - 局部变量表:存储基本类型(如$int\ a=1$)和对象引用(如$Object\ obj$) - 操作数栈:执行字节码指令的工作区(如加法操作$iadd$) - **异常机制**: - 固定栈容量时,深度超限触发$StackOverflowError$(如无限递归)[^4] - 动态扩展失败时触发$OutOfMemoryError$ --- #### 二、核心工作机制 1. **对象生命周期管理** ```text [类加载] → [Eden区分配] → [Minor GC存活→Survivor区] → [年龄阈值→晋升老年代] → [Major GC回收] ``` - **TLAB优化**:每个线程在Eden区预分配私有内存块(Thread Local Allocation Buffer),避免多线程竞争 2. **方法区动态扩展** - 元空间初始大小由`-XX:MetaspaceSize`指定,默认根据系统内存自动调整 - 类卸载时释放元数据(需满足类加载器、类实例、Class对象均不可达) 3. **内存回收机制** - **新生代GC(Minor GC)**: - 触发条件:Eden区满时 - 算法:复制算法(存活对象从Eden+Survivor From复制到Survivor To) - **老年代GC(Major GC/Full GC)**: - 触发条件:老年代空间不足或方法区满 - 算法:标记-清除(产生碎片)或标记-整理(内存规整) --- #### 三、JDK8关键改进 1. **字符串常量池迁移**: - JDK7已将字符串常量池从方法区移至堆内存,JDK8延续此设计 - 示例:`String s = "abc"`实际存储在堆中 2. **元空间内存监控**: - 通过`jstat -gcmetacapacity <pid>`查看元空间使用量 - 内存泄漏特征:元空间占用持续增长且无类卸载 --- #### 四、典型配置示例 ```bash # 设置堆内存(初始4GB/最大4GB) -Xms4G -Xmx4G # 设置元空间初始大小256MB(默认无上限) -XX:MetaspaceSize=256M # 设置单个线程栈大小1MB -Xss1M ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值