垃圾收集器与内存分配策略
三个问题
1、哪些内存需要回收
2、什么时候回收
3、如何回收
在线程内部,随着方法结束或者线程结束时,线程内部内存会跟随者回收。
java堆的内存消耗是由于动态创建对象而产生的,所以具有不确定性。需要
垃圾处理器关注。
确定哪些对象已经“死去”
引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,
计数器值就加一,当引用失效时,计数器值就减一,任何时刻计数器为0的对
象就是不可能再被使用的。
缺点:难以解决对象之间循环引用的问题。
可达性分析算法:通过GC Roots的对象作为起始点,从这些节点开始向下搜索,
搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,
则证明此对象是不可用的。
GC Roots包括:
1、虚拟机栈中引用的对象
2、方法区中类静态属性引用的对象
3、方法区中常量引用的对象
4、本地方法栈中JNI引用的对象
引用的分类
1、强引用:”Object obj = new Object()”,只要强引用存在,垃圾收集器永远不会回收掉被引用的对象
2、软引用:描述还有用但非必须的对象,在系统发生内存溢出异常之前,会把这些对象进行二次回收。提供了SoftReference类来实现软引用
3、弱引用:被弱引用关联的对象只能生存到下一次垃圾收集发生之前。WeakReference
4、虚引用:为了能在这个对象被收集器回收时收到一个系统通知
生存还是死亡
一个对象真正宣告死亡的过程:如果对象在进行可达性分析后发现没有鱼GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,
筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将
这两种情况都视为没有必要执行。如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,
并且在稍后由一个由虚拟机自动建立的,低优先级的Finalizer线程去执行它。对象重新与引用链上的任何一个对象建立关联就可以在第二次标
记时从即将收回的集合中移除。
回收方法区(永久带)
永久带的垃圾回收:废弃常量和无用的类
常量池字面量的回收:”abc”进入常量池,但是没有一个String对象是叫做”abc”的,如果此时发生内存回收而且必要的话,这个”abc”常量会被系统
清理出常量池。其他类、方法、字段的符号引用与此类似。
判断无用的类:
1、该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
2、加载该类的ClassLoader已经被回收
3、该类对应的java.lang.Class对象没有在任何地方被引用,无法通过任何地方通过反射访问该类的方法
使用场景:在大量使用反射、动态代理、CGlib等byteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功
能,以保证永久代永远不会溢出。
垃圾回收算法
标记-清除算法
1、标记处说有需要回收的对象,在标记完成后统一回收所有被标记的对象。
缺点:效率问题;空间问题,标记清楚之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,
无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法(解决效率问题)
1、将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
2、当着一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
优点:实现简单,运行高效
缺点:将内存缩小到了原来的一半
标记-整理算法(用于老年代)
在标记-清除算法执行对可回收对象进行清理之前,让所有存活的对象都向一端进行移动,然后直接清理掉端边界以外的内存。
分代收集算法(商业虚拟机采用的方式)
根据对象存活周期的不同将内存划分为几块。一般是把Java堆分成新生代和老年代。
新生代:采用复制算法
老年代:存活率搞,使用标记-整理算法
HotSpot的算法实现
枚举根节点
使用OopMap
安全点:只有在到达安全点时才能暂停
选定标准:是否具有让程序长时间执行的特征,即指令序列复用,例如方法调用、循环跳转、异常跳转等
如何在GC发生时让所有线程都跑到最近的安全点上再停顿下来:
1、抢先式中断
在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。
2、主动式中断
当GC需要中断线程时,设置一个标志,各个线程在执行是主动去轮询这个标志,发现中断标志位真时就自己中断挂起。
安全区域:在线程处于sleep或者block状态,无法响应JVM的中断请求,走到安全的地方去中断挂起。
定义:在一段代码片段之中,引用关系不会发生变化。在这个区域任何地方开始GC都是安全的。
执行过程:当线程执行到安全区域中的代码时,首先标识自己已经进入了安全区域。在这段时间里,JVM要发起GC时,就不用管
标识自己为安全区域的线程了。在线程要离开安全区域时,要检查系统是否已经完成了根节点枚举,如果完成了,那线程就继续
执行,否则它就必须等待知道收到可以离开安全区域的信号为止。
垃圾收集器
Serial收集器
单线程的收集器,在它进行收集时,必须暂停其他所有的工作线程,知道他收集结束。
在Client模式下的默认新生代收集器。
优点:简单而高效
ParNew收集器
Serial收集器的多线程版本
运行在server模式下的虚拟机中首选的新生代收集器
优点:除了serial收集器以外,目前只有它能与CMS收集器配合工作。
缺点:在单cpu的环境下,比serial的效果差(类似于在计算量比较小的情况下,并行计算比串行计算慢一样,有Cpu进行上下文切换的开销)
默认开启的收集线程数量与CPU的数量相同,在CPU非常多的情况下,使用-XX:ParallelGcThreads来限制垃圾收集器的线程数
Parallel Scavenge收集器(新生代收集器)
特点:达到一个可控制的吞吐量,即CPU用于运行用户代码的时间与CPU总消耗时间的比值
精确控制吞吐量的参数:
最大垃圾收集停顿时间:-XX:MaxGCPauseMillis 一个大于0的毫秒数,保证内存回收花费的时间不超过设定值,GC停顿时间缩短是以牺牲吞吐量和新生代空间换取的
吞吐量大小设置:-XX:GCTimeRatio 大于0且小于100的整数,垃圾收集时间占总时间的比率
别名:吞吐量优先收集器
Serial Old收集器
是serial收集器的老年代版本,单线程,使用标记-整理算法。在Client模式下的虚拟机使用。
Parallel Old收集器
是parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。
意义:吞吐量优先的收集器终于有了比较名副其实的应用组合,在注重吞吐量以及Cpu资源铭感的场景,优先考虑Parallel Scavenge加parallel Old收集器。
CMS收集器:获取最短回收停顿时间为目标
应用场景:互联网站或者B/S系统的服务器上(重视服务的响应速度)
使用标记-清除算法
运作过程:
1、初始标记:标记一下GC Roots能直接关联到的对象
2、并发标记:进行GC Roots Teacing的过程
3、重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
4、并发清除
优点:并发收集、低停顿(也叫并发低停顿收集器)
缺点:
1、CMS收集器对CPU资源非常敏感
2、CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次full GC的产生
3、收集借宿时会有大量空间碎片产生
G1收集器(Garbage-First)
面向服务端应用得额垃圾收集器。在未来可以替换掉CMS收集器。
特点:
1、并行与并发:充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短stop-the-world停顿的时间。
2、分带收集
3、空间整合:从整体上看基于标记-整理算法。从局部上看是基于复制算法。不会产生内存空间碎片,收集后能提供规整的可用内存。
4、可预测的停顿:让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间不得超过N毫秒。(实时垃圾收集器)
实现原理:G!跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,
每次根据运行的手机时间,有限回收价值最大的Region。
G1收集器的运作步骤:
1、初始标记:标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让一下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象
2、并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活的对象
3、最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面
4、筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划
理解GC日志
xx.yy: [GC( or [Full ) [DefNew: 100K->10K(110k) , 0.0001 secs]]
//前面的xx.yy代表了GC发生的时间,这个谁的含义是从Java虚拟机启动以来经过的秒数
//[GC 或 [Fu;;说明了这次垃圾收集的停顿类型,
//[DefNew 代表了GC发生的区域(GC前已使用容量->GC后已使用容量,括号里面的是总容量)
内存分配和回收策略
两个问题
1、给对象分配内存
2、回收分配给对象的内存
对象的内存分配
堆上分配,主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下回直接分配在老年代中。
内存分配策略
1、对象优先在Eden分配,当Eden分区没有足够空间进行分配时,虚拟机将发起一次Minor GC
2、大对象直接进入老年代,大对象是指需要大量连续内存空间的Java对象
3、长期存活的对象将进入老年代
4、动态对象年龄判定
5、空间分配担保
Minor GC和 Full GC的区别
1、新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为java对象大都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快
2、老年代GC(Major GC/Full GC):比新生代GC慢十倍以上