初探java内存机制_堆和栈

时间:2019-05-12 20:54:39下载本文作者:会员上传
简介:写写帮文库小编为你整理了多篇相关的《初探java内存机制_堆和栈》,但愿对你工作学习有帮助,当然你在写写帮文库还可以找到更多《初探java内存机制_堆和栈》。

第一篇:初探java内存机制_堆和栈

初探java内存机制_堆和栈

问题的引入:

问题一:

String str1 = “abc”;

String str2 = “abc”;

System.out.println(str1==str2);//true

问题二:

String str1 =new String(“abc”);

String str2 =new String(“abc”);

System.out.println(str1==str2);// false

问题三:

String s1 = “ja”;

String s2 = “va”;

String s3 = “java”;

String s4 = s1 + s2;

System.out.println(s3 == s4);//false

System.out.println(s3.equals(s4));//true

由于以上问题让人含糊不清,于是特地搜集了一些有关java内存分配的资料,以下是网摘:

Java 中的堆和栈

Java把内存划分成两种:一种是栈内存,一种是堆内存。

在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。

当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

堆内存用来存放由new创建的对象和数组。

在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。

引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。

具体的说:

栈与堆都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。

Java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,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, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。

String是一个特殊的包装类数据。可以用:

String str = new String(“abc”);

String str = “abc”;

两种的形式来创建,第一种是用new()来新建对象的,它会在存放于堆中。每调用一次就会创建一个新的对象。

而第二种是先在栈中创建一个对String类的对象引用变量str,然后查找栈中有没有存放“abc”,如果没有,则将“abc”存放进栈,并令str指向”abc”,如果已经有”abc” 则直接令str指向“abc”。

比较类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==,下面用例子说明上面的理论。

String str1 = “abc”;

String str2 = “abc”;

System.out.println(str1==str2);//true

可以看出str1和str2是指向同一个对象的。

String str1 =new String(“abc”);

String str2 =new String(“abc”);

System.out.println(str1==str2);// false

用new的方式是生成不同的对象。每一次生成一个。

因此用第二种方式创建多个”abc”字符串,在内存中其实只存在一个对象而已.这种写法有利与节省内存空间.同时它可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String(“abc”);的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。

另一方面, 要注意: 我们在使用诸如String str = “abc”;的格式定义类时,总是想当然地认为,创建了String类的对象str。担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。只有通过new()方法才能保证每次都创建一个新的对象。由于String类的immutable性质,当String变量需要经常变换其值时,应该考虑使用StringBuffer类,以提高程序效率。

java中内存分配策略及堆和栈的比较

2.1 内存分配策略

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

静态存储分配要求在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例.堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放.2.2 堆和栈的比较

上面的定义从编译原理的教材中总结而来,除静态存储分配之外,都显得很呆板和难以理解,下面撇开静态存储分配,集中比较堆和栈:

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

JVM是基于堆栈的虚拟机.JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。

我们知道,某个线程正在执行的方法称为此线程的当前方法.我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧。这个帧自然成为了当前帧.在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据.这个帧在这里和编译原理中的活动纪录的概念是差不多的.从Java的这种分配机制来看,堆栈又可以这样理解:堆栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。

每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程共享.跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。

从上面的讲述中大概理清了最初三个问,希望高人能再补充一些您觉得重要的知识点,谢谢!

第二篇:堆和栈全面的总结

操作系统中的栈:

由编译器自动分配和自动释放,一个函数对应一个栈,用于存放函数的参数值、函数调用完成后的返回值和函数体内的局部变量等。栈占用连续的一段内存空间,其操作和组织方式与数据结构中的栈十分相似。栈是为了执行线程留出的内存空间。当调用函数时创建栈,当函数执行完毕,栈就被回收了。

操作系统中的堆:

由程序员手动进行内存的申请与释放。由于程序员手动申请及释放的内存块存放在堆中,堆中有很多内存块,所以堆的组织方式类似于链表。操作系统中的堆与数据结构中的堆完全不同。我觉得通俗的理解可以是这样的:数据结构中的堆是“结构堆”,有严谨的逻辑和操作方式,而操作系统中的堆,更像是使用链表将“一堆杂乱的东西”联系起来。堆是为动态分配预留的内存空间,其生命周期为整个应用程序的生命周期。当应用程序结束以后,堆开始被回收。

