面试时必问的JVM运行时数据区详解

目录
  • 前言
  • 正文
    • 1、运行时数据区(Run-Time Data Areas)
      • 1)程序计数器(Program Counter Register)
      • 2)Java虚拟机栈(Java Virtual Machine Stacks)
      • 3)本地方法栈(Native Method Stacks)
      • 4)堆(Heap)
      • 5)方法区(Method Area)
      • 6)运行时常量池(Run-Time Constant Pool)
    • 2、Java 中有哪几种常量池?
    • 3、class 文件常量池
    • 4、运行时常量池
    • 5、字符串常量池
    • 6、字符串常量池是否属于方法区?
    • 7、运行时常量池和字符串常量池的关联?
    • 8、String#intern 方法
    • 9、永久代(PermGen)
    • 10、永久代和方法区的关系?
    • 11、元空间(metaspace)
    • 12、为什么引入元空间?
    • 13、元空间能彻底解决内存溢出(Out Of Memory)问题吗?
  • 总结

前言

Java 虚拟机的运行时数据区经常在面试中被拿来提问,很多概念在市面上有各种各样的说法,搞的不少同学应该是懵逼的。

当我们陷入不知道哪个说法是正确的情况时,最好的参考就是源码和规范。

在面试中,当面试官反问你:为什么某某是这样?的时候,如果你回答:因为规范是这么写的、因为源码是这么写的。

这个回答是非常有说服力的。

因此,本文在描述一些有争议的问题上,优先以《Java 虚拟机规范》的说法为准。

正文

1、运行时数据区(Run-Time Data Areas)

Java 虚拟机定义了若干种在程序执行期间会使用到的运行时数据区域。

其中一些数据区域在 Java 虚拟机启动时被创建,随着虚拟机退出而销毁。也就是线程间共享的区域:堆、方法区、运行时常量池。

另外一些数据区域是按线程划分的,这些数据区域在线程创建时创建,在线程退出时销毁。也就是线程间隔离的区域:程序计数器、Java虚拟机栈、本地方法栈。

1)程序计数器(Program Counter Register)

Java 虚拟机可以支持多个线程同时执行,每个线程都有自己的程序计数器。在任何时刻,每个线程都只会执行一个方法的代码,这个方法称为该线程的当前方法(current method)。

如果线程正在执行的是 Java 方法(不是 native 的),则程序计数器记录的是正在执行的 Java 虚拟机字节码指令的地址。如果正在执行的是本地(native)方法,那么计数器的值是空的(undefined)。

2)Java虚拟机栈(Java Virtual Machine Stacks)

每个 Java 虚拟机线程都有自己私有的 Java 虚拟机栈,它与线程同时创建,用于存储栈帧。

Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

3)本地方法栈(Native Method Stacks)

本地方法栈与 Java 虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是 Java 虚拟机栈为虚拟机执行 Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的本地(Native)方法服务。

4)堆(Heap)

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

堆在虚拟机启动时创建,堆存储的对象不会被显示释放,而是由垃圾收集器进行统一管理和回收。

5)方法区(Method Area)

方法区是被各个线程共享的运行时内存区域。方法区类似于传统语言的编译代码的存储区。它存储了每一个类的结构信息,例如:运行时常量池、字段和方法数据,构造函数和普通方法的字节码内容,还包括一些用于类、实例、接口初始化用到的特殊方法。

6)运行时常量池(Run-Time Constant Pool)

运行时常量池是 class 文件中每一个类或接口的常量池表(constant_pool table)的运行时表示形式。

它包含了若干种常量,从编译时已知的数值字面量到必须在运行时解析后才能获得的方法和字段引用。运行时常量池的功能类似于传统编程语言的符号表(symbol table),不过它包含的数据范围比通常意义上的符号表要更为广泛。

2、Java 中有哪几种常量池?

现在我们经常提到的常量池主要有三种:class 文件常量池、运行时常量池、字符串常量池。

3、class 文件常量池

class 文件常量池(class constant pool)属于 class 文件的其中一项,class 类文件包含:魔数、类的版本、常量池、访问标志、字段表集合、方发表等信息。

常量池用于存放编译期间生成的各种字面量(Literal)和符号引用(Symbolic References)。

字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为 final 的常量值等。

符号引用则属于编译原理方面的概念。符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。符号引用主要包括下面几类常量:

  • 被模块导出或开放的包(Package)
  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)

