JVM内存结构简介

Java运行时内存结构

+-------------------------------------------------+
|          Runtime Memory Structure Chart         |
|  +--------------+ | +----------+  +----------+  |
|  |              | | |          |  |  Native  |  |
|  |  MethodArea  | | | VM Stack |  |  Method  |  |
|  |   (PermGen)  | | |          |  |   Stack  |  |
|  +--------------+ | +----------+  +----------+  |
|  +--------------+ | +------------------------+  |
|  |              | | |                        |  |
|  |     Heap     | | |    Program Counter     |  |
|  |              | | |        Register        |  |
|  +--------------+   +------------------------+  |
+-------------------------------------------------+
  • 线程共享数据区域
    • Heap: 堆,是JVM最大的内存区。
    • Method Area(PermGen): 方法区,存储类、常量、静态变量等数据。在某些JVM的实现中也称持久代、永久代。

JDK8之后,PermGen被元空间(MetaSpace)替代

  • 线程之间相互独立区域
    • VM Stack: 虚拟机栈
    • Native Method Stack: 本地方法栈
    • Program Counter Register: 程序计数器

通过一张图来了解如何通过参数来控制各区域的内存大小: JVM Memory

堆(Heap)

对于大多数应用来说,java堆(Heap)是JVM管理的最大一块内存。Java堆是被所有线程共享的一块内存,在虚拟机启动时创建,主要用来存放对象实例。

堆也是垃圾收集器管理的主要区域,从内存回收的角度看,大部分收集器采用分代回收,所以Java的堆可以细分为:新生代、老生代。新生代可以分为Eden空间、From空间、To空间。

如果堆中没有内存完成实例分配,并且也无法扩展时将会抛出OutOfMemoryError异常。

+----------------------------------------+
|           Heap Structure               |
| +--------------------+ +-------------+ |
| |  Young Generation  | |             | |
| | +----------------+ | |             | |
| | |       Eden     | | |             | |
| | |      Space     | | |             | |
| | +----------------+ | |             | |
| | +----------------+ | | Old/Tenured | |
| | |    FromSpace   | | |  Generation | |
| | |   (Survivor1)  | | |             | |
| | +----------------+ | |             | |
| | +----------------+ | |             | |
| | |      ToSpace   | | |             | |
| | |   (Survivor2)  | | |             | |
| | +----------------+ | |             | |
| +--------------------+ +-------------+ |
+----------------------------------------+

相关参数设置:

参数名描述默认值备注
-Xms堆的初始值,设置示例:-Xms10m如果没有设置此值,默认值=分配的新生代值+分配的老生代的值。设置的值必须是1KB的倍数,且最小为1MB。等同于-XX:InitialHeapSize
-Xmx堆的最大值,设置示例:-Xmx1g根据运行时系统配置选择。等同于-XX:MaxHeapSize。设置的值必须是1KB的倍数,且最小为2MB。在最为服务器模式运行时,一般都设置-Xms等于-Xmx
-Xmn新生代的最大值,设置示例:-Xmn10m建议设置新生代的大小为整个堆大小的:1/41/2之间。等同于-XX:MaxNewSize
-XX:NewSize新生代的初始值
-XX:SurvivorRation用于设置Eden和其中一个Survivor的比值默认值为8,表示80%的为Eden,两个Survivor各占10%
-XX:MaxTenuringThreshold对象在新生代存活周期的阈值在并行收集器中默认为15,CMS收集器中默认为6最大值15
-XX:+PrintTenuringDistribution用于在Minor GC时打印Survivor中各个年龄段对象的占用空间大小
-XX:NewRatio用于设置老生代和新生代的比例默认为2,即1/3为新生代,2/3为老生代参数命名有些奇怪,实际计算公式: 比例值=老生代大小/新生代大小

新生代(Young Generation)

用来存放新的对象实例,垃圾收集器会频繁的在此区域工作,当新生代的Eden区满了之后,会触发Minor GCYoung GC。因此新生代设置过小会导致频繁的Minor GCYoung GC。如果设置过大,则只会在Full GC时才被执行,这会消耗较长的时间。

为了优化GC的性能,把新生代又细分成了EdenSurvivor1(from)Survivor2(to)三个区域。

Eden

