关于内存管理


---------------------------
java 堆与栈(内存分配策略)
---------------------------
今天复习了一下这些知识,顺便做了下笔记.
   寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制.
   栈:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(字符串常量对象存放在常量池中。)
   堆:存放所有new出来的对象。
   静态域:存放静态成员(static定义的)
   常量池:存放字符串常量和基本类型常量(public static final)。
   非RAM存储:硬盘等永久存储空间

这里我们主要关心栈,堆和常量池,对于栈和常量池中的对象可以共享,对于堆中的对象不可以共享。栈中的数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会消失。堆中的对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定,具有很大的灵活性。
对于字符串:其对象的引用都是存储在栈中的,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)才能确定的就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。
如以下代码:

Java代码
String s1 = "china";  
String s2 = "china";  
String s3 = "china";  
String ss1 = new String("china");  
String ss2 = new String("china");  
String ss3 = new String("china"); 
 String s1 = "china";
 String s2 = "china";
 String s3 = "china";
 String ss1 = new String("china");
 String ss2 = new String("china");
 String ss3 = new String("china");

 

这里解释一下黄色这3个箭头,对于通过new产生一个字符串(假设为”china”)时,会先去常量池中查找是否已经有了”china”对象,如果没有则在常量池中创建一个此字符串对象,然后堆中再创建一个常量池中此”china”对象的拷贝对象。这也就是有道面试题:String s = new String(“xyz”);产生几个对象?一个或两个,如果常量池中原来没有”xyz”,就是两个。

 

对于基础类型的变量和常量:变量和引用存储在栈中,常量存储在常量池中。
如以下代码:

Java代码
int i1 = 9;  
int i2 = 9;  
int i3 = 9;   
public static final int INT1 = 9;  
public static final int INT2 = 9;  
public static final int INT3 = 9; 
 int i1 = 9;
 int i2 = 9;
 int i3 = 9; 
 public static final int INT1 = 9;
 public static final int INT2 = 9;
 public static final int INT3 = 9;


对于成员变量和局部变量:成员变量就是方法外部,类的内部定义的变量;局部变量就是方法或语句块内部定义的变量。局部变量必须初始化。
形式参数是局部变量,局部变量的数据存在于栈内存中。栈内存中的局部变量随着方法的消失而消失。
成员变量存储在堆中的对象里面,由垃圾回收器负责回收。
如以下代码:

Java代码
class BirthDate {  
    private int day;  
    private int month;  
    private int year;      
    public BirthDate(int d, int m, int y) {  
        day = d;   
        month = m;   
        year = y;  
    }  
    省略get,set方法………  
}  
 
public class Test{  
    public static void main(String args[]){  
int date = 9;  
        Test test = new Test();        
           test.change(date);   
        BirthDate d1= new BirthDate(7,7,1970);         
    }    
 
    public void change1(int i){  
        i = 1234;  
    } 
class BirthDate {
    private int day;
    private int month;
    private int year;   
    public BirthDate(int d, int m, int y) {
        day = d;
        month = m;
        year = y;
    }
    省略get,set方法………
}

public class Test{
    public static void main(String args[]){
int date = 9;
        Test test = new Test();     
     test.change(date);
        BirthDate d1= new BirthDate(7,7,1970);      
    } 

