2.2 运行时数据区
方法区、堆、执行引擎、本地库接口
虚拟机栈、本地方法栈、程序计数器
加粗是所有线程共享的数据区,其他是线程隔离的数据区
2.2.1 程序计数器
是较小的内存空间,是当前线程执行的字节码的行号指示器。字节码指示器就是通过改变这个计数器的值来选下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等都需要这个计算器来完成。
多个线程来回切换于处理器上,为保证线程切换后恢复到正确执行位置,每个线程都有一个独立的程序计数器。
如果线程正执行的是一个Java方法,计数器记录的是正执行的虚拟机字节码指令的地址;如果是native方法,计数器值为空。
此区域是唯一一个虚拟机中没规定OutOfMemoryError情况的区域。
2.2.2 虚拟机栈
虚拟机栈是线程私有的,它生命周期和线程相同。是Java方法执行的内存模型。
方法在执行同时会创建栈帧用于存储局部变量表、操作数栈、动态链接、方法出口信息。
方法从调用到执行完全,代表栈帧就是在虚拟机栈中入栈到出栈。
虚拟机栈也就是虚拟机栈中局部变量表部分。
局部变量表存放各种基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址)。
64位长度的long和double类型数据会占用2个局部变量空间slot,其余数据类型只占1个。局部变量表内存空间在编译器完成分配,方法运行期间不会改变局部变量表的大小。
有两种异常情况:线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;虚拟机栈可动态扩展,如果扩展时无法申请到足够内存,就抛出OutOfMemoryErrory异常。
2.2.3 本地方法栈
和虚拟机栈作用是相似的,本地方法栈则为虚拟机使用的native方法服务。本地方法也会抛出出StackOverflowError和OutOfMemoryErrory异常。
2.2.4 Java堆
堆是虚拟机管理的内存最大的一块,堆是所有线程共享的一块内存区域,在虚拟机启动创建。唯一目的就是存放对象实例。
所有的对象实例以及数组都要在堆上分配。
Java堆是GC管理的主要区域,收集器都采用分代收集算法,所以Java堆细分为:
新生代和老年代;再细一点有:Eden空间、From Survivor空间、To Survivor空间。
无论如何划分,都与存放内容无关,各空间存储的都是对象实例。
堆可以处于不连续的空间中,只要逻辑连续。堆是可以扩展的,通过-Xmx和-Xms控制。
如果堆中没有内存完成实例分配,堆也无法扩展,就抛出OutOfMemoryErrory异常。
2.2.5 方法区
和堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区也称为永久代,HotSpot选择把GC扩展至方法区,并不是所有虚拟机都是方法区就是永久代的。
现在会将放弃永久代逐步用Native Memory实现方法区,把永久代的字符串常量池移出。
方法区和堆一样不需要连续的内存和可以选择固定大小和可扩展。
在这个区域的GC行为比较少出现,但是也会有GC,当方法区无法满足内存分配需求,就抛出OutOfMemoryErrory异常。
2.2.6 运行时常量池
是方法区的一部分。Class文件中有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分在类加载后进入方法区的运行时常量池存放。
运行时常量池具备动态性,运行期间也可以将新的常量放入池中,例如String类的intern方法。
2.2.7 直接内存
不是运行时数据区的一部分,但该内存被频繁使用,也可能导致OutOfMemoryErrory异常。
这个内存和NIO类有关,直接内存不受堆大小的限制。但是还会受到本机物理内存大小的限制。当进行参数配置时,有些时候忽略了直接内存,使各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryErrory异常。
2.3 HotSpot虚拟机对象
讲解Hotspot虚拟机和内存区域的Java堆,堆中对象分配、布局和访问全过程。
2.3.1 对象创建
当虚拟机遇到new指令,先去检查指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有,就先执行类加载。
类加载后,虚拟机为新生对象分配内存。对象的内存大小在类加载后便可确定,对象分配空间的任务相当于把一个确定大小的内存从Java堆中分出来。
虚拟机维护一个列表,记录那些内存块是可用的,分配给对象的时候会更新记录。
对象创建是虚拟机中非常频繁的行为,修改指针指向内存的位置,在并发下也不是线程安全的。这有两种解决方法:第一个是对分配内存空间的动作进行同步处理,虚拟机采用CAS加失败重试保证更新操作的原子性;
第二种是将内存分配按线程划分在不同的空间中。
每个线程在Java堆先分配一小块内存,称为本地线程分配缓冲,各个线程在自己的TLAB上分配,TLAB用完并分配新的TLAB时,才同步锁定。
之后就是虚拟机对对象进行设置,该对象是哪个类的实例、找到类的元数据信息、对象哈希码、对象GC分代年龄等。
对虚拟机来说新对象已经产生,但从程序看对象刚才开始创建,此时init方法还没有执行。
2.3.2 对象内存布局
对象在内存中存储的布局分为3块区域:对象头、实例数据、对齐填充。
对象头包含两部分:
第一个存储对象自身运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。
对象头另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过该指针确定这个对象是哪个类的实例。
如果对象是数组,对象头还必须有用于记录数组长度的数据,虚拟机可以用过元数据信息确定对象的大小,但是数组不行。
实例数据部分是对象真正存储的有效信息,也是程序中定义的各种类型的字段内容。无论是父类继承还是子类定义的。
对齐填充不是必然存在也没什么含义。起着占位符的作用。对象起始地址必须是8字节的整数倍,对象实例数据需要通过该方式补全。
2.3.3 对象的访问定位
程序通过栈上的reference数据操作堆上的具体对象。
目前主流访问对象的方式有两种:句柄和直接指针。
1. 使用句柄,堆会划分出一块内存作为句柄池,reference存储的就是对象的句柄地址,句柄中包含对象实例数据和类型数据各自的具体地址信息。
2. 直接指针访问,堆对象的布局中必须考虑如果放置访问类型数据的相关信息,reference存储的直接就是对象地址。
这两种各有优势:
句柄好处就是reference存储稳定的句柄地址,对象被移动时只会改变句柄中的实例数据指针,reference本身不会改变。
直接指针最大好处就是速度更快,节省一次指定定位的时间开销。Hotspot就是用直接指针。
2.4 OutOfMemoryError异常
2.4.1 Java堆溢出
堆用于存储对象实例,只要不断创建对象,保证GC到对象间有可达路径避免回收,数量到达最大堆容量限制就产生内存溢出。
堆大小为20M,不可扩展(堆最小值-Xms参数和最大值-Xmx参数设置一样可避免自动扩展)
参数:-XX:+HeapDumpOnOutOfMemoryError可让虚拟机出现内存溢出异常时Dump出堆转储快照以便分析
/**
* VM:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while(true){
list.add(new OOMObject());
}
}
}
堆得OOM异常时常见的内存溢出异常。
要解决这个区域的异常,首先用内存映像分析工具对dump出来的堆转储快照进行分析,确认内存中对象是否是必要的,判断到底是内存泄漏还是内存溢出。
如果是内存泄漏,通过工具进一步查看泄漏对象到GC Roots的引用链。掌握泄漏对象的信息和引用链信息,可以定位出泄漏代码位置。
不是泄漏,检查虚拟机对参数-Xmx和-Xms,看是否还可以调大,检查某些对象是否生命周期过长、持有状态时间过长,减少程序运行期的内存消耗
2.4.2 虚拟机栈和本地方法栈溢出
Hotspot不区分虚拟机栈和本地方法栈,-Xoss设置本地方法栈大小,是无效的
栈只由-Xss参数设置
关于虚拟机栈和本地方法栈有两种异常:
1. 如果线程请求栈深度大于虚拟机允许的最大深度,抛出StackOverflowError异常
2. 如果虚拟机扩展栈时申请不到足够内存空间,抛出OutOfMemoryError异常
-Xss参数减少栈内存容量。
/**
* VM:-Xss128k
*/
public class StackSOF {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
StackSOF oom = new StackSOF();
try{
oom.stackLeak();
}catch(Throwable e){
System.out.println(oom.stackLength);
throw e;
}
}
}
单线程下,无论是栈帧太大还是栈容量太小,都抛出StackOverflowError异常。
多线程下,每个线程分配内存越大,越容易产生内存溢出异常。
相对于内存溢出,出现StackOverflowError异常有错误栈可以阅读,较容易找出问题所在。默认情况下栈深度在1000-2000没问题,满足正常方法调用。
但如果多线程导致内存溢出,只能通过减少最大堆和减小栈容量换取更多线程。
创建线程导致内存溢出
/**
* VM:-Xss2m
*/
public class StackOOM {
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) throws Throwable {
StackOOM oom = new StackOOM();
oom.stackLeakByThread();
}
}
2.4.3 方法区和运行时常量池溢出
运行时常量池是方法区的一部分,JDK1.7开始在逐步去除永久代。
String.intern()是native方法。
作用:如果字符串常量池中包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串放到常量池中,返回此String对象的引用。
方法区用于存放Class相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。这些区域的测试,基本思路是运行时产生大量的类填满方法区。
借助CGLib直接操作字节码运行时生成大量动态类。
很多主流框架都会运用CGLib技术,因此很容易遇到这样的内存溢出异常。
/**
* VM:-XX:PermSize=10m -XX:MaxPermSize=10m
*/
public class MethodAreaOOM {
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{
}
}
2.4.4 本机直接内存溢出
可通过-XX:MaxDirectMemorySize指定直接内存,不指定默认和堆最大值一样
使用unsafe分配本机内存
/**
* VM:-Xmx20m -XX:MaxDirectMemorySize=10m
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe)unsafeField.get(null);
while(true){
unsafe.allocateMemory(_1MB);
}
}
}
直接内存导致的溢出,堆dump文件不会看出明显异常,如果发现OOM之后dump文件很小,程序又直接或间接调用了NIO,可以考虑一下。