前言
最近打算以后还是继续走安卓原生开发之路,然后之前看过大厂安卓工程师的面试题,里面有很多 jvm 的题目,所以就决定要好好学习一下 Java 虚拟机了。

然后又因为直接学的话不那么容易掌握,打算边学边写博客,这样记忆起来更加深刻,然后因为学业繁忙,更新起来估计会比较慢,废话不多说,开始学习吧!
运行时数据区域
先来一张我自己画的总体概览图
图中说明了数据区的两个部分:线程共享数据区和线程独有数据区,下面一个一个区域来分别进行介绍。
程序计数器
就像在计算机组成原理里学到的程序计数器一样,是用来存储程序运行的当前代码的地址的,不过与之不同的是,这是用软件而不是硬件实现的。它是一块很小的内存区域,每个进程都有一个自己的程序计数器,互不干扰,独立存储。
值得注意的是,只有运行的是 Java 方法的时候计数器才有值,记录的是当前执行的字节码指令的地址;如果是 native 方法,则计数器值为空。由于记录的只是地址,自然也不会产生OutOfMemoryError
。
虚拟机栈
也是属于进程独有的内存空间,其生命周期与进程相同。既然虚拟机栈是以“栈”字结尾,说明它本身就是一个栈,其中的元素是一个叫“栈帧”的数据类型。


其实,我们平常说的堆内存和栈内存中的栈内存就是对应的虚拟机栈,或者更具体一点,是对应虚拟机栈中的局部变量表部分。
局部变量表里存储了编译器可知的各种基本数据类型(boolean,byte,char,short,float,long,double)、对象引用以及 returnAddress 类型(书上说是一条字节码指令的地址,还不知道是干什么用的)
要注意上面的加粗部分,编译器可知,也就是说局部变量表所需的内存空间在编译阶段就可以完全确定,在方法运行期间不会修改局部变量表的大小。
由于虚拟机栈是有大小限制的,栈深度大于虚拟机规定的深度时会抛出 StackOverflowError
异常。
本地方法栈
与虚拟机栈类似,不过运行的不是 Java 代码,而是虚拟机所用到的Native方法服务,有些虚拟机甚至把虚拟机栈和本地方法栈合二为一。内存不足时按情况不同会抛出StackOverflowError
或OutOfMemoryError
。
堆
Java 堆是 Java 虚拟机所管理的内存中最大的一块了,也是所有线程都共享的一块内存区域,在虚拟机启动时创建。按照虚拟机规范,所有的对象实例以及数组都是在堆上分配的,但随着新技术的发展,这句话也变得没那么“绝对”了。
由于现在收集器都普遍采用分代收集算法(将在下一节简要介绍),所以Java 堆还可以细分为新生代和老年代两个部分,用来存放不同年龄的对象,但无论位置如何,它们里面存放的都是对象实例。
现在主流的Java堆都可以设置其最小和最大的大小(通过 -Xmx 和 -Xms 控制)。如果在堆中没有内存可以完成实例分配,堆也不能再拓展大小时会抛出OutOfMemoryError
。
方法区
这个区域对我们来说相对没那么熟悉,它存储的是已被虚拟机加载的类信息、常量、静态变量等。虽然这些存储的内容基本都是静态的,运行时基本不会增多,但依旧有可能产生内存溢出问题。例如Spring和Hibernate等主流框架,它们都可以动态增加类,也就是已被虚拟机加载的类信息会增多,这样方法区也有可能出现内存溢出的问题,也需要多多留意。
HotSpot 虚拟机对象探秘
在了解完虚拟机里的运行时数据区域的划分之后,就让我们进入内存分配中更重要的一个细节——对象。
对象的创建
- new 指令的参数能否在常量池中找到这个类的符号定义,并检查这个类是否已被加载、解析和实例化。
- 为对象分配内存,即从堆中找出一块确定大小的空余空间分配给对象。
- 将分配到的内存空间都初始化为零(不包括对象头)。这也是为什么对象的成员变量可以不初始化就直接使用的原因。
- 对对象进行必要的设置,例如对象属于哪个类的实例、对象的哈希码等等,它们都保存在对象头之中。
- 调用类的构造函数。
对象的内存布局
在 HotSpot 虚拟机中,对象的存储布局可分为3块区域,分别是对象头、实例数据和对齐填充。
对象头有两个部分,第一个部分是存储对象自身运行时的一些数据,官方称它位“Mask Word”,例如哈希码、GC 分代年龄、锁状态标志、线程持有的锁等等。对于32位的虚拟机来说,这部分数据只有32字节,但这个容量不足以存储下所有的这些信息,所以就用了共用体的结构,即不同的数据元素共享同一段存储空间,然后用另一个锁标志位变量来判断存储的是哪些数据。具体存储策略可以看下面这张表。

对象的访问方式
虚拟机规范并没有强制规定对象引用该如何访问堆中的对象。目前主流的访问方式有两种:
句柄访问
这种情况下,Java 堆中会划分一个区域来专门作为句柄池,句柄中包含了类对象实例数据和类型数据的地址信息。
它的优点是在对象被移动时只需要修改句柄中实例数据的地址信息,而不需要修改引用本身。
直接指针访问

它的主要优点是因为只有一个指针,所以对象的访问速度比较快,由于对象的访问在 Java 中非常频繁,所以这类开销积少成多也能积累出非常可观的执行成本。而 Sun HotSpot也是才用直接指针访问对象的。
总结
本篇文章介绍了虚拟机中的内存是如何划分的以及对象的创建和内存布局,了解它们之后可以对产生内存溢出的原因更加明确,从而迅速提出解决方案。
网友评论