    public void change1(int i){
     i = 1234;
    }

}


对于以上这段代码,date为局部变量,i,d,m,y都是形参为局部变量,day,month,year为成员变量。下面分析一下代码执行时候的变化:
 main方法开始执行:int date = 9;
date局部变量,基础类型,引用和值都存在栈中。
 Test test = new Test();
test为对象引用,存在栈中,对象(new Test())存在堆中。
 test.change(date);
i为局部变量,引用和值存在栈中。当方法change执行完成后,i就会从栈中消失。
 BirthDate d1= new BirthDate(7,7,1970); 
d1为对象引用,存在栈中,对象(new BirthDate())存在堆中,其中d,m,y为局部变量存储在栈中,且它们的类型为基础类型,因此它们的数据也存储在栈中。day,month,year为成员变量,它们存储在堆中(new BirthDate()里面)。当BirthDate构造方法执行完之后,d,m,y将从栈中消失。
main方法执行完之后,date变量,test,d1引用将从栈中消失,new Test(),new BirthDate()将等待垃圾回收。

 

---------------------------
java 堆与栈(内存分配策略)
---------------------------
内存分配策略
按照编译原理的观点,程序运行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的.
静态存储分配是指在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间.这种分配策略要求程序代码中不允 许有可变数据结构(比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求.
栈式存储分配也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的.和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知 的,只有到运行的时候才能够知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存.和我们在数据结构所熟知 的栈一样,栈式存储分配按照先进后出的原则进行分配。
静态存储分配要求在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在编译时或运行时 模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例.堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释 放.

堆和栈的比较
上面的定义从编译原理的教材中总结而来,除静态存储分配之外,都显得很呆板和难以理解,下面撇开静态存储分配,集中比较堆和栈:
从堆和栈的功能和作用来通俗的比较,堆主要用来存放对象的,栈主要是用来执行程序的.而这种不同又主要是由于堆和栈的特点决定的:
在编程中,例如C/C++中,所有的方法调用都是通过栈来进行的,所有的局部变量,形式参数都是从栈中分配内存空间的。实际上也不是什么分配,只是从栈顶 向上用就行,就好像工厂中的传送带(conveyor belt)一样,Stack Pointer会自动指引你到放东西的位置,你所要做的只是把东西放下来就行.退出函数的时候,修改栈指针就可以把栈中的内容销毁.这样的模式速度最快, 当然要用来运行程序了.需要注意的是,在分配的时候,比如为一个即将要调用的程序模块分配数据区时,应事先知道这个数据区的大小,也就说是虽然分配是在程 序运行时进行的,但是分配的大小多少是确定的,不变的,而这个"大小多少"是在编译时确定的,不是在运行时.
堆是应用程序在运行的时候请求操作系统分配给自己内存,由于从操作系统管理的内存分配,所以在分配和销毁时都要占用时间,因此用堆的效率非常低.但是堆的 优点在于,编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间,因此,用堆保存数据时会得到更大的灵活性。事实上,面 向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定.在C++中,要求创建一个对象时,只需用 new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存.当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花 掉更长的时间!这也正是导致我们刚才所说的效率低的原因,看来列宁同志说的好,人的优点往往也是人的缺点,人的缺点往往也是人的优点(晕~).


JVM中的堆和栈
JVM是基于堆栈的虚拟机.JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。
我们知道,某个线程正在执行的方法称为此线程的当前方法.我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧。这个帧自然成为了当前帧.在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据.这个帧在这里和编译 原理中的活动纪录的概念是差不多的.
从Java的这种分配机制来看,堆栈又可以这样理解:堆栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。
每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程 共享.跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也 就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。

具体的说:
      栈与堆都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。
Java的堆是一个运行时数据区,类的对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等指 令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动 态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。java中的对象和数组都存放在堆中。
栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(,int, short, long, byte, float, double, boolean, char)和对象引用。


栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:
int a = 3;
int b = 3;
编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。这时,如果再令a=4;那么编译器 会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。要注意这 种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。

ps:关于c++的内存分配

一个由C/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。
二、例子程序
这是一个前辈写的,非常详细
//main.cpp
int a = 0; 全局初始化区
char *p1; 全局未初始化区
main()
{
int b; 栈
char s[] = "abc"; 栈
char *p2; 栈
char *p3 = "123456"; 123456/0在常量区,p3在栈上。
static int c =0; 全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); 123456/0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}

 

------------------------------------
JVM内存管理:深入Java内存区域与OOM
------------------------------------
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。

 

概述:
对于从事C、C++程序开发的开发人员来说,在内存管理领域,他们即是拥有最高权力的皇帝又是执行最基础工作的劳动人民——拥有每一个对象的“所有权”,又担负着每一个对象生命开始到终结的维护责任。

 

对于Java程序员来说,不需要在为每一个new操作去写配对的delete/free,不容易出现内容泄漏和内存溢出错误,看起来由JVM管理内存一切都很美好。不过,也正是因为Java程序员把内存控制的权力交给了JVM,一旦出现泄漏和溢出,如果不了解JVM是怎样使用内存的,那排查错误将会是一件非常困难的事情。

 

VM运行时数据区域
JVM执行Java程序的过程中,会使用到各种数据区域,这些区域有各自的用途、创建和销毁时间。根据《Java虚拟机规范(第二版)》(下文称VM Spec)的规定,JVM包括下列几个运行时数据区域:

 

1.程序计数器(Program Counter Register):

 

每一个Java线程都有一个程序计数器来用于保存程序执行到当前方法的哪一个指令,对于非Native方法,这个区域记录的是正在执行的VM原语的地址,如果正在执行的是Natvie方法,这个区域则为空(undefined)。此内存区域是唯一一个在VM Spec中没有规定任何OutOfMemoryError情况的区域。

 

2.Java虚拟机栈(Java Virtual Machine Stacks)

与程序计数器一样,VM栈的生命周期也是与线程相同。VM栈描述的是Java方法调用的内存模型:每个方法被执行的时候,都会同时创建一个帧(Frame)用于存储本地变量表、操作栈、动态链接、方法出入口等信息。每一个方法的调用至完成,就意味着一个帧在VM栈中的入栈至出栈的过程。在后文中,我们将着重讨论VM栈中本地变量表部分。

经常有人把Java内存简单的区分为堆内存(Heap)和栈内存(Stack),实际中的区域远比这种观点复杂,这样划分只是说明与变量定义密切相关的内存区域是这两块。其中所指的“堆”后面会专门描述,而所指的“栈”就是VM栈中各个帧的本地变量表部分。本地变量表存放了编译期可知的各种标量类型(boolean、byte、char、short、int、float、long、double)、对象引用(不是对象本身,仅仅是一个引用指针)、方法返回地址等。其中long和double会占用2个本地变量空间(32bit),其余占用1个。本地变量表在进入方法时进行分配,当进入一个方法时,这个方法需要在帧中分配多大的本地变量是一件完全确定的事情,在方法运行期间不改变本地变量表的大小。

在VM Spec中对这个区域规定了2中异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果VM栈可以动态扩展(VM Spec中允许固定长度的VM栈),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。

 

3.本地方法栈(Native Method Stacks)

本地方法栈与VM栈所发挥作用是类似的,只不过VM栈为虚拟机运行VM原语服务,而本地方法栈是为虚拟机使用到的Native方法服务。它的实现的语言、方式与结构并没有强制规定,甚至有的虚拟机(譬如Sun Hotspot虚拟机)直接就把本地方法栈和VM栈合二为一。和VM栈一样,这个区域也会抛出StackOverflowError和OutOfMemoryError异常。


4.Java堆(Java Heap)

对于绝大多数应用来说,Java堆是虚拟机管理最大的一块内存。Java堆是被所有线程共享的,在虚拟机启动时创建。Java堆的唯一目的就是存放对象实例,绝大部分的对象实例都在这里分配。这一点在VM Spec中的描述是:所有的实例以及数组都在堆上分配(原文:The heap is the runtime data area from which memory for all class instances and arrays is allocated),但是在逃逸分析和标量替换优化技术出现后,VM Spec的描述就显得并不那么准确了。

Java堆内还有更细致的划分:新生代、老年代,再细致一点的:eden、from survivor、to survivor,甚至更细粒度的本地线程分配缓冲(TLAB)等,无论对Java堆如何划分,目的都是为了更好的回收内存,或者更快的分配内存,在本章中我们仅仅针对内存区域的作用进行讨论,Java堆中的上述各个区域的细节,可参见本文第二章《JVM内存管理:深入垃圾收集器与内存分配策略》。

根据VM Spec的要求,Java堆可以处于物理上不连续的内存空间,它逻辑上是连续的即可,就像我们的磁盘空间一样。实现时可以选择实现成固定大小的,也可以是可扩展的,不过当前所有商业的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中无法分配内存,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

 

5.方法区(Method Area)

叫“方法区”可能认识它的人还不太多,如果叫永久代(Permanent Generation)它的粉丝也许就多了。它还有个别名叫做Non-Heap(非堆),但是VM Spec上则描述方法区为堆的一个逻辑部分(原文:the method area is logically part of the heap),这个名字的问题还真容易令人产生误解,我们在这里就不纠结了。

方法区中存放了每个Class的结构信息,包括常量池、字段描述、方法描述等等。VM Space描述中对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存,也可以选择固定大小或者可扩展外,甚至可以选择不实现垃圾收集。相对来说,垃圾收集行为在这个区域是相对比较少发生的,但并不是某些描述那样永久代不会发生GC(至少对当前主流的商业JVM实现来说是如此),这里的GC主要是对常量池的回收和对类的卸载,虽然回收的“成绩”一般也比较差强人意,尤其是类卸载,条件相当苛刻。

 

6.运行时常量池(Runtime Constant Pool)

Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量表(constant_pool table),用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。但是Java语言并不要求常量一定只有编译期预置入Class的常量表的内容才能进入方法区常量池,运行期间也可将新内容放入常量池(最典型的String.intern()方法)。

运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法在申请到内存时会抛出OutOfMemoryError异常。

 

7.本机直接内存(Direct Memory)

直接内存并不是虚拟机运行时数据区的一部分,它根本就是本机内存而不是VM直接管理的区域。但是这部分内存也会导致OutOfMemoryError异常出现,因此我们放到这里一起描述。

在JDK1.4中新加入了NIO类,引入一种基于渠道与缓冲区的I/O方式,它可以通过本机Native函数库直接分配本机内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java对和本机堆中来回复制数据。

显然本机直接内存的分配不会受到Java堆大小的限制,但是即然是内存那肯定还是要受到本机物理内存(包括SWAP区或者Windows虚拟内存)的限制的,一般服务器管理员配置JVM参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),而导致动态扩展时出现OutOfMemoryError异常。

 

