面试常问的 Java 虚拟机运行时数据区

写在前面

本文描述的有关于 JVM 的运行时数据区是基于 HotSpot 虚拟机。html

概述

JVM 在执行 Java 程序的过程当中会把它所管理的内存划分为若干个不一样的数据区域。这些区域都有各自的用途,以及建立和销毁的时间,有的区域随着虚拟机的进程启动而存在,有的区域则依赖于用户线程的启动和结束而创建和销毁。java

HotSpot 运行时数据区

运行时数据区在 HotSpot 1.8 以前的版本和 1.8 版本有所不一样,主要是 方法区移到元空间 了。git

图 1-1:JDK1.8 以前 JVM 运行时数据区
图 1-2:JDK1.8 JVM 运行时数据区

线程私有区域

程序计数器(PROGRAM COUNTER REGISTER)

程序计数器是一块很小的区域,它存储的是当前线程正在执行的字节码的地址(在这里,其实有两个“当前”,一个是:当前正在被 CPU 执行的线程,另外一个是:当前这个被执行的线程中正在被执行的字节码指令)。字节码解释器工做时就是改变程序计数器的值来选取下一条须要执行的字节码。对于单核心而言,多线程是经过线程轮流切换的方式实现的,在任一时刻只有一个线程可以获得 CPU 的执行权从而执行线程中的字节码指令,所以,为了使线程切换后可以恢复到正在执行的字节码的位置,每一个线程都须要拥有本身的程序计数器。面试

注意:程序计数器是惟一的一块在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 的区域。因为它是线程私有的,因此它的生命周期随着线程的建立而建立,随着线程的结束而死亡 。算法

虚拟机栈(VM STACK)

虚拟机栈也是线程私有的,因此它的生命周期与程序计数器相同。虚拟机栈描述的是 Java 方法执行的内存模型。sql

每一个方法在执行的时候都会建立一个栈帧(一个方法对应一个栈帧,栈帧即栈的基本单位)用于存储局部变量表、操做数栈、动态连接、方法出口等信息。每一个方法被线程执行从开始到结束,就对应着一个栈帧在虚拟机栈中入栈(压栈)和出栈(弹栈)的过程。局部变量表中存放了编译可知的各类基本数据类型(byte,short,int,long,float,double,char,boolean)、对象引用(reference 类型,它存储的是:对象的地址或者是指向表明对象的句柄)。数组

Java 虚拟机规范中规定了虚拟机栈可能出现的两种异常情况:StackOverflowError 和 OutOfMemoryError。缓存

StackOverflowError: 若当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候会抛出 StackOverflowError。多线程

OutOfMemoryError: 若虚拟机栈动态扩展过程当中,若是线程请求申请栈空间没法申请到足够的内存,就会抛出 OutOfMemoryError。框架

本地方法栈(NATIVE METHOD STACK)

本地方法栈与虚拟机栈相似,虚拟机栈是执行 Java 方法开辟的内存空间,而本地方法栈是执行 Native 方法开辟的内存空间。

与虚拟机栈同样,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常,抛出条件也是相似的。

线程间共享的内存区域

堆(HEAP)

堆是全部线程共享的一块区域,主要用来存放对象和数组。

在 Java 虚拟机规范中有描述:全部的对象实例和数组都要在堆上分配,可是 随着 JIT(JUST-IN-TIME)编译器的发展与逃逸分析技术的逐渐成熟,并非全部对象都只在堆上分配了,好比:随着逃逸分析技术的逐渐成熟,在即时能被回收的对象也有可能会在虚拟机栈上分配。

因为如今都采用分代回收算法,因此从内存回收的角度来看,堆还能够细分为:新生代、老年代。新生代又能够分为:Eden 空间、From Survivor 空间、To Survivor 空间。

注意:1.8 中已经完全将方法区的实现由以前的永久代改成元空间。

方法区(METHOD AREA)