存储新生的对象。一般新创建的对象都会被分配到Eden区中,某些对象会特殊处理。默认Eden占新生代80%的大小。

Eden区满了之后会触发Minor GC

Survivor

新生代中有两个Survivor区,一个标记为From,一个标记为TO,在GC开始时,被标记为TO的空间一定是空的。

Minor GCYoung GC发生时,Eden区中没有被引用(ref)的对象将被清除,需要存活的对象都会被复制到一个标记为TOSurvivor区中,From区中需要继续存活的对象会根据存活周期来决定去向,如果超过存活的周期来会被移动到老生代中,反之也会被复制到标记为TOSurvivor区中,如果TO被填满,则TO中所有的对象都会被移动到老生代中。GC完成之后,每个对象的生命周期年龄都会被加1EdenFrom都被清空,FROMTO也会互换角色,上一次的TO变成新的FROM,新的TO又将是一个空的区域。

老生代(Old Generation)

存放生命周期长的对象。也称为“老年代”。对老生代的垃圾回收称为Old GC,当老生代满了之后会触发此GC

注意

Old GC并不等同于Major GCFull GC,根据不同的GC的实现,它们所指的范围都不一样。

方法区(MethodArea/Perm Genration)

方法区称作“非堆(Non-Heap)”,用来存放类对象、常量、静态变量、即时编译后的代码数据。与Heap一样都属于线程共享。

在习惯在HotSpot虚拟机上开发和部署的程序员来说,很多人把它称为“永久代(Permanent Generation)”,平常所说的永久代也是指这个区域。

尽管这个区域被称为永久代,但有些垃圾收集器也会在此区域执行回收,这个区域的回收主要是常量池的回收、以及类型的卸载。JVM规范没对此区域的限制非常宽松,允许不对此区域实现垃圾收集。

当方法区无法满足内存分配需求时,将抛出OutOfMemoryError: PermGen异常。

此空间的调整参数:

参数名描述默认值备注
-XX:PermSize永久代内存初始值物理内存的1/64,例如:2G内存的机器初始值为32M
-XX:MaxPermSize永久代内存最大值物理内存的1/4,例如:2G内存的机器初始值为512M

元空间(MetaSpace)

JDK8开始,PermGen被元空间(MetaSpace)替代, PermGen被移除。

其实移除PermGen的工作从JDK7就开始了,但并没有完全移除,譬如类的静态变量、字面量(interned strings)都转移到了java heap中,符号引用转移到了native heap。

元空间的本质与PermGen类似,都是对JVM规范中方法区的实现。最大区别是元空间并不在虚拟机中,而是使用本地内存,因此元空间的大小受本地内存限制。

当方法区无法满足内存分配需求时,将抛出OutOfMemoryError: PermGen异常。

元空间的大小是JVM根据垃圾收集的结果来自动调整的。也可以通过如下参数来调整:

参数名描述默认值备注
-XX:MetaspaceSize元空间初始值在默认情况下,这个值大小根据不同的平台在12M到20M浮动该值越大触发Metaspace GC的时机就越晚,达到该值就会触发垃圾收集进行类型卸载。同时垃圾收集器会对该值进行调整:如果释放了大量空间,就会适当降低该值。如果释放了很少的空间,在不超过MaxMetaspaceSize的时,会适当提高该值。受本机最大可用内存限制,受32位与64位的JVM、操作系统限制
-XX:MaxMetaspaceSize元空间最大值无限制超过最大值时,将抛出OutOfMemoryError: PermGen异常。
-XX:MinMetaspaceFreeRatio元空间最小空闲占比NA当进行过元空间GC之后,如果当前元空间的空闲占比小于此值,则增长元空间的大小。此参数可以控制元空间的增长速度,如果该值过小会导致元空间的增长缓慢,可能会影响之后的类加载,如果该值过大会导致元空间增长过快,浪费内存。本机测试效果来看默认值在40左右,也就是40%
-XX:MaxMetaspaceFreeRatio元空间最大空闲占比NA当进行过元空间GC之后,如果当前元空间的空闲占比超过此值,则会释放部分元空间。本机测试效果来看默认值在70左右,也就是70%
-XX:MinMetaspaceExpansion元空间增长时的最小幅度NA在本机上该参数的默认值为340784B(大约330KB为)
-XX:MaxMetaspaceExpansion元空间增长时的最大幅度NA在本机上该参数的默认值为5452592B(大约为5MB)

