jvm - 运行时内存结构

原创
2017/10/23 08:19
阅读数 2.3K

jvm - 运行时内存结构

注意 : 本系列文章为学习系列,部分内容会取自相关书籍或者网络资源,在文章末尾处会有标注

内存模型示意图

Jvm运行时内存结构

每个区域的作用简述

pc寄存器 (program counter)

每一条java虚拟机线程都有自己的pc寄存器

在任意时刻,一条java虚拟机线程只会执行一个方法的代码,正在被线程执行的方法称为该线程的当前方法

(如果这个方法不是native的,那pc寄存器就保存java虚拟机正在执行的字节码指令的地址)

(如果这个方法是natice的,那pc寄存器的值是undefined)

pc寄存器的容量至少应当能保存一个returnAddress类型的数据或者一个与平台相关的本地指针的值

虚拟机栈 (virtual machine stack)

每条java虚拟机线程都有自己私有的java虚拟机栈,这个栈与线程同时创建,用于存储栈帧 (用于存储局部变量与一些尚未算好的结果)

除了栈帧的入栈和出栈之外,不会再受其他因素影响,所以栈帧可以在堆中分配,java虚拟机栈所使用的内存不需要保证是连续的

java虚拟机规范允许java虚拟机栈被实现成固定大小的,也允许根据计算来动态扩展和收缩

(如果是固定大小的,那每一个线程的java虚拟机栈的容量可以在线程创建时独立选定)

如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机将抛出一个StackOverflowError异常

如果java虚拟机栈可以动态扩展,并且在尝试扩展时无法申请到足够内存,或者在创建新线程的时候没有足够的内存去创建对应的虚拟机栈,那么java虚拟机将会抛出一个OutOfMemoryError异常

堆 (heap)

堆是可供各个线程共享的运行时区域,也是提供所有类实例和数组对象分配内存的区域

堆在java虚拟机启动的时候被创建,它存储了被自动内存管理系统(垃圾收集器)所管理的各种对象,这些受管理的对象无需也无法显示地销毁

堆的用量可以是固定的,也可以是随程序执行的需求动态扩展,并在不需要过多空间的时候自动收缩

堆所使用的内存空间不需要保证是连续的

如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那java虚拟机将会抛出一个OutOfMemoryError异常

方法区 (method area)

的逻辑组成部分

可供各个线程共享的运行时区域

存储了每个类的结构信息

在虚拟机启动的时候创建

可以选择不实现垃圾收集和压缩

用量可以是固定的,也可以是随程序执行的需求动态扩展,并在不需要过多空间的时候自动收缩

所使用的内存空间不需要保证是连续的

如果方法区的内存空间不能满足内存分配请求,那java虚拟机将会抛出一个OutOfMemoryError异常

运行时常量 (runtime constant pool)

它是class文件中每一个类或者接口的的常量池表的运行时表示形式,包括了多种不同的常量,从编译期可知的数值字面量到必须在运行期解析后才能获得的方法和引用

每一个运行时常量都在java虚拟机的方法区中分配,在加载类和接口到虚拟机后,就创建对应的运行时常量池

创建类或者接口时,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,那java虚拟机将会抛出一个OutOfMemoryError异常

本地方法栈 (native method stack)

Java虚拟机可能会用到传统的栈(称为C stack)来支持native方法的执行,这个栈就是本地方法栈

如果java虚拟机支持本地方法栈,那么每一个线程的本地方法栈容量可以在创建栈的时候独立选定

如果线程请求分配的栈容量超过本地方法栈的最大容量,java虚拟机将抛出一个StackOverflowError异常

如果本地方法栈可以动态扩展,并且在尝试扩展时无法申请到足够内存,或者创建新线程时没有足够的内存区创建对应的本地方法栈,那java虚拟机将会抛出一个OutOfMemoryError异常

栈帧 (frame)

栈帧的存储空间由创建它的线程分配在java虚拟机栈中

栈帧是用来存储数据和部分过程结果的数据结构,同时也用来处理动态链接,方法返回值,异常分派

栈帧随着方法的调用而创建,随着方法的结束而销毁,无论方法是正常完成还是异常完成,都算作方法结束

每个栈帧都有自己的本地变量表,操作数栈,指向当前方法所属的类的运行时常量池的引用

在某条线程执行过程中的某个时间点上,只有目前正在执行的那个方法的栈帧是活动的

调用新的方法时,新的栈帧会随之而创建,并且会随着程序控制权移交到新方法,而称为新的当前栈帧

方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,然后虚拟机会丢弃掉当前栈帧,使得前一个栈帧重新称为当前栈帧

注意 : 栈帧是线程本地私有的数据,不可能在一个栈帧之中引用另外一个线程的栈帧

栈帧 - 局部变量表

每个栈帧内部都包含一组称为局部变量表的变量列表

栈帧中的局部变量表的长度由编译期决定

存储于类或接口的二进制表示之中(class),即通过方法的code属性保存以及提供给栈帧使用

一个局部变量可以保存一个类型为如下的数据,boolean,byte,char,short,int,float,reference,returnAddress

两个连续的局部变量可以保存一个类型为long或double的数据

局部变量表使用索引来进行定位访问

java虚拟机使用局部变量表来完成方法调用时的参数传递

栈帧 - 操作数栈

