Java内存区域与内存溢出异常
@(Java虚拟机)[jvm, 内存]
[TOC]
运行时数据区域
Java虚拟机执行Java程序时会将内存分为不同的数据区域。
程序计数器
PC可看作当前线程执行字节码的行号指示器。执行Java方法时PC记录的是正在执行虚拟机字节码指令的地址,如果是Native方法,则计数器的值为空。在Java虚拟机规范中唯一一个没有规定OutOfMemoryError的区域
Java虚拟机栈
线程私有,和线程的生命周期相同。
每个方法执行时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
每个方法从调用到执行完毕,就是栈帧的入栈出栈过程。
(传说中堆内存(Heap)和栈内存(Stack)中的栈)-->关注的局部变量部分
规范中规定了两种异常:
- StackOverflowError :线程请求的栈深度超过虚拟机的允许范围。
- OutOfMemoryError: 虚拟机可以动态扩展(规范中允许固定长度的虚拟机栈),扩展时无法申请足够的内存。
本地方法栈
为虚拟机使用到Native方法服务,规范没有强制规定实现。
同上也有两个异常
Java堆
Java虚拟机管理内存最大的一块,线程共享,虚拟机启动时创建,存放所有对象实例和数组(非绝对)。
Java堆是GC的主要区域,现在垃圾收集器基本都采用分代收集算法。后面详解
Java堆可以处于物理上不连续的内存空间,逻辑连续即可。实现可以是固定和可扩展的。主流按可扩展实现(通过-Xmx和-Xms控制)。
规定异常:
OutOfMemoryError:没有内存完成分配实例并无法扩展
方法区
线程共享,存储虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。规范把方法区描述为堆的一部分,但是有个别名Non-Heap,目的应该是区分Java堆。
HotSpot把GC分代手机扩展至方法区。实现方法区不受规范约束。HotSpot用永久代实现方法区会有一定问题(-XX:MaxPermSize上限)容易内存溢出,而且有极少数方法(例如String.intern())在不同的虚拟机有不同表现。HotSpot已经逐步放弃使用永久代实现方法区,改为采用Native Memory实现。在1.7JDK中将放在永久代的字符串常量池移除。
和Java堆一样可使用不连续的内存,可选择固定大小和可扩展,可选择不实现垃圾收集。该区域内存回收的主要目标是常量池和类型的卸载。
规定异常:
OutOfMemoryError:方法区无法满足内存分配需求时。
运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本,字段,方法,接口等描述信息,还有常量池,用于存放编译期生成的各种字面量和符号引用。
运行时常量池具备动态性,Java语言不要求常量编译期才能生成,运行期间也可将新的常量放入池中,如String的intern方法。
异常同方法区
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域,这部分内存频繁使用,也可导致OutOfMemoryError。
NIO,引入了基于通道与缓冲区的IO方式,可以使用Native函数库直接分配堆外内存,通过存储在Java堆上的DirectByteBuffer对象作为这块内存的引用进行操作。
受本机总内存大小和处理器寻址空间的限制。配置虚拟机参数时不能忽略。不能使内存区域总和大于物理内存限制。
HotSpot虚拟机对象探秘
剖析HotSpot的Java堆的对象分配,布局和访问过程。
对象的创建
虚拟机遇到new时,先检查这个指令的参数是否能找到一个类的符号引用,在检查该代表的类是否被加载,解析和初始化过。没有则先执行类加载过程。
类加载检查通过后,虚拟机需要为新生对线分配内存,大小在类加载完成后便已经确定。
分配内存方式分为两种:
- 指针碰撞:Java堆内存规整,分配时移动指针即可
- 空闲列表:Java堆内存不规整,需要记录那些内存块可用。分配完时应更新记录。
Java堆是否规整又由垃圾收集器是否带压缩整理功能决定。
Java堆是线程共享的,存在线程安全问题。解决办法两种:
- 对分配内存动作同步处理:虚拟机采用CAS+失败重试保证更新操作的原子性。
- 把内存分配按线程划分在不同空间下:每个线程在Java堆中预先分配一块内存,称本地线程分配缓冲(TLAB),TLAB用完时并重新分配TLAB才需要同步锁定。通过-XX:+/-UseTLAB设定是否使用TLAB
内存分配完成后需要对空间初始化话零值。可以在Java代码中使用不赋初始值的字段。然后虚拟机需要对对象进行设置,如该对象是那个类的实例,如何找到类的元数据信息,对象的哈希吗,对象的GC分代年龄等信息。这些信息存放在对象头中。
从Java视角看对象的< init >方法还没执行,还需要执行init方法。
对象的内存布局
Java对象头mark word存储自身运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。
Java数组对象头:
- java数组对象头在未压缩指针情况下16B+8B(存储数组长度)=24B
- java数组对象头在压缩指针情况下12B+4B(存储数组长度)=16B
reference压缩下占4B
测试代码:
使用详情jvm-obj-size
public class ObjSizeFetcherTest {
public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {
long size = 0;
long fullSize = 0;
Object obj=new Object();
size = ObjSizeFetcher.sizeOf(obj);
fullSize = ObjSizeFetcher.fullSizeOf(obj);
msg("obj", size, fullSize);
// +UseCompressedOops: size = 16=12(对象头)+0(实例数据)+4(对齐填充), fullSize = 16
// -UseCompressedOops: size = 16=16(对象头)+0(实例数据)+0(对齐填充),fullSize = 16
A a = new A();
size = ObjSizeFetcher.sizeOf(a);
fullSize = ObjSizeFetcher.fullSizeOf(a);
msg("a", size, fullSize);
// +UseCompressedOops: size = 16=12(对象头)+4*1(实例数据)+0(对齐填充), fullSize = 16
// -UseCompressedOops: size = 24=16(对象头)+4*1(实例数据)+4(对齐填充), fullSize = 24
B b = new B();
size = ObjSizeFetcher.sizeOf(b);
fullSize = ObjSizeFetcher.fullSizeOf(b);
// +UseCompressedOops: size = 24=12(对象头)+4*2(实例数据)+4(对齐填充), fullSize = 24
// -UseCompressedOops: size = 24=16(对象头)+4*2(实例数据)+0(对齐填充), fullSize = 24
msg("b", size, fullSize);
size = ObjSizeFetcher.sizeOf(new int[0]);
fullSize = ObjSizeFetcher.fullSizeOf(new int[0]);
msg("int[0]", size, fullSize);
// +UseCompressedOops: size = 16=12(对象头)+4(存放长度)+0(实例数据)+0(对齐填充), fullSize = 16
// -UseCompressedOops: size = 24=16(对象头)+8(存放长度)+0(实例数据)+0(对齐填充), fullSize = 24
size = ObjSizeFetcher.sizeOf(new Integer[0]);
fullSize = ObjSizeFetcher.fullSizeOf(new Integer[0]);
msg("Integer[0]", size, fullSize);
// +UseCompressedOops: size = 16, fullSize = 16
// -UseCompressedOops: size = 24, fullSize = 24
size = ObjSizeFetcher.sizeOf(new int[2]);
fullSize = ObjSizeFetcher.fullSizeOf(new int[2]);
msg("int[2]", size, fullSize);
// +UseCompressedOops: size = 24, fullSize = 24
// -UseCompressedOops: size = 32, fullSize = 32
size = ObjSizeFetcher.sizeOf(new Integer[2]);
fullSize = ObjSizeFetcher.fullSizeOf(new Integer[2]);
msg("Integer[2]", size, fullSize);
// +UseCompressedOops: size = 24, fullSize = 24
// -UseCompressedOops: size = 40, fullSize = 40
size = ObjSizeFetcher.sizeOf(new float[0]);
fullSize = ObjSizeFetcher.fullSizeOf(new float[0]);
msg("float[0]", size, fullSize);
// +UseCompressedOops: size = 16, fullSize = 16
// -UseCompressedOops: size = 24, fullSize = 24
size = ObjSizeFetcher.sizeOf(new Float[0]);
fullSize = ObjSizeFetcher.fullSizeOf(new Float[0]);
msg("Float[0]", size, fullSize);
// +UseCompressedOops: size = 16, fullSize = 16
// -UseCompressedOops: size = 24, fullSize = 24
size = ObjSizeFetcher.sizeOf(new A[0]);
fullSize = ObjSizeFetcher.fullSizeOf(new A[0]);
msg("A[0]", size, fullSize);
// +UseCompressedOops: size = 16, fullSize = 16
// -UseCompressedOops: size = 24, fullSize = 24
size = ObjSizeFetcher.sizeOf(new char[2]);
fullSize = ObjSizeFetcher.fullSizeOf(new char[2]);
msg("char[2]", size, fullSize);
// +UseCompressedOops: size = 24, fullSize = 24
// -UseCompressedOops: size = 32, fullSize = 32
String s = new String("aaaaaaaa");
size = ObjSizeFetcher.sizeOf(s);
fullSize = ObjSizeFetcher.fullSizeOf(s);
msg("s", size, fullSize);
//String有2个变量
// +UseCompressedOops: size = 24=12+4(hash)+4(value指针)+4(padding),fullSize = 56
// -UseCompressedOops: size = 32, fullSize = 72
C c = new C(a, b);
size = ObjSizeFetcher.sizeOf(c);
fullSize = ObjSizeFetcher.fullSizeOf(c);
msg("c", size, fullSize);
// +UseCompressedOops: size = 24=12(头)+2*4(引用)+4(填充), fullSize = 64=12+2*4(引用)+16(A)+24(B)+4(padding)
// -UseCompressedOops: size = 32, fullSize = 80
}
private static void msg(String obj, long size, long fullSize) {
System.err.println(obj + "--> size = " + size + ", fullSize = " + fullSize);
}
}
class A {
int a;
}
class B {
int a;
int b;
}
class C {
A a;
B b;
public C(A a, B b) {
this.a = a;
this.b = b;
}
}
对象的定位访问
两种主流方式:
- 句柄
- 直接指针
句柄方式需要在内存中划分一块作为句柄池。
直接指针方式需要考虑如何放置访问类型数据的相关信息,reference中存储的是对象的地址。
两种方式各有优势:
句柄方式最好的是reference中稳定的句柄地址。对象移动改变句柄的实例数据指针即可。reference不需改变。
直接访问方式最好的是速度快,节省指针定位开销。HotSpot采用直接访问方式。