常量池中每一项常量都是一个表,截至JDK 13,常量表中分别有17种不同类型的常量。17种常量类型所代表的具体含义如图所示。

关于 class 文件常量池的更多内容可以阅读周志明的《深入理解Java虚拟机》6.3.2 章节。

4、运行时常量池

class 文件常量池是在类被编译成 class 文件时生成的。而当类被加载到内存中后,JVM 就会将 class 文件常量池中的内容存放到运行时常量池中。

Java 虚拟机规范中对运行时常量池的定义如下:

A run-time constant pool is a per-class or per-interface run-time representation of the constant_pool table in a class file.

运行时常量池是 class 文件中每一个类或接口的常量池表(constant_pool table)的运行时表示形式。

因此,根据规范定义,可以说运行时常量池是 class 文件常量池的运行时表示,每个类在运行时都有自己的一个独立的运行时常量池。

5、字符串常量池

简单来说,HotSpot VM 里的字符串常量池(StringTable)是个哈希表,全局只有一份,被所有的类共享。

StringTable 具体存储的是 String 对象的引用,而不是 String 对象实例自身。String 对象实例在 JDK 6 及之前是在永久代里,从JDK 7 开始放在堆里。

根据 Java 虚拟机规范的定义,堆是存储 Java 对象的地方,其他地方是不会有 Java 对象实体的,如果有的话,根据规范定义,这些地方也要算堆的一部分。

6、字符串常量池是否属于方法区?

我认为是不属于的。

在读本文之前,我相信很多同学会有如下观点:因为运行时常量池属于方法区,所以很多同学认为字符串常量池也应该属于方法区。

但是相信看了上面的内容后,会开始意识到,运行时常量池和字符串常量池其实是不同的两个东西,当然它们在字符串解析时会有关联。

Java 虚拟机规范中对方法区的定义如下:

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization

在 Java 虚拟机中,方法区是被各个线程共享的运行时内存区域。方法区类似于传统语言的编译代码的存储区,或者类似于操作系统进程中的文本段。它存储了每一个类的结构信息,例如:运行时常量池、字段和方法数据,构造函数和普通方法的字节码内容,还包括一些用于类、实例、接口初始化用到的特殊方法。

这边的关键在于 “它存储了每一个类的结构信息”,而字符串常量池并不属于某个类,字符串常量是全局共享的,因此,根据规范定义,我们可以说字符串常量池不属于方法区。

那字符串常量池(StringTable)究竟存在哪里了?

StringTable 本体是存储在 native memory(本地内存)里,不是在永久代里,不是在方法区里,当然,更不是在堆里。

7、运行时常量池和字符串常量池的关联?

上面说了,运行时常量池和字符串常量池在字符串解析时会有关联,具体如下。

类的运行时常量池中有 CONSTANT_String_info(见题3表格)类型的常量,CONSTANT_String_info 类型的常量的解析(resolve)过程如下:

首先到字符串常量池(StringTable)中查找是否已经有了该字符串的引用,如果有,则直接返回字符串常量池的引用;如果没有,则在堆中创建 String 对象,并在字符串常量池驻留其引用,然后返回该引用。

也就说,运行时常量池里的 CONSTANT_String_info 类型的常量,经过解析(resolve)之后,同样存的是字符串的引用,并且和 StringTable 驻留的引用的是一致的。

8、String#intern 方法

在 JDK 7 及之后的版本中,该方法的作用如下:如果字符串常量池中已经有这个字符串,则直接返回常量池中的引用;如果没有,则将这个字符串的引用保存一份到字符串常量池,然后返回这个引用。

下面的例子可以进行简单的验证:

public static void main(String args[]) {

    // 创建2个对象,str持有的是new创建的对象引用
    // 1)驻留(intern)在字符串常量池中的对象
    // 2)new创建的对象
    String str = new String("joonwhee");
    // 字符串常量池中已经有了,返回字符串常量池中的引用
    String str2 = "joonwhee";
    // false,str为new创建的对象引用,str2为字符创常量池中的引用
    System.out.println(str == str2);
    // str修改为字符串常量池的引用,所以下面为true
    str = str.intern();
    // true
    System.out.println(str == str2);
}

9、永久代(PermGen)

永久代在 Java 8 被移除。根据官方提案的描述,移除的主要动机是:要将 JRockit 和 Hotspot 进行融合,而 JRockit 并没有永久代。

而据我们所了解的,还有另外一个重要原因是永久代本身也存在较多的问题,经常出现OOM,还出过不少bug。

