JVM技术之旅-了解分析内存布局区域

简介: JVM技术之旅-了解分析内存布局区域

image.png



前提概要


本文主要针对于JVM的内存布局以及相关的关联性和特性进行相关的分析,后续会针对于更加详细以及深入的分析文字作为补充。针对于每个“Java爱好者”,如果希望可以探究其本质,减少内心的疑惑,最好的办法就是研究底层的原理,而JVM的内存管理既是Java的独特魅力之处,又是开发人员 “未知的圣地”,接下来我们就慢慢迈入这个领域。



JVM内存布局


如果想要了解JVM的内存管理,那么首先我们要知道JVM内存都由什么组成,如下图所示

image.png

从图中 我们可以清晰的了解到相关的内存分布结构,发现整体体系里面主要由运行时数据区域和其他几个子系统组成,那我们就先来看看这个运行时数据区



运行时数据区域


JVM虚拟机在运行时java程序的时候,会把它所管理的内存划分成若干个不同的数据区域。其中jdk1.8前后版本有差别。



jdk1.6版本结构模型

image.png


jdk1.8版本结构模型


image.png


从上面的两个版本的细节图可以看出来,整体的运行时数据区主要可分为:


PC Register(程序计数器)、VM Stack(虚拟机栈)、Native Method Stack(本地方法栈)、Heap(堆)、Method Area(方法区)和Direct Memory(直接内存)。


  • 整个内存数据区域是属于当前进程的,当前进程拥有所有的资源和数据。而直接内存是所有进程共享的


  • 其中栈和程序计数器是线程私有的,也就是每一个线程拥有自己独立的区域。互相不干扰



程序计数器(PC Register)


代码在程序执行之前就被编译成字节码,而程序计数器不是我们计算机组成原理的程序计数器(存放的计算机指令地址),而JVM的PC是字节码解释器的指示器存放的是字节码的地址。基本上会指向方法区元数据以及对象的首地址进行计算


如果执行的是java方法,这里存储的就是正在执行的字节码的地址,如果执行的是本地方法存储的就是undefined。


字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。


在多线程的环境下,pc还能保证恢复到原来线程的位置。


注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。



Java虚拟机栈



描述的是Java方法执行的内存模型,每次方法调用的数据都是通过栈传递的,虚拟机栈是Java执行方法的内存模型。每个方法被执行的时候,都会创建一个栈帧,把栈帧压人栈,当方法正常返回或者抛出未捕获的异常时,栈帧就会出栈


栈帧:


  • 操作数栈(Operand Stack):操作变量的内存模型,操作数栈的最大深度在编译的时候已经确定(写入方法区code属性的max_stacks项中)。操作数栈的的元素可以是任意Java类型,包括long和double,32位数据占用栈空间为1,64位数据占用2。方法刚开始执行的时候,栈是空的,当方法执行过程中,各种字节码指令往栈中存取数据。


  • 动态链接(Dynamic Linking):栈帧都持有在运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接


  • 方法出口信息(return Adress 指向了一个字节码指令的地址):如果有返回值的话,压入调用者栈帧中的操作数栈中,并且把PC的值指向方法调用指令后面的一条指令地址


  • 局部变量表(Local Variable table):包含了方法执行过程中的所有变量。局部变量数组所需要的空间在编译期间完成分配,在方法运行期间不会改变局部变量数组的大小。(写入方法区code属性的max_locals项)


  • 程序计数器:指向当前线程正在执行的字节码指令,线程私有的。



各种基本数据类型和引用类型Java栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。在java方法中y有两钟返回方法:抛出异常和return语句。两种方式都回将栈帧弹出。



Java堆内存


Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。这段区域也是垃圾回收器经常光顾的地方。


Java 堆还可以细分为:新生代和老年代、永久代(方法区):再细致一点有:Eden 空间、From Survivor、To Survivor 空间(这三个都是新生代)



image.png

大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。



方法区


方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来


对于方法区的实现,不同虚拟机中策略也不同。以我们常用的HotSpot虚拟机为例,其设计团队使用永久带来实现方法区,并把GC的分代收集扩展至永久带。这样设计的好处就是能够省去专门为方法区编写内存管理的代码。但是在实际的场景中,这样的实现并不是一个好的方式,因为永久带有MAX上限,所以这样做会更容易遇到内存溢出问题


另外还需要注意的是在HotSpot虚拟机中永久带和堆虽然相互隔离,但是他们的物理内存是连续的。而且老年代和永久带的垃圾收集器进行了捆绑,因此无论谁满了都会触发永久带和老年的GC


在jdk1.8之后HotSpot虚拟机已经将方法区(永久带)移除,取而代之的就是元空间(移入直接内存)




运行时常量池


JDK1.8版本的JVM已经将字符串常量池从方法区中移了出来,在元数据空间(Metadataspace)中开辟了一块区域存放运行时常量池,用于存放类的信息、常量信息、常量池信息、包括类数据常量和数字常量。常用的反射就是从这个方法区里读取的类信息,此外heap堆中开辟了内存空间存放字符串常量池


