Java内存管理
简介
Java虚拟机的内存管理分为以下几个运行时数据区:
- 方法区
- 堆
- 虚拟机栈
- 本地方法栈
- 程序计数器
其中,方法区和堆是所有线程共享的数据区,而其他的是线程隔离的数据区。
堆
Java堆,又称GC堆,是GC的管理的主要区域。在虚拟机启动时创建。主要作用是存放对象实例,几乎所有的对象实例都会存放在Java堆中。Java堆可以处于物理不连续的内存空间中,只要逻辑连续即可。通常Java堆是可扩展的。当Java堆无法申请到所需的内存空间来存放实例,也无法扩展时,会抛出,OutOfMemoryError异常。
OOM实践
public class HeapOOM{
static class OOMObject{
}
public static void main (String[]args) {
List<OOMObject> list=new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject);
}
}
}
如上面demo代码所展示的。由于OOMObject的实例是存放在堆上。当使用死循环进行创建时,便会逐渐占满堆的空间,最后产生OutOfMemoryError。
虚拟机栈
Java虚拟机栈是线程私有的,它的生命周期与线程相同。虚拟机栈是Java方法执行的内存模型。每个方法在执行的同时会创建一个栈帧。用于存放局部变量表、操作数栈、动态链接和方法出口等信息。
局部变量表存放了编译期可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double)、对象引用(reference)、returnAddress类型(指向一条字节码指令的地址)。
局部变量表所需的内存空间在编译期内完成分配。当进入一个方法时,这个方法需要帧中分配多大的局部内存是完全确定的,期间不会改变大小。
在这一区域中,虚拟机有两种异常。
- 线程请求的栈深度大于虚拟机所允许的深度。
- 虚拟机栈无法申请足够的动态内存。
OOM实践
线程请求的栈深度大于虚拟机所允许的深度
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 ;
}
}
}
虚拟机栈无法申请足够的动态内存
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) throws Throwable{
JavaVMStackOOM oom=new JavaVMStackOOM();
oom.stackLeakByThread();
}
我们通过两种形式的迭代,展现了虚拟机栈两种OOM的产生原因。
方法区
与Java堆一样是各个线程共享的内存区域。它用于存放虚拟机加载的类信息、常量、静态变量和即时编译器编译后的代码。
OOM实践
/**
*VM Args -XX PermSize=10M-XX MaxPermSize=10M *@author zzm
*/
public class RuntimeConstantPoolOOM{
public static void main (String[] args) {
List<String> list = new ArrayList<String>();
int i=0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
intern
public String intern()
返回字符串对象的规范化表示形式。
一个初始时为空的字符串池,它由类 String 私有地维护。
当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。
它遵循对于任何两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。
所有字面值字符串和字符串赋值常量表达式都是内部的。
返回:
一个字符串,内容与此字符串相同,但它保证来自字符串池中。
说明中的字符串池,也就是我们上文说的方法区。
本地方法栈
本地方法栈与虚拟机栈的机理类似,不再赘述。
程序计数器
-
简述
程序计数器(program counter register)只占用了一块比较小的内存空间,有时可以忽略不计的。
-
作用
可以看作是当前线程所执行的字节码文件(class)的行号指示器。在虚拟机的世界中,字节码解释器就是通过改变计数器的值来选取下一条执行的字节码指令,分支、循环、跳转、异常处理、线程恢复都需要它完成/
-
特性
- 因为处理器在一个确定是时刻只会执行一个线程中的指令,线程切换后,是通过计数器来记录执行痕迹的,因而可以看出,程序计数器是每个线程私有的。
- 如果执行的是java方法,那么记录的是正在执行的虚拟机字节码指令的地址的地址,如果是native方法,计数器的值为空(undefined)。
- 这个内存区域是唯一一个在java虚拟界规范中没有规定任何OutOfMemoryError的情况的区域。
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。
在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。显然,本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页文件)的大小及处理器寻址空间的限制。
服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
如有问题,欢迎指正。
网友评论