方法区和堆同样也是全部线程共享的一块区域,主要用来存储已经被虚拟机加载的类信息、常量(final 修饰的)、静态变量、即时编译器(JIT)编译后产生的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,可是它却有一个别名叫作 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

永久代就是方法区域?

早些时候,不少开发者更愿意称方法区为“永久代”。其实“永久代”这个称呼的由来是由于 HotSpot 团队并不打算为方法区从新设计垃圾回收算法,为了在方法区中可以沿用堆中的分代回收算法,因此按照堆中的命名方式,将方法去称为“永久代”。对于 JRocket、J9 而言是不存在“永久代”的概念的,因此当 HotSpot 1.8 和 JRocket 合并时,就完全放弃了“永久代”的概念(其实从 1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。

方法区的垃圾回收很困难!!!

因为 Java 虚拟机规范对方法区的限制很是松,甚至能够不实现垃圾回收,通常而言,这个区域的内存回收很不使人满意,尤为是类型的卸载,条件很是苛刻,可是因为现代框架大量的依赖于 JIT 技术,致使方法区的占用比逐渐提升,因此对于方法区的回收相当重要。根据 Java 虚拟机规范规定,当方法区没法知足内存分配需求时,将抛出 OutOfMemoryError 异常。

运行时常量池(RUNTIME CONSTANT POOL)

JDK1.7 及以后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

这块区域在 1.7 以前原来是方法区的一部分,Class 文件中有一项信息是常量池(或者说是一张常量表,Class 文件以表存储数据)。

图 1-3:Class 文件常量池

运行时常量池存储的东西较为复杂,主要分为字面量和符号引用

字面量

存放的字面量主要包括 常量(final 修饰的),好比:final int x = 1、静态变量(static 修饰的),还有一些其余的字面量。

符号引用

符号引用主要包括:类的彻底限定名、字段名称和描述符、方法名称和描述符,包括不少符号,好比:() 也能够看作符号引用。

字面量和符号引用将在类加载(ClassLoader 加载 Class 字节码文件)后进入方法区的运行时常量池中存放。不过,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。运行时常量池相对于 Class 文件常量池一个重要的特征就是具有动态性,Java 语言并不要求常量必定产生于编译期的 Class 文件的常量池中,也并非只有 Class 文件常量池中的常量才可以进入运行时常量池中,在线程执行方法的过程中可能产生新的常量存放到运行时常量池中,例如:String 类的 intern() 方法。当运行时常量池没法申请到内存的时候就会抛出 OutOfMemoryError 异常。

直接内存(DIRECT MEMORY)

直接内存并非 JVM 运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,可是这部份内存也被频繁地使用。并且也可能致使 OutOfMemoryError 错误出现。

在 JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它能够直接使用 Native 函数库直接分配堆外内存,而后经过一个存储在 Java 堆中的 DirectByteBuffer 对象做为这块内存的引用进行操做。这样就能在一些场景中显著提升性能,由于避免了在 Java 堆和 Native 堆之间来回复制数据。

本机直接内存的分配不会受到 Java 堆的限制,可是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

总结

Java 虚拟机包含的内容不少,本篇文章也只是对 Java 内存管理模块的 Java 虚拟机运行时数据区作了简要的分析,关于内存管理模块的其余部分后续会继续更新,敬请期待!

参考

公众号

若是你们想要实时关注我更新的文章以及我分享的干货的话,能够关注个人公众号 咱们都是小白鼠。公众号内有一些整理过的 原创精品脑图,不只包含技术点的知识脉络,更多的底层原理的梳理,目前涵盖 Redis,RabbitMQ,Mysql,Java 虚拟机等 ,这些都是博主本身的学习笔记,整理的过程花费了不少心血,除此以外还有一些整理过的 面试题 以及平常开发经常使用到的一些 开发工具 等,在公众号内分别回复【技术脑图】、【面试题】、【开发工具】便可获取。

干活分享

相关文章
相关标签/搜索