Java JVM内存模型

简介: 运行时的数据区(Runtime Data Area)1. 虚拟机栈(Stack) 栈帧 局部变量表 操作数栈 动态链接 返回地址2. 本地方法栈(Native Method Stack)3. 程序计数器(Program Counter Register)4.堆(Heap) Ecen区 Survivor区 Old区5. 方法区内存溢出与内存泄漏 堆溢出 内存泄露 内存溢出

运行时的数据区(Runtime Data Area)


       本文主要看 JVM 内存模型主要指运行时的数据区,包括 5 个部分,如下图所示。    

微信图片_20220523170220.png


  虚拟机栈:也叫方法栈,是线程私有的,线程在执行每个方法时,JVM 都会在虚拟机栈中创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息。调用方法时执行入栈,方法返回时执行出栈。


     本地方法栈:与栈类似,也是用来保存线程执行方法时的信息,不同的是,执行 Java 方法使用栈,而执行 native 方法使用本地方法栈。


     程序计数器:保存着当前线程所执行的字节码位置,每个线程工作时都有一个独立的计数器。程序计数器为执行 Java 方法服务,执行 native 方法时,程序计数器为空(Undefined)。


栈、本地方法栈、程序计数器这三个部分都是线程独占的。


  堆:是 JVM 管理的内存中最大的一块,堆被所有线程共享,目的是为了存放对象实例,几乎所有的对象实例都在这里分配。当堆内存没有可用的空间时,会抛出 OOM 异常。根据对象存活的周期不同,JVM 把堆内存进行分代管理,由垃圾回收器来进行对象的回收管理。


  方法区:也是各个线程共享的内存区域,又叫非堆区。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,JDK 1.7 中的永久代和 JDK 1.8 中的 Metaspace 都是方法区的一种实现。


注意:面试回答这个问题时,要答出两个要点:一个是各部分的功能,另一个是哪些线程共享,哪些独占。


下面咱们详细来了解这五个部分。


1. 虚拟机栈(Stack)


       虚拟机栈也是线程私有的,与线程的生命周期同步。在 Java 虚拟机规范中,对这个区域规定了两种异常状况:


StackOverflowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出。


OutOfMemoryError:当 Java 虚拟机动态扩展到无法申请足够内存时抛出。


       在我们学习 Java 虚拟机的的过程当中,经常会看到一句话:


JVM 基于栈,而 Dalvik(DVM) 基于寄存器。


这里的"基于栈"指的就是虚拟机栈。虚拟机栈的初衷是用来描述 Java 方法执行的内存模型,线程在执行每个方法时,JVM 都会在虚拟机栈中创建一个栈帧,下面咱们来看看这个栈帧?

 

栈帧


       栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,每一个线程在执行某个方法时,都会为这个方法创建一个栈帧。


       我们可以这样理解:一个线程包含多个栈帧,而每个栈帧内部包含局部变量表、操作数栈、动态连接、返回地址等。


微信图片_20220523170559.png


局部变量表


       局部变量表是变量值的存储空间,我们调用方法时传递的参数,以及在方法内部创建的局部变量都保存在局部变量表中。在 Java 编译成 class 文件的时候,就会在方法的 Code 属性表中的 max_locals 数据项中,确定该方法需要分配的最大局部变量表的容量。


注意:系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值),也就是说不存在类变量那样的准备阶段。


操作数栈


       操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。


       同局部变量表一样,操作数栈的最大深度也在编译的时候写入方法的Code属性表中的max_stacks数据项中。栈中的元素可以是任意Java数据类型,包括long和double。


       当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的。在方法执行的过程中,会有各种字节码指令被压入和弹出操作数栈(比如:iadd指令就是将操作数栈中栈顶的两个元素弹出,执行加法运算,并将结果重新压回到操作数栈中)。


动态链接


       动态链接的主要目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。


       在一个 class 文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其所在内存地址中的直接引用,而符号引用存在于方法区中。


       Java 虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的就是为了支持方法调用过程中的动态连接(Dynamic Linking)。


返回地址


