内存泄漏
-
概念:
- 内存溢出指的是内存不够用了;
- 内存泄漏是指对象可达,但是没用了。即本该被 GC 回收的对象并没有被回收;
- 内存泄露是导致内存溢出的原因之一;内存泄露积累起来将导致内存溢出。
-
内存泄漏原因分析:
- 长生命周期的对象引用短生命周期的对象;
- 没有将无用对象置为 null。
GC的意义/任务
- 有效防止内存泄漏和内存溢出
- 确保被引用对象的内存不被错误回收,而对于不再被引用的内存空间可以准确回收
为何要了解GC
构建大型程序时,GC直接影响着内存优化和运行速度。
finalize()
GC仅释放那些经由new创建的对象的内存,即发生在堆内存中,对于非new创建的对象将不知道如何释放,补助措施是在类中定义finalize()方法
finalize()工作原理:垃圾回收器准备释放对象,先调用其finalize(),在下一次垃圾回收动作发生时,真正回收对象的内存
finalize()不等同于C++析构函数:java中
- 对象可能不被垃圾回收
- 垃圾回收并不等于析构
垃圾回收并不总是发生,发生时会调用finalize()以及释放一些无用对象,但也会不发生,因而其对应的finalize()函数也将永远得不到调用,即对象将得不到清理
在执行过程中若调用system.gc()可以强制执行finalize()动作,但是用system.gc()会停止所有响应
java的堆管理不同于C++,效率还OK,对java而言,堆就像一个传送带,带上每个位置放东西(尽管不完全像,因为在传送带上放对象会造成大量空间间隙浪费),其“堆指针”直接指向空的位置,垃圾回收器在工作时,一面回收空间,一面将堆中的对象紧凑排列,实现一种高速、无限空间可分配的堆模型
垃圾回收机制中的算法:
GC将程序员从释放内存的复杂工作中解放出来的同时,为了实现GC,garbage collector必须跟踪内存的使用情况,释放没用的对象,在完成内存的释放之后还需要处理堆中的碎片, 这样做必然会增加JVM的负担。
JVM并无明确说明使用了哪种算法,但任一种算法均要做两件事:1.发现无用信息 2.回收无用对象的空间
引用计数法
给每个对象赋予一个计数器,对象有新的引用时+1,当某一个引用超过生命周期或者设置为新值时,-1,计数器为0的对象垃圾回收器会回收
优点:垃圾回收快,伴随着程序运行
缺点:无法检测出循环引用,例如:
MyObject obj1 = new MyObject();
MyObject obj2 = new MyObject();
obj1.object = obj2;(obj2的对象引用+1)
obj2.object = obj1;(obj1的对象引用+1)
obj1 = null;
obj2 = null;
代码中obj1和obj2相互引用,最后都为null,但是计数器都是1,。
其他算法
以下所有算法的基本思路大致相同(有向图的思路——GC Roots可达性分析算法):对任何“活”的对象,一定能够追溯到其存活在堆栈或者静态存储区中的引用(亦或者称为GC Roots)。
反过来,遍历在堆栈或者静态存储区中的引用(亦或者称为GC Roots),以这些GC Roots为起始点,从这些节点开始向下搜索,搜索所经过的路径称为引用链,在此路径的任何对象内部如果包含其余对象,会将其余对象性也包含进入此引用链,如此反复,定能找到所有“活”的对象,而没有与此引用链关联的对象则为不可用,也解决了以上循环引用的问题
可作为GC Roots的节点有:
- 虚拟机栈中引用的对象(局部变量)
- 方法区中类静态属性引用的对象
- 方法区常量引用的对象
- JNI(native方法)引用的对象
停止-复制法
将内存划分为大小相等的两块,一块满后,标记活的对象后,将标记的对象复制到一个新空间,此处需要将旧地址映射为新地址(修正引用),删掉原内存。执行过程需要暂停程序,回收动作非后台运行。
缺点:程序进入稳定状态后,可能仅产生少量垃圾,复制动作太浪费;代价是将内存缩小为原来的一半
新生代就是采用改进的复制算法。
标记-清除法
对停止-复制法的补充,在没有很多垃圾产生时,JVM自动切换,也就是“自适应”。(但对很多垃圾时,会特慢)
思路:首先标记所有需要回收的对象,标记完成后,删除标记对象。
缺点:产生内存碎片,若想清除的,就得重新整理剩下的对象。效率问题,标记和清除过程效率都较低。
标记-整理法
标记过程与标记-清除法同,后续步骤不是清理,而是将存活对象往一端移动覆盖,然后清理端边界以外的内存
generation法
目前大部分JVM使用的算法,基于一个事实:不同对象生命周期不同,对不同生命周期对象采用不同算法
新生代(Young Generation)
采用复制算法
1.所有新生成的对象首先都是放在年轻代的。新生代的目标就是尽可能快速的收集掉那些生命周期短的对象,是垃圾回收的主要对象。
2.新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空,如此往复。
3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收
4.新生代发生的GC也叫做MinorGC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
年老代(Old Generation)
由于老年代对象存活率高,没有额外空间对它进行分配单薄啊,因而采用“标记-清理”或者“标记-整理”算法
1.在年轻代中经历了N次(默认16)垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
2.一般来说,较大的对象会直接存放到年老代,例如一个大型数组
int[] a = new int[99999999];
3.内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
持久代(Permanent Generation)
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。
典型的垃圾收集器:
横线上方的为新生代、下方为老年代
Serial收集器(停止-复制算法)
新生代单线程收集器,标记和清理都是单线程,必须停止其他线程,优点是简单高效,没有线程切换开销,使用于client端。
Serial Old收集器(标记-整理算法)
单线程收集器,Serial收集器的老年代版本,client模式下使用。可以用作CMS收集器的后备预案,在并发收集器发生Concurrent Mode Failure时使用
ParNew收集器(停止-复制算法)
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现,除了Serial收集器,目前只有其可以与CMS收集器(负责老年代)配合工作。
Parallel Scavenge收集器(停止-复制算法)
并行收集器,要其他收集器不同之处在与关注点不同,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。
Parallel Old收集器(标记-整理算法)
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先
CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择
分为四个阶段执行:
-
初始标记
GC Roots能直接关联到的对象,速度快
-
并发标记
GC Roots的Tracing阶段,耗时长
-
重新标记
修正并发标记期间因用户程序继续运作而导致标记产生变动的一部分对象的标记记录,比初始标记耗时长,但比并发标记短得多
-
并发清除
耗时长
过程中耗时最长的并发标记与并发清除与用户线程一起工作。
缺点:
- 对CPU资源敏感
- 无法处理浮动垃圾,并发标记阶段已经确定了GC Roots,而此阶段长且与用户线程并发进行,因而无法处理这个阶段产生的垃圾,也因此由于垃圾回收时用户线程还在运行,这意味着还需要预留足够的内存给用户线程使用,CMS收集器不能像其他收集器等到老年代快满了再手机,需要预留一部分空间提供并发收集时程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,将会临时启动Serial Old收集器,这样停顿时间将很长
- 基于标记-清除,因而产生空间碎片
G1收集器(标记-整理算法)
面向服务端,追求低停顿,建立可预测的停顿时间模型(使得使用者明确指定一个长度为M毫秒的时间片段,消耗在垃圾收集器上的时间不超过N毫秒)
使用此收集器java堆的布局与其他收集器不同,其将整个java堆划分成多个大小相等的独立区域(但还是保留了新生代、老年代的概念),这是新生代和老年代就不再物理隔离,都是一部分区域的集合
也分为四个阶段:
-
初始标记
GC Roots能直接关联到的对象,速度快,停顿线程
-
并发标记
GC Roots的Tracing阶段,耗时长,虚拟机将这阶段对象变化记录在线程Remembered Set Logs中(由于使用区域的概念,因而新生代和老年代没有了物理限制,为了防止全栈扫描,每个区域都有一个Remembered Set,在并发标记阶段如果存在用户对对象进行写操作将会产生中断记录在此)
-
重新标记
将Remembered Set Logs整合到Remembered Set中,停止线程,但可并发执行
-
并发清除
时间可控,只回收部分区域,对各个区域的回收价值和成本进行排序,根据用户期望的时间执行回收计划
GC的执行机制
GC分两种类型:Minor GC和Full GC
Minor GC
新对象生成,申请Eden空间失败是触发,对Eden空间进行上述对新生代的清楚,比较频繁。
Full GC
对整个堆进行清理,慢,因而对JVM的调优主要是对FullGC的调节,有以下原因可能导致Full GC:
1.年老代(Tenured)被写满
2.持久代(Perm)被写满
3.System.gc()被显示调用
4.上一次GC之后Heap的各域分配策略动态变化
尽管如此,JAVA有了GC还是存在着内存泄漏问题
1.静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。(这里其实是语义上的泄露——申请了一堆用不着的空间,却没删掉)
2.各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。
3.监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。
Minor GC和Full GC的区别:
-
新生代GC(Minor GC)
发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快
-
老年代GC(Major GC——清理/Full GC——清理整个堆)
发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC(但并不是绝对的)。Major GC的速度一般要比Minor GC慢上10倍以上
新生代为何需要使用survivor区
假设没有survivor区,那么Eden区每进行一次Minor GC,存活的对象将会被送到年老代,年老代将很快被填满,触发Full GC,Full GC的时间消耗比较长,可以考虑两种方案:
- 增加年老代的空间,这样可以减少Full GC发生的次数,但是会增加每次Full GC执行的时间
- 减少年老代的空间,减少每次Full GC执行的时间,但增加了GC的发生次数
可见,Survivor区存在的意义:减少被送到年老区的对象,从而减少Full GC的发生;Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
新生代为何需要设置2个survivor区
意义:解决碎片化
假设只有一个survior,第一次Minor GC之后survivor会有内容,第二次需要GC的时候,Eden和Survivor区都需要更新,如果此时直接将Eden复制到Survivor,那么Eden区将会产生碎片,因而有了第二个survivor
而添加了第二个survivor区之后的一个好处就是总有一个survivor区是空的
而为何不要更多个survivor呢?因为2个已经达到了去碎片化的要求,更多个只会造成survivor区的更容易满
要注意的是,调优GC要利用好日志
参考:
《JAVA编程思想》
《深入理解Java虚拟机》
http://www.cnblogs.com/sunniest/p/4575144.html
http://blog.csdn.net/u014381710/article/details/48554465