本人本科毕业,21届毕业生,一年工作经验,简历专业技能如下,现根据简历,并根据所学知识复习准备面试。
记录日期:2021.12.30
大部分知识点只做大致介绍,具体内容根据推荐博文链接进行详细复习。
JVM - 运行时数据区
书籍推荐:《深入理解JVM虚拟机》、《Java 虚拟机规范》
博客文章推荐:面试必问的 JVM 运行时数据区,你懂了吗?
JavaGuide:听说又被 JVM 内存区域方面的面试题给虐了?看看这篇文章吧!
首先说明,文中大部分都是以JDK1.8的JVM运行时数据区为准。
运行时数据区域(基础知识点)
参照JavaGuide中的图片,举出JDK1.6、JDK1.8时不同内存划分。
JDK1.6:
JDK1.8:
还有两个在图中没有提到,一个是执行引擎
,另一个是本地方法接口
。
线程私有
程序计数器
程序计数器介绍
程序计数器(Program Counter Register
),Java 虚拟机可以支持多个线程同时执行,每个线程都有自己的程序计数器。
程序计数器中记录内容是什么?
在任何时刻,每个线程都只会执行一个方法的代码,这个方法称为该线程的当前方法(current method)。
如果线程正在执行的是 Java 方法(不是 native 的),则程序计数器记录的是正在执行的 Java 虚拟机字节码指令的地址。如果正在执行的是本地(native)方法,那么计数器的值是空的(undefined)。
程序计数器在线程切换时发生什么?
字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支
、循环
、跳转
、异常处理
、线程恢复
等功能都需要依赖这个计数器来完。
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
程序计数器作用总结
- 字节码解释器通过改变程序计数器来依次读取指令,从而
实现代码的流程控制
,如:顺序执行、选择、循环、异常处理。 - 在多线程的情况下,程序计数器用于
记录当前线程执行的位置
,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
虚拟机栈
虚拟机栈介绍
Java虚拟机栈(Java Virtual Machine Stacks
),每个 Java 虚拟机线程都有自己私有的 Java 虚拟机栈,它与线程同时创建,用于存储栈帧。
虚拟机栈中保存内容是什么?
Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表
、操作数栈
、动态链接
、方法出口
等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程,执行完成行为包括return和throw。
其中局部变量表
主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针
,也可能是指向一个代表对象的句柄或其他与此对象相关的位置
)。
Java 虚拟机栈会出现两种异常,分别是StackOverFlowError 栈溢出异常和 OutOfMemoryError内存溢出异常。
StackOverFlowError: 若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
tip : 创建对象也有可能存在于栈中,面试可能会问到。
本地方法栈
本地方法栈介绍
本地方法栈(Native Method Stacks
),本地方法栈与 Java 虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是 Java 虚拟机栈为虚拟机执行 Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的本地(Native
)方法服务。(调用本地JNI接口)
本地方法栈中保存内容是什么?
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
线程共享
堆
堆介绍
堆(Heap
),Jvm所管理的内存中最大的一部分,堆是被各个线程共享
的运行时内存区域,也是供所有类实例和数组对象分配内存的区域
。
堆在虚拟机启动时创建,堆存储的对象不会被显示释放,而是由垃圾收集器进行统一管理和回收。
堆中保存内容是什么?
此内存区域的唯一目的就是存放对象实例
,几乎所有的对象实例以及数组都在这里分配内存
。(这里是几乎所有,并不是所有)
堆中空间划分
从目前主流的收集器采用的分代垃圾收集算法
来说,Java堆可以分为:新生代(Young)、老年代(Old)。
新生代还可以分为:Eden区、幸存0区(From Survivor)、幸存1区(To Survivor)。(这里没有规定0区和1区,只是在说明上方便)
这样划分的目的就是为了更好地回收内存,以及更快地分配内存
。
对象创建如何分配空间?
- 大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden区->Survivor 区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数
-XX:MaxTenuringThreshold
来设置。 - 少部分情况,对象的大小过大,则直接在老年代进行分配。
tip :这里有几个点,第一点,为什么是15岁呢?一方面因为15只占4bit,另一方面15是均衡测试得出的比较好的阈值。第二点,关于对象的实例化过程,在后面会讲
。
方法区
首先说明,方法区只是一个概念,《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规范如何区实现它,在不同的 JVM 上方法区的实现肯定是不同的了。 同时大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。所以说,永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现。
方法区介绍
方法区(Method Area
),方法区是被各个线程共享的运行时内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区类似于传统语言的编译代码的存储区。它存储了每一个类的结构信息,例如:运行时常量池
、字段
和方法数据
,构造函数
和普通方法
的字节码内容,还包括一些用于类
、实例
、接口初始化
用到的特殊方法。
下面以HotSpot虚拟机中为例,介绍JDK1.7版本前后的不同实现。
JDK1.7以前:
JDK1.7之前在(JDK1.2 ~ JDK6)的实现中,HotSpot 使用永久代
实现方法区,HotSpot 使用 GC分代来实现方法区内存回收。
JDK1.7以后:
JDK1.8之后, HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在。那么取代永久代的就是元空间
。
永久代和元空间的区别?
- 存储位置不同,永久代物理只是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存;
- 存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。
运行时常量池
运行时常量池介绍
运行时常量池(Run-Time Constant Pool
),它是方法区的一部分,是一块内存区域。Class 文件常量池将在类加载后进入方法区的运行时常量池中存放。一个类加载到 JVM 中后对应一个运行时常量池,运行时常量池相对于 Class 文件常量池来说具备动态性
,Class 文件常量只是一个静态存储结构
,里面的引用都是符号引用
。而运行时常量池可以在运行期间将符号引用解析为直接引用
。可以说运行时常量池就是用来索引和查找字段和方法名称和描述符的
。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息,这涉及到方法的调用和字段获取。
JDK1.7之后的位置
JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 异常出现。
JDK1.4 中新加入的NIO(New Input/Output) 类
,引入了一种基于通道(Channel)
与缓存区(Buffer)
的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在Java
堆中的 DirectByteBuffer
对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在Java
堆和Native
堆之间来回复制数据。
本机直接内存的分配不会收到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
在Redission之前的版本中,底层连接使用lettuce,因为源码编写不当,会抛出堆外内存溢出异常。
Java 中有哪几种常量池?
class 文件常量池、运行时常量池、字符串常量池。
class 文件常量池
编译生成的 class 字节码文件中包括:魔数(0xCAFEBABE
)、版本号、常量池、访问标志、字段表集合、方法表等信息,其中的常量池,就是我们说的class文件常量池。
Class 文件常量池(class constant pool
),class常量池用于存放编译期生成的各种字面量
和符号引用
,这部分内容将在类加载后进入方法区的运行时常量池中存放。
- 这里的字面量是指
字符串字面量
和声明为 final 的(基本数据类型)常量值
,这些字符串字面量除了类中所有双引号括起来的字符串(包括方法体内的)
,还包括所有用到的类名
、方法的名字
和这些类与方法的字符串描述
、字段(成员变量)的名称
和描述符
;声明为final的常量值指的是成员变量
,不包含本地变量,本地变量是属于方法的。这些都在常量池的 UTF-8 表中(逻辑上的划分)。 - 符号引用,就是指指向 UTF-8 表中向这些字面量的引用,包括
类和接口的全限定名(包括包路径的完整名)
、字段的名称和描述符
、方法的名称和描述符
。只不过是以一组符号来描述所引用的目标,和内存并无关,所以称为符号引用,直接指向内存中某一地址的引用称为直接引用。
具体参照《深入理解JVM虚拟机》6.3.2节。
运行时常量池
上面说过了,class 文件常量池是在类被编译成 class 文件时生成的。而当类被加载到内存中后,JVM 就会将 class 文件常量池中的内容存放到运行时常量池中。
简而言之,编译完成的class字节码文件中保存的时class文件常量池,当类被加载进入内存时,JVM就会把class文件常量池中的内容放到运行时常量池中,可以说运行时常量池是 class 文件常量池的运行时表示,每个类在运行时都有自己的一个独立的运行时常量池。
字符串常量池
字符串常量池是全局的,JVM 中独此一份,因此也称为全局字符串常量池
,HotSpot VM 里的字符串常量池(StringTable)是个哈希表。
StringTable 具体存储的是String 对象的引用
,而不是 String 对象实例自身
。
运行时常量池中的字符串字面量若是成员的,则在类的加载初始化阶段就使用到了字符串常量池;若是本地的,则在使用到的时候(执行此代码时)才会使用到字符串常量池。
其实,“使用常量池”对应的字节码是一个 ldc 指令,在给 String 类型的引用赋值的时候会先执行这个指令,看常量池中是否存在这个字符串对象的引用,若有就直接返回这个引用,若没有,就在堆里创建这个字符串对象并在字符串常量池中记录下这个引用(jdk1.7)。String 类的 intern() 方法还可在运行期间把字符串放到字符串常量池中。JVM 中除了字符串常量池,8种基本数据类型中除了两种浮点类型剩余的6种基本数据类型的包装类,都使用了缓冲池技术,但是 Byte、Short、Integer、Long、Character 这5种整型的包装类也只是在对应值在 [-128,127] 时才会使用缓冲池,超出此范围仍然会去创建新的对象。其中:
- 在 jdk1.6(含)之前也是方法区(hotspot中的永久代)的一部分,并且其中存放的是字符串的实例;
- 在 jdk1.7(含)之后是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中;
- jdk1.8 已移除永久代,字符串常量池是在本地内存当中,存储的也只是引用。
字符串的intern()方法 以及 包装类的缓冲池技术 都在1.2中讲过。
面试问题注意点
1.运行时常量池和字符串常量池的关联?
见囧辉的文章。
2.intern()方法在JDK1.7前后的实现?
在1.2中说过。
3.为什么要移除永久代?
永久代在 Java 8 被移除,取而代之的是元空间。
第一个原因,官方提案的描述,移除的主要动机是:要将 JRockit 和 Hotspot 进行融合,而 JRockit 并没有永久代。
第二个原因,永久代本身也存在较多的问题,经常出现OOM,还出过不少bug。
4.永久代被移除后,里面的数据分别存储在了哪里?
根据官方提案的描述,永久代主要存储了三种数据:
1)Class metadata,类元数据,也就是方法区中包含的数据,除了编译生成的字节码被放在 native memory
(本地内存)。
2)interned Strings,也就是字符串常量池中驻留引用的字符串对象,字符串常量池只驻留引用,而实际对象是在永久代中。
3)class static variables,类静态变量。
移除永久代后,interned Strings
和class static variables
被移动了堆中,Class metadata
被移动到了后来的元空间。
4.为什么要引入元空间?
在 Java 8 之前,Java 虚拟机使用永久代来存放类元信息,通过-XX:PermSize
、-XX:MaxPermSize
来控制这块内存的大小,随着动态类加载的情况越来越多,这块内存变得不太可控,到底设置多大合适是每个开发者要考虑的问题。
如果设置小了,容易出现内存溢出;如果设置大了,又有点浪费,尽管不会实质分配这么大的物理内存。
而元空间可以较好的解决内存设置多大的问题:当我们没有指定 -XX:MaxMetaspaceSize
时,元空间可以动态的调整使用的内存大小,以容纳不断增加的类。
当然使用元空间还是无法避免存在内存溢出问题。