实战OutOfMemoryError
上述区域中,除了程序计数器,其他在VM Spec中都描述了产生OutOfMemoryError(下称OOM)的情形,那我们就实战模拟一下,通过几段简单的代码,令对应的区域产生OOM异常以便加深认识,同时初步介绍一些与内存相关的虚拟机参数。下文的代码都是基于Sun Hotspot虚拟机1.6版的实现,对于不同公司的不同版本的虚拟机,参数与程序运行结果可能结果会有所差别。

 

Java堆

 

Java堆存放的是对象实例,因此只要不断建立对象,并且保证GC Roots到对象之间有可达路径即可产生OOM异常。测试中限制Java堆大小为20M,不可扩展,通过参数-XX:+HeapDumpOnOutOfMemoryError让虚拟机在出现OOM异常的时候Dump出内存映像以便分析。(关于Dump映像文件分析方面的内容,可参见本文第三章《JVM内存管理:深入JVM内存异常分析与调优》。)

 

清单1:Java堆OOM测试

/**

 * 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());

              }

       }

}
 

 

运行结果:

java.lang.OutOfMemoryError: Java heap space

Dumping heap to java_pid3404.hprof ...

Heap dump file created [22045981 bytes in 0.663 secs]
 

 

 

VM栈和本地方法栈
 

Hotspot虚拟机并不区分VM栈和本地方法栈,因此-Xoss参数实际上是无效的,栈容量只由-Xss参数设定。关于VM栈和本地方法栈在VM Spec描述了两种异常:StackOverflowError与OutOfMemoryError,当栈空间无法继续分配分配时,到底是内存太小还是栈太大其实某种意义上是对同一件事情的两种描述而已,在笔者的实验中,对于单线程应用尝试下面3种方法均无法让虚拟机产生OOM,全部尝试结果都是获得SOF异常。

 

1.使用-Xss参数削减栈内存容量。结果:抛出SOF异常时的堆栈深度相应缩小。

2.定义大量的本地变量,增大此方法对应帧的长度。结果:抛出SOF异常时的堆栈深度相应缩小。

3.创建几个定义很多本地变量的复杂对象,打开逃逸分析和标量替换选项,使得JIT编译器允许对象拆分后在栈中分配。结果:实际效果同第二点。

 

清单2:VM栈和本地方法栈OOM测试(仅作为第1点测试程序)

/**

 * 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;

              }

       }

}
 

 

运行结果:

stack length:2402

Exception in thread "main" java.lang.StackOverflowError

        at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:20)

        at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)

        at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)
 

 

如果在多线程环境下,不断建立线程倒是可以产生OOM异常,但是基本上这个异常和VM栈空间够不够关系没有直接关系,甚至是给每个线程的VM栈分配的内存越多反而越容易产生这个OOM异常。

 

原因其实很好理解,操作系统分配给每个进程的内存是有限制的,譬如32位Windows限制为2G,Java堆和方法区的大小JVM有参数可以限制最大值,那剩余的内存为2G(操作系统限制)-Xmx(最大堆)-MaxPermSize(最大方法区),程序计数器消耗内存很小,可以忽略掉,那虚拟机进程本身耗费的内存不计算的话,剩下的内存就供每一个线程的VM栈和本地方法栈瓜分了,那自然每个线程中VM栈分配内存越多,就越容易把剩下的内存耗尽。

 

清单3:创建线程导致OOM异常

/**

 * VM Args:-Xss2M (这时候不妨设大些)

 * @author zzm

 */

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();

       }

}
 

 

