《深入理解Java虚拟机》第2章读书笔记与实验记录。
1、对象创建内存分配方式
- 指针碰撞: Java堆内存绝对规整,使用指针作为分界点指示器
- 空闲列表: 已使用的内存的空闲内存相互交错,虚拟机需要维护一个列表,记录哪些内存可用
2、对象内存布局
HotSpot虚拟机中对象内存布局可以分为三块区域:对象头、实例数据、对齐数据
3、对象访问定位
- 句柄访问:Java堆划分出一块内存区域作为句柄池,句柄中包含了实例数据和类型数据的具体地址信息,reference则存储的是对象的句柄地址,如下图所示:
优势:reference中存储的是稳定的句柄地址,在对象被移动(垃圾回收时移动对象时普遍的行为)时只改变句柄中实例数据指针,reference无需更改。
- 直接指针访问:reference中存储的直接是对象地址,Java堆中对象的布局必须考虑如何存放访问类型数据相关信息,如下图所示:
优势:访问速度快,节省了一次指针定位的开销,由于对象的访问在java中非常频繁,因此这类开销极少成多后也是一项非常可观的执行成本。Sun HotSpot也是使用该种方式进行对象访问的。
4、实战:OutOfMemoryError异常
4.1Java堆溢出
虚拟机参数配置: -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
代码清单:Java堆内存溢出异常测试
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List list = new ArrayList();
while(true) {
list.add(new OOMObject());
}
}
}
运行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid10816.hprof ...
Heap dump file created [28348501 bytes in 0.080 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at com.mpoom.jvm.HeapOOM.main(HeapOOM.java:21)
通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析:
4.2虚拟机栈和本地方法栈溢出
在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此,对于HotSpot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定。
关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
虚拟机参数配置:-Xss128K
代码清单:虚拟机栈和本地方法栈OOM测试(仅作为第1点测试程序)
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength ++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF sof = new JavaVMStackSOF();
try {
sof.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + sof.stackLength);
throw e;
}
}
}
运行结果:
stack length:19211
Exception in thread "main" java.lang.StackOverflowError
at com.mpoom.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
at com.mpoom.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
at com.mpoom.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
at com.mpoom.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
at com.mpoom.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
......
实验结果表明:在单个线程下,无论是由于栈帧太大还是由于虚拟机容量太小,当内存无法分配的时候,虚拟机都抛出的是StackOverflowError异常。
如果测试不局限于单线程,通过不断建立线程倒是可以产生内存溢出异常
虚拟机参数配置:-Xss2M
代码清单: 创建线程导致内存溢出异常
public class JavaVMStackOOM {
private void dontStop() {
while(true) {}
}
public void stackLeakByThread() {
while(true) {
Thread thread = new Thread(new Runnable() {
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
注意:在windows平台上运行上述代码有风险,会导致操作系统假死(做好重启的准备— _ —)。因为在windows在平台的虚拟机中,Java的线程是映射到操作系统的内核线程上的。
本次实验由于系统假死,没有查看到,下面的实验结果是从别处获取的:
Exception in thread"main"java.lang.OutOfMemoryError:unable to create new native thread
4.3方法区和运行时常量池溢出
由于运行时常量池属于方法区的与部分,两个区域的溢出测试放在一起。
4.3.1运行时常量池溢出
String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等
于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包
含的字符串添加到常量池中,并且返回此String对象的引用。
虚拟机参数配置:-XX:PermSize=10M-XX:MaxPermSize=10M
JDK:1.6
代码清单:运行时常量池导致的内存溢出异常
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
// 使用List保持着常量池引用, 避免Full GC回收常量池行为
List list = new ArrayList();
int i = 0;
while(true) {
list.add(String.valueOf(i++).intern());
}
}
}
运行结果
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at com.mpoom.jvm.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)
从运行结果中可以看到,运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是“PermGen space”,说明运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部分。
更改JDK版本为1.8,代码中的while循环将一直执行下去。关于这个字符串常量池的实现问题,还可以引申出一个更有意思的影响,代码如下:
JDK:1.8
代码清单:String.intern()返回引用的测试
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
String str1=new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern()==str1);
String str2=new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern()==str2);
}
}
运行结果
true
false
这段代码在JDK 1.6中运行,会得到两个false,而在JDK 1.7中运行,会得到一个true和一个false。 产生差异的原因是:在JDK 1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。 而JDK 1.7(以及部分其他虚拟机,例如JRockit)的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。 对str2比较返回false是因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true。
4.3.2方法区溢出
方法区用于存放Class的相关信息,如类名、 访问修饰符、 常量池、 字段描述、 方法描等。 对于这些区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。这里借助CGLib[1]直接操作字节码运行时生成了大量的动态类。
虚拟机参数配置:-XX:PermSize=10M-XX:MaxPermSize=10M
JDK:1.6
Maven依赖
cglib
cglib
2.1_3
代码清单:借助CGLib使方法区出现内存溢出异常
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
}
);
enhancer.create();
}
}
static class OOMObject {
}
}
运行结果
Caused by: java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631)
at java.lang.ClassLoader.defineClass(ClassLoader.java:615)
... 8 more
方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是比较苛刻的。 在经常动态生成大量Class的应用中,需要特别注意类的回收状况。
注意:上述在JDK1.6环境下实验的,JDK1.8下while循环将一直运行下去。
4.4本机直接内存溢出
DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样。下面代码越过了DirectByteBuffer类,直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回实例,也就是设计者希望只有rt.jar中的类才能使用Unsafe的功能)。 因为,虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。
虚拟机参数配置:-Xmx20M -XX:MaxDirectMemorySize=10M
JDK:1.8
代码清单:使用unsafe分配本机内存
public class DirectMemoryOOM {
private static final int _1MB = 1024*1024;
public static void main(String[]args)throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}
运行结果
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.mpoom.jvm.DirectMemoryOOM.main(DirectMemoryOOM.java:21)
总结:本章主要学习了虚拟机的内存是如何划分的,哪部分区域、什么样的代码和操作会导致内存溢出异常。