当一个方法开始执行后,只有两种方式可以退出这个方法:


  • 正常退出:指方法中的代码正常完成,或者遇到任意一个方法返回的字节码指令(如return)并退出,没有抛出任何异常。


  • 异常退出:指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。


       无论当前方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行。而虚拟机栈中的“返回地址”就是用来帮助当前方法恢复它的上层方法执行状态。


       一般来说,方法正常退出时,调用者的 PC 计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。


2. 本地方法栈(Native Method Stack)


       本地方法栈和上面介绍的虚拟栈基本相同,只不过是针对本地(native)方法。在开发中如果涉及 JNI 可能接触本地方法栈多一些,在有些虚拟机的实现中已经将两个合二为一了(比如HotSpot)。


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


       Java 程序是多线程的,CPU 可以在多个线程中分配执行时间片段。当某一个线程被 CPU 挂起时,需要记录代码已经执行到的位置,方便 CPU 重新执行此线程时,知道从哪行指令开始执行。这就是程序计数器的作用。


       "程序计数器"是虚拟机中一块较小的内存空间,主要用于记录当前线程执行的位置。


       实际上一些我们熟悉的恢复线程操作、分支操作、循环操作、跳转、异常处理等都需要依赖这个计数器来完成。


关于程序计数器还有几点需要格外注意:


在 Java 虚拟机规范中,对程序计数器这一区域没有规定任何 OutOfMemoryError 情况(或许是感觉没有必要吧)。


线程私有的,每条线程内部都有一个私有程序计数器。它的生命周期随着线程的创建而创建,随着线程的结束而死亡。


当一个线程正在执行一个 Java 方法的时候,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。


4.堆(Heap)


       Java 堆(Heap)是 JVM 所管理的内存中最大的一块,该区域唯一目的就是存放对象实例,几乎所有对象的实例都在堆里面分配,因此它也是 Java 垃圾收集器(GC)管理的主要区域,有时候也叫作"GC 堆"。


       同时它也是所有线程共享的内存区域,因此被分配在此区域的对象如果被多个线程访问的话,需要考虑线程安全问题。


       按照对象存储时间的不同,堆中的内存可以划分为新生代(Young)和老年代(Old),其中新生代又被划分为 Eden 和 Survivor 区。具体如下图所示:


微信图片_20220523171128.png


  • 堆空间=新生代(1/3)+老年代(2/3)


  • 新生代= Eden(8/10)+from(1/10)+to(1/10)


       图中不同的区域存放具有不同生命周期的对象。这样可以根据不同的区域使用不同的垃圾回收算法,从而更具有针对性,进而提高垃圾回收效率。


   

Ecen区


大多数情况下,对象会在新生代 Eden区中进行分配,当Eden区没有足够空间进行分配时,虚拟机会发起一次Minor GC,Minor GC 相比 Major GC更频繁,回收速度也更快。


通过Minor GC之后,Eden 会被清空,Eden区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到Survivor 的 From区(若 From区不够,则直接进入Old 区)。


Survivor区


       Survivor区相当于是 Eden区和 Old区的一个缓冲,类似于我们交通灯中的黄灯。


       Survivor 又分为2个区,一个是 From 区,一个是To 区。每次执行 Minor GC,会将Eden区和 From 存活的对象放到Survivor 的 To 区(如果To区不够,则直接进入Old 区)。


       Survivor 的存在意义就是减少被送到老年代的对象,进而减少Major GC的发生。Survivor的预筛选保 证,只有经历16次 Minor GC还能在新生代中存活的对象,才会被送到老年代。


Old区

       老年代占据着2/3的堆内存空间,只有在Major GC 的时候才会进行清理,每次GC都会触发"Stop-The-World"。


       内存越大,STW的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记-整理算法。


5. 方法区


       方法区(Method Area)也是 JVM 规范里规定的一块运行时数据区。方法区主要是存储已经被 JVM 加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码和数据。该区域同堆一样,也是被各个线程共享的内存区域。


注意:关于方法区,很多开发者会将其跟“永久区”混淆。