特别提示一下,如果读者要运行上面这段代码,记得要存盘当前工作,上述代码执行时有很大令操作系统卡死的风险。

 

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
 

 

 

运行时常量池
 

要在常量池里添加内容,最简单的就是使用String.intern()这个Native方法。由于常量池分配在方法区内,我们只需要通过-XX:PermSize和-XX:MaxPermSize限制方法区大小即可限制常量池容量。实现代码如下:

 

清单4:运行时常量池导致的OOM异常

/**

 * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M

 * @author zzm

 */

public class RuntimeConstantPoolOOM {

 

       public static void main(String[] args) {

              // 使用List保持着常量池引用,压制Full GC回收常量池行为

              List<String> list = new ArrayList<String>();

              // 10M的PermSize在integer范围内足够产生OOM了

              int i = 0;

              while (true) {

                     list.add(String.valueOf(i++).intern());

              }

       }

}
 

 

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

       at java.lang.String.intern(Native Method)

       at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)
 

 

 

方法区

 

上文讲过,方法区用于存放Class相关信息,所以这个区域的测试我们借助CGLib直接操作字节码动态生成大量的Class,值得注意的是,这里我们这个例子中模拟的场景其实经常会在实际应用中出现:当前很多主流框架,如Spring、Hibernate对类进行增强时,都会使用到CGLib这类字节码技术,当增强的类越多,就需要越大的方法区用于保证动态生成的Class可以加载入内存。

 