image.png

image.png



对象的创建过程


image.png


Java8的运行时数据区域如图所示。永久代已经不见了踪影,多出来的是叫做元数据区的区域。元空间在1.8中不在与堆是连续的物理内存,而是改为使用本地内存(Native memory)。元空间使用本地内存也就意味着只要本地内存足够,就不会出现OOM的错误。默认情况下元空间大小是无限的,但是JVM同样提供了参数来控制它的使用


-XX:MetaspaceSize
    class metadata的初始空间配额,以bytes为单位,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当的降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize(如果设置了的话),适当的提高该值。
-XX:MaxMetaspaceSize
    可以为class metadata分配的最大空间。默认是没有限制的。
-XX:MinMetaspaceFreeRatio
    在GC之后,最小的Metaspace剩余空间容量的百分比,减少为class metadata分配空间导致的垃圾收集。
-XX:MaxMetaspaceFreeRatio
    在GC之后,最大的Metaspace剩余空间容量的百分比,减少为class metadata释放空间导致的垃圾收集。



  • (1)首先jvm遇到一个new的指令时,咱们的常量池去查询是否有这个类的符号引用(全限定类名),如果没有表示未加载解析过这个类,就需要加载解析,同时在常量池中添加符号引用,在方法区添加类的信息


  • (2)然后分配内存,分配的方式有两种(指针碰撞和空闲列表)


  • (3)内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值


  • (4)设置对象头,这个对象头哦存储的是:这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息和否启用偏向锁等


  • (5)执行init方法,把对象按照程序员的意愿进行初始化(也就是构造函数之类的开始初始化)


对象访问定位


(1)句柄方式:如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;


image.png


(2)直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。


image.png


这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。



image.png



内存溢出


  • StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈最大深度的时候,抛出StackOverFlowError错误


  • OutOfMemoryError:若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 错误



类加载系统


负责从文件系统或是网络中加载class信息,加载的信息存放在一个称之为方法区的内存空间




直接内存


直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但这部分也是被频繁的读写使用,也可能会导致OutOfMemoryError异常的出现。 Java的NIO中的allocateDirect方法是可以直接使用直接内存的,能显著的提高读写的速度。Java8的MetadataSpace元数据空间使用的就是对直接内存,之前的方法区还逻辑属于heap的一部分,采用的是jvm的内存空间。




本地方法栈


本地方法栈和Java栈不同之处在于,可以直接调用Java本地方法,即JDK中用native修饰的方法,调用本地native的内存模型,线程私有


虚拟机栈,本地方法栈以及程序计数器为线程隔离。方法区和堆是所有线程共享的数据区域。










相关文章
|
1月前
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
25天前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
208 1
|
5天前
|
人工智能 物联网 C语言
SVDQuant:MIT 推出的扩散模型后训练的量化技术,能够将模型的权重和激活值量化至4位,减少内存占用并加速推理过程
SVDQuant是由MIT研究团队推出的扩散模型后训练量化技术,通过将模型的权重和激活值量化至4位,显著减少了内存占用并加速了推理过程。该技术引入了高精度的低秩分支来吸收量化过程中的异常值,支持多种架构,并能无缝集成低秩适配器(LoRAs),为资源受限设备上的大型扩散模型部署提供了有效的解决方案。
33 5
SVDQuant:MIT 推出的扩散模型后训练的量化技术,能够将模型的权重和激活值量化至4位,减少内存占用并加速推理过程
|
18天前
|
存储 编译器 程序员
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
在C语言中,内存布局是程序运行时非常重要的概念。内存布局直接影响程序的性能、稳定性和安全性。理解C程序的内存布局,有助于编写更高效和可靠的代码。本文将详细介绍C程序的内存布局,包括代码段、数据段、堆、栈等部分,并提供相关的示例和应用。
30 5
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
|
12天前
|
机器学习/深度学习 人工智能 缓存
【AI系统】推理内存布局
本文介绍了CPU和GPU的基础内存知识,NCHWX内存排布格式,以及MNN推理引擎如何通过数据内存重新排布进行内核优化,特别是针对WinoGrad卷积计算的优化方法,通过NC4HW4数据格式重排,有效利用了SIMD指令集特性,减少了cache miss,提高了计算效率。
31 3
|
15天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
20天前
|
监控 算法 Java
jvm-48-java 变更导致压测应用性能下降,如何分析定位原因?
【11月更文挑战第17天】当JVM相关变更导致压测应用性能下降时,可通过检查变更内容(如JVM参数、Java版本、代码变更)、收集性能监控数据(使用JVM监控工具、应用性能监控工具、系统资源监控)、分析垃圾回收情况(GC日志分析、内存泄漏检查)、分析线程和锁(线程状态分析、锁竞争分析)及分析代码执行路径(使用代码性能分析工具、代码审查)等步骤来定位和解决问题。
|
20天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
49 1
|
22天前
|
JavaScript
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
39 3
|
24天前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80