垃圾回收算法
Java中的引用定义很很纯粹:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用
强引用:如“Object obj = new Object()”,这类引用是Java程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
软引用:用来描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。JDK1.2之后提供了SoftReference类来实现软引用。
弱引用:也是用来描述非需对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。
虚引用:最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。JDK1.2之后提供了PhantomReference类来实现虚引用。
垃圾对象判定
引用计数器
给每个对象分配一个计数器,当有引用指向这个对象时,计数器加1,当指向该对象的引用失效时,计数器减一。最后如果该对象的计算器为0时,java垃圾回收器会认为该对象是可回收的。
实时性 : 无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。
应用无需挂起 : 在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报outofmemery 错误。
区域性 : 更新对象的计数器时,只是影响到该对象,不会扫描全部对象
缺点:无法解决循环引用问题
根搜索算法
Java和C#中都是采用根搜索算法来判定对象是否存活的。算法的基本思路是通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。在Java语言里,可作为GC Roots的兑现包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中的类静态属性引用的对象。
方法区中的常量引用的对象。
本地方法栈中JNI(Native方法)的引用对象
在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的finalize()方法最多只会被系统自动调用一次),稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。
垃圾收集算法
标记—清除算法分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。 算法一般应用于老年代,因为老年代的对象生命周期比较长。标记—清除算法的执行情况如下图所示
回收前状态:
可访问的对象
如果栈中有一个变量a引用一个对象,那么该对象是可访问的,如果该对象中的某一个字段引用了另一个对象b,那么b也是可访问的。可访问的对象也称之为live对象
该算法有两个阶段。
1. 标记阶段:找到所有可访问的对象,做个标记
2. 清除阶段:遍历堆,把未被标记的对象回收
可以解决循环引用的问题 :原因是标记清除算法是从栈中根对象开始的,改算法走完后,循环引用对象是没有被标记的,会被直接回收。
回收时,应用需要挂起,也就是stop the world
- 标记和清除过程的效率都不高。
- 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不触发另一次垃圾收集动作。
标记_整理
复制算法比较适合于新生代,在老年代中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。标记—整理算法的回收情况如下所示:
移动所有的可达对象到堆内存的同一个区域中,使他们紧凑的排列在一起,从而将所有非可达对象释放出来的空闲内存都集中在一起,通过这样的方式来达到减少内存碎片的目的。
解决内存碎片问题,压缩阶段,由于移动了可用对象,需要去更新引用。
回收前状态:
回收后状态:
复制算法
一开始就会将可用内存分为两块,from域和to域, 每次只是使用from域,to域则空闲着。当from域内存不够,开始执行GC操作,这个时候,会把from域存活的对象拷贝到to域,然后直接把from域进行内存清理。
优点:内存回收时不用考虑内存碎片的出现
缺点:可一次性分配的最大内存缩小了一半
回收前状态:
coping算法一般是使用在新生代中,因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多,这样使用coping算法进行拷贝时效率比较高。
jvm将Heap 内存划分为新生代与老年代,将新生代划分为Eden(伊甸园) 与2块Survivor Space(幸存者区) ,然后在Eden –>Survivor Space 以及From Survivor Space 与To Survivor Space 之间实行Copying 算法。 Eden区:From区:To区域的比例是8:1:1

