美文网首页
JVM内存结构总结

JVM内存结构总结

作者: still_loving | 来源:发表于2018-06-24 12:15 被阅读0次

    前言

        在研读了许多关于JVM的相关博客以及论坛讨论内容后,这里针对JVM内存做一个初步的整理与总结,这里将内容分为JVM的内外两方面来分析总结内存的相关概念。

    概念

    首先明确以下几点概念:

        1、堆内内存:它是受Java虚拟机的内存管理所控制,对于Java开发者来说,它是透明的,不可控制的。开发者可以通过一些参数手段来进行相应的运行调控,具体调控方法这里不再赘述。

        2、堆外内存:有内就会有外,堆外内存指的是JVM以外的内存区域,这块区域的管理不受JVM控制,而是由操作系统直接管理,Java中NIO模块就是用于堆外内存的使用,具体情况后面详说。


    JVM内存

    一、内存结构

        因为Java版本的变更与迭代,JVM内部的结构也在跟着变化,这里主要以JDK1.6为主来讨论JVM内存结构,先看下面两张图:

    JVM组成结构 JVM内存区域

        首先JVM内部是有很多部分组成,具体各个组成的模块的含义不再具体讨论,可以自行百度百科,图二就是针对JVM中的内存区域之间特性以及功能做了划分。可以看到,实际Java内存可以分为两大区域,共享区(堆)和私有区(栈)。下面分别讨论堆区和栈区的一些主要的模块功能。

    二、栈区(Stack)

    2.1深入理解栈的组成:

            栈帧由三部分组成:局部变量区、操作数栈、帧数据区。局部变量区和操作数栈的大小要视对应的方法而定,他们是按字长计算的。但调用一个方法时,它从类型信息中得到此方法局部变量区和操作数栈大小,并据此分配栈内存,然后压入Java栈。

    2.2局部变量区:

            局部变量区被组织为以一个字长为单位、从0开始计数的数组,类型为short、byte和char的值在存入数组前要被转换成int值,而long和double在数组中占据连续的两项,在访问局部变量中的long或double时,只需取出连续两项的第一项的索引值即可,如某个long值在局部变量区中占据的索引时3、4项,取值时,指令只需取索引为3的long值即可。

    代码截图1 局部变量区内部情况

    2.3操作数栈:

            和局部变量区一样,操作数栈也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的。可把操作数栈理解为存储计算时,临时数据的存储区域。下面我们通过一段简短的程序片段外加一幅图片来了解下操作数栈的作用。

    操作数栈的执行情况

    2.4栈数据区

            除了局部变量区和操作数栈外,java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些数据都保存在java栈帧的帧数据区中。

            当JVM执行到需要常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问它。

             除了处理常量池解析外,帧里的数据还要处理java方法的正常结束和异常终止。如果是通过return正常结束,则当前栈帧从Java栈中弹出,恢复发起调用的方法的栈。如果方法又返回值,JVM会把返回值压入到发起调用方法的操作数栈。

            为了处理java方法中的异常情况,帧数据区还必须保存一个对此方法异常引用表的引用。当异常抛出时,JVM给catch块中的代码。如果没发现,方法立即终止,然后JVM用帧区数据的信息恢复发起调用的方法的帧。然后再发起调用方法的上下文重新抛出同样的异常。


    2.2虚拟机栈:

        它描述的是java 方法执行的内存模型:每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。生命周期与线程相同,是线程私有的。

    2.3本地方法栈:

        它与虚拟机栈类似,只是服务对象不同,虚拟机栈服务于虚拟机执行的Java方法,而本地方法栈服务于Native方法。

    2.4程序计数器

            是最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。

    三、堆区

    3.1Java堆

            也叫做GC堆,是java虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,在JVM启动时创建。该内存区域存放了对象实例及数组(所有new的对象)。其大小通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。

    Java堆内存结构

    说明:

    1、这里把永久代也算在了老年代中,毕竟其中存放的都是“年龄”比较大的对象

    2、新生代中的s0区和s1区之所以说大小相等且角色可以互换,这里跟Java的垃圾回收算法中的一种复制算法有关,具体各种垃圾回收算法不做详细讨论,这里简单说明以下复制算法在这里的应用:在对象扛过了Eden区的垃圾回收后,会进入s0区或s1区,这里具体进入哪个区由具体的JVM来决定,但是一段时间内只能使用s0区或s1区中的一个,这里暂时假设进入了s0区,那么一段时间后,s0区在进行垃圾回收的时候,复制算法会把此时s0区中的不会被回收的对象复制一份到s1区,然后直接释放s0的内存,这样就完成了一次复制算法的垃圾回收。然后就是反复如此操作,不停地在s0区和s1区之间转移,所以说s0区和s1区大小相等并且角色可以互换。

    3.2方法区:

            也称”永久代” 、“非堆”, 它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。

            运行时常量池:是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。

            注:在 JDK1.2 ~ JDK6 的实现中,HotSpot 使用永久代实现方法区;HotSpot 使用 GC 分代实现方法区带来了很大便利;但同时由于 GC 分代技术的影响,使之许多优秀的内存调试工具无法在 Oracle HotSpot之上运行,必须单独处理;并且 Oracle 同时收购了 BEA 和 Sun 公司,同时拥有 JRockit 和 HotSpot,在将 JRockit 许多优秀特性移植到 HotSpot 时由于 GC 分代技术遇到了种种困难,所以从 JDK7 开始 Oracle HotSpot 开始移除永久代。在 JDK8 中,永久代已完全被元空间(Meatspace)所取代。


    四、堆外内存

            由于在JVM内部的内存受到GC的管理,为开发者带来了方便的同时也带了性能的损耗问题,因为垃圾回收工作会导致其他工作的暂停(Stop the world);另外,堆内在flush到远程时,会先复制到直接内存(非堆内存),然后再发送,这些对性能都有一定的损耗。因此Java引入了堆外内存,它解除了JVM的GC回收控制问题,同时在flush到远程的时候,省去了复制步骤,可以直接进行发送效率较高。

            但是同样也带来了一些挑战:堆外内存的缺点就是内存难以控制,使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难。

            我们经常用java.nio.DirectByteBuffer对象进行堆外内存的管理和使用,它会在对象创建的时候就分配堆外内存。DirectByteBuffer类是在Java Heap外分配内存,对堆外内存的申请主要是通过成员变量unsafe来操作。具体构造方法在源码中查看。

    4.1使用DirectByteBuffer的注意事项

            java.nio.DirectByteBuffer对象在创建过程中会先通过Unsafe接口直接通过os::malloc来分配内存,然后将内存的起始地址和大小存到java.nio.DirectByteBuffer对象里,这样就可以直接操作这些内存。这些内存只有在DirectByteBuffer回收掉之后才有机会被回收,因此如果这些对象大部分都移到了old,但是一直没有触发CMS GC或者Full GC,那么悲剧将会发生,因为你的物理内存被他们耗尽了,因此为了避免这种悲剧的发生,通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc来做一次full gc,以此来回收掉没有被使用的堆外内存。

        具体使用情况分析,可以参考这篇博客:从0到1起步-跟我进入堆外内存的奇妙世界

    参考资料

    1. Java堆和栈看这篇就够

    2. Java虚拟机的内存组成以及堆内存介绍

    3. Java 内存之方法区和运行时常量池

    4. 知乎--关于堆、栈和堆栈的理解

    5. 从0到1起步-跟我进入堆外内存的奇妙世界

    相关文章

      网友评论

          本文标题:JVM内存结构总结

          本文链接:https://www.haomeiwen.com/subject/vqejyftx.html