JVM的基本结构
标签:JVM

运行时数据区域

Java虚拟机运行时数据区(绿色区域都是线程共享的区域,白色区域则是线程运行时独有的内存区域)

1. 程序计数器

可以看作当前线程所执行的行号指示器,每个线程有独立的PC寄存器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。

  1. 如果该方法不是Native的,那么PC寄存器就保存JVM正在执行的字节码指令地址
  2. 如果该方法是Native的,那么PC寄存器的值就为空(undefined)

此内存区域是唯一一个在java虚拟机规范中没有任何OutOfMemoryError情况的区域

2. Java虚拟机栈

它也是线程私有的,生命周期和线程相同。描述了Java方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧(Stack Frame)用于存储局部变量,操作数栈,动态链接,方法出口等信息,每一个方法的调用直到执行完成,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程

局部变量表存放了编译期可知的各种数据类型,对象引用和returnAddress(指向一条字节码指令的地址)

64位长度的long和double类型占用两个局部空间变量(Slot),其余数据占一个

在Java栈中保存的主要内容为栈帧,每一次函数调用对应着栈帧入栈,函数调用结束,栈帧出栈。当前正在执行的函数所对应的栈帧就是当前的帧(位于栈顶),它保存着当前函数的局部变量,中间运算结果等数据。

函数返回有两种方式,一是正常的return指令返回,二是发生异常了,不管哪种方式,都会导致栈帧被弹出。

当请求的栈深度大于最大可用的栈深度时,系统就会抛出StackOverflowError栈溢出错误

-Xss 用来指定线程最大的栈空间,也直接决定了函数调用的最大深度

2.1 局部变量表

函数嵌套调用的层次在很大程度上由栈的大小决定,栈越大,函数可以支持的嵌套调用次数就越多。

由于局部变量表在栈帧中,因此,如果函数的参数和局部变量过多的话,会使得局部变量表膨胀,从而每一次函数调用就会占用更多的栈空间。最终导致函数的嵌套调用次数减少。

2.2 操作数栈

操作数栈保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

2.3 帧数据区

除了局部变量表以外,Java栈帧还需要一些数据用来支持常量解析,正常返回和异常处理等。大部分Java字节码指令需要进行常量池访问,在帧数据区保存着访问常量池的指针,方便程序访问常量池。

此外,当函数返回或者出现异常时,虚拟机必须恢复调用者函数的栈帧,并让调用者函数继续执行下去,对于异常处理,虚拟机必须有一个异常处理表,方便在发生异常时找到处理异常的代码,因此异常处理表也是帧数据区中的一部分。

2.4 异常情况

  1. 如果线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常。
  2. 虚拟机内存可以扩展(当前大部分虚拟机可扩展,但也支持固定长度),如果扩展时无法申请到内存,抛出OutOfMemoryError异常

3. 本地方法栈

本地方法栈为Native方法服务,也会抛出StackOverflowError和OutOfMemoryError

4. Java堆

Java堆是Java虚拟机管理的内存最大的一块区域,被所有线程共享,在虚拟机启动时创建,唯一目的就是存放对象实例和数组

Java堆可以分为新生代老年代,再细致一点可以分为Eden区域From Survivor区域To Survivor区域

从内存分配来看,堆上还可以分配出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。

堆的内存空间不用连续,只要逻辑上是连续的就可以了。

4.1异常情况

如果堆上没有内存完成实例分配,将抛出OutOfMemoryError异常

5. 方法区

方法区是线程共享的区域,存放已被虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据

许多人把方法区又叫作永久代,但本质上并不等价,这样做,主要是HotStop虚拟机为了将GC的分代收集扩展至方法区,故把方法区称为永久代。

永久代(Permanent Generation)只有Hotspot才有,除了和堆一样不需要连续的内存以外,还可以选择不实现垃圾收集

5.1 运行时常量池

运行时常量池是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息就是常量池(Constant Pool Table):用于存放编译期产生的各种字面量和符号引用,这部分内容将在类加载进入方法区的运行时常量池存放,一般来说,除了存放Class文件中的符号引用外,也会把翻译过来的直接引用也存放在运行时常量池中

常量池具备动态性,Java语言不要求常量一定要在编译时产生,即不一定要在编译时就放入,运行期间可能将新的常量方法池中,比如String类的intern()方法