清单5:借助CGLib使得方法区出现OOM异常

/**

 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M

 * @author zzm

 */

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) throws Throwable {

                                   return proxy.invokeSuper(obj, args);

                            }

                     });

                     enhancer.create();

              }

       }

 

       static class OOMObject {

 

       }

}
 

 

运行结果:

Caused by: java.lang.OutOfMemoryError: PermGen space

       at java.lang.ClassLoader.defineClass1(Native Method)

       at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)

       at java.lang.ClassLoader.defineClass(ClassLoader.java:616)

       ... 8 more
 

 

本机直接内存

 

DirectMemory容量可通过-XX:MaxDirectMemorySize指定,不指定的话默认与Java堆(-Xmx指定)一样,下文代码越过了DirectByteBuffer,直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回实例,也就是基本上只有rt.jar里面的类的才能使用),因为DirectByteBuffer也会抛OOM异常,但抛出异常时实际上并没有真正向操作系统申请分配内存,而是通过计算得知无法分配既会抛出,真正申请分配的方法是unsafe.allocateMemory()。

 

/**

 * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M

 * @author zzm

 */

public class DirectMemoryOOM {

 

       private static final int _1MB = 1024 * 1024;

 

       public static void main(String[] args) throws Exception {

              Field unsafeField = Unsafe.class.getDeclaredFields()[0];

              unsafeField.setAccessible(true);

              Unsafe unsafe = (Unsafe) unsafeField.get(null);

              while (true) {

                     unsafe.allocateMemory(_1MB);

              }

       }

}
 

 

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError

       at sun.misc.Unsafe.allocateMemory(Native Method)

       at org.fenixsoft.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:20)
 

 

 

