一、JVM的内存区域
对于C、C++程序员来说,在内存管理领域,他们既拥有每一个对象的“所有权”,又担负着每一个对象生命开始到终结的维护责任。
对Java程序员来说,在虚拟机的自动内存管理机制的帮助下,不再需要为每个new操作去写匹对的 delete/free 代码,不容易出现内存泄露和内存溢出的问题。
1、内存区域
根据《Java虚拟机规范(Java SE 7版)》规定,Java虚拟机所管理的内存将包括以下几个运行时数据区域,如图:

线程私有的内存区域:
- 程序计数器:可看做当前线程执行字节码的行号指示器,字节码解释器工作时通过改变计数器的值来选择下一条所需执行的字节码指令
- 虚拟机栈:Java方法执行的栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用至执行完成的过程,都对应一个栈帧在虚拟机栈的入栈到出栈的过程
- 局部变量表:存放编译期可知的基本数据类型(boolean、byte、char、int等)、对象引用(reference类型)和 returnAddress类型(指向一条字节码指令的地址)
- 本地方法栈:Native方法执行的栈帧
所有线程共享的内存区域:
- 堆:存放对象实例和数组
- 方法区:存储被虚拟机加载的Class类信息、final常量、static静态变量、即时编译器编译后的代码等数据
- 运行时常量池:存放编译生成的各种字面量和符号引用,运行期间也可能将新的常量放入池中
2、对象的创建
在语言层面,创建对象(例如:clone,反序列化)通常是一个 new 关键字,而在虚拟机中,对象创建的过程是如何呢?
在虚拟机遇到 new 指令时:
1. 类加载:确保常量池中存放的是已解释的类,且对象所属类型已经初始化过,如果没有,则先执行类加载
2. 为新生对象分配内存:对象所需内存大小在类加载时可以确定,将确定大小的内存从Java堆中划分出来
- 分配空闲内存方法:
- 指针碰撞:假如堆是规整的,用过的内存和空闲的内存各一边,中间使用指针作为分界点,分配内存时则将指针移动对象大小的距离
- 空闲列表:假如堆是不规整的,虚拟机需要维护哪些内存块是可用的列表,分配时候从列表中找出足够大的空闲内存划分,并更新列表记录
- 对象创建在并发情况下保证线程安全:例如,正在给对象A分配内存,指针还没修改,对象B同时使用了原来的指针来分配内存
- CAS配上失败重试
- 本地线程分配缓冲TLAB(ThreadLocal Allocation Buffer):将内存分配动作按线程划分到不同空间中进行,即每个线程在Java堆中预先分配一小块内存
3. 将分配的内存空间初始化为零值:保证对象的实例在Java代码中可以不赋值就可直接使用,能访问到这些字段的数据类型对应的零值(例如,int类型参数默认为0)
4. 设置对象头:设置对象的类的元数据信息、哈希码、GC分代年龄等
5. 执行<init>方法初始化:将对象按照程序员的意愿初始化
3、对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局分为3个区域,如下图所示:

- 对象头(Header):
- MarkWord:存储对象自身的运行时数据,例如:哈希码HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等。考虑空间效率,MarkWord设计为非固定的数据结构,它根据对象的不同状态复用自己的空间,如下图所示:

-
- 指向Class的指针:即对象指向它的类的元数据的指针,虚拟机通过这个指针来确定是哪个类的实例
- 如果对象是Java数组,对象头中还需要一块记录数组长度的数据
- 实例数据(Instance Data):对象真正存储的有效信息,也是程序代码中定义的各种类型字段的内容
- 对齐填充(Padding):起占位符的作用。因为HotSpot VM的要求对象起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。当对象实例数据部分没有对齐时,需要对齐填充来补充
4、内存溢出异常
除程序计数器外,JVM其他几个运行时区域都可能发生OutOfMemoryError异常。
1. 堆内存溢出,OutOfMemoryError:java heap space
原因:Java堆用于存储对象实例,只要不断创建对象,并保证GC Roots到对象间有可达路径避免这些对象的GC,当对象数量达到堆的最大容量限制后就会产生OOM
解决方法:
- 通过参数 -XX:HeapDumpOnOutOfMemoryError 可以让虚拟机在内存溢出异常时Dump当前内存堆转储快照
- 通过内存映像分析工具(如:Eclipse Memory Analyzer)对Dump出的堆转储快照分析,判断是内存泄露还是内存溢出
- 如果是内存泄露:通过工具查看泄露对象的类型信息和它们到 GC Roots 的引用链信息,分析GC收集器无法自动回收它们的原因,定位内存泄露的代码位置
- 如果是内存溢出:检查堆参数 -Xms和-Xmx,看是否可调大;代码上检查某些对象生命周期过长,持有时间过长的情况,尝试减少程序运行期间内存消耗
2. 栈内存溢出,StackOverflowError
原因:
- StackOverFlowError异常:线程请求的栈深度大于虚拟机所允许的最大深度
- OutOfMemoryError异常:虚拟机扩展栈时无法申请足够的内存空间
解决方法:
- 检查代码中是否有死递归;配置 -Xss 增大每个线程的栈内存容量,但会减少工作线程数,需要权衡