存在OutOfMemoryError异常,由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存。最典型的场景就是,在 Jsp 页面比较多的情况,容易出现永久代内存溢出。

5.2 Metaspace (元空间)

其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)和类的静态变量(class statics)转移到了Java Heap。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
  -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
  -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

6. 堆、方法区、栈的关系

示例代码

package com.liuyao;

/**
 * Created By liuyao on 2018/4/6 17:02.
 */
public class SimpleHeap {
    private int id;

    public SimpleHeap(int id) {
        this.id = id;
    }

    public void show(){
        System.out.println("My id is "+id);
    }

    public static void main(String[] args) {
        SimpleHeap s1=new SimpleHeap(1);
        SimpleHeap s2=new SimpleHeap(2);
        s1.show();
        s2.show();
    }
}

图解:

7. 直接内存

他不是虚拟机允许时的内存区域,也不是Java虚拟机规范定义的内存区域

JDK1.4引入的NIO(New Input/Output),引入通道(Channel)和缓冲区(Buffer)的方式,它可以直接使用Native函数库直接分配堆外内存,然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用操作。

7.1 测试读写

package com.liuyao;

import java.nio.ByteBuffer;

/**
 * Created By liuyao on 2018/4/6 17:17.
 */
public class AccessDirectBuffer {
    public void directAccess(){
        long starttime=System.currentTimeMillis();
        ByteBuffer b=ByteBuffer.allocateDirect(500);
        for (int i = 0; i < 1000000; i++) {
            for (int j = 0; j < 99; j++) {
                b.putInt(j);
            }
            b.flip();
            for (int j = 0; j < 99; j++) {
                b.getInt();
            }
            b.clear();
        }
        long endtime=System.currentTimeMillis();
        System.out.println("testDirectWrite:"+(endtime-starttime));
    }

    public void bufferAccess(){
        long starttime=System.currentTimeMillis();
        ByteBuffer b=ByteBuffer.allocate(500);
        for (int i = 0; i < 1000000; i++) {
            for (int j = 0; j < 99; j++) {
                b.putInt(j);
            }
            b.flip();
            for (int j = 0; j < 99; j++) {
                b.getInt();
            }
            b.clear();
        }
        long endtime=System.currentTimeMillis();
        System.out.println("testBufferWrite:"+(endtime-starttime));
    }

    public static void main(String[] args) {
        AccessDirectBuffer alloc=new AccessDirectBuffer();
        alloc.bufferAccess();
        alloc.directAccess();

        alloc.bufferAccess();
        alloc.directAccess();
    }
}

运行结果为:

testBufferWrite:177
testDirectWrite:107
testBufferWrite:330
testDirectWrite:128

7.2 测试申请内存

package com.liuyao;

import java.nio.ByteBuffer;

/**
 * Created By liuyao on 2018/4/6 17:25.
 */
public class AllocDirectBuffer {
    public void directAllocate(){
        long starttime=System.currentTimeMillis();
        for (int i = 0; i < 200000; i++) {
            ByteBuffer b=ByteBuffer.allocateDirect(1000);
        }
        long endtime=System.currentTimeMillis();
        System.out.println("directAllocate:"+(endtime-starttime));
    }

    public void bufferAllocate(){
        long starttime=System.currentTimeMillis();
        for (int i = 0; i < 200000; i++) {
            ByteBuffer b=ByteBuffer.allocate(1000);
        }
        long endtime=System.currentTimeMillis();
        System.out.println("bufferAllocate:"+(endtime-starttime));
    }

    public static void main(String[] args) {
        AllocDirectBuffer allocDirectBuffer=new AllocDirectBuffer();
        allocDirectBuffer.bufferAllocate();
        allocDirectBuffer.directAllocate();

        allocDirectBuffer.bufferAllocate();
        allocDirectBuffer.directAllocate();
    }
}

运行结果:

bufferAllocate:60
directAllocate:135
bufferAllocate:125
directAllocate:109

**可见直接内存的访问比堆内存访问快,而申请空间时,堆空间的速度高于直接内存,故:直接内存适合申请次数较少,访问较频繁的场合,如果内存空间本身需要频繁申请,则并不适合使用直接内存 **

7.3 异常情况

超过本机内存,将会抛出OutOfMemoryError异常。

参考文章

Java8内存模型—永久代(PermGen)和元空间(Metaspace)

  • 10 min read

CONTRIBUTORS


  • 10 min read