所以我在这里对这两个概念进行一下对比:


  • 方法区是 JVM 规范中规定的一块区域,但是并不是实际实现,切忌将规范跟实现混为一谈,不同的 JVM 厂商可以有不同版本的“方法区”的实现。


  • HotSpot 在 JDK 1.7 以前使用“永久区”(或者叫 Perm 区)来实现方法区,在 JDK 1.8 之后“永久区”就已经被移除了,取而代之的是一个叫作“元空间(metaspace)”的实现方式。


总结一下就是:


  • 方法区是规范层面的东西,规定了这一个区域要存放哪些数据。


  • 永久区或者是 metaspace 是对方法区的不同实现,是实现层面的东西。


内存溢出与内存泄漏


  • 内存泄漏:分配出去的内存回收不了
  • 内存溢出:指系统内存不够用了


堆溢出


       可以分为:内存泄漏和内存溢出,这两种情况都会抛出OutOfMemoryError异常。


内存泄露


       内存泄漏是指对象实例在新建和使用完毕后,仍然被引用,没能被垃圾回收释放,一直积累,直到没有剩余内存可用。如果内存泄露,我们要找出泄露的对象是怎么被GC ROOT引用起来,然后通过引用链来具体分析泄露的原因。


内存溢出


       内存溢出是指当我们新建一个实力对象时,实例对象所需占用的内存空间大于堆的可用空间。如果出现了内存溢出问题,这往往是程序本生需要的内存大于了我们给虚拟机配置的内存,这种情况下,我们可以采用调大-Xmx来解决这种问题。


栈溢出


       栈(JVM Stack)存放主要是栈帧( 局部变量表,操作数栈, 动态链接,方法出口信息)的地方。注意区分栈和栈帧:栈里包含栈帧。


与线程栈相关的内存异常有两个:


  • StackOverflowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出(方法调用层次太深,内存不够新建栈帧) 。
  • OutOfMemoryError:当 Java 虚拟机动态扩展到无法申请足够内存时抛出(线程太多,内存不够新建线程) 。


StackOverflowError


       递归调用是造成StackOverflowError的一个常见场景。因此当需要使用递归时,需要格外谨慎。


OutOfMemoryError


       理论上,虚拟机栈、堆、方法区都有发生OutOfMemoryError的可能。但是实际项目中,大多发生于堆当中。


小结


       千万不要将内存模型理解为虚拟机的"具体实现",虚拟机的具体实现有很多,比如 Sun 公司的 HotSpot、JRocket、IBM J9、以及我们非常熟悉的 Android Dalvik 和 ART 等。这些具体实现在符合上面 5 种运行时数据区的前提下,又各自有不同的实现方式。


       JVM 的运行时内存结构中一共有两个"栈"和一个"堆",分别是:Java 虚拟机栈和本地方法栈,以及"GC堆"和方法区。除此之外还有一个程序计数器(几乎不会用到这一部分,忽略)。


       JVM 内存中只有堆和方法区线程共享的数据区域,其它区域都是线程私有的。并且程序计数器是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。


相关推荐


Java JVM知识汇总

相关文章
|
13天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
21 0
|
16天前
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
25 0
|
15天前
|
存储 监控 算法
Java内存管理深度剖析:从垃圾收集到内存泄漏的全面指南####
本文深入探讨了Java虚拟机(JVM)中的内存管理机制,特别是垃圾收集(GC)的工作原理及其调优策略。不同于传统的摘要概述,本文将通过实际案例分析,揭示内存泄漏的根源与预防措施,为开发者提供实战中的优化建议,旨在帮助读者构建高效、稳定的Java应用。 ####
31 8
|
13天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
17天前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
45 5
|
15天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
15天前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
|
15天前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
19天前
|
机器学习/深度学习 监控 算法
Java虚拟机(JVM)的垃圾回收机制深度剖析####
本文深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法、性能调优策略及未来趋势。通过实例解析,为开发者提供优化Java应用性能的思路与方法。 ####
31 1
|
12天前
|
存储 监控 算法
Java内存管理的艺术:深入理解垃圾回收机制####
本文将引领读者探索Java虚拟机(JVM)中垃圾回收的奥秘,解析其背后的算法原理,通过实例揭示调优策略,旨在提升Java开发者对内存管理能力的认知,优化应用程序性能。 ####
27 0