为什么要将PermGen切换为Metaspace?

  1. 字符串存在永久代中,容易出现性能问题和内存溢出。
  2. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老生代溢出。
  3. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  4. Oracle 可能会将HotSpot 与 JRockit 合二为一。

程序计数器(Program Counter)

程序计数器也被称为“PC寄存器”。JVM支持多线程同时运行,每个线程都有自己独立且私有的程序计数器,占用空间极少,在线程创建时创建。

解释器通过它来获取下一条的字节码执行指令。如果执行的是java的方法,该程序计数器中保存的是当前执行指令的地址,如果是native方法,则该程序计数器中的值为undefined。

不会有OutOfMemoryError抛出。

栈(Stack)

虚拟机栈(VM Stack)

虚拟机栈所使用的空间也是线程私有的,以栈帧为单位进行压栈和出栈。

            当前线程                 线程2           线程n
+-----------------------------+   +------+        +------+
|           当前栈帧           |   |      |        |      |
|     Current Stack Frame     |   |      |        |      |
+-----------------------------+   |      |        |      |
| +-------------------------+ |   |      |        |      |
| |         局部变量         | |   |      |        |      |
| |   Local Variable Table  | |   |      |        |      |
| +-------------------------+ |   |      |        |      |
| +-------------------------+ |   |      |        |      |
| |         操作数栈         | |   |      |        |      |
| |      Operand Stack      | |   |      |        |      |
| +-------------------------+ |   |      |        |      |
| +-------------------------+ |   |      |        |      |
| |         动态连接         | |   |      |        |      |
| |     Dynamic Linking     | |   |      |        |      |
| +-------------------------+ |   |      |        |      |
| +-------------------------+ |   |      |        |      |
| |         返回地址         | |   |      |        |      |
| |      Return Address     | |   |      | ...... |      |
| +-------------------------+ |   |      |        |      |
| +-------------------------+ |   |      |        |      |
| |         附加信息         | |   |      |        |      |
| |      Additional Info    | |   |      |        |      |
| +-------------------------+ |   |      |        |      |
|           ......            |   |      |        |      |
+-----------------------------+   |      |        |      |
|                             |   |      |        |      |
|         Stack Frame n       |   |      |        |      |
+-----------------------------+   |      |        |      |
|                             |   |      |        |      |
|         Stack Frame 2       |   |      |        |      |
+-----------------------------+   |      |        |      |
|                             |   |      |        |      |
|         Stack Frame 1       |   |      |        |      |
+-----------------------------+   +------+        +------+
  • 局部变量表

    每一个方法都拥有一块属于自己的内存区域来保存方法内部定义的局部变量,这块区域就是局部变量表,当这个方法运行结束后,这个局部变量的生命周期也就宣告结束。我们平常工作中所指的栈,实际上指的是虚拟机栈中的栈帧中的局部变量表。

  • 操作数栈

    每个方法的内部都可以计算数据,而计算数据势必需要拥有一块内存区域,为虚拟机用来进行数值计算。因此在栈帧中,就需要有一块区域专门为当前方法计算数据使用,它就是操作数栈。

    在每进行一次完整的计算之后,栈中的数据都已经出栈,所以操作数栈的空间在一个方法内部是可以反复使用的。所以虚拟机在分配内存大小时,只分配当前方法,单次完整计算所需要的最大内存空间给当前栈帧,以减少内存的消耗。

    同时为了增加运行效率,减少数据的不断复制,在大部分虚拟机的实现中,将当前方法的局部变量表和上层方法的操作数栈的内存形成部分重叠,从而减少参数的不断复制而引起的性能消费。

  • 动态连接

    虚拟机在执行方法时有两种形式被用来确定执行指令所对应的方法,第一种是类加载时,可以直接确定要执行的方法,譬如静态方法,私有方法,final方法等。这种形式叫做静态解析。第二种是在真正运行时,根据对象的真实引用来判断当前真正要执行的方法,这种形式称之为动态连接。

    在字节码文件中,都存在一个常量池,在这个常量池中保存有大量的符号引用,这个符号引用是每一个方法的间接引用。在字节码指令的中,使用的是这个符号引用。但是在运行时阶段,肯定需要调用到要执行方法在内存中真实的地址。这就需要将间接引用转化成直接引用。而这里的“动态连接”就是为了保证在运行时阶段,方法可以正确的找到要调用的方法,每个栈帧将自己在运行时常量池中所对应的真实地址记录的位置。

    这里需要注意的是,在栈帧中的动态连接和查找符号引用为真实引用中的动态连接,是两个概念。前者表示的是一个区域,后者表示的是一种查找方式。

  • 返回地址

    退出当前方法的方式有两种,第一种是遇到返回指令时,正常的退出当前方法。另一种形式是遇到没有捕获而被抛出的异常。无论何种返回形式,在方法退出后,栈帧的顶端都应是当前退出方法的上层方法。同时上层方法的执行状态也需要根据当前的返回结果重新调整。所以每个栈帧可以利用“返回地址”这块区域帮助上层方法恢复状态。

  • 附加信息

    对于虚拟机规范中没有申明的,拥有指定存放位置的信息可以由各个虚拟机自己决定,放置到这个区域中。

