JVM的内存模型

一、JVM内存结构

JVM 内存共分为虚拟机栈,堆,方法区,程序计数器,本地方法栈五个部分。JVM的内存模型和类加载机制是分不开的 JVM类加载机制

JVM的内存模型

二、JVM内存区域功能

  1. 堆内存
    堆内存是JVM内存模型中最大的一块区域,被所有线程共享,是在JVM启动时候进行创建的。几乎所有的对象的空间分配都是在堆内存上进行分配的。

    考虑到JVM的内存回收机制,堆内存可以划分为新生代和老年代两个区域(默认新生代与老年代的空间大小为1:2)。新生代可以再划分为Eden区、From Survivor区和To Survivor区(三者比例为8:1:1)。几乎所有的新对象的创建都是在Eden区进行的。在垃圾回收(GC)过程中,Eden中的活跃对象会被转移到Survivor区,当再到达一定的年龄(经历Minor GC15次),会被转移到老年代中。

    堆可以处于物理上不连续的内存空间中,但是需要满足逻辑上的连续。在实现时,可以实现成固定大小的, 也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的

  2. 方法区
    方法区又被成为永久代(HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区),同样也是被所有的线程共享的。该区域中主要保存类的信息、静态变量、常量和即时编译器编译后的代码等数据。
    JDK1.7中,已经把放在永久代的字符串常量池移到堆中。JDK1.8撤销永久代,引入元数据区域。

    方法区不需要连续的内存,可以选择固定大小或者可扩展。并且还可以选择不实现垃圾收集。
    相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
    JVM的内存模型

    老年代:2/3的堆空间
    年轻代:1/3的堆空间
    eden区:8/10 的年轻代
    survivor0: 1/10 的年轻代
    survivor1:1/10的年轻代

  3. Java栈
    java栈是线程私有的内存区域,其中存储的是栈帧。对于一个java的方法,其开始调用,则会创建一个栈帧,保存到java栈中;当该方法执行完成,则对应的是出栈的过程。

    每个栈帧中,保存执行对应方法所必须的信息,主要包含局部变量表,操作数栈,动态链接和方法出口信息。
    JVM的内存模型

  • 局部变量表:
    局部变量表中,可以存放的数据有8种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用和returnAddress类型。其中long和double因为是64位,会占用两个局部变量的空间。

    在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度(比如递归调用的时候),将抛出*Error异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

  • 操作数栈
    后进先出LIFO,最大深度由编译期确定。栈帧刚建立使,操作数栈为空,执行方法操作时,操作数栈用于存放JVM从局部变量表复制的常量或者变量,提供提取,及结果入栈,也用于存放调用方法需要的参数及接受方法返回的结果。
    操作数栈可以存放一个jvm中定义的任意数据类型的值。
    在任意时刻,操作数栈都一个固定的栈深度,基本类型除了long、double占用两个深度,其它占用一个深度

  • 动态连接:
    每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如final、static域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。

  • 方法返回地址:
    当一个方法被执行后,有两种方式退出该方法:执行引擎遇到了任意一个方法返回的字节码指令或遇到了异常,并且该异常没有在方法体内得到处理。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。
    方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。

  1. 本地方法栈
    本地方法栈也是线程私有的内存区域,与java栈比较相似,不同之处在于该区域主要是保存Native方法相关的数据。Native方法是非Java语言编写的方法。

    与虚拟机栈一样,本地方法栈区域也会抛出*Error和OutOfMemoryError异常。

  2. 程序计数器
    程序计数器是用于标识当前线程执行的字节码文件的行号指示器。多线程情况下,每个线程都具有各自独立的程序计数器,所以该区域是非线程共享的内存区域。

    当执行java方法时候,计数器中保存的是字节码文件的行号;当执行Native方法时,计数器的值为空。

三、JVM内存参数设置

JVM的内存模型

  • -Xms设置堆的最小空间大小,默认是物理内存的1/64。
  • -Xmx设置堆的最大空间大小,默认是物理内存的1/4。
  • -XX:NewSize设置新生代最小空间大小。
  • -XX:MaxNewSize设置新生代最大空间大小。
  • -XX:PermSize设置永久代最小空间大小。
  • -XX:MaxPermSize设置永久代最大空间大小。
  • -Xss设置每个线程的堆栈大小。