总结
到此为止,我们弄清楚虚拟机里面的内存是如何划分的,哪部分区域,什么样的代码、操作可能导致OOM异常。虽然Java有垃圾收集机制,但OOM仍然离我们并不遥远,本章内容我们只是知道各个区域OOM异常出现的原因,下一章我们将看看Java垃圾收集机制为了避免OOM异常出现,做出了什么样的努力。

 

 

 

---------------------------
UNIX下的堆与栈
---------------------------
见《UNIX环境高级编程》中文版第七章7.6节第127页:C程序空间的布局。

过去将stack译为堆栈。现在基本上都译成“栈”了。
Heap译为“堆”。

以下讨论假定楼主的“堆”与“栈”分别为“heap”与“
stack”。

一般而言:
1. 初始化的全局变量,放在数据段。未初始化的全局变量,放在未初始化数据段,一般为BSS段。函数内部的静态变量(static)放在数据段或者BSS段,与初始化与否有关。

2. 函数内部的局部自动变量(不通过malloc机制),使用进程或线程的栈空间。

3. 动态内存分配使用堆空间。不同的操作系统对堆有不同的管理方式。windows有局部堆与全局堆的区别。细节我也不太清楚。在很多嵌入式OS将称堆为内存池(memory pool)。

如:stack_buf使用栈。

char global_buf[1024];char global_c = 'A';void func(){    char stack_buf[1024];    static char static_buf[1024];    char *heap_buf = malloc(1024);    ...}

数据段:global_c
BSS段:global_buf, static_buf
栈:stack_buf
堆:heap_buf


补充说明几点:
1. 堆不一定就是从低地址向高地址增长。
不同的系统实现是不一样的。在许多嵌入式系统中,堆是从未初始化数据段到某一个高地址的。移植过libc的malloc函数的朋友应该会很清楚这一点。一个系统的堆的位置与行为模式完全由系统程序员来定义。

2. 栈也不一定是高地址向低地址增长。栈的位置也与系统有关。
有些系统将栈空间定义到一个未初始化的数组中,这样栈空间其实是在BSS段中。也可以将栈空间用malloc来分配,这样栈又落在了堆中。也可以用初始化为0的数组来定义栈空间。这样栈又落到数据段中。
比如:UC/OS-II中,栈的增长方向是可以由程序员定义的。相信熟悉这个系统的朋友都有深刻体会。

理论与实践并不完全相同。不同系统的行为是不大一样的。咱们需要鉴别、吸收。

但是堆与栈的作用基本上都是一样的。栈对CPU可见,是CPU指令分配空间的一种自然手段。对于C语言而言,栈基本上就是给函数内部的局部自动变量准备的。在汇编语言级别上,很多栈空间内变量都被优化成使用寄存器了。heap_buf指向的内存空间为堆空间。而heap_buf指针变量自身使用的内存空间却是栈空间。优化后可能会使用寄存器。
在类UNIX系统里,系统为进程分配空间有三种形式:静态分配、栈分配和堆分配(动态分配)。
静态分配
整个空间在进程产生时一次性分配,空间内所有实体也分配到位。
外部变量、静态变量,以及文字量,都属此。
整个空间的大小在进程运行中保持不变,直至进程终结。
栈分配
在进程产生时,一次性分配一块空间。空间的大小有个缺省值,可以用链接器开关调整。在进程运行中,在栈里以先进后出的方式生成自动变量和函数参数。
整个空间的大小在进程运行中保持不变,直至进程终结。
堆分配
在进程运行中,动态为每个实体向系统申请所需的空间。不用时,可以告知系统,让系统回收。

 


