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的内存模型,线程私有


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










目录
打赏
0
0
0
0
379
分享
相关文章
JVM实战—8.如何分析jstat统计来定位GC
本文详细介绍了使用jstat、jmap和jhat等工具分析JVM运行状况的方法,以及如何合理优化JVM性能。内容涵盖新生代与老年代对象增长速率、Young GC和Full GC的触发频率及耗时等关键指标的分析。通过模拟BI系统和计算系统的案例,展示了如何根据实际场景调整JVM参数以减少FGC频率,提升系统性能。最后汇总了常见问题及其解决方案,帮助开发者更好地理解和优化JVM运行状态。
JVM实战—8.如何分析jstat统计来定位GC
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
121 29
JVM简介—1.Java内存区域
JVM实战—2.JVM内存设置与对象分配流转
本文详细介绍了JVM内存管理的相关知识,包括:JVM内存划分原理、对象分配与流转、线上系统JVM内存设置、JVM参数优化、问题汇总。
JVM实战—2.JVM内存设置与对象分配流转
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
24天前
|
课时4:对象内存分析
接下来对对象实例化操作展开初步分析。在整个课程学习中,对象使用环节往往是最棘手的问题所在。
go的内存逃逸分析
内存逃逸分析是Go编译器在编译期间根据变量的类型和作用域,确定变量分配在堆上还是栈上的过程。如果变量需要分配在堆上,则称作内存逃逸。Go语言有自动内存管理(GC),开发者无需手动释放内存,但编译器需准确分配内存以优化性能。常见的内存逃逸场景包括返回局部变量的指针、使用`interface{}`动态类型、栈空间不足和闭包等。内存逃逸会影响性能,因为操作堆比栈慢,且增加GC压力。合理使用内存逃逸分析工具(如`-gcflags=-m`)有助于编写高效代码。
|
1月前
|
JVM: 内存、类与垃圾
分代收集算法将内存分为新生代和老年代,分别使用不同的垃圾回收算法。新生代对象使用复制算法,老年代对象使用标记-清除或标记-整理算法。
28 6
|
4月前
|
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
165 62
SVDQuant:MIT 推出的扩散模型后训练的量化技术,能够将模型的权重和激活值量化至4位,减少内存占用并加速推理过程
SVDQuant是由MIT研究团队推出的扩散模型后训练量化技术,通过将模型的权重和激活值量化至4位,显著减少了内存占用并加速了推理过程。该技术引入了高精度的低秩分支来吸收量化过程中的异常值,支持多种架构,并能无缝集成低秩适配器(LoRAs),为资源受限设备上的大型扩散模型部署提供了有效的解决方案。
171 5
SVDQuant:MIT 推出的扩散模型后训练的量化技术,能够将模型的权重和激活值量化至4位,减少内存占用并加速推理过程
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。

热门文章

最新文章