java造成内存泄露的几种情况
一、内存泄漏的概念
所谓内存泄漏就是指一个不在被程序使用或变量一直被占据在内存中
二、GC的概念
GC就是垃圾收集的意思。java提供的GC功能可以自动检测对象是否存活,和C语言不同的是没有提供显式的操作内存的方式
三、如何判断一个对象是否存活
1. 引用计数法
给每一个对象增加一个引用计数器,每有一个地方引用该对象,将引用计数器加一,引用失效时,引用计数器减一,如果引用计数器为0,则证明该对象没有被引用,则认为该对象是一个‘死对象’,将会被垃圾回收。
不足之处:当循环引用的时候就会造成无法回收,导致空间泄漏。例子:A引用B,B引用A,则A,B中的引用计数器都不为0,所以无法被回收
2.可达性算法
该算法的大致意思是:从一个GC roots开始向下搜索,如果一个对象到达GC roots没有任何引用链相连,则证明此对象为‘死对象’,会被垃圾回收
可作为GC roots的对象:
– 方法区的静态属性引用的对象
– 方法区常量池应用的对象
– 虚拟机栈中引用的对象
– 本地方法栈JNI引用的对象
四、垃圾回收算法
1. 标记清除算法
概念:分为两个阶段,‘标记’和‘清除’。标记阶段将需要回收的对象进行标记,标记完成之后统一回收
缺点: – 效率低
– 会产生大量的空间碎片,导致大的对象找不到足够的连续空间,会导致另外一次GC
2. 标记整理算法
概念:标记阶段将需要回收的对象进行标记,标记之后将存活的对象向一端移动,然后清除掉其他的内存区域
优点:不会产生空间碎片
缺点:移动对象需要成本
3. 复制算法
概念:为了优化标记清除算法,将可用内存分为两个大小完全相同的区域,当其中一块内存区域用完之后,将存活的对象复制到另一块区域上,然后将第一块区域的对象回收
jvm中新生代的垃圾回收就是采用的复制算法,新生代的对象98%的对象都是“ 朝生夕死 ”,所以不需要把内存按照1:1来分配,而是把虚拟机分成较大的Eden区和较小的Survivor区, HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是说,每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的空间会被浪费。
当Survivor区不够用时,需要老年代来分担,当一个对象经历一次GC且没有被回收,则年龄加1,当年龄为15岁的时候,就会移至老年代,大对象直接移至老年代
优点:- 不会产生空间碎片
– 只需移动栈顶指针,按顺序分配内存即可,实现简单
– 一次只对一块内存进行回收,效率高
缺点:可用空间变成一半
补充:
增量算法 (Incremental Collecting)
在垃圾回收过程中,应用软件将处于一种 CPU 消耗很高的状态。在这种 CPU 消耗很高的状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。
增量算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降
四、内存泄漏的情况
搞懂GC是怎么回事之后,看看内存泄漏的几种情况:
1. 长生命周期的对象持有短生命周期的引用就可能发生内存泄漏,短生命周期对象已经不再需要,但是因为长生命周期的对象的引用导致无法被回收
2.如果一个外部类的实例对象的方法返回一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类的实例对象不再被使用,但由于内部类持久外部类的实例对象,这个外部类对象将不会被会回收
3.当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存入集合中的哈希值不同了,这种情况下,引用当前引用区检索对象,也找不回对象的结果,这也会导致无法从hashset中单独删除当前对象,造成内存泄漏