Java虚拟机运行时数据区
程序计数器(线程私有)
是一个较小的内存区域,可以看做是当前线程所执行的字节码的行号指示,由于Java是多线程的,线程中断切换需要恢复执行位置,所以每个线程都需要一个独立的程序计数器来记录当前线程执行的位置。虚拟机栈(线程私有)
是Java方法执行的内存模型,生命周期和线程相同。每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口灯信息。每个方法调用到执行完成,就是对应栈帧在虚拟机栈中入栈出栈的过程。
局部变量表用于存放基本数据类型、对象引用、returnAddress类型(执行字节码指令地址)
局部变量表所需内存空间在编译期间完全分配,当进入一个方法需要在帧中分配多少局部变量空间完全确定,运行期间不会改变。
如果线程请求的栈深度大于虚拟机允许的深度,抛出StackOverflowError;
如果虚拟机栈可以动态分配(大部分都可以),但无法得到足够的内存,抛出OutOfMemoryError;本地方法栈(线程私有)
与虚拟机栈一样,区别在于。虚拟机栈是为执行Java方法服务,本地方法栈是为执行Native方法服务,没有规定具体的语言,所以虚拟机可以自由实现。Java堆/Gc堆(线程共享)
是Java虚拟机所管理的内存最大的一块。在虚拟机启动的时候创建,唯一目的就是存放内存实例。Java堆是垃圾收集器管理的主要区域。因为分代收集算法,所有可以划分为新生代和老年代。Java堆可扩展(通过-Xmx和-Xms控制)。如果堆内有内存完成实例分配,并且堆也无法扩展时,抛出OutOfMemoryError方法区(线程共享)
用于存储已经被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。这个区域的内存回收目的主要是针对常量池的回收和对类型卸载,这个区域回收相当严苛,但是也有必要,未完全回收会导致内存泄露,当方法区无法满足内存分配需求时,抛出OutOfMemoryError运行时常量池
是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息的常量池。用于存放编译期生成的各种字面量和符号引用。这部分内容在类加载后进入方法区的运行时常量池存放。运行期间也可以产生常量,也可以放入常量池。当常量池无法满足内存分配需求时,抛出OutOfMemoryError直接内存
并不是虚拟机运行时数据区的一部分,但是这部分内存也被频繁使用,也会导致OutOfMemoryError。
Nio引入基于Channel和Buffer的I/O方法,使用Native函数库直接分配堆外内存,通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。避免了Java堆和Navice堆中来回复制数据。
分配内存的时候需要注意这块内存,不要出现哥哥内存区域总和大于物理内存,抛出OutOfMemoryError
对象的创建过程
- new
- 检查参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否被加载、解析、初始化过,没有,则进行类加载过程
- 为新生对象分配内存,内存大小在类加载后完全确定。过程等于把一块内存从Java堆中划分出来
- 如果Java堆是规则的,则使用“指针碰撞”,空闲内存在一边,用过的内存在另一边,直接移动指针就行
- 如果不规则,使用“空闲列表”,从列表中找到一块足够大的内存分配出去
- 对象创建并发,一种是分配动作同步处理(CAS配上失败重试保证原子性);另一种为每个线程预先分配一小块内存,叫做“本地线程分配缓冲”(TLAB),那个线程要分配就用那个内存区,只有在用完并分配新的时候才需要同步锁,通过-XX:+/-UseTLAB来设定
- 分配内存后,需要初始零值(不包括对象头)
- 执行方法,进行初始化。
对象的内存布局
对象内存布局分为3块区域
- 对象头
包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳。32 64位虚拟机分别是32bit和64bit;第二部分是类型指针,即对象指向它的类元数据的指针,用于确定对象是那个类的实例。 - 实例数据
是对象真正存储的有效信息,也是程序代码里面定义的字段内容。 - 对齐填充
占位符,保证8字节的整数倍数,补齐。
对象访问定位
Java程序需要通过栈上的reference数据来操作堆上的具体对象。
目前两种主流的访问方式
- 句柄访问
需要Java堆划分出一块内存区域作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据指针和对象类型数据指针,分别指向Java堆中的对象实例数据和方法区的对象类型数据
优点:reference存储的是稳定的句柄地址,由于垃圾收集时对象移动非常普遍,只要改变句柄的实例数据指针就行,reference本身不需要修改 直接指针访问
reference中存储的直接是对象地址,对象实例数据中存储对象类型数据的指针,指向方法区的对象类型数据
优点:速度快,节省了一次指针定位开销,HotSpot使用这种,但是从软件开发范围看,句柄比较常见
OutOfMemoryError异常实战
堆溢出
设置VM参数
-verbose:gc //表示输出虚拟机中GC的详细情况
-Xms20M //最小Java堆内存20M
-Xmx20M //最大Java堆内存20M
-Xmn10M //最大Java堆内存新生代10M
-XX:+PrintGCDetails //打印GC详细信息
-XX:SurvivorRatio=8 //Eden和Survivor空间占比
运行方法
public class Test {
public static void main(String[] args){
List<String> list=new ArrayList<>();
while (true){
list.add("hello");
}
}
}
结果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3719)
at java.base/java.util.Arrays.copyOf(Arrays.java:3688)
at java.base/java.util.ArrayList.grow(ArrayList.java:237)
at java.base/java.util.ArrayList.grow(ArrayList.java:242)
at java.base/java.util.ArrayList.add(ArrayList.java:467)
at java.base/java.util.ArrayList.add(ArrayList.java:480)
at Test.main(Test.java:10)
遇到堆OutOfMemoryError先确定是内存泄露还是内存溢出
内存泄露:通过工具查看泄露对象到GC Roots的引用链,确定通过怎样的路径与GC Roots关联导致无法回收
内存溢出:检查-Xms -Xmx,是否可以调大
虚拟机栈和本地方法栈溢出
设置VM参数
-Xss20M //栈内存容量
运行方法
public class Test {
private int l=1;
public void stack(){
l++;
stack();
}
public static void main(String[] args){
Test test=new Test();
test.stack();
}
}
结果
Exception in thread "main" java.lang.StackOverflowError
默认的栈深度,在大多数情况下,达到1000-2000完全没问题,对正常的方法调用(包括递归),这个深度是完全够了,如果建立多线程导致内存溢出,在不能减少线程数的情况下,只能通过减少最大堆和减少栈容量来换取线程。
方法区和运行时常量池溢出
设置VM参数
-XX:PermSize=8M //最小方法区
-XX:MaxPermSize=16M //最大方法区
运行方法
public class Test {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
int i=0;
while(true){
list.add(String.valueOf(i++).intern());
}
}
}
String.intern()是一个Native方法,作用:如果字符串常量池中已经包括一个等于此String对象的字符串,则返回常量池中的这个字符串String对象,否则,将此String对象添加到常量池,并返回引用。
1.6以前的版本,由于常量池分配在永久代,会出现OutOfMemoryError,跟随“PermGen space”,
1.7会一直运行下去
本机直接内存溢出
设置VM参数
-Xmx20M //最大堆内存
-XX:MaxDirectMemorySize=10M //直接内存大小,默认是Xmx的大小一样
由DirectMemory导致的内存溢出,特征是Heap Dump文件中看不到明显的异常,Dump文件很小,如果程序使用Nio考虑检查是这个问题导致的。