垃圾收集器和内存分配策略
垃圾收集器回收哪些虚拟机内存区域
java堆和方法区
回收什么样的内存区域
回收“已死”的对象(即不再使用的对象)占用的内存
怎么判断对象“已死”
引用计数法
做法:给对象中添加一个引用计数器,每当被引用时,计数器就加1;每当引用失效时,计数器就减1;任何时刻计数器为0的对象就是不可能再被使用的。
使用:客观上,引用计数法实现简单,判断效率高,在很多情况下都是一个不错的算法,但是,在java虚拟机里面并没有选择引用计数算法来管理内存,其中最主要的原因就是它很难解决对象之间相互循环引用的问题。
可达性分析算法
做法:通过一系列的称为"GC Roots“的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为”引用链“,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的,如下图所示,object5、object6、object7虽然相互有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收对象。
在java语言中,可作为GC Roots的对象
- 虚拟机栈中(栈帧中本地变量表)引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(即一般的Native方法)引用的对象;
怎么判定对象是否会被最终回收
即使可达性分析算法分析完毕后,也不代表不可达的对象会被立即回收,此时不可达对象将会进入一个“缓刑期”。一个对象要被最终回收,将会进行两次标记过程,如果在可达性分析后,对象不可达,则会进行一次标记并且判断对象是否有必要执行finalize()方法,如果对象没有执行finalize()方法权限,或者finalize()方法已经被虚拟机调用过,则表示对象没有必要执行finalize()方法。
如果对象被判定有必要执行finalize()方法,那么对象将会被放置在一个"F-QUEQUE"队列中,虚拟机会自动创建一个finalizer线程,操作队列中的对象,这是对象最后一次“逃生(不被垃圾收集)”的机会,如果在finalizer线程处理过程中,能够与任何一条引用链挂钩,从而可达,那么该对象将逃离被虚拟机回收的结局;否则,对象将会被进行第二次标记,最终被垃圾回收。
引用
java引用分为强引用、软引用、弱引用、虚引用:
强引用:类似new关键字产生的引用,只要强引用还在,垃圾收集器永远不会回收这个对象;
软引用:描述有用但不是必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收;如果回收后还没有足够的内存,才会抛出内存溢出异常;
弱引用:描述的也是非必需对象,但是比软引用更弱一些,只能存活在下一次垃圾收集之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉弱引用引用的对象内存;
虚引用:也称为幽灵引用或者幻影引用,它是最弱的引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间产生影响,也无法通过虚引用来取得一个对象实例。为一个对象这只虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
回收方法区
方法区(或者HotSpot虚拟机中的永久代)进行垃圾收集的效率是非常低的;
主要回收两部分内容:废弃常量和无用的类;
废弃常量回收类似java堆中的回收过程;
无用的类回收需要满足苛刻的条件:
- 该类所有的实例已被回收;
- 加载该类的类加载器已经被回收;
- 该类的字节码Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法
满足上述三个条件可以对类进行回收,这里仅是“可以”,而非想对象那样,不用了就必然被回收。是否对类回收,HotSpot虚拟机提供-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:TraceClassUnLoading参数需要FastDebug版的虚拟机支持。
垃圾收集算法
标记-清除算法(mark-sweep)
步骤:
- 标记:对要回收的对象进行标记
- 清除:统一对标记的对象进行回收
缺点:效率低;易产生内存碎片,碎片太多,导致程序在运行过程中需要分配较大对象时,无法找到足够的内存而不得不提前出发一次垃圾收集动作
如图:
复制算法
实现思路:将内存划分为大小相等的两份,每次只使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另一半上面,然后把已使用过的内存空间一次性清理掉,这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要一动堆定指针,按顺序分配内存即可,实现简单,运行高效。
缺点:代价太大,将内存缩小为原来的一般
使用情况:虚拟机新生代都采用复制算法进行内存回收,IBM研究,新生代对象98%都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才使用的Survivor空间。HotSpot虚拟机默认Eden : Survivor = 8 : 1;也就是说新生代中可用内存空间为整个新生代容量的90%(80% + 10%),只有10%的内存会被“浪费”。当存活对象占用内存大于10%的Survivor空间时,就需要依赖其他内存(这里指老年代)进行分配担保。
如图:
标记-整理算法(mark-compact)
实现思路:标记过程仍与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一边移动,然后直接清理掉端边界以外的内存;
如图:
分代收集算法
将内存根据对象存活周期的不同划分为新生代和老年代。这样就可以根据各个年代的特点采用最合适的收集算法。
新生代:每次垃圾收集时都发现有大批对爱那个死去,是有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代:对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来回收。
虚拟机垃圾收集器亘古难题
GC停顿——Stop The World
在可达性分析时,必须在一个能确保一致性的快照中进行——这里的“一致性”指的是整个分析期间整个执行系统看起来就像被冻结在某个时间点一样,不可以出现分析过程中对象引用关系还在不断变化的情况,这点是导致GC进行时必须停顿所有java执行线程的其中一个重要原因
解决思路——提示几个关键词,具体内容省略
OopMap数据结构、JVM安全点、JVM安全区域
垃圾收集器
HotSpot虚拟机包含的所有收集器如图:
上图展示了新生代老年代分别适用的垃圾收集器,有连线表示收集器之间可以互相搭配使用
说明;没有最好最优的垃圾收集器,只有适合不同场景下最合适的垃圾收集器
Serial垃圾收集器
单线程收集器;
在进行垃圾收集时,必须暂停其他所有的工作线程,直到收集结束;
适用于单CPU的环境,Serial收集器由于没有线程交互的开销,专心收集可以获得更高的单线程收集效率,对于运行在Client模式下的虚拟机来说是一个很好的选择;
ParNew垃圾收集器
多线程收集器;
运行在Server模式下的虚拟机中的首选的新生代收集器;
只有Serial和ParNew收集器可以配合CMS老年代收集器协同工作,ParNew收集器也是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来指定它;
在多CPU情况下,ParNew收集器开启的线程数默认等于CPU的数量,在CPU数量非常多(譬如32个)的环境下,可以使用-XX:ParallelGCThreads参数来限定垃圾收集的线程数;
Parallel Scavenge收集器
并行的多线程收集器;
特点:和其他收集器关注点不一样,其他收集器关注的是尽可能的缩短垃圾收集时用户线程的停顿时间,而parallel scavenge收集器的目的则是达到一个可控制的吞吐量(吞吐量=用户线程执行时间/cpu执行总时间);
减少GC停顿,良好的响应速度提升用户体验,适于与用户交互较多的程序;提高吞吐量,高效利用cpu时间,尽快完成任务,适用于后台运行,与用户交互较少的程序;
参数:
- -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间,大于0的毫秒数,不是设置的越小,系统垃圾收集越快,GC停顿时间的缩短是用吞吐量和新生代空间换取的;
- -XX:GCTimeRatio:设置吞吐量大小,大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数;
- -XX:UseAdaptiveSizePolicy:当打开这个参数,就不需要手工指定新生代的大小等垃圾收集细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数,这种调节方式称为GC自适应的调节策略。在使用这个参数时,只需要把基本的内存数据设置好(-Xmx最大堆),然后使用-XX:MaxGCPauseMillis(更关注最大停顿时间)参数或-XX:GCTimeRatio(更关注吞吐量)参数给虚拟机设置一个优化目标。这也是与ParNew收集器的区别之一。
Serial Old收集器
老年代单线程收集器;
用途:
- Client模式下的虚拟机使用;
- Server模式下两个用途:
- 与Parallel Scavenge收集器搭配使用;
- 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用;
Parallel Old收集器
老年代并发多线程收集器;
与Parallel Scavenge新生代收集器搭配使用,被称为“吞吐量优先组合”,在注重吞吐量以及CPU资源敏感的场合使用;
CMS收集器(Concurrent Mark Sweep)
老年代并发收集器;
执行步骤:
- 初始标记,需要gc停顿,标记GC Roots能直接关联的对象,速度很快;
- 并发标记,可与用户线程并发执行,标记GC Roots到对象的引用链,向下搜索;
- 重新标记,需要gc停顿,标记在并发标记时由用户线程产生的记录变动的那部分引用;
- 并发清除,可与用户线程并发执行;
优点:
- 大大降低GC停顿时间
- 垃圾收集线程占用CPU资源比例随着服务器CPU个数的增加而减少,优越性越好
缺点:
- 会产生浮动垃圾,由于并发清理阶段用户线程也在运行,程序伴随会产生垃圾,而这些垃圾是在标记之后产生,故CMS无法在当前收集行为中清理它们,只能等到下次收集;
- 需要较大的内存空间运行,因为在很多并发的阶段,需要考虑用户线程的执行也需要分配内存空间,所以选择在堆利用率达到一个常数时就开启CMS垃圾收集,这个常数可以通过参数-XX:CMSInitiatingOccupancyFraction来设置,0-100。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”,这是虚拟机将自动启动后备预案:临时启动Serial Old收集器来重新进行老年代的垃圾收集。所以,参数值设置低了,会频繁进行CMS垃圾收集,设置高了,可能会导致大量的“Concurrent Mode Failure”,都会使性能降低。
G1收集器(Garbage-First)
G1收集器的原理比较复杂,此处不做介绍,后续会单独写一篇关于G1收集器的博文较少。
垃圾收集器参数总结
参数 | 描述 |
UseSerialGC | 虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial+Serial Old的收集器组合进行内存回收 |
UseParNewGC | 打开此开关后,使用ParNew + Serial Old的组合进行内存回收 |
UseConcMarkSweepGC | 打开此开关后,使用ParNew + CMS + Serial Old的收集器组合进行内存回收。Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器使用 |
UseParallelGC | 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行内存回收 |
UseParallelOldGC | 打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收 |
SurvivorRatio | 新生代中Eden区域和Survivor区域的容量比智,默认为8,代表Eden :Survivor = 8 :1 |
PretenureSizeThreshold | 直接晋升老年代的对象大小,设置这个参数后,大于这个参数对象将直接在老年代分配 |
MaxTenuringThreshold | 晋升老年代的对象年龄。每个对象在坚持过一次MInor GC后,年龄就加1,当超过这个参数值时就进入老年代 |
UseAdaptiveSizePolicy | 动态调整java堆中各个区域的大小以及进入老年代的年龄 |
HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况 |
ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
GCTimeRatio | 吞吐量的倒数,GC时间占总时间的比率,默认值为99,即允许1%的GC时间。仅在Parallel Scavenge收集器时生效 |
MaxGCPauseMillis | 设置GC最大停顿时间,仅在Parallel Scavenge收集器时生效 |
CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后出发垃圾收集。默认值为68%,仅在CMS收集器时生效 |
UseCMSCompactAtCollection | 设置在CMS收集器完成垃圾收集后是否进行一次内存碎片整理。仅在CMS收集器时生效 |
CMSFullGCsBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理。仅在CMS收集器时生效 |
内存分配与回收策略
对象优先分配在Eden区
大多情况,对象分配在新生代Eden区。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
大对象直接进入老年代
所谓的大对象是指,需要大量连续内存空间的java对象。经常出现大对象容易导致内存还有不少空间就提前触发垃圾收集以获取足够的连续空间来安置它们。
虚拟机提供了一个参数-XX:PretenureSizeThreshold参数,让大于这个设置值的对象直接在老年代分配。这样做的目的避免在Eden区以及两个Survivor之间发生大量的内存复制。(新生代采用复制算法)
长期存活的对象将进入老年代
虚拟机采用分代收集算法管理内存,那么内存回收时就应该识别哪些对象应该放在新生代,哪些放在老年代。,为了做到这一点,虚拟机为每个对象定义了一个对象年龄计数器。
对象每次熬过Minor GC,则年龄加1,当年龄增加到一定程度(默认15岁),将会被晋升到老年代中。对于晋升老年代的年龄阙值,可以通过参数-XX:MaxTenuringThreshold设置。
动态对象年龄判断
为了适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,则年龄大于或等于该年龄对象的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
空间分配担保
在发生MInor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果这个条件成立,那么MInor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置的值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均水平,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒风险,这是改为进行一次Full GC
一般会将HandlePromotionFailure打开(允许担保失败),即时担保失败,失败后重新发起一次Full GC(缺点:绕的圈子大点),也总比关闭这个参数,导致频繁Full GC的情况要好一些