Java内存区域和内存溢出异常

时间:2021-11-23 20:57:47

Java运行时内存区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。如图2-1所示

 Java内存区域和内存溢出异常

程序计数器Program Counter Register):程序计数器是一块较小的内存空间。它可以看作是当前线程所执行的字节码的行号指示器。系统运行时,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响。线程执行Java方法时,程序计数器记录的是虚拟机正在执行的字节码的指令地址,如果正在执行的是Native方法,这个计数器值则为空(Undefined)。此区域是唯一没有OutOfMemoryError异常的区域。

Java虚拟机栈Java Virtual Machine StacksJava虚拟机栈是线程私有的,它的生命周期与线程相同。一个Java方法在调用执行的时候会创建一个栈帧,方法执行结束时栈帧会出栈。此区域可能出现*Error异常(线程请求栈深度超过虚拟机允许的深度)或OutOfMemoryError(内存不足了)异常。

本地方法栈(Native Method Stack):与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。本地方法栈区域也会抛出*Error异常和OutOfMemoryError异常。

Heap):对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。几乎所有的对象实例和数组都是在堆上分配的。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。从内存分配角度看:堆内存可以分为新生代和老年代。

Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,堆既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx-Xms控制)。堆中没有足够内存分配实例,并且堆无法再拓展的时候就会抛出OutOfMemory异常。

方法区Method Area)方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

总结:Java堆和方法区是线程共享的,Java虚拟机栈、本地方法栈、程序计数器是线程私有的

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方 法区运行时常量池,运行期间也可能将新的常量放入池中,String类的intern()方法(可以看看intern在JDK1.6和JDK1.6后的区别

对象的创建

Java虚拟机是如何创建一个对象的呢?(HotSpot为例)

第一步检查类是否被加载过虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没 有,那必须先执行相应的类加载过程。

第二步在堆中找一块足够大的内存空间,分配给对象。在划分空闲内存空间的时候,在并发情况下可能出现线程安全问题。有两种方式来解决这个问题:

(1)使用CAS操作和失败重试方式保证更新操作的原子性

(2)把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。

第三步内存空间初始化零值这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

第四步:初始化。从虚拟机的角度看,三面三个步骤执行完,对象就创建出来了。从Java程序的角度看,还需要一个<init>方法执行,这个方法执行后,字段零值就被初始化为程序中定义的值。这时一个可用的对象才被创建出来。

对象的内存布局

HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。

对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和 64bit,官方称它为“Mark Word”。对象头的另外一部分是类型指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

 Java内存区域和内存溢出异常

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。

由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说, 就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全

对象的访问定位

Java程序需要通过栈上的reference数据来操作堆上的具体对象。主流的访问方式有两种:

(1)句柄如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,如图2-2所示。

 Java内存区域和内存溢出异常

(2)直接指针如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址,如图2-3所示。

两种方式访问对象方式的比较:在对象移动时,reference本身方式不需要频繁的修改,而只需要改变句柄中对象实例数据的指针。指针方式的优点就是速度快,少一次指针定位的开销。HotSpot虚拟机是使用直接指针方式的。

内存溢出

内存溢出(OutOfMemoryError):也叫OOM,内存不足导致无法满足程序运行的需要。

1)模拟堆内存溢出实验: 

/**
*VM Args:-Xms20m-Xmx20m-XX:+HeapDumpOnOutOfMemoryError
*@author zzm
*/
public class HeapOOM{
static class OOMObject{
}
public static void main(String[]args){
List<OOMObject>list=new ArrayList<OOMObject>();
while(true){
list.add(new OOMObject());
}
}
}

上面的代码运行使用了参数:-Xms 20m -Xms 20m的意思是堆内存设置为最大最小都是20m。这样就避免的堆内存自动拓展而方便程序模拟内存溢出效果。

上面代码的运行结果:

java.lang.OutOfMemoryError:Java heap space
Dumping heap to java_pid3404.hprof……
Heap dump file created[22045981 bytes in 0.663 secs]

(3)栈内存溢出实验: 

/**
*VM Args:-Xss128k
*@author zzm
*/
public class JavaVMStackSOF{
private int stackLength=1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[]args)throws Throwable{
JavaVMStackSOF oom=new JavaVMStackSOF();
try{
oom.stackLeak();
}catch(Throwable e){
System.out.println("stack length:"+oom.stackLength);
throw e;
}
}

-Xss 128k是限制栈大小为128k。然后不停的递归导致栈深度超虚拟机的最大深度。

运行结果:

stack length:2402
Exception in thread"main"java.lang.*Error
at org.fenixsoft.oom.VMStackSOF.leak(VMStackSOF.java:20)
at org.fenixsoft.oom.VMStackSOF.leak(VMStackSOF.java:21)
at org.fenixsoft.oom.VMS

如果线程请求栈深度超过虚拟机允许的最大深度,抛出

*Error异常。虚拟机在拓展栈时候没有足够的内存则抛出OOM异常。

方法区和运行时常量池溢出。(下面这个两种溢出书中给出介绍很少)

本机直接内存溢出。