-----------------------------------
ruby内存泄漏的罪魁祸首 - 幽灵指针
-----------------------------------
ruby内存泄漏问题由来已久,几乎是一个无法克服的顽症。JavaEye对该问题有过探讨:Ruby VM的GC的思考。最近Ruby核心开发团队的邮件列表上面也对该问题进行了深刻的讨论,并且取得了一些相当不错的进展。

最早是有人报告了ruby的callcc调用引起的一个非常明显的内存泄漏现象:


Ruby代码

while true 
  @x = proc {|c| c}  
end # 运行正常  
 
 
while true 
  x = callcc {|c| c}  
end # 运行也正常  
 
 
while true 
  @x = callcc {|c| c}  
end 
# 严重泄漏内存!.   
 
while true 
  g = Generator.new {|x| (1..3).each {|i| x.yield i}}  
end # 严重泄漏内存 
while true
  @x = proc {|c| c}
end # 运行正常


while true
  x = callcc {|c| c}
end # 运行也正常


while true
  @x = callcc {|c| c}
end
# 严重泄漏内存!.

while true
  g = Generator.new {|x| (1..3).each {|i| x.yield i}}
end # 严重泄漏内存


随后邮件列表围绕该问题进行了激烈的探讨:改进的C编码技巧解决Ruby内存泄漏

Brent Roman表示上述代码的内存泄漏根源在于GCC编译器对栈的分配的特点以及Ruby传统垃圾收集器的弱点造成的。

Brent Roman在memory leak in calcc这个帖子中指出:

gcc编译器不会自动初始化那些未被使用的、未初始化赋值的变量,而这些变量一旦被那些老的,未使用的,残留在内存栈中合法指针所引用以后,垃圾收集器GC就无法收集这些未被使用的变量了。

Brent Roman在ARM芯片的设备上面用Ruby 1.6.8开发机器人程序,在他的设备上内存只有32MB,但是ruby程序一天跑下来,就会吃掉超过20MB的内存。所以Brent花了很多时间给他的ruby 1.6.8打补丁。Brent介绍说,经过他自己的hack,目前他的ruby程序吃掉的内存已经稳定在10MB以下了。

Ruby内存泄漏的罪魁祸首原来在于“幽灵指针”!当应用程序的一个新的内存栈帧被推入到内存栈顶的时候,gcc编译的程序并不是让新的栈帧简单的覆盖先前的保存在该位置的栈帧,而且还会创建出来一些空闲的栈帧。而这些新的空闲栈帧有可能会被那些老的、残留在栈中的合法指针(即幽灵指针)所引用(例如空闲栈帧的地址刚好有一个老的指针指向该地址),这样一旦Ruby的传统GC垃圾收集器访问这些指针,这些指针指向的栈帧就被激活了,这就意味着这些空闲的栈帧再也无法被垃圾收集器回收,于是内存泄漏就诞生了!

此外ruby的eval方法调用的实现也有很大的问题,他会严重导致gcc创建出来大量的体积庞大的空闲栈帧。eval方法的每次调用会导致4KB的栈地址分配,这其中只有不到20%的栈空间会被真正初始化,这意味着:

1、幽灵指针有很高的可能性引用到超过80%的空闲栈帧,从而导致内存泄漏;
2、GC必须扫描一个包含了大量空闲栈帧的内存地址空间,把很多幽灵指针指向的永远不会被用到的对象标记为可用,而无法回收;
3、callcc方法调用(用来实现Continuations编程)和Ruby多线程应用程序的线程上下文切换会导致大量无用堆栈的拷贝操作
4、ruby递归调用很容易产生堆栈溢出

Brent Roman修改了eval实现以后,测试结果堆栈空间下降了超过2/3,同时线程上下文切换速度提高了3-4倍的速度。

