面试必问的 JVM 运行时数据区,你懂了吗?

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 本文在描述一些有争议的问题上,优先以《Java 虚拟机规范》的说法为准。

前言


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

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

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

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

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


正文


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

image.png

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

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

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


1)程序计数器(Program Counter Register

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

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


2Java虚拟机栈(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种常量类型所代表的具体含义如图所示。

image.png

关于 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

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

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

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

3class 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)问题吗?


很遗憾,答案是不行的。

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


最后



我是囧辉,一个坚持分享原创技术干货的程序员,我的目标是帮助大家拿到心仪的大厂 Offer

目录
打赏
0
0
0
0
14
分享
相关文章
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
67 14
Java JVM 面试题
Java JVM(虚拟机)相关基础面试题
大厂面试高频:4 大性能优化策略(数据库、SQL、JVM等)
本文详细解析了数据库、缓存、异步处理和Web性能优化四大策略,系统性能优化必知必备,大厂面试高频。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:4 大性能优化策略(数据库、SQL、JVM等)
|
2月前
|
JVM运行时数据区
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一
37 2
JVM常见面试题(四):垃圾回收
堆区域划分,对象什么时候可以被垃圾器回收,如何定位垃圾——引用计数法、可达性分析算法,JVM垃圾回收算法——标记清除算法、标记整理算法、复制算法、分代回收算法;JVM垃圾回收器——串行、并行、CMS垃圾回收器、G1垃圾回收器;强引用、软引用、弱引用、虚引用
|
3月前
|
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
40 3
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程 ?
尼恩提示: G1垃圾回收 原理非常重要, 是面试的重点, 大家一定要好好掌握
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程  ?
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等