JVM内存的那些事儿

2017-07-07  本文已影响0人  guqj

生活赋予我们一种巨大的和无限高贵的礼品,这就是青春:充满着力量,充满着期待志愿,充满着求知和斗争的志向,充满着希望信心和青春。 —— 奥斯特洛夫斯基

JVM内存简介

Java虚拟机在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间。Java虚拟机所管理的内存区域包括以下几个运行时数据区域,如下图所示:

JVM内存区域

程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  在多线程环境中,每个线程都有一个独立的程序计数器,各线程之间的计数器互不影响,独立存储,因此程序计数器是线程私有的。
  如果线程正在执行的是一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,这个计数器则为空(Undefined)。

Java虚拟机栈

Java虚拟机栈也是线程私有的,它和Java线程在同一时间创建,它保持方法的局部变量、部分结果,并参与方法的调用和返回。
  Java虚拟机栈规范允许Java栈的大小是动态是动态或者固定的。在Java虚拟机栈规范中,定义了两种异常与栈空间有关:
StackOverflowError和OutOfMemoryError。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如果虚拟机动态扩展时无法申请到足够内存时,将抛出OutOfMemoryError异常。
  在Hotspot虚拟机中,可以使用-Xss参数来设置栈的大小。栈的大小直接决定了函数调用的可达深度。
  以下示例展示了栈的溢出。

public class StackTest {
    private int count = 0;
    public void recursion() {
        count++;
        recursion();
    }
    @Test
    public void testStack() {
        try {
            recursion();
        } catch (Throwable e) {
            System.out.println("deep of stack is " + count);
            e.printStackTrace();
        }
    }
}

默认情况下,程序输出结果:

deep of stack is 18904
java.lang.StackOverflowError

使用参数-Xss2M再次执行程序,程序输出结果:

deep of stack is 42442
java.lang.StackOverflowError

很明显,栈的内存增大后,程序支持的函数调用深度也同时增大。

public class StackTest {
    private int count = 0;
    public void recursion(long a, long b, long c, long d) {
        long e = 0, f = 0, g = 0;
        count++;
        recursion(a, b, c, d);
    }

    @Test
    public void testStack() {
        try {
            recursion(1L, 2L, 3L, 4L);
        } catch (Throwable e) {
            System.out.println("deep of stack is " + count);
            e.printStackTrace();
        }
    }
}

同样使用参数-Xss2M执行程序,程序输出结果:

deep of stack is 21055
java.lang.StackOverflowError

随着参数和局部变量的增多,栈帧的空间也随之增大。(函数调用次数由无参时的42442降至21055)。

虚拟机栈在运行时使用栈帧的数据结构保存上下文数据。在栈帧中,存放了方法的局部变量表、操作数栈、动态连接方法和返回地址等信息。每一个方法的调用都伴随着栈帧的入栈操作,相应地,方法的返回则表示栈帧的出栈操作。方法调用时,方法参数和局部变量相对较多,那么局部变量表会比较大,栈帧会膨胀以满足需求,因此单个方法调用所需的栈空间大小也会比较多。

栈帧结构图如下:

栈帧结构

注意:对一个函数而言,它的参数越多,内部局部变量越多,它的栈帧就越大,其可达深度就越低。

动态连接.

本地方法栈

本地方法栈和虚拟机栈类似,两者之间的区别是本地方法栈为Native方法服务。

Java堆

Java堆是Java运行时内存中最为重要的部分,几乎所有的对象实例以及数组都都是在堆中分配空间的。Java堆是所有线程共享的内存区域。
Java堆分为新生代和老年代两部分,新生代用于存放刚刚产生的对象和年轻的对象,(大对象除外,直接进入老年代,因为大对象占用空间多,为了有足够空间容纳大对象,JVM不得不移动大量新生代中的年轻对象至老年代,这对GC来说是不利的,另外,若是由于内存空间紧张,JVM很可能不得不将部分年轻对象提前向老年代压缩),如果对象经历过N(该次数可通过参数配置,默认是15)次GC而未被回收,则会被移入老年代。
  新生代又可进一步分为eden、from space(s0)、to space(s1)(默认eden:s0:s1=8:1:1,该比例可配置)。eden,即对象的出生地,大部分对象刚建立时,都会存放在这里。s0和s1为survivor空间,直译为幸存者,也就是说存放在其中的对象,至少经历了一次垃圾回收,并得以幸存,如果在幸存区的对象到了指定年龄仍未被回收,则有机会进入老年代。

方法区

方法区(又称永久代)和Java堆一样,是所有线程共享的内存区域。方法区主要保存的是类的元数据。
  方法区中最为重要的是类的类型信息、常量池、域信息、方法信息。类型信息包括类的完整名称、父类的完整名称、类型修饰符(public/protected/private)和类型的直接接口类表。常量池包括这个类的方法、域等信息所引用的常量。域信息包括域名称、域类型和域修饰符。方法信息包括方法名称、返回类型、方法参数、方法修饰符、方法字节码、操作数栈和方法栈帧的局部变量区大小以及异常表。总之,方法区内保存的信息,大部分是来自于class文件。
  运行时常量池用于存放编译期间生成的各种字面常量(文本字符串、声明为final的常量值)和符号引用(类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符)。在JDK1.6中,常量池是方法区的一部分,在JDK1.7中,常量池存放在堆内存里,在JDK1.8中,常量池存放在MetaSpace里。
  目前1.8的HotSpot中,已经将方法区移除,取而代之的是MetaSpace。
  当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
  在HotSpot虚拟机中,在永久区中的对象,同样也是可以被回收的。对永久区GC的回收,主要从以下两个方面分析:一是GC对永久区的常量池的回收,二是永久区对类元数据的回收。

JVM内存分配参数

上一篇下一篇

猜你喜欢

热点阅读