由于Ruby on Rails框架大量使用了eval方法调用,产生内存泄漏的现象是非常明显的。Rails2.2已经开始支持多线程运行了,但是由于幽灵指针导致的内存泄漏问题,多线程切换会是一个严重的性能瓶颈,所以用Rails多线程,还是要三思而后行为好。

最后,Ruby的创始人松本行弘也发表了看法,鼓励Brent Roman早日将他的补丁移植到ruby 1.8.7版本和ruby 1.9版本上:


matz 写道
We are troubled by the "ghost references from the machine stack" generated by GCC for years.  We are more than happy to see the patch, and merge it if it's acceptable.


所以让我们耐心的期待一段时间吧。也许困扰我们的ruby内存泄漏问题,很快就将成为历史,不复存在!

 

------------------------------------
解决ruby内存泄漏的超级大补丁发布啦
------------------------------------
JavaEye在12月初发布了新闻ruby内存泄漏的罪魁祸首 - 幽灵指针,介绍了当前Ruby解析器内存泄漏的根本原因,并且透露了Brent Roman正在打算给ruby提供补丁程序解决内存泄漏问题。

如今Brent Roman的超级大补丁终于发布!该超级大补丁命名为:“1.8.7-p72 MBARI Patch”。因为这个补丁是给Ruby当前最广泛使用的生产环境的版本ruby 1.8.7-p72版本提供的,而Brent Roman本人在Monterey Bay Aquarium Research Institute工作,因此该补丁被成为MBARI patch。

MBARI补丁总共包含了6个补丁文件,他们分别是:

1、MBARI1.patch: 修复Ruby多线程Continuations的bug导致的段地址错误和内存泄漏
2、MBARI2.patch: 修改Ruby多线程的栈帧分配策略,解决多线程栈帧分配策略导致的内存泄漏
3、MBARI3.patch: 修复幽灵指针造成的Ruby内存泄漏。
4、MBARI4.patch: 修复Ruby的eval()方法调用造成的大量内存泄漏
5、MBARI5.patch: 修复Ruby的异常处理的代码跳转造成的内存泄漏
6、MBARI6.patch: 提供了Method和Proc对象的source_location()方法

根据Brent Roman自己对ruby自带的测试套件测试的结果表明,应用该补丁以后,内存泄漏问题有极大改善。

       版本                     初始内存    结束内存       耗时
未打补丁1.8.7-p72:      30MB             97MB         92 seconds
打了补丁1.8.7-p2:        30MB            57MB        100 seconds

该测试套件执行完毕以后,内存占用从97MB下降到57MB,效果十分明显!

目前MBARI Patch还处于alpha阶段,Brent Roman本人的开发环境是Linux 32bit,GCC 4.3.2,他呼吁更多人帮助他测试该补丁文件,在各种不同环境下测试,向他提交bug,便于他更好的完善这个内存泄漏补丁。

安装MBARI补丁很简单:

1、下载ruby 1.8.7-p72,并且解压缩
2、下载MBARI补丁,并且解压缩
3、执行命令:MBARIp72patches/apply  ruby-1.8.7-p72 打补丁
4、编译ruby

C代码
CFLAGS="-O2 -fno-stack-protector -mpreferred-stack-boundary=2" ./configure  
make && make install 
CFLAGS="-O2 -fno-stack-protector -mpreferred-stack-boundary=2" ./configure
make && make install

如果是gcc3.3版本,要去掉 -fno-stack-protector编译参数;如果是64位机器,-mpreferred-stack-boundary=4才行。

然后 ruby -v  应该显示:

1.8.7 MBARI 6 on patchlevel 72


应用该补丁在JavaEye服务器上面简单的测试对比如下:

ruby 1.8.7 p72        fcgi进程占用物理内存129MB
ruby 1.8.7(gc patch)  fcgi进程占用物理内存176MB
ruby 1.8.7 MBARI      fcgi进程占用物理内存99MB

效果还是比较明显的,详细的性能测试报告请看:
http://www.javaeye.com/topic/299708

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值