有两种可能的异常抛出:StackOverflowErrorOutOfMemoryErrorStackOverflowError指的是内存中的栈结构在不断的入栈,最终导致栈的深度超过了虚拟机所允许的栈深度时,所抛出的错误

相关参数设置:

参数名描述默认值备注
-Xss线程栈大小,设置示例:-Xss320k不同的平台默认值不同。32位环境一般为320kb,64位环境一般为1024kb。此参数等同于XX:ThreadStackSize

本地方法栈(Native Method Stack)

在虚拟机中,不但运行java方法,还会运行本地方法,也就是常见的native关键字修饰的方法。本地方法运行所使用的空间就是本地方法栈,其也是线程私有的。

它的作用跟虚拟机栈基本相似,其区别就是一个为java方法服务,一个为Native发光法服务。在虚拟机规范中,对于本地方法栈中的结构、方法的语言、方式,都没有强制规定,各个虚拟机可以自由的实现它。

直接内存(Direct Memory)

这块内存不属于运行时数据区,所以不受JVM堆大小的限制。

从Jdk1.4开始,NIO(new I/O)变可以直接使用Native函数直接分配这块内存。使用Java堆中的DirectByteBuffer对象作为这块内存的引用。

在使用NIO的应用中,配置虚拟机参数需要考虑到这块内存的大小分配,申请不到内存时也会抛出OutOfMemoryError

相关参数设置:

参数名描述默认值备注
-XX:MaxDirectMemorySize最大直接内存值,设置示例:-XX:MaxDirectMemorySize=10m默认情况下,大小设置为0,这意味着JVM将自动分配和扩展。

关于GC

针对HotSpot VM的实现,GC的分类只有两大种:

  • Partial GC: 局部GC
    • Young GC: 只收集新生代
    • Old GC: 只收集老生代,只有CMSconcurrent collection是这个模式
    • Mixed GC: 收集所有新生代以及部分老生代。只有G1才有此模式。
  • Full GC: 全量GC,收集整个堆(包括新生代老生代)、以及方法区(java8之前的PermGen, java8开始的metaspace)。

通常所说的Major GCFull GC是等价的。但由于HotSpot VM发展了这么多年,很对名词解读已经混乱,当有人说Major GC时,一点要问清楚他说的是Full GC还是Old GC

对于HotSpot VM的串行收集器(Serial GC)的实现来看,各GC场景的触发条件是:

  • Young GC: Eden区没有足够空间进行分配时触发;
  • Old GC: 老生代没有足够空间进行分配时触发;只有CMSconcurrent collection是这个模式
  • Full GC:
    • 方法区PermGenMetaspace没有足够空间进行分配时触发
    • 在准备触发Young GC时,如果发现之前Young GC移动到老生代的平均大小大于当前老生代剩余空间时,会取消Young GC转而触发Full GC (除CMSconcurrent collection之外,其它的针对老生代的回收一般都会包含对新生代的处理)
    • 程序调用System.gc()
    • HeapDump时带GC,默认也会触发

