我们知道计算机的基本构成是:运算器、控制器、存储器、输入和输出设备,那这个JVM也是有这成套的元素,运算器是当然是交给硬件CPU还处理了,只是为了适应“一次编译,随处运行”的情况,需要做一个翻译动作,于是就用了JVM指令集,这与汇编的命令集有点类似,每一种汇编命令集针对一个系列的CPU,比如8086系列的汇编也是可以用在8088上的,但是就不能跑在8051上,而JVM的命令集则是可以到处运行的,因为JVM做了翻译,根据不同的CPU,翻译成不同的机器语言。
其虚拟机的基本结构如下图所示。
其中需要我们重点理解的就是Stack和Heap,也就是堆和栈,他们在实际的应用中非常的广泛。
1、栈
Java栈是由许多栈帧(frame)组成,一个栈帧包含一个Java方法的调用状态。当现成调用一个Java方法时,JVM压入一个新的栈帧到该线程的Java栈中;当方法返回时,这个栈帧被从Java栈中弹出并抛弃。
栈帧由三部分构成:局部变量区、操作数栈和帧数据区。
- 局部变量区被组织为一个数组,该数组单个元素长度为一个JVM字(注意不是一个机器字),用于保存本地变量(Local Variables),包括输入参数和输出参数以及方法内的变量;
- 操作数栈也是被组织为一个单位长度为一个JVM字的数组,不过该数组只能通过标准栈操作(push和pop)访问,该栈是被(逻辑上的)CPU的ALU唯一识别的操作数来源。当方法返回值时,通常都是从栈中弹出一个数据来返回。
- 帧数据区保存一些数据来支持常量池解析、正常方法返回以及异常派发机制。
如有分别存在局部变量区的a,b,c,要计算c=a+b,则使用4条指令:
- 读局部变量区[0]的值,压入操作数栈;
- 读局部变量区[1]的值,压入操作数栈;
- 弹出操作数栈栈顶值,再弹出操作数栈栈顶值,相加,把结果压入操作数栈;
- 弹出操作数栈栈顶值,写入局部变量区[2]。
- public class test {
- public static void main(String args[]) {
- int a = 2;
- int b = 3;
- int c = a + b;
- }
- }
- 0 iconst_2
- 1 istore_1 [a]
- 2 iconst_3
- 3 istore_2 [b]
- 4 iload_1 [a]
- 5 iload_2 [b]
- 6 iadd
- 7 istore_3 [c]
- 8 return
可以看到,是先将值为2和2存储到局部变量区,然后分别弹出压入操作数栈,最后进行相加操作,将结果填入局部变量区,返回操作数栈中的数据。
2、堆
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行。 一般的,对象的内存分配都是在堆上进行。
在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
如上可以看出,不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。 虚拟机中的共划分为三个代:年轻代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。如下图。
新生代(Young Generation):
对象被创建时,内存的分配首先发生在新生区(大对象可以直接被创建在新生区),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被新生区的GC机制清理掉。
新生区上的内存分配是这样的,新生区可以分为3个区域:Eden(伊甸园)区(表示内存首次分配的区域)和两个存活区(Survivor 0 、Survivor 1)。其内存的分配及回收过程应用的主要策略算法就是停止-复制(Stop-and-copy)清理法,但是这不代表着停止 - 复制清理法很高效。。
- 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;
- 当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的),此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0;
- 当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的)。
- 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。
在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),这两种技术的做法分别是:由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;而对于TLAB技术是对于多线程而言的,将Eden区分为若干段,每个线程使用独立的一段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每个线程都使用Eden区的一段,并快速的分配内存。
年老代(Old Generation):
对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC。
如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上。
可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。
老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效。一般,老年代用的算法是标记-整理算法。
分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,清除未标记对象,并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法不会产生碎片。
持久代(Permanent Generation):
用于存放静态文件,如 Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置。
永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:
- 类的所有实例都已经被回收
- 加载类的ClassLoader已经被回收
- 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)
1)栈是运行时的单位,而堆是存储的单位。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
在Java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而堆只负责存储对象信息。
2)为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
第一,从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。
第二,堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。
第三,栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。
第四,面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。不得不承认,面向对象的设计,确实很美。
3)堆中存什么?栈中存什么?
堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说是可以动态变化的,但是在栈中,一个对象只对应了一个4byte的引用(堆栈分离的好处)。
为什么不把基本类型放堆中呢?因为其占用的空间一般是1~8个字节——需要空间比较少,而且因为是基本类型,所以不会出现动态增长的情况——长度固定,因此栈中存储就够了,如果把他存在堆中是没有什么意义的(还会浪费空间,后面说明)。可以这么说,基本类型和对象的引用都是存放在栈中,而且都是几个字节的一个数,因此在程序运行时,他们的处理方式是统一的。但是基本类型、对象引用和对象本身就有所区别了,因为一个是栈中的数据一个是堆中的数据。最常见的一个问题就是,Java中参数传递时的问题。
4)Java中的参数传递时传值呢?还是传引用?(引用类型包括:类类型,接口类型和数组)
在运行栈中,基本类型和引用的处理是一样的,都是传值,所以,如果是传引用的方法调用,也同时可以理解为“传引用值”的传值调用,即引用的处理跟基本类型是完全一样的。但是当进入被调用方法时,被传递的这个引用的值,被程序解释(或者查找)到堆中的对象,这个时候才对应到真正的对象。如果此时进行修改,修改的是引用对应的对象,而不是引用本身,即:修改的是堆中的数据。所以这个修改是可以保持的了。
- public class test2 {
- public void change(StringBuffer a, int b,String c) {
- a.append("xxxx");
- b = 2;
- c+="nnn";
- System.out.println(c);//mmmmnnn
- }
- public static void main(String[] args) {
- StringBuffer a=new StringBuffer("yyyy");
- String c="mmmm";
- int b=3;
- new test2().change(a, b,c);
- System.out.println(a);// yyyyxxxx
- System.out.println(b);// 3
- System.out.println(c);// mmmm
- }
- }
Java中,栈的大小通过-Xss来设置,当栈中存储数据比较多时,需要适当调大这个值,否则会出现java.lang.StackOverflowError异常。常见的出现这个异常的是无法返回的递归,因为此时栈中保存的信息都是方法返回的记录点。
内存的分配:
这一篇将简单介绍一下Java内存的分配,下一篇将介绍内存的回收及内存泄漏等知识。
1、JVM内存模型
1、程序计数器(Program Counter Register):
程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。每个程序计数器只用来记录一个线程的行号,所以它是线程私有的。
如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。
2、虚拟机栈(JVM Stack):
一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作栈、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。
局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。
虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出OutOfMemoryError(内存溢出)。
每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的。
3、本地方法栈(Native Method Statck):
本地方法栈在作用,运行机制,异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。
本地方法栈也是线程私有的。
4、堆区(Heap):
堆区是理解Java GC机制最重要的区域,没有之一。在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存(不过现代技术里,也不是这么绝对的,也有栈上直接分配的)。
一般的,根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前主流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。
5、方法区(Method Area):
在Java虚拟机规范中,将方法区作为堆的一个逻辑部分来对待,但事实上,方法区并不是堆(Non-Heap);另外,不少人的博客中,将Java GC的分代收集机制分为3个代:青年代,老年代,永久代,这些作者将方法区定义为“永久代”,这是因为,对于之前的HotSpot Java虚拟机的实现方式中,将分代收集的思想扩展到了方法区,并将方法区设计成了永久代。不过,除HotSpot之外的多数虚拟机,并不将方法区当做永久代,HotSpot本身,也计划取消永久代。本文中,由于笔者主要使用Oracle JDK6.0,因此仍将使用永久代一词。
方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。
方法区在物理上也不需要是连续的,可以选择固定大小或可扩展大小,并且方法区比堆还多了一个限制:可以选择是否执行垃圾收集。一般的,方法区上执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一(HotSpot),但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。
在方法区上进行垃圾收集,条件苛刻而且相当困难,效果也不令人满意,所以一般不做太多考虑,可以留作以后进一步深入研究时使用。
在方法区上定义了OutOfMemoryError:PermGen space异常,在内存不足时抛出。
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)。
6、直接内存(Direct Memory):
直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区(Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。
2、数据在内存中的分配
1、基本类型及包装类型
基本类型都存储在栈内,这样可以提高程序执行的效率。但是需要注意基本类型的包装类型。包装类型是一个对象,所以属于引用类型,存储在堆中。其所需要的存储空间大小至少是12byte(声明一个空Object至少需要的空间),而且12byte没有包含任何有效信息,同时,因为Java对象大小是8的整数倍,因此一个基本类型包装类的大小至少是16byte。这个内存占用是很恐怖的,它是使用基本类型的N倍(N>2),有些类型的内存占用更是夸张。因此,可能的话应尽量少使用包装类。在JDK5.0以后,因为加入了自动类型装换,因此,Java虚拟机会在存储方面进行相应的优化。
2、引用类型
基本数据的类型的大小是固定的,对于非基本类型的Java对象,需要经过计算得知。 在Java中,一个空Object对象的大小是8byte,这个大小只是保存堆中一个没有任何属性的对象的大小。如
- Object ob = new Object();
这样在程序中完成了一个Java对象的生命,但是它所占的空间为:4byte+8byte。4byte是上面部分所说的Java栈中保存引用的所需要的空间。而那8byte则是Java堆中对象的信息。因为所有的Java非基本类型的对象都需要默认继承Object对象,因此不论什么样的Java对象,其大小都必须是大于8byte。
- Class NewObject {
- int count;
- boolean flag;
- Object ob;
- }
其大小为:空对象大小(8byte)+int大小(4byte)+Boolean大小(1byte)+空Object引用的大小(4byte)=17byte。但是因为Java在对对象内存分配时都是以8的整数倍来分,因此大于17byte的最接近8的整数倍的是24,因此此对象的大小为24byte。
接下来看一下Java的引用类型访问。一般来说,一个Java的引用访问涉及到3个内存区域:虚拟机栈,堆,方法区:
- Object obj表示一个本地引用,存储在JVM栈的本地变量表中,表示一个reference类型数据,值为null,空对象不不能使用,因为它没有任何的引用实体。
- new Object()作为实例对象数据存储在堆中,,在堆内存中为类的成员变量分配内存,并将其初始化为各数据类型的默认值;接着进行显式初始化;最后调用构造方法,为成员变量赋值。返回堆内存中对象的引用(相当于首地址)给引用变量obj,以后就可以通过obj来引用堆内存中的对象了。
- 堆中还记录了Object类的类型信息(接口、方法、field、对象类型等)的地址,这些地址所执行的数据存储在方法区中;
在Java虚拟机规范中,对于通过reference类型引用访问具体对象的方式并未做规定,目前主流的实现方式主要有两种:
1、通过句柄访问(图来自于《深入理解Java虚拟机:JVM高级特效与最佳实现》):
通过句柄访问的实现方式中,JVM堆中会专门有一块区域用来作为句柄池,存储相关句柄所执行的实例数据地址(包括在堆中地址和在方法区中的地址)。这种实现方法由于用句柄表示地址,因此十分稳定。
2、通过直接指针访问:(图来自于《深入理解Java虚拟机:JVM高级特效与最佳实现》)
通过直接指针访问的方式中,reference中存储的就是对象在堆中的实际地址,在堆中存储的对象信息中包含了在方法区中的相应类型数据。这种方法最大的优势是速度快,在HotSpot虚拟机中用的就是这种方式。
接下来看一下实际中最常用的数组的存储。在栈内存中创建一个数组引用,通过该引用(即数组名)来引用数组。x=new int[3];将在堆内存中分配3个保存 int型数据的空间,堆内存的首地址放到栈内存中,每个数组元素被初始化为0。
3、静态变量
其实在引用类型中最为特殊的就是加static关键字的全局变量。存储在线程可以共享的方法区中,这个区中包含所有的class和static变量,在整个程序中永远唯一的元素。所以如果某一处修改了static变量,则其它地方的引用将全部受到修改的影响。
4、特殊的String字符串
String是一个特殊的包装类数据。可以用以下两种方式创建:
- String str = new String("abc");
- String str = "abc";
第一种创建方式,和普通对象的的创建过程一样;
第二种创建方式,java内部将此语句转化为以下几个步骤:
- (1)先定义一个名为str的对String类的对象引用变量:String str;
- (2)在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc"地址,接着创建一个新的String类的对象o,并将o的字符串值指向这个地址,而且在栈这个地 址旁边记下这个引用的对象o。如果已经有了值为"abc"的地址,则查找对象o,并回o的地址。
- (3)将str指向对象o的地址。
为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。
- String str1="abc";
- String str2="abc";
- System.out.println(s1==s2);//true
注意,这里并不用 str1.equals(str2);的方式,因为这将比较两个字符串的值是否相等。==号,根据JDK的说明,只有在两个引用都指向了同一个对象时才返回真值。而我们在这里要看的是,str1与str2是否都指向了同一个对象。
我们再接着看以下的代码。
- String str1= new String("abc");
- String str2= "abc";
- System.out.println(str1==str2); //false
创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。
以上两段代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。
Java内存在分配和回收的过程中会产品很多的问题,下面来说一说可能会产生的问题。
1、垃圾处理
在下面的情境中,对象A引用了对象B. A的生命周期(t1 - t4) 比 B的(t2 - t3)要长得多. 当对象B在应用程序逻辑中不会再被使用以后, 对象 A 仍然持有着 B的引用. (根据虚拟机规范)在这种情况下垃圾收集器不能将 B 从内存中释放. 这种情况很可能会引起内存问题,假若A 还持有着其他对象的引用,那么这些被引用的(无用)对象都不会被回收,并占用着内存空间.
甚至有可能 B 也持有一大堆其他对象的引用。 这些对象由于被 B 所引用,也不会被垃圾收集器所回收. 所有这些无用的对象将消耗大量宝贵的内存空间。
2. 注意事件监听和回调.当注册的监听器不再使用以后,如果没有被注销,那么很可能会发生内存泄露.
3. "当一个类自己管理其内存空间时,程序员应该注意内存泄露." 常常是一个对象的成员变量需要被置为null 时仍然指向其他对象,
(3)内存泄漏举例
- import java.util.EmptyStackException;
- public class test {
- private Object[] elements = new Object[10];
- private int size = 0;
- public void push(Object e) {
- ensureCapacity();
- elements[size++] = e;
- }
- public Object pop() {
- if (size == 0)
- throw new EmptyStackException();
- return elements[--size];
- }
- private void ensureCapacity() {
- if (elements.length == size) {
- Object[] oldElements = elements;
- elements = new Object[2 * elements.length + 1];
- System.arraycopy(oldElements, 0, elements, 0, size);
- }
- }
- }
上面的原理应该很简单,假如堆栈加了10个元素,然后全部弹出来,虽然堆栈是空的,没有我们要的东西,但是这是个对象是无法回收的,这个才符合了内存泄露的两个条件:无用,无法回收。
但是就是存在这样的东西也不一定会导致什么样的后果,如果这个堆栈用的比较少,也就浪费了几个K内存而已,反正我们的内存都上G了,哪里会有什么影响,再说这个东西很快就会被回收的,有什么关系。下面看两个例子。
- import java.util.Stack;
- public class test3 {
- public static Stack<Object> s = new Stack<>();
- static {
- s.push(new Object());
- s.pop(); // 这里有一个对象发生内存泄露
- s.push(new Object()); // 上面的对象可以被回收了,等于是自愈了 } }
- }
- }
因为是static,就一直存在到程序退出,但是我们也可以看到它有自愈功能,就是说如果你的Stack最多有100个对象,那么最多也就只有100个对象无法被回收其实这个应该很容易理解,Stack内部持有100个引用,最坏的情况就是他们都是无用的,因为我们一旦放新的进去,以前的引用自然消失!
内存泄露的另外一种情况:当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露。
- import java.util.HashSet;
- import java.util.Set;
- public class HashSetTest {
- public static void main(String[] args) {
- Set<Person> set = new HashSet<Person>();
- Person p1 = new Person("唐僧", "pwd1", 25);
- Person p2 = new Person("孙悟空", "pwd2", 26);
- Person p3 = new Person("猪八戒", "pwd3", 27);
- set.add(p1);
- set.add(p2);
- set.add(p3);
- System.out.println("总共有:" + set.size() + " 个元素!"); // 结果:总共有:3 个元素!
- p3.setAge(2); // 修改p3的年龄,此时p3元素对应的hashcode值发生改变
- set.remove(p3); // 此时remove不掉,造成内存泄漏
- set.add(p3); // 重新添加,居然添加成功
- System.out.println("总共有:" + set.size() + " 个元素!"); // 结果:总共有:4 个元素!
- for (Person person : set) {
- System.out.println(person);
- }
- }
- }
- package test6;
- public class Person {
- private String username;
- private String password;
- private int age;
- public Person(String username, String password, int age){
- this.username = username;
- this.password = password;
- this.age = age;
- }
- public String getUsername(){
- return username;
- }
- public void setUsername(String username) {
- this.username = username;
- }
- public String getPassword() {
- return password;
- }
- public void setPassword(String password) {
- this.password = password;
- }
- public int getAge() {
- return age;
- }
- public void setAge(int age) {
- this.age = age;
- }
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + age;
- result = prime * result + ((password == null) ? 0 : password.hashCode());
- result = prime * result + ((username == null) ? 0 : username.hashCode());
- return result;
- }
- public boolean equals(Object obj) {
- if (this == obj)
- return true;
- if (obj == null)
- return false;
- if (getClass() != obj.getClass())
- return false;
- Person other = (Person) obj;
- if (age != other.age)
- return false;
- if (password == null) {
- if (other.password != null)
- return false;
- }
- else if (!password.equals(other.password))
- return false;
- if (username == null) {
- if (other.username != null)
- return false;
- }
- else if (!username.equals(other.username))
- return false;
- return true;
- }
- public String toString() {
- return this.username+"-->"+this.password+"-->"+this.age;
- }
- }
总共有:3 个元素!
总共有:4 个元素!
猪八戒-->pwd3-->2
孙悟空-->pwd2-->26
唐僧-->pwd1-->25
猪八戒-->pwd3-->2