每个线程都有一个属于自己的栈,但每一个应用程序通常只有一个堆(一个应用程序使用了多个堆的情况也是有的)。当线程被创建的时候,设置了栈的大小。在应用程序启动的时候,设置了堆的大小。栈的大小通常是固定的,但是堆可以在需要的时候进行扩展,如程序员向操作系统申请更多内存的时候。

由于栈的工作方式类似于数据结构中的栈,堆的工作方式类似于链表,所以栈显然会比堆快得多。按照栈的存取方式,想要释放内存或是新增内存,只需要相应移动栈顶指针即可。堆则要首先在内存的空闲区域寻找合适的内存空间,然后占用,然后指向这块空间。显然堆比栈要复杂得多。

接下来本来是想将栈和堆分开进行陈述,斟酌了一下还是决定从同一方面对栈和堆进行比较。有了比较才明显。

1.在创建栈的时候栈的大小就固定了,因为栈要连续占用一段空间。根据上文所属的堆的特性,决定了堆的大小是动态的,其分配和释放也是动态的。

2.栈中的数据过多会导致爆栈,比如dfs写搓了。而假如堆也爆了的话。。那说明内存也爆了。

3.每个函数的栈都是各自独立的,但是一个应用程序的堆是被所有的栈共享。既然提到共享,那么这里就有“并行存取”的问题了。实际上并行存取是由堆控制的,而不是被栈控制的。

4.栈的作用域仅限于函数内部,栈在函数结束的时候会自行释放掉空间。但是创建于堆上的变量必须要手动释放,堆中的变量不存在作用域的问题,因为堆是全局的。

5.栈中存放的是函数返回值地址、函数参数,函数内的局部变量等。堆中存放的是由程序员手动进行申请的内存块(malloc、new等)。

6.堆和栈都按需进行分配。栈有严格的容量上限,而堆的容量上限则是“不严格”的。堆并没有固定的容量上限,它与当前的剩余内存量有关(其实还不准确,操作系统还有虚拟内存或其他概念,所以堆的工作方式较为抽象)。

7.通过移动栈顶指针即可实现栈内存的分配。在堆上分配内存的做法则是从当前空闲的内存中找一块满足大小的区域,就像链表的工作方式一样。

8.只要没有超出栈容量,栈可以进行任意的释放和申请内存,并不会造成内存出现问题,是安全的。而堆不同,大量申请和释放小内存块可能会造成内存问题,这些小的内存块零散的分布在内存中,导致后续大块的内存申请失败,因为虽然空闲的内存足够多,但是并不连续。这种情况下的小块内存叫做“堆碎片”。不过这并不是什么大问题,具体详见“操作系统”的有关知识。

9.栈在确定了栈底地址后,其栈顶指针从栈底地址开始,逐渐向低地址走。也就是说栈的存储空间是从高地址走向低地址的。堆则相反,堆在申请空间的时候通常逐渐往高地址的方向来寻找可用内存。

纯粹的文字描述显得枯燥无味,我们来看一些代码:

[cpp] view plaincopyprint?

#include

using namespace std;

void func()

{

int i = 5;

int j = 3;

int k = 7;

int *p = &i;

printf(“%dn”, *p);

printf(“%dn”, *(p-1));

printf(“%dn”, *(p-2));

}

int main()

{

func();

getchar();

return 0;

}

上述代码的结果是:5 3 7

从结果中我们可以看出两件事:

一是栈地址是连续的,我们可以通过一个指针和一个相对的大小,来“偏移”到别的变量上去。二是从中可以看出栈地址是从高到低分布的,栈底在高地址,朝低地址的方向生长。所以程序中是p-1而不是p+1。

[cpp] view plaincopyprint?

void func()