1、当Eden区满的时候,会触发第一次young gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发young gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。
2、当后续Eden又发生young gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
3、可见部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代
注意:万一存活对象数量比较多,那么To域的内存可能不够存放,这个时候会借助老年代的空间。
垃圾回收分析
对内存的分配策略明确以下三点:
对象优先在Eden分配。
大对象直接进入老年代。
长期存活的对象将进入老年代。
对垃圾回收策略说明以下两点:
新生代GC(Minor GC):发生在新生代的垃圾收集动作,因为Java对象大多都具有朝生夕灭的特性,因此Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC/Full GC):发生在老年代的GC,出现了Major GC,经常会伴随至少一次Minor GC。由于老年代中的对象生命周期比较长,因此Major GC并不频繁,一般都是等待老年代满了后才进行Full GC,而且其速度一般会比Minor GC慢10倍以上。另外,如果分配了Direct Memory,在老年代中进行Full GC时,会顺便清理掉Direct Memory中的废弃对象。
分代收集
在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集
老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须“标记-清除-压缩”算法进行回收。
Young区Survivor满后触发minor GC后仍然存活的对象,当Eden区满后会将存活的对象放入Survivor区域,如果Survivor区存不下这些对象,GC收集器就会将这些对象直接存放到Old区中,如果Survivor区中的对象足够老,也直接存放到Old区中。如果Old区满了,将会触发Full GC回收整个堆内存
CMS(并发标记清除)
JAVA应用程序有以下几个特点,那么可以使用Concurrent Mark Sweep (CMS) 垃圾收集器。
- 希望JAVA垃圾回收器回收垃圾的时间尽可能短;
- 应用运行在多CPU的机器上,有足够的CPU资源;
- 有比较多生命周期长的对象;
- 希望应用的响应时间短。
CMS也是采用分代策略的,用于收集老年代的垃圾对象
- 初始标记(CMS-initial-mark):扫描root对象直接关联的可达对象。注意不会递归的追踪下去,只是到达第一层而已。这个过程,会STW(STOP THE WORLD),但是时间很短。
- 并发标记(CMS-concurrent-mark):
- 重新标记(CMS-remark): 在并发mark阶段,应用的线程可能产生新的垃圾,所以需要重新标记,这个阶段也是会STW
- 并发清除(CMS-concurrent-sweep):
- 并发重置状态等待下次CMS的触发(CMS-concurrent-reset):
并发标记、清除和重置这三个阶段: 应用线程和垃圾回收线程是可以一起工作的,垃圾回收线程会占用部分CPU资源。
Concurrent Mode Failure
由于cms垃圾回收线程可以和应用的线程一起工作,那么应用线程仍然需要申请内存,如果这个时候老年代的空间已经不够用了。那么会有Concurrent Mode Failure 这样的日志输出,之后会进行一次Full GC的操作,所有的应用线程都会停止工作。
浮动垃圾
由于cms垃圾回收线程可以跟应用的线程一起工作,那么应用的线程也会产生垃圾,这些称之为浮动垃圾。
降低吞吐量
由于应用线程和垃圾回收线程一起工作,那么垃圾回收线程也就抢占了系统资源,会对应用的吞吐量造成一定的影响。为了保证垃圾回收过程中,应用线程有足够的内存可以使用,当堆内存的空间使用率达到68%的时候,CMS开始触发垃圾回收。
内存碎片
CMS是基于标记-清除算法的,会造成内存碎片
性能调优
性能调优需要具体情况具体分析,而且实际分析时可能需要考虑的方面很多,这里仅就一些简单常用的情况作简要介绍。
我们可以通过给Java虚拟机分配超大堆(前提是物理机的内存足够大)来提升服务器的响应速度,但分配超大堆的前提是有把握把应用程序的Full GC频率控制得足够低,因为一次Full GC的时间造成比较长时间的停顿。控制Full GC频率的关键是保证应用中绝大多数对象的生存周期不应太长,尤其不能产生批量的、生命周期长的大对象,这样才能保证老年代的稳定。
Direct Memory在堆内存外分配,而且二者均受限于物理机内存,且成负相关关系,因此分配超大堆时,如果用到了NIO机制分配使用了很多的Direct Memory,则有可能导致Direct Memory的OutOfMemoryError异常,这时可以通过-XX:MaxDirectMemorySize参数调整Direct Memory的大小。
除了Java堆和永久代以及直接内存外,还要注意下面这些区域也会占用较多的内存,这些内存的总和会受到操作系统进程最大内存的限制:
1、线程堆栈:可通过-Xss调整大小,内存不足时抛出StackOverflowError(纵向无法分配,即无法分配新的栈帧)或OutOfMemoryError(横向无法分配,即无法建立新的线程)。
2、Socket缓冲区:每个Socket连接都有Receive和Send两个缓冲区,分别占用大约37KB和25KB的内存。如果无法分配,可能会抛出IOException:Too many open files异常。
3、JNI代码:如果代码中使用了JNI调用本地库,那本地库使用的内存也不在堆中。
4、虚拟机和GC:虚拟机和GC的代码执行也要消耗一定的内存。
参考:
https://blog.csdn.net/linsongbin1/article/details/51686158
https://blog.csdn.net/ns_code/article/details/18076173
浙公网安备 33010602011771号