由于每个虚拟机的细节实现的不同,这里以HotSpot为例。
1. 对象的创建
第一步 - new指令
虚拟机首先去检查这个指令的参数是否可以在常量池中定位到一个类的符号引用,并检查这个符号引用是否已经被加载、解析和初始化过。如果没有,那必须先执行相应类的类加载过程。
- 划分内存的两种方式
# 指针碰撞
用过的内存和未使用的内存是分开的,中间有一个指针作为分界点的指示器,移动这个移动即可分配内存。
# 空闲列表
内存不连续,需要借助一个列表来记录哪些内存块是可以用的。- 线程安全
# 同步
采用CAS和失败重试,保证虚拟机更新操作的原子性。
# 本地线程分配缓冲(TLAB)
将每个线程划分到不同的内存空间中。虚拟机可以通过-XX:+/UseTLAB来设置是否使用TLAB。- 初始化
将分配的内存空间都初始化为0(不包括对象头)。- 对象的设置
哪个类创建的、如何才能找到类的元数据信息、哈希码、GC等。
第二步 - <init>方法
执行完new指令后,<init>方法把对象按照程序员的想法进行初始化。
2. 对象的内存布局
对象头
包含以下部分信息:
- 存储对象自身的运行时数据
哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。- 类型指针
指向对象的元数据的指针,虚拟机可以通过这个指针来确定这个对象是哪个类的实例。- 如果对象是一个Java数组,那在对象头中,还得有一块用于记录数组长度的数据。
实例数据
- 存储真正的有效信息。
- 这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。相同宽度的字段存储会分配到一起。
对齐填充
- 并不是必然存在的。
- 仅仅起着占位符的作用。使得对象的大小必须是8字节的整数倍。
3. 对象的访问定位
句柄
- Java堆中划分一块内存来作为句柄池,存储:到对象实例数据的指针、到对象类型数据的指针。
- 优势
Java栈本地变量表中reference存储的是稳定的句柄地址。不会随着对象的移动而改变。
直接指针
- 存储:对象实例数据、到对象类型数据的指针。
- 优势
速度更快。
4. OutOfMemoryError异常
Java堆溢出
Connected to the target VM, address: '127.0.0.1:49331', transport: 'socket' Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at HeapOOM$OOMObject.<init>(HeapOOM.java:11) at HeapOOM.main(HeapOOM.java:18) Disconnected from the target VM, address: '127.0.0.1:49331', transport: 'socket' Process finished with exit code 1
要确定是内存泄露,还是内存溢出。
- 内存泄露
进一步使用工具查看泄露对象到GC Roots的引用链。找到垃圾收集器无法回收的原因。- 内存溢出
检查虚拟机的堆参数(-Xmx和-Xms),看是否可以调大。
从代码上检查是否存在某些对象的生命周期过长、持有状态时间过长的情况。
虚拟机栈和本地方法栈溢出
栈容量通过-Xss设置。
两种异常:
- StackOverFlowError
- OutOfMemoryError
注:
在单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的异常都是StackOverFlowError。
方法区和运行时常量池异常
- 方法区用于存储Class的相关信息:类名、访问修饰符、常量池、字段描述、方法描述等。
- 在经常动态生成大量class的应用中,需要特别注意类的回收状况。
- CGLib、动态语言、大量JSP或动态产生JSP的应用、基于OSGi的应用。
本机直接内存溢出
- DirectMemory的容量,通过指定-XX:MaxDirectMemorySize指定。
- 明显特征
在Heap Dump文件中不会看见明显的异常。如果发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查一下是不是DirectMemomry异常。