JVM内存划分

2017年04月20日

jvm 内存使用划分

方法区

方法区和Java堆一样是各线程共享的内存区域。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

运行时常量池

运行时常量池是方法区的一部分,Class文件除了有类的版本,字段,方法,接口等各种描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载进入方法区的运行时常量池存放。有些常量也可在运行时放入(例如String类的intern()方法,可以将字符串放入常量池,JDK1.7之后已经不会再将实例复制到常量池,而是再常量池中记录首次出现的实例引用)。

    String str1 = new StringBuilder("计算机").append("软件").toString();
    System.out.println(str1.intern() == str1);

    String str2 = new StringBuilder("ja").append("va").toString();
    System.out.println(str2.intern() == str2);

    在1.6中的执行结果为false,false。
    在JDK1.7及以后的版本的执行结果,为 true,false, 第二个返回false,是因为“java”在 StringBuilder.toString()调用前已经出现过了。

虚拟机栈

虚拟机栈和程序计数器一样也是线程私有的,它的生命周期与线程相同。每个方法执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。

java虚拟机中对这个区域规定了两种异常情况,如果线程请求的栈深度大雨虚拟机所允许的深度,将跑出 StackOverflowError 异常;如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的。它们的区别不过是虚拟机栈为执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。有些虚拟机(例如 Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一了。和虚拟机栈一样也会抛出 StackOverflowError 和 OutOfMemoryError。

对大多数应用来说,Java堆是Java虚拟机所管理的内存最大的一块,Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,几乎所有的对象实例都在这里分配。

程序计数器

是一块较小的内存区域,可以看作是当前线程所执行的字节码的行号指示器。每条线程都有一个独立的程序计数器,线程之间互不影响。独立存储,可称这类内存为“线程私有”的内存。如果正在执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器为空(undefined),此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

直接内存

除了上面提到的内存区域划分,还有一块叫 直接内存 的内存区域,它并不是虚拟机运行时的数据区的一部分,也不是虚拟机规范中定义的内存区域。在JDK1.4中假如的NIO,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。在一些场景中能显著提高性能,因为它避免了在Java堆和Native堆中来回复制数据。(如果忽略直接内存,根据实际内存设置 -Xmx等参数,可能会是的各个内存区域总和大于物理内存限制。)

对象内存分配的方式

  • 指针碰撞:Java堆中的内存是规整的。则分配内存只需要将空闲内存的记录指针向空闲空间移动与对象大小相等的距离即可。
  • 空闲列表:Java堆中的内存是不规整的。需要一个列表来记录哪些内存是可用以分配的。

内存分配并发问题的处理方式

  • 使用 CAS 加上失败重试的方式保证更新操作的原子性。
  • 使用 TLAB(Thread Local Allocation Buffer)即每个线程预先分配一小块内存,对象的内存分配都在TLAB上进行,只有在TLAB空间不足,需要重新分配时,才需要同步锁定(可通过 -XX:/-UseTLAB 参数来设定)

对象的内存布局

对象头

对象头中有两部分信息,第一个部分用户存储对象自身的运行数据,例如哈希码,GC分代年龄,锁状态标志,线程持有锁,偏向线程ID,偏向时间戳等(在32和64位虚拟机中分别位32bit和64bit)。另外一部分是类型指针,即对象指向它的类元数据的指针。虚拟机通过这个指针来确定这个对象是哪个类的实例。(如果对象是数组,对象头中还必须有一块用于记录数据长度的数据)

实例数据

这一部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。无论是从父类继承下来的,还是子类中定义的,都需要记录下来。

对齐填充

对齐填充并不是必然存在的,也没有特别的含义。仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说,对象的大小必须是8字节的整数倍,而对象头部分正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问定位

  • 直接指针:最大的好处就是速度更快。(Sun HotSpot 中使用直接指针进行对象访问)

  • 句柄 :好处是refernce中存储的是稳定的句柄地址,在对象被移动(例如GC时)时只会改变句柄的实例数据指针,而reference无需修改。

内存泄漏 与 内存溢出

虚拟机栈和本地方法栈溢出

HotSpot 不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,虽然 -Xoss 参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只有 -Xss 参数来设定。(默认大小为 1M)

直接内存溢出

使用 -XX:MaxDirectMemorySize指定,如果不指定,默认与 -Xmx 指定的Java堆最大值一样。

由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常。如果发现OOM之后的Dump文件很小,而程序中又直接或者间接的使用了NIO,那就可以考虑检查一下是不是这方面的原因了。