JVM内存回收
jvm内存结构分为四大区域:程序计数器、栈、堆区、方法区。其中栈与程序计数器这2个区域随线程而生、随线程而灭,因此就不需要考虑过多内存垃圾回收问题,因为一个方法调用结束或者线程结束时,内存自然就跟随着回收了。
方法区与堆区这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分
GC中的算法
1.垃圾收集器在对堆区和方法区进行回收工作前,首先肯定确定这些区域内对象哪些可以被回收,哪些暂时还不能回收,这时就要用到判断对象是否存活的算法!如何失去任何引用,垃圾收集器就把它收走。
(1)引用计数算法
早期策略。在这种算法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
缺点:循环引用时无效
如:如父对象有一个对子对象的引用,子对象反过来引用父对象。
(2)可达性分析算法
可达性分析算法是从离散数学中引入的,也是如今正在使用的策略,程序把所有的引用关系看作一张图(DOM图类似),从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,就如递归思想一般,遍历所有,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。
java中可作为GC Root的对象有
1.虚拟机栈中引用的对象(本地变量表)
2.方法区中静态属性引用的对象
3. 方法区中常量引用的对象
4.本地方法栈中引用的对象(Native对象)
(3)标记-清除算法分析
标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
(4)标记-整理算法
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。
(5)copying算法
该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。
引用和回收问题
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。
当对象失去所有引用时,我们就可以说对象生命周期结束了,即为死亡,就该回收它啦。
常见的垃圾回收器
1. Serial收集器
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式。
2. Serial Old收集器
老年代单线程收集器,Serial收集器的老年代版本。
3. ParNew收集器
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
4. Parallel Scavenge收集器
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式。
5. Parallel Old收集器
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。
6. CMS(Concurrent Mark Sweep)收集器
高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。
垃圾回收机制什么时候触发?
由于会遇到的问题不一样,因此垃圾回收区域、时间、方法也不一样。GC有两种类型:Full GC & Scavenge GC 。
1. FULL GC
GC在优先级最低的线程中运行,一般在应用程序空闲即没有应用线程在运行时被调用。
对整个堆进行整理,包括Young(新生区)、Tenured(老年区)和Perm(静态区)。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:
1.年老代(Tenured)被写满
2.持久代(Perm)被写满
3.System.gc()被显示调用
4.上一次GC之后Heap的各域分配策略动态变化
2. Scavenge GC
(1)Java堆内存不足时,Scavenge GC会被调用。
(2)当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足即在Eden申请空间失败时,JVM就会强制调用Scavenge GC线程。对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区, 若GC一次之后仍不能满足内存分配,JVM会再进行两次GC,若仍无法满足要求,则JVM将报“out of memory”的错误,java虚拟机将停止运行。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来
六、GC的两个重要方法
(1)System.gc()方法
使用System.gc()可以不管JVM使用的是哪一种垃圾回收的算法,都可以请求Java的垃圾回收。
在命令行中有一个参数-verbosegc可以查看Java使用的堆内存的情况,由于这种方法会影响系统性能,不推荐使用。
(2)finalize()方法
在JVM垃圾回收器收集一个对象之前,一般要求程序调用适当的方法释放资源,但在没有明确释放资源的情况下,Java提供了缺省机制来终止该对象心释放资源,这个方法就是finalize()。
它的原型为:protected void finalize() throws Throwable
在finalize()方法返回之后,对象消失,垃圾收集开始执行。之所以要使用finalize(),是存在着垃圾回收器不能处理的特殊情况。
七、GC使用与性能优化管理
(1)不要显式调用System.gc()。此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。大大的影响系统性能。
(2)尽量减少临时对象的使用。临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。
(3)对象不用时最好显式置为Null。一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。
(4)尽量使用StringBuffer,而不用String来累加字符串。由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。
(5)能用基本类型如Int,Long,就不用Integer,Long对象。基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。
(6)尽量少用静态对象变量。静态变量属于全局变量,不会被GC回收,它们会一直占用内存。
(7)注意分散对象创建或删除的时间。集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的