{

int *p = NULL;

// 上行代码是个重点。这个指针待会会用于申请新的内存。

// 此时除了它自身作为一个变量需要占用4字节的空间(指针都占4字节),没有任何其他空间被申请。

// 这个指针变量是函数的局部变量,所以它被创建在栈上。

int num = 100;// 这个变量同样创建于栈上。

int buffer[100];// 同样的,buffer占用了栈的400字节的空间

p = new int[100];// 注意,程序员手动申请了一块空间,这400字节的内存创建于堆上。

// 所以此刻p的状态是:p为函数局部变量,它指向了一块全局范围的内存空间。}

// 函数体结束。上述函数有个严重的问题,那就是指针p的内存泄露。

// 正确的做法是在函数最后delete掉这块内存,或是返回这块内存的地址以供继续使用。

接下来我们来了解一下当调用一个函数的时候所发生的事情:

首先操作系统为这个函数分配了一个栈,因为在调用完这个函数以后需要能正确返回到下一条语句并继续执行,所以第一步是将调用完函数的下一条指令的地址压入栈。这样当函数调用完成,栈顶指针一点点释放内存以后,栈顶指针指向了这个地址,就能返回到正确的位置继续执行了。

[cpp] view plaincopyprint?

int main()

{

func();

printf(“%dn”, 100);

return 0;

}

比如上述代码,在调用func之前,首先把func的下一条语句,也就是printf语句的地址,存在栈中。这样函数调用完成后就能正确返回到这个printf并继续往后执行了。注意这里的地址是指令地址,而不是变量地址什么的。它有那么点类似于操作系统中的程序计数器(PC,即Program Counter)。然后把实参从右到左的顺序依次入栈(大多数的C/C++编译器为从右到左)接着是函数中的各种局部变量。要注意的是函数中的static变量是不入栈的。全局变量和static变量在编译的时候就已经在静态存储区分配好内存了。

如果这个时候该函数又调用了其它函数,过程也是一样的,首先是返回地址,然后是参数和局部变量。这样在每层调用结束,栈顶指针不断下降(释放内存)的时候,就能正确返回到之前调用的位置并继续往下执行了。

出栈,或者说释放内存的过程,根据栈的特性,是相反的,所以就不赘述了。

一个 C或C++程序,它眼中的内存地址分分为这么五个区域:

栈区(stack)、堆区(heap)、全局静态区(static)、文字常量区和程序指令区。

栈区和堆区前面已经介绍过,全局静态区用于存放全局变量和静态static静态变量,全局静态区分为两块内容:一块用于初始化以后的全局变量和静态变量,一块用于未初始化的全局变量和静态变量。全局静态区和堆一样,程序结束后由操作系统进行释放。文字常量区用于存放常量字符串,程序结束后由操作系统进行释放。程序指令区最好理解,就是存放程序代码的二进制指令。

[cpp] view plaincopyprint?

int cnt;// 存放在全局静态区的未初始化区

int num = 0;// 存放在全局静态区的已初始化区

int *p;// 存放在全局静态区的未初始化区

int main()

{

int i, j, k;// 存放在栈区

int *pBuffer =(int *)malloc(sizeof(int)* 10);// 指针pBuffer在栈中,该内存在堆中char *s = “hactrox”;// 指针s存放在栈中,字符串存放在文字常量区中char str[] = “hactrox”;// str和字符串存放在栈中

static int a = 0;// a存放在全局静态区的已初始化区

}

char *s = “hactrox”;// “hactrox”在文字常量区,s指向这个区域中的“hactrox”,所以这可以理解为,首先在文字常量区创建了这个字符串,然后s指向这个字符串这样两个步骤。s本身作为一个局部变量存储在栈中。

// 下面的代码是错误的,指针还没指向就直接赋值了?

int *p = 5;

// 下面的代码才是正确的,首先要创建这个int型变量,然后p指向这个变量。new来的int变量在堆中。

int *p = new int(5);

接下来我们看一看一个非常常见的问题:下述代码有没有什么问题?有问题的话问题在哪里?

[cpp] view plaincopyprint?

#include

using namespace std;

char* f1()

{

char *s = “hactrox”;

return s;

}

char* f2()

{

char s[] = “hactrox”;

return s;

}

int main()

{

printf(“%sn”, f1());

printf(“%sn”, f2());

getchar();

return 0;

}

问题在于第二个函数,f2并不能正确返回那个字符串。在函数f1中,“hactrox”字符串创建于文字常量区,然后返回该常量字符串的地址,因为文字常量区的字符串是全局的,虽然指针s是局部变量,但是s在消亡前已经把目标地址送出来了,所以s消亡与否不是重点,重点是返回的地址所指向的区域还在,所以能正确显示。在函数f2中,“hactrox”与s均为局部变量,它们保存在栈中。虽然s同样返回了一个地址,但这个地址所指向的内存已经被释放掉了。地址有效,但目标已无效。所以输出的只是乱码。

[cpp] view plaincopyprint?

#include

using namespace std;

void func()

{

char *str1 = “123”;

printf(“%xn”, str1);

char *str2 = “123”;

// 同在文字常量区,编译器可能会将str2直接指向str1所指向的内存,// 而不是开辟新的空间来存放第二个相同字符串。

// 通过打印str2的指针可验证

printf(“%xn”, str2);

char *s1 = “hactrox”;

printf(“%xn”, s1);

char *s2 = “hactrox”;

printf(“%xn”, s2);

}

int main()

{

func();

getchar();

return 0;

}

char s[] = “hactrox”;

char *s = “hactrox again”;

第二段代码,即文字常量区变量在编译的时候就已经确定了,而第一段代码,是在运行的时候进行赋值的。

这样看起来貌似第二段代码的效率要高,其实不然,当在运行时刻用到这两个变量的时候,对于第一段代码,直接读取字符串,而对于第二段代码,首先读取该字符串指针,然后根据指针再读取字符串,显然效率就下降了。其实我觉得关注栈和堆,其实主要是关注作用域、生命周期和有效性的问题。

指针被释放了,不代表指针指向的内存会被释放。同样的,指针指向的内存被释放了,不代表指针会被同步释放或自动指向NULL,指针依旧指向那块已经失效了的地址。这块地址不能用,谁都不能保证一块已经失效的地址接下来会发生什么。

第三篇:Java的内存泄漏 总结分析

Java的内存泄漏

欧阳辰(yeekee@sina.com), 周欣(mailto:zhouxin@sei.pku.edu.cn), 简介: Java的一个重要优点就是通过垃圾收集器(Garbage Collection,GC)自动管理内存的回收,程序员不需要通过调用函数来释放内存。因此,很多程序员认为Java不存在内存泄漏问题,或者认为即使有内存泄漏也不是程序的责任,而是GC或JVM的问题。其实,这种想法是不正确的,因为Java也存在内存泄露,但它的表现与C++不同。本文的标签: j2se, java, java内存泄露, 内存, 内存泄漏, 内存泄露 标记本文!

发布日期: 2002 年 10 月 21 日 级别: 初级

访问情况: 18862 次浏览

评论: 4(查看 | 添加评论-登录)平均分(59个评分)为本文评分

问题的提出

Java的一个重要优点就是通过垃圾收集器(Garbage Collection,GC)自动管理内存的回收,程序员不需要通过调用函数来释放内存。因此,很多程序员认为Java不存在内存泄漏问题,或者认为即使有内存泄漏也不是程序的责任,而是GC或JVM的问题。其实,这种想法是不正确的,因为Java也存在内存泄露,但它的表现与C++不同。

随着越来越多的服务器程序采用Java技术,例如JSP,Servlet,EJB等,服务器程序往往长期运行。另外,在很多嵌入式系统中,内存的总量非常有限。内存泄露问题也就变得十分关键,即使每次运行少量泄漏,长期运行之后,系统也是面临崩溃的危险。

回页首

Java是如何管理内存

为了判断Java中是否有内存泄露,我们首先必须了解Java是如何管理内存的。Java的内存管理就是对象的分配和释放问题。在Java中,程序员需要通过关键字new为每个对象申请内存空间(基本类型除外),所有的对象都在堆(Heap)中分配空间。另外,对象的释放是由GC决定和执行的。在Java中,内存的分配是由程序完成的,而内存的释放是有GC完成的,这种收支两条线的方法确实简化了程序员的工作。但同时,它也加重了JVM的工作。这也是Java程序运行速度较慢的原因之一。因为,GC为了能够正确释放对象,GC必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。

监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

为了更好理解GC的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从main进程开始执行,那么该图就是以main进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象(连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。

以下,我们举一个例子说明如何用有向图表示内存管理。对于程序的每一个时刻,我们都有一个有向图表示JVM的内存分配情况。以下右图,就是左边程序运行到第6行的示意图。

Java使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。这种方式的优点是管理内存的精度很高,但是效率较低。另外一种常用的内存管理技术是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相比,精度行低(很难处理循环引用的问题),但执行效率很高。

回页首

什么是Java中的内存泄露

下面,我们就可以描述什么是内存泄漏。在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。

通过分析,我们得知,对于C++,程序员需要自己管理边和顶点,而对于Java程序员只需要管理边就可以了(不需要管理顶点的释放)。通过这种方式,Java提高了编程的效率。

因此,通过以上分析,我们知道在Java中也有内存泄漏,但范围比C++要小一些。因为Java从语言上保证,任何对象都是可达的,所有的不可达对象都由GC管理。

对于程序员来说,GC基本是透明的,不可见的。虽然,我们只有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义,该函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。下面给出了一个简单的内存泄露的例子。在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个Vector中,如果我们仅仅释放引用本身,那么Vector仍然引用该对象,所以这个对象对GC来说是不可回收的。因此,如果对象加入到Vector后,还必须从Vector中删除,最简单的方法就是将Vector对象设置为null。

Vector v=new Vector(10);for(inti=1;i<100;i++){

} Object o=new Object();v.add(o);o=null;

//此时,所有的Object对象都没有被释放,因为变量v引用这些对象。

回页首

如何检测内存泄漏

最后一个重要的问题,就是如何检测Java的内存泄漏。目前,我们通常使用一些工具来检查Java程序的内存泄漏问题。市场上已有几种专业检查Java内存泄漏的工具,它们的基本工作原理大同小异,都是通过监测Java程序运行时,所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。开发人员将根据这些信息判断程序是否有内存泄漏问题。这些工具包括Optimizeit Profiler,JProbe Profiler,JinSight , Rational 公司的Purify等。

下面,我们将简单介绍Optimizeit的基本功能和工作原理。

Optimizeit Profiler版本4.11支持Application,Applet,Servlet和Romote Application四类应用,并且可以支持大多数类型的JVM,包括SUN JDK系列,IBM的JDK系列,和Jbuilder的JVM等。并且,该软件是由Java编写,因此它支持多种操作系统。Optimizeit系列还包括Thread Debugger和Code Coverage两个工具,分别用于监测运行时的线程状态和代码覆盖面。

当设置好所有的参数了,我们就可以在OptimizeIt环境下运行被测程序,在程序运行过程中,Optimizeit可以监视内存的使用曲线(如下图),包括JVM申请的堆(heap)的大小,和实际使用的内存大小。另外,在运行过程中,我们可以随时暂停程序的运行,甚至强行调用GC,让GC进行内存回收。通过内存使用曲线,我们可以整体了解程序使用内存的情况。这种监测对于长期运行的应用程序非常有必要,也很容易发现内存泄露。

在运行过程中,我们还可以从不同视角观查内存的使用情况,Optimizeit提供了四种方式:

 堆视角。这是一个全面的视角,我们可以了解堆中的所有的对象信息(数量和种类),并进行统计、排序,过滤。了解相关对象的变化情况。

 方法视角。通过方法视角,我们可以得知每一种类的对象,都分配在哪些方法中,以及它们的数量。

 对象视角。给定一个对象,通过对象视角,我们可以显示它的所有出引用和入引用对象,我们可以了解这个对象的所有引用关系。

 引用图。给定一个根,通过引用图,我们可以显示从该顶点出发的所有出引用。在运行过程中,我们可以随时观察内存的使用情况,通过这种方式,我们可以很快找到那些长期不被释放,并且不再使用的对象。我们通过检查这些对象的生存周期,确认其是否为内存泄露。在实践当中,寻找内存泄露是一件非常麻烦的事情,它需要程序员对整个程序的代码比较清楚,并且需要丰富的调试经验,但是这个过程对于很多关键的Java程序都是十分重要的。

综上所述,Java也存在内存泄露问题,其原因主要是一些对象虽然不再被使用,但它们仍然被引用。为了解决这些问题,我们可以通过软件工具来检查内存泄露,检查的主要原理就是暴露出所有堆中的对象,让程序员寻找那些无用但仍被引用的对象。

下载初探java内存机制_堆和栈word格式文档
下载初探java内存机制_堆和栈.doc
将本文档下载到自己电脑,方便修改和收藏,请勿使用迅雷等下载。
点此处下载文档

文档为doc格式


声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:645879355@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。

相关范文推荐