① 内存泄露(Memory Leak)是指一个不再被使用的对象或者变量还在内存中占有存储空间。
在C/C++语言中,内存泄露出现在开发人员忘记释放已分配的内存就会造成内存泄露。
在java语言中引入垃圾回收机制,有GC负责进行回收不再使用的对象,释放内存。但是还是会存在内存泄露的问题。
内存泄露主要有两种情况:
- 1.在堆中申请的空间没有释放。
- 2.对象已不再被使用(注意:这里的不在被使用是指对程序来说没有用处,如数据库连接使用后没有关。但是还是存在着引用),但是仍然在内存中保留着。GC机制的引入只能解决第一种情况,对于第2种情况无法保证不再使用的对象会被释放。java语言中的内存泄露主要指第2种情况。
内存泄露的原因:
- 1.静态集合类。如HashMap和Vector。这些容器是静态的,生命周期和程序的生命周期一致,那么在容器中对象的生命周期也和其一样,对象在程序结束之前将不能被释放,从而会造成内存泄露。
- 2.各种连接,如数据库连接,网络连接,IO连接,不再使用时如果连接不释放容易造成内存泄露。
- 3.监听器,释放对象时往往没有相应的删除监听器,可能会导致内存泄露。
内存溢出(OOM)是指程序在申请内存时没有足够的内存供使用,进而导致程序崩溃这是结果描述。
内存泄露(Memory Leak)最终会导致内存溢出。
② 垃圾回收机制。
年轻代:对象被创建时(new)的对象通常被放在Young(除了一些占据内存比较大的对象),经过一定的Minor GC(针对年轻代的内存回收)还活着的对象会被移动到年老代(一些具体的移动细节省略)。
年老代:就是上述年轻代移动过来的和一些比较大的对象。Minor GC(FullGC)是针对年老代的回收
永久代:存储的是final常量,static变量,常量池。
经过fullgc之后,年老区的内存回收,而年轻区的占了15个,不算PermGen。
1.范围:要回收哪些区域?
2.前提:如何判断对象已死?
(1)引用计数法
引用计数法就是通过一个计数器记录该对象被引用的次数,方法简单高效,但是解决不了循环引用的问题。比如对象A包含指向对象B的引用,对象B也包含指向对象A的引用,但没有引用指向A和B,这时当前回收如果采用的是引用计数法,那么对象A和B的被引用次数都为1,都不会被回收。
1 package com.cdai.jvm.gc; 2 public class ReferenceCount { 3 final static int MB = 1024 * 1024; 4 byte[] size = new byte[2 * MB]; 5 Object ref; 6 public static void main(String[] args) { 7 ReferenceCount objA = new ReferenceCount(); 8 ReferenceCount objB = new ReferenceCount(); 9 objA.ref = objB; 10 objB.ref = objA; 11 objA = null; 12 objB = null; 13 System.gc(); 14 System.gc(); 15 } 16 } 17 [Full GC (System) [Tenured: 2048K->366K(10944K), 0.0046272 secs] 4604K->366K(15872K), [Perm : 154K->154K(12288K)], 0.0046751 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
(2)根搜索
因为每个线程都有一个栈,所以我们需要选取多个根对象。
【附】:对象复活
1 package com.cdai.jvm.gc; 2 public class DeadToRebirth { 3 private static DeadToRebirth hook; 4 @Override 5 public void finalize() throws Throwable { 6 super.finalize(); 7 DeadToRebirth.hook = this; 8 } 9 public static void main(String[] args) throws Exception { 10 DeadToRebirth.hook = new DeadToRebirth(); 11 DeadToRebirth.hook = null; 12 System.gc(); 13 Thread.sleep(500); 14 if (DeadToRebirth.hook != null) 15 System.out.println("Rebirth!"); 16 else 17 System.out.println("Dead!"); 18 DeadToRebirth.hook = null; 19 System.gc(); 20 Thread.sleep(500); 21 if (DeadToRebirth.hook != null) 22 Systemystem.out.println("Rebirth!"); 23 else 24 System.out.println("Dead!"); 25 } 26 }
要注意的两点是:
第一,finalize()方法只会被执行一次,所以对象只有一次复活的机会。
3.策略:垃圾回收的算法
(1)标记-清除
没错,这里的标记指的就是之前我们介绍过的两次标记过程。标记完成后就可以对标记为垃圾的对象进行回收了。怎么样,简单吧。但是这种策略的缺点很明显,回收后内存碎片很多,如果之后程序运行时申请大内存,可能会又导致一次GC。虽然缺点明显,这种策略却是后两种策略的基础。正因为它的缺点,所以促成了后两种策略的产生。
(2)标记-复制
将内存分为两块,标记完成开始回收时,将一块内存中保留的对象全部复制到另一块空闲内存中。实现起来也很简单,当大部分对象都被回收时这种策略也很高效。但这种策略也有缺点,可用内存变为一半了!
怎样解决呢?聪明的程序员们总是办法多过问题的。可以将堆不按1:1的比例分离,
而是按8:1:1分成一块Eden和两小块Survivor区,每次将Eden和Survivor中存活的对象复制到另一块空闲的Survivor中。这三块区域并不是堆的全部,而是构成了新生代。
(3)标记-整理
Serial:单线程的收集器,只使用一个线程进行收集,并且收集时会暂停其他所有工作线程(Stop the world)。它是Client模式下的默认新生代收集器。
ParNew:Serial收集器的多线程版本。在单CPU甚至两个CPU的环境下,由于线程交互的开销,无法保证性能超越Serial收集器。
(2)老年代上的GC实现
Serial Old:Serial收集器的老年代版本。
Parallel Old:Parallel Scavenge的老年代版本。此前,如果新生代采用PS GC的话,老年代只有Serial Old能与之配合。现在有了Parallel Old与之配合,可以在注重吞吐量及CPU资源敏感的场合使用了。
5.触发:何时开始GC?
(1)老年代空间不足
(2)PermSpace空间不足
(3)统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间
这里注意一点:PermSpace并不等同于方法区,只不过是Hotspot JVM用PermSpace来实现方法区而已,有些虚拟机没有PermSpace而用其他机制来实现方法区。
6.补充:对象的空间分配和晋升
(1)对象优先在Eden上分配
(2)大对象直接进入老年代
虚拟机提供了-XX:PretenureSizeThreshold参数,大于这个参数值的对象将直接分配到老年代中。因为新生代采用的是标记-复制策略,在Eden中分配大对象将会导致Eden区和两个Survivor区之间大量的内存拷贝。
(3)长期存活的对象将进入老年代
============================================================================================================================================================================================================================================
堆内存设置
原理
- Permanent 即 持久代(Permanent Generation),主要存放的是Java类定义信息,与垃圾收集器要收集的Java对象关系不大。
- Heap = { Old + NEW = {Eden, from, to} },Old 即 年老代(Old Generation),New 即 年轻代(Young Generation)。年老代和年轻代的划分对垃圾收集影响比较大。
年轻代
所有新生成的对象首先都是放在年轻代。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代一般分3个区,1个Eden区,2个Survivor区(from 和 to)。
大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当一个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当另一个Survivor区也满了的时候,从前一个Survivor区复制过来的并且此时还存活的对象,将可能被复制到年老代。
2个Survivor区是对称的,没有先后关系,所以同一个Survivor区中可能同时存在从Eden区复制过来对象,和从另一个Survivor区复制过来的对象;而复制到年老区的只有从另一个Survivor区过来的对象。而且,因为需要交换的原因,Survivor区至少有一个是空的。特殊的情况下,根据程序需要,Survivor区是可以配置为多个的(多于2个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
针对年轻代的垃圾回收即 Young GC。
年老代
在年轻代中经历了N次(可配置)垃圾回收后仍然存活的对象,就会被复制到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
针对年老代的垃圾回收即 Full GC。
持久代
用于存放静态类型数据,如 Java Class, Method 等。持久代对垃圾回收没有显著影响。但是有些应用可能动态生成或调用一些Class,例如 Hibernate CGLib 等,在这种时候往往需要设置一个比较大的持久代空间来存放这些运行过程中动态增加的类型。
所以,当一组对象生成时,内存申请过程如下:
- JVM会试图为相关Java对象在年轻代的Eden区中初始化一块内存区域。
- 当Eden区空间足够时,内存申请结束。否则执行下一步。
- JVM试图释放在Eden区中所有不活跃的对象(Young GC)。释放后若Eden空间仍然不足以放入新对象,JVM则试图将部分Eden区中活跃对象放入Survivor区。
- Survivor区被用来作为Eden区及年老代的中间交换区域。当年老代空间足够时,Survivor区中存活了一定次数的对象会被移到年老代。
- 当年老代空间不够时,JVM会在年老代进行完全的垃圾回收(Full GC)。
- Full GC后,若Survivor区及年老代仍然无法存放从Eden区复制过来的对象,则会导致JVM无法在Eden区为新生成的对象申请内存,即出现“Out of Memory”。
OOM(“Out of Memory”)异常一般主要有如下2种原因:
2. 持久代溢出,表现为:java.lang.OutOfMemoryError:PermGenspace