每个栈帧内部都包含一个称谓操作数栈的后进先出栈

栈帧中的操作数栈的最大深度由编译期决定,并且通过方法的code属性保存以及提供给栈帧使用

栈帧在刚刚创建时,操作数栈是空的

java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或者变量的值到操作数栈中,也提供了一些指令用于操作数栈,取走数据,操作数据,以及把操作结果重新入栈

在调用方法时,操作数栈也用来准备调用方法的参数以及接收方法的返回结果

操作数栈的每一个位置上可以保存一个java虚拟机定义的任意数据类型的值,包括long和double

任意时刻,操作数栈都会有一个确定的深度,一个long或者double类型的数据会占用两个单位的栈深度,其他数据类型则会占用一个单位的栈深度

栈帧 - 动态链接

每个栈帧内部都包含一个指向当前方法所在类型的运行时常量池的引用,以便对当前方法实现动态链接

在class文件里,一个方法若要调用其他方法,或者访问成员变量,则需要通过符号引用来表示,动态链接的作用就是将这些符号引用所表示的方法转换为对实际方法的直接引用

溢出的实例代码

栈溢出

public class StackOom {

    public int num = 1;

    public void stack() {
        num++;
        this.stack();
    }
    public static void main(String[] arge) {
        StackOom stackOom = new StackOom();
        stackOom.stack();
    }

}
Exception in thread "main" java.lang.StackOverflowError
	at org.itkk.learn.StackOom.stack(StackOom.java:15)
	at org.itkk.learn.StackOom.stack(StackOom.java:15)
	at org.itkk.learn.StackOom.stack(StackOom.java:15)
	at org.itkk.learn.StackOom.stack(StackOom.java:15)

以上这段代码,stack方法不断的递归调用,最终达到java虚拟机栈的最大容量,就抛出了StackOverflowError异常

堆溢出


public class HeapOom {

    private List<byte[]> data = new ArrayList<>();

    public void heap() {
        while (true) {
            data.add(new byte[1024 * 1024]);
        }
    }

    public static void main(String[] arge) {
        HeapOom heapOom = new HeapOom();
        heapOom.heap();
    }
}


Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at org.itkk.learn.HeapOom.heap(HeapOom.java:21)
	at org.itkk.learn.HeapOom.main(HeapOom.java:32)

以上这段代码,heap方法,以死循环的方式不断的往data中添加1M大小的数组对象,最终堆中的内存空间不能满足data存放的要求,而抛出了OutOfMemoryError异常

元空间溢出


public class MetaspaceOom {

    static String str = "string";

    public static void main(String[] arge) {
        List<String> list = new ArrayList<>();
        while (true) {
            str += str;
            list.add(str.intern());
        }
    }
}

java.lang.OutOfMemoryError: Metaspace
        at sun.misc.Launcher.<init>(Unknown Source)
        at sun.misc.Launcher.<clinit>(Unknown Source)
        at java.lang.ClassLoader.initSystemClassLoader(Unknown Source)
        at java.lang.ClassLoader.getSystemClassLoader(Unknown Source)

以上这段代码使用如下命令执行 :

java -XX:MetaspaceSize=2m -XX:MaxMetaspaceSize=2m org.itkk.learn.MetaspaceOom

在运行MetaspaceOom类时,限定了元空间的大小,而main方法中,则以死循环的方式不停的拷贝生成新的字符串常量(intern方法),而字符串常量存储于元空间中,最终字符串常量超出了元空间的容量,从而抛出OutOfMemoryError异常(Metaspace)

java8中永久代的变化

在java8中,永久代已经由元空间替代了,在上面章节中的元空间溢出的实例代码中,在jdk6中,会出现"PermGen Space"溢出,在jdk7和jdk8中,则是出现"Java heap space"溢出

而且在java8中,-XX:PermSize和-XX:MaxPermGen已经提示无效了,如下:

使用如下命令运行
java -XX:PermSize=8m -XX:MaxPermSize=8m -Xmx16m  org.itkk.learn.MetaspaceOom

D:\develop\JetBrains\IdeaProjects\learn\leanmain\target\classes>java -XX:PermSize=8m -XX:MaxPermSize=8m -Xmx16m  org.itkk.learn.MetaspaceOom
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=8m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=8m; support was removed in 8.0

会得出如下两个警告,可以得知,Perm在jdk8中已经不存在了

元空间和永久代最大的区别在于,元空间不再java虚拟机内存中,而是直接使用的本地内存,空间大小仅受本地内存的限制,并且可以使用jvm参数来指定元空间的大小

上面元空间溢出的实例代码中就使用了-XX:MetaspaceSize和-XX:MaxMetaspaceSize来指定元空间的最大和最容量

参考文献

文章中部分内容取自<<Java虚拟机规范.Java SE 8版>>

结束

俗话说,不了解jvm的java程序员,等于耍流氓

深入的了解底层的能有助于我们更好的理解java程序运行的机制,从而帮助我们写出更好的代码.

关于本文内容 , 欢迎大家的意见跟建议

代码仓库 (博客配套代码)


想获得最快更新,请关注公众号

输入图片说明

展开阅读全文
加载中

作者的其它热门文章

打赏
2
54 收藏
分享
打赏
3 评论
54 收藏
2
分享
返回顶部
顶部