Java虚拟机运行时数据区域

69 阅读6分钟

image.png

程序计数器(Program Counter Register)

程序计数器表示当前线程所执行字节码的行号指示器,内存占用较小。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖它。

  1. 为什么线程私有? 为了避免线程切换后原线程能恢复到正确执行位置,所以每个线程都需要有自己独立的程序计数器。
  2. OOM吗? 不会。因为其大小根据处理器体系结构而改变。也就是说,64位体系结构将需要64位来保存程序计数器,32位将需要32位。

Java虚拟机栈(Java Virtual Machine Stack)

Java虚拟机栈表示线程执行Java方法的内存模型。

  1. Java虚拟机栈什么时候创建? 创建线程的时候,就会创建一个Java虚拟机栈。

  2. 栈帧是什么? 栈帧就是方法。线程执行过程中,每调用一个方法就会向栈中压入一个栈帧(Stack Frame)。

  3. 栈帧存储什么? 存储局部变量表、操作数栈、动态连接、方法出口等信息。

  4. 栈帧什么时候出栈? return语句或抛出异常。

  5. 栈帧生命周期? 方法被调用到执行结束,就对应栈帧入栈到出栈的过程。

  6. 什么时候抛出StackOverflowError? Java虚拟机栈的容量是固定的,如果线程不停的调用方法,对应栈帧不断的入栈,当栈容量不足时,就会抛出StackOverflowError

  7. 什么时候抛出OOM

    • 如果虚拟机栈可以动态扩展(HotSpot虚拟机不可以动态扩展),当无法申请到足够的内存会抛出OOM
    • 操作系统分配给每个进程的内存是有限制的。当不断的创建线程申请虚拟机栈空间时,达到了进程的内存限制就会抛出OOM。在这种情况下,虚拟机栈容量越大,更容易出现OOM
  8. 虚拟机栈容量参数是什么? -Xss228k

本地方法栈

本地方法栈与虚拟机栈的作用是相似的,表示线程执行本地方法(Native修饰的方法)的内存模型。HotSpot虚拟机是直接把本地方法栈和Java虚拟机栈合二为一实现的。

Java堆(Java Heap)

Java堆是内存最大的一块区域,用于存放对象实例信息(如成员变量)。垃圾回收主要在这个区域工作。

  1. Java堆什么时候创建?

    在虚拟机启动时创建

  2. 是否有线程安全问题?

    • 对于成员变量和静态变量,多线程对其进行读写操作是有线程安全问题,只读或只写没有线程安全问题
    • 对于局部变量,如果存在逃逸,也就是地址指向外部的成员变量或静态变量实例,多线程对其进行读写操作也会有线程安全问题
  3. 是否所有对象实例都在堆上分配?

    《Java虚拟机规范》表示所有的对象实例和数组都应在堆上分配内存,但由于即时编译器技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段导致对象实例也可能会直接在栈上分配内存了,以后可能并不是所有对象实例都在堆上分配内存。

  4. 什么时候抛出OOM

    Java堆的大小可以是固定的,也可以是动态扩展的。主流虚拟机都是按照可扩展来实现的。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,将会产生OOM

  5. 堆容量参数是什么?

    -Xmx2g: 最大堆内存

    -Xms2g: 最小堆内存

    建议设置为一样大,避免动态扩展时损耗性能。

字符串常量池

字符串常量池是针对字符串提升性能和减少内存消耗专门开辟的一块区域,主要目的是为了避免字符串的重复创建。低版本Java字符串常量池存储在方法区中,Java1.7开始字符串常量池存储为堆中。

  • 解析如下例子

     [final] String str1 = "str"; // 字符串常量池对象 str
     [final] String str2 = "ing"; // 字符串常量池对象 ing
     String str3 = "str" + "ing"; // 编译优化为字符串常量池对象 string
     // 如果str1和str2是final的,那么也会编译优化为字符串常量池对象 string,
     // 否则因无法确定str1和str2是否会更改,无法进行编译优化,只能运行时在堆上创建新对象
     String str4 = str1 + str2;
     String str5 = "string"; // 常量池中的对象 string
     // 当str1和str2为final时, 全输出true; 非final如下所示
     System.out.println(str3 == str4); //false
     System.out.println(str3 == str5); //true
     System.out.println(str4 == str5); //false
    

方法区(Method Area)

方法区和堆类似,为了与堆区分也叫非堆。是线程共享的。它用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

  1. 是否有线程安全问题?

    多线程读写静态变量时会有线程安全问题。

  2. 什么时候OOM

因为Java可以在运行时动态加载类型,所以方法区无法满足新的内存分配需求时,将抛出OOM

  1. 永久代是什么?

    永久代存在于JDK8之前,是实现方法区的手段。为了兼容JRockit的优秀功能以及为后续发展,已经舍弃了。

  2. 元空间是什么?

    JDK8开始使用元空间来实现方法区,其使用本地内存的方式。默认情况方法区的大小就由操作系统来控制,有效避免OOM的情况。

  3. 元空间参数?

    -XX:MetaspaceSize: 最大元空间大小,默认值为 unlimited,这意味着它只受操作系统进程内存的限制。

    -XX:MaxMetaspaceSize: 元空间的初始大小,如果未指定此标志,则将根据运行时的应用程序需求动态地重新调整大小。

运行时常量池(Runtime Constant Pool)

运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用。因其存储于方法区中,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OOM

直接内存

直接内存表示操作系统内存,并不是虚拟机运行时区域的一部分,但Java也可以直接操作这块内存,而且也可能导致OOM

Java1.4中加入了NIO,其提供了Native函数来直接分配堆外内存,通过一个DirectByteBuffer对象来进行操作,因避免了在Native堆和Java堆中来回复制数据,可以显著提高性能。虽然其不会受到Java堆大小的限制,但既然是内存,还是会受到本机总内存的限制。所以需要避免设置Java堆内存过大,为直接内存留一些空间,否则在堆动态扩展时可能会出现内存不足而抛出OOM