根据官方提案的描述,永久代主要存储了三种数据:

1)Class metadata(类元数据),也就是方法区中包含的数据,除了编译生成的字节码被放在 native memory(本地内存)。

2)interned Strings,也就是字符串常量池中驻留引用的字符串对象,字符串常量池只驻留引用,而实际对象是在永久代中。

3)class static variables,类静态变量。

移除永久代后,interned Strings 和 class static variables 被移动了堆中,Class metadata 被移动到了后来的元空间。

10、永久代和方法区的关系?

方法区是 Java 虚拟机规范中定义的一种逻辑概念,而永久代是对方法区的实现。但是永久代并不等同于方法区,方法区也不等同于永久代。

永久代中的 interned Strings 并不属于方法区,按规范:堆是存储 Java 对象的地方 ,这部分应该属于堆,因此永久代并不是只用于实现方法区。

方法区中 JIT 编译生成的代码并不是存放在永久代,而是在 native memory 中,因此可以说方法区也并不只是由永久代来实现。

11、元空间(metaspace)

元空间在 Java 8 移除永久代后被引入,用来代替永久代,本质和永久代类似,都是对方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存(native memory)。

元空间主要用于存储 Class metadata(类元数据),根据其命名其实也看得出来。

可以通过 -XX:MaxMetaspaceSize 参数来限制元空间的大小,如果没有设置该参数,则元空间默认限制为机器内存。

12、为什么引入元空间?

在 Java 8 之前,Java 虚拟机使用永久代来存放类元信息,通过-XX:PermSize、-XX:MaxPermSize 来控制这块内存的大小,随着动态类加载的情况越来越多,这块内存变得不太可控,到底设置多大合适是每个开发者要考虑的问题。

如果设置小了,容易出现内存溢出;如果设置大了,又有点浪费,尽管不会实质分配这么大的物理内存。

而元空间可以较好的解决内存设置多大的问题:当我们没有指定 -XX:MaxMetaspaceSize 时,元空间可以动态的调整使用的内存大小,以容纳不断增加的类。

13、元空间能彻底解决内存溢出(Out Of Memory)问题吗?

很遗憾,答案是不行的。

元空间无法彻底解决内存溢出的问题,只能说是有所缓解。当内存使用完毕后,元空间一样会出现内存溢出的情况,最典型的场景就是出现了内存泄漏时。

总结

