深入理解java虚拟机学习笔记-第2章
Table of Contents
1 第2章 Java内存区域与内存溢出异常
1.1 运行时数据区域
- Java虚拟机运行时数据区
- 方法区(Method Area)
- 虚拟机栈(VM Stack)
- 本地方法栈(Native Method Stack)
- 堆(Heap)
- 程序计数器(Program Counter Register)
1.1.1 程序计数器
- 程序计数器(Program Counter Register)是一块比较小的内存空间,它可以看作是当前线程执行的字节码的行号指示器.
- 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令.
- 因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程计数器互不影响,独立存储,我们称这类内存区域为"线程私有"的内存.
- 如果线程正在执行的是一个Java方法,这个计数器记录的正是在执行的虚拟机字节码指令的地址.
- 如果正在执行的是Native方法,这个计数器则为空(Undefined).
- 此内存区域是唯一一个在Java虚拟机规范中没有任何OutOfMemoryError情况的区域.
1.1.2 Java虚拟机栈
- Java虚拟机栈(Java Virtual Machine Stacks)是线程私有的,它的生命周期与线程相同.
- 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息.每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程.
- 局部变量表存放了在编译期可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址).
- 其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个.
- 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小.
- 在Java虚拟机规范中,对Java虚拟机栈区域规定了两种异常情况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常.
- 如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,会抛出OutOfMemoryError异常.
1.1.3 本地方法栈
- 本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是:
- 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务.
- 本地方法栈则为虚拟机使用到的Native方法服务.
- 本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常.
1.1.4 Java堆
- Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块.
- Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建.此内存的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存.
- Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为"GC堆"(Garbage Collected Heap).
1.1.5 方法区
- 方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据.
- 方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常.
1.1.6 运行时常量池
- 运行时常量池(Runtime Constant Pool)是方法区的一部分.
- Class文件除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放.
1.1.7 直接内存
- 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域.但这部分内存也被频繁使用,也可能倒置OutOfMemoryError异常出现.
1.2 HotSpot虚拟机对象
1.2.1 对象的创建
- 假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,所有分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为"指针碰撞"(Bump the Pointer).
- 如果Java堆中的内存是不规整的,已使用的内存和空闲的内存相互交错,虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为"空闲列表"(Free List).
1.2.2 对象的内存布局
- 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
- HotSpot虚拟机的对象头包括两部分信息
- 一部分用于存储对象自身的运行时数据,如哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等.
- 另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,并不是所有虚拟机实现都必须在对象数据上保留类型指针(查找对象的元数据信息并不一定要经过对象本身).
- 实例数据部分是对象真正存储的有信息,也是在程序代码中所定义的各种类型的字段内容.这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源代码中定义顺序的影响.
- 对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用.
- 如HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,对象的大小必须是8字节的整数倍,不满足则对齐.
1.2.3 对象的访问定位
- 由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位,访问堆中的对象的具体位置,所以对象的访问方式是取决于虚拟机实现而定的.
- 主流的访问对象方式有
- 句柄
- 直接指针
- 使用句柄访问对象,Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息.
- 使用直接指针访问对象,Java堆对象的布局就必须考虑如何访问类型数据的信息,而reference中存储的就是对象的地址.
- 使用句柄来访问方式的最大好处就是reference中存储的是稳定的句柄对象,在对象被移动(垃圾收集时移动是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改.
- 使用指针访问方式的最大好处是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销也很大.
1.3 实战:OutOfMemoryError异常
1.3.1 Java堆溢出
- 将堆的最小值
-Xms
参数与最大值-Xmx
参数设置为一样即可避免堆自动扩展. - 通过参数
-XX:+HeapDumpOnOutOfMemoryError
可以让虚拟机在出现内存溢出异常时Dump出当前内存堆转储快照以便时候进行分析.
import java.util.ArrayList;
import java.util.List;
/**
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @author devinkin
*/
public class HeapOOM {
static class OOMObject {}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}
1.3.2 虚拟机栈和本地方法栈溢出
- 由于HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,栈容量只由
-Xss
参数设定. - 关于虚拟机栈和本地方法栈,在虚拟机规范中描述了两种异常
- 如果线程请求栈的深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常.
- 如果虚拟机在扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError异常.
- 虚拟机栈和本地方法栈OOM测试
/**
* VM Args: -Xss128k
* @author devinkin
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length: " + oom.stackLength);
throw e;
}
}
}
- 创建线程导致内存溢出异常
/**
* VM Args: -Xss2M(这时候不妨设置大些)
* @author devinkin
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
1.3.3 方法区和运行时常量池溢出
- String.intern()是一个Native方法,它的作用是:如果字符串常量池中包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象.否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用.
- JDK1.6及之前的版本中,由于常量池放在永久代内, 可以通过
-XX:PermSize
和XX:MaxPermSize
限制方法区大小,从而间接限制其中常量池的容量. - JDK1.7后开始逐步"去永久代".
- 运行时常量池导致的内存溢出异常
import java.util.ArrayList;
import java.util.List;
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* @author devinkin
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
// 使用List保持着常量池引用,避免Full GC回收常量池行为
List<String> list = new ArrayList<String>();
// 10MB的PermSize在integer范围内足够产生OOM了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
- 运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部分.
- String.intern()返回引用的测试
- JDK1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代这个字符串实例的引用.
- JDK1.7中的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个.
public class RuntimeConstantPoolOOM2 {
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);
}
}
- 借助CGLib使方法区出现内存溢出异常
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* @author devinkin
*/
public class JavaMethodAreaOOM {
static class OOMObject {}
public static void main(final String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, objects);
}
});
enhancer.create();
}
}
}
1.3.4 本机直接内存溢出
- DirectMemory容量可以通过
-XX:MaxDirectMemorySize
指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样. - unsafe分配本机内存
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}