Java内存区域
运行时数据区域
程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器。
Java线程的多线程是通过线程轮流切换并分配处理器执行时间来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程之间程序计数器互不影响,独立存储。
Java虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有。每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就是一个对应栈帧入栈出栈的过程。
局部变量表:8中基本数据类型(boolean、byte、char、short、int、long、float、double)、对象引用、returnAddress类型。
虚拟机栈两种异常:
- StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度
- OutOfMemoryError:如果虚拟机可以动态扩展,如果扩展时无法申请到足够内存。
本地方法栈
本地方法栈与虚拟机栈作用相似,虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用的Native方法服务。
Java堆
Java堆是Java虚拟机管理的内存中最大的一块。Java堆是被线程共享的一块内存区域,在虚拟机启动时创建。
Java堆分类:
-
新生代:Eden空间、FromSurvivor空间、ToSurvivor空间
-
老年代
初始化参数: -
-Xms:初始堆内存空间
-
-Xmx:最大堆内存空间
如果在堆中没有完成内存分配,并且堆也无法再扩展,将会抛出OutOfMemoryError异常。
方法区
方法区与Java堆一样,是各个线程共享区域。用于存储虚拟机已加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
-XX:MaxPermSize设置上限。
运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
HotSpot虚拟机对象
对象创建
- 虚拟机遇到new指令,首先检查这个指令参数是否能够在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已加载、解析和初始化。如果没有,那必须先执行乡应的加载过程。
- 类加载检查过后,虚拟机为新生对象分配内存。
- 内存空间初始化为零值
- 对象必要设置,例如:对象是哪个类的实例、如何才能找到类的元数据信息、对象哈希码、GC分代年龄信息等。
对象内存布局
对象内存中存储布局分为三部分:对象头(Header)、实例数据、对齐填充。
1、对象头包含两部分信息:
- 第一部分用于存储自身运行时数据,如哈希码,GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据长度在32位和64位虚拟机中分别位32bit和64bit,官方称为Mark Word。
- 另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象属于哪个类。
2、实例数据部分是对象存储的有效信息,也是程序代码中所定义的各种类型的字段内容。
3、对齐填充并不是必然存在的,也没有特别含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统中要求对象起始地址必须是8字节的整数倍。而对象头部分刚好是8字节的倍数,因此,当实例数据部分没有对齐时,就需要通过对齐填充来补全。
对象的访问定位
- 句柄:如果使用句柄访问的话,Java堆会划分出一块内存区域来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象的实例数据与类型数据各自的具体地址信息。
- 直接指针:如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象的地址。
OutOfMemoryError异常
Java堆异常
Java堆用于存储对象实例,只要不断创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么对象数量到达最大堆的容量限制后就会产生内存溢出异常。
-Xms -Xmx可设置堆内存空间,-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出时dump当前内存堆转储快照以便事后进行分析。
解决这个区域异常,一般是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的快照进行分析,重点是确认对象是否是必要的,确定是内存泄漏还是内存溢出。
- 内存泄露:查看泄露对象的GC Roots引用链,找到泄露对象是通过怎样的路径与GC Roots关联并导致垃圾收集器无法自动回收他们,定位出泄露代码位置。
- 内存溢出:如果不存在泄露,就是内存中对象确实都还必须活着,则需检查虚拟机的堆参数(-Xms 与 -Xmx),调整内存大小。
虚拟机栈和本地方法栈溢出
栈容量由-Xss参数设定
- 如果线程请求的栈深度大于虚拟机栈所允许的最大深度,将抛出StackOverflowError
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError
方法区和运行时常量池溢出
-XX:PremSize和-XX:MaxPremSize设置
-
String.intern()是一个native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象,否则,将此String对象添加到常量池中,并返回此String对象的引用。通过此方法可以测试常量池溢出。
-
方法区存放Class信息,可以通过产生大量的类填满方法区,直至溢出。
生成类的方法: -
可以通过JavaSE API动态生成类(如反射时的GeneratedMethodAccessor和动态代理等)
-
CGLIB操作字节码
package com.java.one;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 借助CGLib使方法区出现内存溢出异常
* 借助CGLib直接操作字节码运行时产生大量的动态类
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* */
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) {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject {
}
}
本地直接内存溢出
可以通过-XX:MaxDirectMemorySize指定,如果不指定,默认与Java堆最大内存一样。
DirectMemoryOOM类直接通过反射获取Unsafe实例进行内存分配。
public class DirectMemoryOOM{
private static final int _1M = 1024 *1024;
public static void main(String[] args){
Field unsafeField = Unsafe.class.getDelaredFields() [0];
unsafeField.setAccessible(treu);
Unsafe unsafe = (Unsafe) unsafeFiled.get(null);
while(true){
unsafe.allocateMemory(_1M);
}
}
}