垃圾回收(Garbage Collection, GC)主要需要考虑的问题:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
哪些内存需要回收
虚拟机栈、本地方法栈、程序计数器由于是线程独有,且栈帧的内存大小在类结构确定时已经相对确定所以不需要考虑回收问题,方法结束或线程结束自然回收。需要回收的是Java堆以及方法区
什么时候回收以及如何回收
在一个对象不可能再被任何途径使用时回收;
微软的COM(Component Object Model)技术、使用ActionScript3的FlashPlayer、Python语言和Squirrel使用了引用计数算法进行内存管理,而主流商业语言(Java、C#、Lisp等)主流实现算法都是可达性分析算法。
- 引用计数算法(Reference Counting):给对象添加一个引用计数器,当有一个地方引用它时,计数器加1,引用失效计数器减1,当计数器为0时,表示此对象不再被引用,可以回收;缺点:无法解决对象相互循环引用问题
- 可达性分析算法(Reachability Analysis):通过一系列称为“GC Roots”的对象作为起始点,从这个点开始向下搜索,所走的路径称为引用链(Reference Chain),当一个对象没有任何引用链也GC Roots相连时,将对对象进行第一次标记并通过该对象的分析(1.是否重写了finalize()方法,2.finalize()方法有没有被虚拟机调用过),如果重写了方法且没有被调用过将会被放置到一个叫做F-Queue的队列中,稍后将被逐个执行finalize()方法,之后虚拟机将对这些对象进行二次标记,如果在执行了finalize()方法后,可达性分析可以通过将被移出“待回收”集合继续存活。
回收方法区
回收运行时常量池,与Java堆对象回收类似
回收无用类,满足一下3个条件才算是“无用类”:
- 该类的所有实例已被回收,即在Java堆中不再存在该类实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有任何地方被引用,无任何地方通过反射访问该类的方法
可以使用-Xnoclassgc关闭这部分的回收,还可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息。
垃圾回收算法
- 标记-清除算法:
- 基本算法:判断每个对象是否应该回收,需要回收的标记,最后一起清除;缺点:会出现很多不连续空内存,标记和清除小女都不高
- 衍生:复制算法:用一半内存空着,标记完后,将没标记的复制到空内存,清除之前所有;缺点:只有一半内存可以使用
- 优化复制算法:将内存分为了80%的Eden以及两块10%的Survivor,每次使用90%内存,10%用于留空复制,不够像老年代借
- 标记-整理算法
- 基本算法:标记和上同,将存活的对象移动到前面,清除后面的内存
- 分代算法:根据对象存活周期的不同将内存分为几块,一般把Java堆分为新生代和老年代,新生代中每次有大量的对象死亡,所以使用优化的复制算法;而老年代存活率高,所以使用“标记-清除”或者“标记-整理”算法
垃圾回收器
Java虚拟机规范中堆垃圾收集器应该如何实现并没有明确的规定,针对JDK1.7的Hotspot虚拟机,实现了其中不同的垃圾回收器:
图片来源(https://blogs.oracle.com/jonthecollector/entry/our_collectors),上图中的连线两端的垃圾回收器可以配合使用
- Serial收集器:单线程的垃圾收集器,当使用它收集垃圾时,必须停止其他所有线程,曾经是JDK1.3.1之前的唯一新生代垃圾会回收器,client版本JVM的默认选择
- ParNew(new parallel)收集器:Serial的多线程版本,与Serial公用了大量相同的算法,Server版本JVM新生代的首选,参数: -XX:+UserParNewGC
- CMS(Concurrent Mark Sweep)收集器:第一个实现了让垃圾回收线程与用户回收线程(基本上)同时工作的收集器,一般用于老年代收集,参数: -XX:+UseConcMarkSweepGC
- Parallel Scavenge收集器:新生代多线程收集器,使用复制算法,他与上面收集器的注重点不同,上面收集器注重点是在尽量减少垃圾收集时用户线程的停顿时间,而次收集器注重达到一个可控的吞吐量(throughput)。通过两个参数精确控制吞吐量:-XX:MaxGCPauseMillis设置最大停顿时间、-XX:GCTimeRatio直接设置吞吐量大小。还有一个开关参数参数-XX:+UserAdaptiveSizePolicy,打开后可以不用手动设置新生代大小(-Xmn)、Eden与Survivor区比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)
- Serial Old:是Serial的老年代版本,使用“标记-整理”算法
- Parallel Old:是Parallel Scavenge的老年代版本,使用“标记-整理”算法
*吞吐量(Throughput) = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
CMS(Concurrent Mark Sweep)收集器是以获得最短回收停顿时间为目的的收集器,从名字可以看出CMS是基于“标记-清除”算法实现的,主要分为四个步骤:
- 初始标记(CMS Initial mark)
- 并发标记(CMS Concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS Concurrent sweep)
其中初始标记会停止所有用户进程,标记出所有与GC Roots直接相连的对象,这个过程耗时极短;之后并发标记时,标记线程将和用户线程并发执行,这个过程耗时最长,但由于和用户线程并发执行所以这段时间并不会造成停顿;重新标记将对并发标记阶段用户进程造成的对象引用变更进行再次标记,这个阶段也会停止所有用户线程;并发清除阶段,清除进程将和用户进程并发进行;
优点:极低的停顿时间,在注重交互的系统中,体验极佳
缺点:
- 在并发阶段,收集器线程将一直独占一部分CPU资源,默认是(CPU数量+3)/4的CPU核数,当CPU核数<=4时,占用资源过多,将造成用户进程执行速度降低;
- 在并发清除期间参数的浮动垃圾(Floating Garbage)无法处理,只能下次GC处理,所以CMS收集器的激活频率会高于其他,默认当老年代使用了68%空间激活,而其他同代收集器几乎完全填满才进行收集,可以通过参数-XX:CMSInitiatingOccupancyFraction设置激活所需要的内存占用百分比;
- 由于CMS使用“标记-清除”算法,将产生大量内存碎片,可能造成老年代内存剩余足够,但找不到合适大小的内存空间,不得不提前进行一次Full GC。为了解决这个问题CMS收集器提供了-XX:+UseCMSCompactAtFullCollection,当出现上述情况时,进行内存碎片合并,还有另一个参数-XX:CMSFullGCsBeforeCompaction,默认为0,即每次进入Full GC时都进行内存碎片整理
G1(Garbage first)收集器:
Sun赋予它的使命是未来可以替代CMS,
G1优势:
- 并行与并发
- 分代收集
- 空间整合
- 可预测的停顿
G1的运作大致可分为下面的几个步骤:
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
对象分配空间的规则:
大多数时候,新对象分配在Eden区,当Eden区内存不足时,系统将发起一次Minor GC,但也有特殊情况:
- 需要大量连续空间的Java对象,如长字符串,数组,需要内存超过-XX:PretenureSizeThreshold参数设置值,将直接在老年代分配
当对象在Eden出生每经过一次Minor GC并存活下来,年龄计数加1,当年龄大于-XX:MaxTensuringThreshold设置值,将进入老年代,也有特殊情况:
- 当出现Survivor区中相同年龄对象总大小大于Survivor区一半,年龄大于等于该年龄的对象都进老年代
在每次Minor GC 开始之前,虚拟机将进行以下流程:
*是否允许失败,有参数-XX:+HandlePromotionFailure设置
来源:oschina
链接:https://my.oschina.net/u/1413049/blog/684017