本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • Java内存模型与JVM运行时数据区的区别详解

    首先,这两者是完全不同的概念,绝对不能混为一谈. 1.什么是Java内存模型? Java内存模型是Java语言在多线程并发情况下对于共享变量读写(实际是共享变量对应的内存操作)的规范,主要是为了解决多线程可见性.原子性的问题,解决共享变量的多线程操作冲突问题. 多线程编程的普遍问题是: 所见非所得 无法肉眼检测程序的准确性 不同的运行平台表现不同 错误很难复现 故JVM规范规定了Java虚拟机对多线程内存操作的一些规则,主要集中体现在volatile和synchronized这两个关键字. vo

  • JVM运行时数据区原理解析

    前言 Java虚拟机定义了若干种程序运行期间会使用的运行时数据区域,其中一些会随着虚拟机启动而创建,随着虚拟机的退出而销毁.另外一些则是和线程一一对应,这些与线程对应的数据区域随着线程开始而创建,线程的结束而销毁. PC寄存器 PC寄存器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器,每条线程都要一个独立的PC寄存器,这个内存也是线程私有的内存.正在执行 java 方法的话,PC寄存器是记录的是虚拟机字节码指令的地址(当前指令的地址).如果还是 Native 方法,则为und

  • JVM运行时数据区划分原理详解

    Java内存空间 内存是非常重要的系统资源,是硬盘和cpu的中间仓库及桥梁,承载着操作系统和应用程序的实时运行.JVM内存布局规定了JAVA在运行过程中内存申请.分配.管理的策略,保证了JVM的高效稳定运行.不同的jvm对于内存的划分方式和管理机制存在着部分差异(对于Hotspot主要指方法区) (图源阿里)JDK8的元数据区+JIT编译产物 就是JDK8以前的方法区 JavaAPI中的Runtime public class Runtime extends Object Every Java

  • Java JVM运行时数据区(Run-Time Data Areas)

    1.官网概括 引用官网说法: The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits.

  • 面试时必问的JVM运行时数据区详解

    目录 前言 正文 1.运行时数据区(Run-Time Data Areas) 1)程序计数器(Program Counter Register) 2)Java虚拟机栈(Java Virtual Machine Stacks) 3)本地方法栈(Native Method Stacks) 4)堆(Heap) 5)方法区(Method Area) 6)运行时常量池(Run-Time Constant Pool) 2.Java 中有哪几种常量池? 3.class 文件常量池 4.运行时常量池 5.字符串

  • JAVA JVM运行时数据区详解

    目录 一.前言 二.运行时数据区整体概架构 三.程序计数器 四.虚拟机栈 1.栈的特点 2.栈帧的内部结构 3.局部变量表 4.操作数栈 5.动态链接 6.方法返回地址 五.本地方法栈 六.堆 1.设置堆大小的参数 2.对象分配过程 3.堆中的GC 4.内存分配策略 5.什么是TLAB 6.堆是分配对象存储的唯一选择吗? 七.方法区 1.方法区概述 2.设置方法区内存大小 3.如何解决OOM问题? 4.方法区存储什么 5.方法区的演进细节 6.方法区的GC 总结 一.前言 这是JVM系列文章的第

  • 面试必时必问的JVM 类加载机制详解

    目录 前言 正文 1.类加载的过程. 1)加载 2)验证 3)准备 4)解析 5)初始化 2.Java 虚拟机中有哪些类加载器? 1)启动类加载器(Bootstrap ClassLoader): 2)扩展类加载器(Extension ClassLoader): 3)应用程序类加载器(Application ClassLoader): 3.什么是双亲委派模型? 4.为什么使用双亲委派模式? 5.有哪些场景破坏了双亲委派模型? 6.为什么要破坏双亲委派模型? 7.如何破坏双亲委派模型? 8.Tomc

  • JVM核心教程之JVM运行与类加载全过程详解

    为什么要使用类加载器? Java语言里,类加载都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会给java应用程序提供高度的灵活性.例如: 1.编写一个面向接口的应用程序,可能等到运行时再指定其实现的子类: 2.用户可以自定义一个类加载器,让程序在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分:(这个是Android插件化,动态安装更新apk的基础) 为什么研究类加载全过程? 有助于连接JVM运行过程 更深入了解java动态性(解热部署,动态加载),提高程

  • java应用开发之JVM运行时内存分析

    目录 1.JVM的运行时内存也叫JVM堆 2.JVM新创建的对象 3.新生代详解 4.老年代详解 5.永久代 1.JVM的运行时内存也叫JVM堆 从GC的角度可以将JVM分为新生代,老年代,永久代.其中新生代默认占1/3的堆内存空间,老年代默认占2/3内存空间,永久代占非常少的堆内存空间方式. 而新生代分为Eden,SurvivorFrom,SurvivorTo区,Eden默认占8/10新生代区域空间,SurviorFrom和SurviorTo则占1/10. 2.JVM新创建的对象 JVM新创建

  • java学习之JVM运行时常量池理解

    运行时常量池 运行时常量池是方法区的一部分.Class文件中除了有类的版本.字段.方法.接口等描述信息外,还有一项信息时常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放. 运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是S

  • JVM 运行时数据区与JMM 内存模型

    目录 1. JVM 运行时数据区 2. JMM 内存模型 硬件内存模型 JMM 3. 可见行与 volatile 关键字 1. JVM 运行时数据区 JVM运行时数据区可以分为元空间,堆,虚拟机栈,本地方法栈,程序计数器五大块. 元空间(方法区):存放类模版对象,是线程共享的区域,在磁盘上,一般不会GC 堆空间:线程共享的区域,对象创建与GC的主要阵地 虚拟机栈:线程私有的,基本组成单位是栈帧,每个栈帧对应一个方法,栈帧组成如下 局部变量表:存放方法变量信息 操作数栈:方法运行的区域 动态链接:

  • JVM分析之类加载机制详解

    目录 1.前言 2.类加载是什么 3.类加载过程 3.1 加载 3.2 链接 3.3 初始化 4.总结 1.前言 JVM内部架构包含类加载器.内存区域.执行引擎等.日常开发中,我们编写的java文件被编译成class文件后,jvm会进行加载并运行使用类.本次仅对JVM加载部分进行分析,了解并掌握加载机制. 2.类加载是什么 类加载是一种过程,是将class文件加载到jvm内存的过程.当代码逻辑中需要引用类时,通过类加载器加载引用类对象并存放堆中,以供代码调用. 3.类加载过程 注:类加载过程包含

随机推荐