没有特殊说明,JVM都是指HotSpot虚拟机。
常见的面试题:
- 介绍下Java内存区域(运行时数据区)
- Java对象的创建过程(五个重要的步骤)
- 对象的访问定位的两种方式(句柄和直接指针)
运行时数据区
JVM在执行Java程序的过程中会将它管理的内存划分成若干个不同的数据区域。
「JDK7时期的运行时数据区」
「JDK8时期的运行时数据区」
线程之间共享的区域:
- 堆
- 方法区
- 直接内存(非运行时数据区的一部分)
线程私有的区域:
- 程序计数器
- 虚拟机栈
- 本地方法栈
Java虚拟机规范对于运行时数据区的规定是非常宽松的。
拿堆举例子:堆可以是连续空间,也可以不连续。堆的大小可以固定,也可以在运行时按需扩展。虚拟机实现方可以使用任何的垃圾回收算法管理堆,甚至完全不进行垃圾收集也是允许的。
程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作的时候通过改变PC寄存器的值来获取下一条需要执行的字节码指令。
除此之外,为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程计数器之间互不影响,独立存储。
=> PC寄存器的的两个主要作用:
- 字节码解释器通过改变计数器的值依次获取指令,从而实现代码的流程控制,比如:顺序执行、选择、循环、异常处理
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来工作的时候能够继续上次运行位置继续执行
📢注意:程序计数器是唯一一个不出现OOM的内存区域,它的生命周期随着线程的创建而创建,随着线程的消亡而消亡。
Java虚拟机栈
和程序计数器一样,Java虚拟机栈也是线程私有的,生命周期和线程相同。
栈是JVM运行时数据区的一大核心,除了一些native方法调用是通过本地方法栈实现的之外,其他所有的Java方法调用都是通过栈来实现的,这期间,也需要和其他运行时数据区(比如程序计数器)配合。
方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
栈由一个个栈帧组成,每个栈帧中拥有:局部变量表、操作数栈、动态链接、方法返回地址。Java虚拟机栈只支持入栈和出栈操作。
局部变量表
主要存放了编译器可知的各种数据类型(boolean, byte, char, short, int, long, float, double)、对象引用(reference类型)。
reference类型不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置。
操作数栈
主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间结果,此外,计算过程中产生的临时变量也会存放在操作数栈中。
动态链接
主要应用于一个方法需要调用其他方法的场景。字节码文件的常量池中保存有大量的符号引用,比如方法引用的符号引用。当一个方法要调用其他方法的时候,需要将常量池中指向方法引用的符号引用转化为其内存地址中的直接引用。
动态链接的作用就是为了将符号引用转换为调用方法的直接引用。
出现在栈中的错误
栈空间虽然不是无限的,但是一般正常调用方法的情况下是不会出现问题的。但是如果函数调用陷入了无限循环,就会导致栈中被压入了太多栈帧而占用太多空间,导致栈空间过深。当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就会抛出StackOverFlowError
错误。
Java方法的两种返回方式:
- return语句正常返回
- 排除异常后异常返回
不论是哪种返回方式,都会导致栈帧被弹出 => 栈帧随着方法调用而创建,随着方法结束而销毁。
除了SOF错误之外,栈还有可能出现OOM错误,这是因为如果栈的内存大小是可以动态扩展的,如果虚拟机在动态扩展栈的时候没有申请到足够的内存空间,就会抛出OutOfMemoryError
异常。
总结:
- SOF错误:栈内存空间不允许动态扩展,当线程请求的栈深度超过当前Java虚拟机栈的最大深度,抛出SOF错误
- OOM错误:栈的内存大小是可以动态扩展的,虚拟机在动态扩展栈的时候没有申请到足够的内存空间,抛出OOM错误
本地方法栈
和Java虚拟机栈的作用类似,区别是:虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机执行Native方法服务。在HotSpot虚拟机中将二者合二为一。
堆
堆是Java虚拟机所管理的内存中最大的一块,Java堆是所有线程共享的一块区域,在虚拟机启动时创建,该内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都是在堆中分配。
”几乎“:随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术导致了所有对象都分配到堆中变得不那么绝对了。
Java堆是垃圾收集器管理的主要区域,也被成为GC堆。从垃圾回收的角度看堆,由于现在的垃圾收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为新生代(Eden、Survivor)、老年代等空间。进一步划分的目的是为了更好地回收内存或者更快的分配内存。
「JDK7 和 JDK8 堆内存区域的划分」
JDK8版本之后永久代被元空间取代,元空间使用的是本地内存。
大多数情况下,对象首先在Eden区域分配,在一次新生代垃圾回收之后,如果对象还存活,则会进入S0或者S1,并且对象的年龄加一,当它的年龄增加到一定程度(默认是15),就会被晋升到老年代中。
对象晋升到老年代的年龄阈值,可以通过参数
-XX:MaxTenuringThreshold
来设置。HotSpot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 Survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值。
方法区
方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。
《Java 虚拟机规范》只是规定了有方法区概念和它的作用,方法区到底要如何实现那就是虚拟机自己要考虑的事情了。也就是说,在不同的虚拟机实现上,方法区的实现是不同的。
当虚拟机要使用一个类时,它需要读取并解析Class文件获取相关信息,再将信息存入方法区。方法区会存储已被虚拟机加载的类信息、字段信息、方法信息、常量、静态变量、JIT编译后的代码缓存等数据。
「方法区、永久代、元空间之间的关系」
为什么要将永久代(PermGen)替换成元空间(MetaSpace)呢?
整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是本地内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
当元空间溢出时会得到如下错误:
java.lang.OutOfMemoryError: MetaSpace
可以使用
-XX:MaxMetaspaceSize
标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize
调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。元空间里面存放的是类的元数据,这样加载多少类的元数据就不由
MaxPermSize
控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。在JDK8合并HotSpot和JRockit的代码时,JRockit从来没有一个叫永久代的东西,合并之后就没有必要额外的设置这么一个永久代的地方了。
运行时常量池
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量和符号引用的 常量池表(Constant Pool Table) 。
字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量。常见的符号引用包括类符号引用、字段符号引用、方法符号引用、接口方法符号。
常量池表会在类加载后存放到方法区的运行时常量池中。运行时常量池的功能类似于传统编程语言的符号表,尽管它包含了比典型符号表更广泛的数据。既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出
OutOfMemoryError
错误。
字符串常量池
字符串常量池是JVM为了提升性能和减少内存消耗针对字符串专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
JDK7之前,字符串常量池存放在永久代。JDK7字符串常量池和静态变量从永久代移动了Java堆中。原因是因为永久代的GC回收效率太低,只有在Full GC的时候才会被执行GC。Java程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放在堆中,能够更高效及时地回收字符串内存。
直接内存
直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的。
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致
OutOfMemoryError
错误出现。
JDK1.4中新加入的 NIO(Non-Blocking I/O,也被称为 New I/O),引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在Java堆和Native堆之间来回复制数据。
直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。