对于HotSpot VM的并行收集器(Parallel GC)的实现则不一样,以CMS为例,它会定时去检查老生代的是用量,超过一定的比例就会触发。

相关测试代码

堆(Heap)内存溢出测试代码

import java.util.ArrayList;
import java.util.List;

/**
 * 堆(Heap)内存溢出测试代码
 * 启动时添加如下参数可以观察GC日志:
 *  -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:-UseCompressedClassPointers
 * 添加如下参数设置JVM堆大小:
 *  -Xms16m -Xmn8m -Xmx16m
 */
public class HeapTest {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<byte[]>();
        int i = 0;
        boolean flag = true;
        while (flag){
            try {
                i++;
                list.add(new byte[1024 * 1024]);//每次增加一个1M大小的数组对象
            }catch (Throwable e){
                e.printStackTrace();
                flag = false;
                System.out.println("count=" + i);//记录运行的次数
            }
        }
    }

方法区(PermGen/Metaspace)内存溢出测试代码

import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.MethodVisitor;
import jdk.internal.org.objectweb.asm.Opcodes;

import java.util.ArrayList;
import java.util.List;

/**
 * 方法区(PermGen/Metaspace)内存溢出测试代码
 * 启动时添加如下参数可以观察GC日志:
 *  -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:-UseCompressedClassPointers
 */
public class MetaspaceTest extends ClassLoader {

    public static void main(String[] args) {
        // 类持有
        List<Class<?>> classes = new ArrayList<>();

        // 死循环不断的生成不同的类。
        for (int i = 1; i > 0; i++) {
            ClassWriter cw = new ClassWriter(0);
            // 定义一个类名称为Class{i},它的访问域为public,父类为java.lang.Object,不实现任何接口
            cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null,
                    "java/lang/Object", null);
            // 定义构造函数<init>方法
            MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
                    "()V", null, null);
            // 第一个指令为加载this
            mw.visitVarInsn(Opcodes.ALOAD, 0);
            // 第二个指令为调用父类Object的构造函数
            mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
                    "<init>", "()V", false);
            // 第三条指令为return
            mw.visitInsn(Opcodes.RETURN);
            mw.visitMaxs(1, 1);
            mw.visitEnd();

            MetaspaceTest test = new MetaspaceTest();
            byte[] code = cw.toByteArray();
            // 定义类
            Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);
            classes.add(exampleClass);
        }
    }
}

虚拟机栈(VM Stack)溢出测试源码

/**
 * 虚拟机栈(VM Stack)溢出测试源码
 * 本机测试大概在栈深度达到22217时会出现溢出,每次运行值存在一定偏差
 */
public class StackTest {
    private static int index = 1;

    public void call(){
        index++;
        call();
    }

    public static void main(String[] args) {
        StackTest mock = new StackTest();
        try {
            mock.call();
        }catch (Throwable e){
            System.out.println("Stack deep : " + index);
            e.printStackTrace();
        }
    }
}

字符串常量溢出测试源码

/**
 * 字符串常量溢出测试源码.
 * jvm 6中运行会抛出`OutOfMemoryError: PermGen space`
 * jvm 7和jvm 8中运行会抛出`OutOfMemoryError: Java heap space`
 */
public class StringTest {
    static String static_str = "xxxxxx";
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();

        // 以2的指数级不断的生成新的字符串
        while(true){
            String str = static_str + static_str;
            base = static_str;
            list.add(str.intern());
        }
    }
}

参考资料


   转载规则


《JVM内存结构简介》 Angus_Lu 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
RocketMQ Performance Test RocketMQ Performance Test
测试环境硬件配置4C 4G SSD操作系统centeOS 6.5MQ版本rocketmq-broker-4.2.0-incubating-SNAPSHOT (2017-08-23)测试程序运行机器:Macbook Pro i7 2.3GHz
2018-01-15 15:45:23
下一篇 
Hystrix 配置属性参考 Hystrix 配置属性参考
介绍Hystrix使用Archaius作为配置属性的默认实现。下面的文档描述了默认使用的HystrixPropertiesStrategy实现,你也可以使用插件的方式来覆盖它。每个属性有四个优先级:代码的全局默认值如果没有设置以下3个,则这
2018-01-04 10:39:38
  目录