1、引用
引用分为四类(引用强度依次减弱):
- 强引用:new的对象的引用
- 软引用:有用但非必需的对象
- 弱引用:也是非必须的对象,只能生存到下一次垃圾收集发生之前
- 虚引用:幽灵引用或者幻影引用,是否存在此引用对对象没有任何影响,也无法获得实例,唯一的目的就是在这个对象被回收时受到一个系统通知
2、扩展垃圾收集算法
(1)引用计数法
这个了解一下就可以,主流的java虚拟机没有用这个的,因为解决不了对象之间循环引用的问题
算法原理:给对象添加一个引用计数器,每当有一个地方引用它时,计数器+1,引用失效时,计数器值减一,任何时刻计数器为0的对象不可能再被利用了,就可以被回收了。
优点:实现简单,判定效率很高。
缺点:解决不了对象之间循环引用的问题
(2)可达性分析算法
这个就需要看一看,java虚拟机用的都是这个算法的变种
原理: 通过一系列的GC Roots 作为起点,从这些节点开始向下搜索,搜索的过程称为引用链,当一个对象到GC Roots没有任何引用链时则证明此对象是不可用
3、finalize()方法
为什么在这说一下这个呢?
因为这个和垃圾收集有关系
在上面已经看到怎么判定对象已经没有被引用,需要进行回收了,但是回收的步骤是什么,中间到底发生了什么?
即使在可达性分析算法中不可达的对象,也并非是非死不可的(不是立马执行回收),这时只是进入了缓刑 状态,
真正宣布一个对象死亡需要进行两次标记过程,如果此对象没有引用了,那就会被第一次标记,并且进行一次筛选,筛选的条件就是此对象是否有必要执行finalize() 方法,
当对象没有覆盖finalize() 方法或者此对象的 finalize() 已经被虚拟机调用过了(只有一次机会),就认为没必要被拯救了,就等待第二次标记回收吧
如果有必要就会进入一个F-Queue队列 由虚拟机去创建低优先级的Finalizer 线程执行它,但是这个对列也执行一个等待队列,等待新的引用降临到自己身上,而GC 也会对F-Queue进行扫描,如果还没被移出这个队列,那就被第二次标记,再被回收
4、方法区的垃圾回收
提前说说,下一部分主要讲堆的垃圾回收算法,而垃圾收集器的主要任务就是堆,怕搞混了
主要回收两种:废弃的常量和无用的类
判定一个无用类的标准:
- 该类的所有实例都已经被回收,
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用 ,无法在任何地方通过反射访问该类的方法
5、java的垃圾收集算法
- 标记-清除算法
- 复制算法
- 标记-整理算法
- 分代收集算法
(1)标记-清除算法
标记所有需要回收的对象,统一回收所有被标记的对象。
缺点:效率不高和产生大量不连续的内存碎片
(2)复制算法
将内存划分大小相同的两块,当一块用完了,把这一块存活着的放到另一块中去
优点:实现简单,运行高效,没有产生大量的不连续的内存碎片
缺点:代价太高,只能利用一半
适合新生代,因为新生代存活率高的对象很少
(3)标记-整理算法
让所有存活的对象都向一端移动,然后直接清理掉端边界意外的内存
适合老年代,老年代的对象大部分都是存活率较高的对象
(4)分代收集算法
根据各个年代生成的特点用不同的收集算法
新生代用复制算法
老年代用标记-整理算法
6、java的垃圾收集器
(1)新生代垃圾收集器
- Serial(单线程)收集器:简单而高效,Client默认模式下的新生代收集器
- ParNew(多线程)收集器:单线程收集器的多线程模式,Server模式下首选的新生代收集器
- Parallel Scavenge (并行)收集器:可控制的吞吐量,追求停顿时间短,也被称为"吞吐量优先"收集器,
上面三种垃圾收集器用的都是复制算法
并行收集器
控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis
设置吞吐量大小 -XX:GCTimeRatio
还有一个特别的参数:-XX:+UseAdaptivesSizePolicy,打开这个参数就不用指定新生代的大小,Eden和Survior区的比例了,晋升老年代对象年龄等细节参数,只需要设定堆的 -Xmx 和并行收集器的最大吞吐量和最大收集时间就行了,这种叫GC自适应的调节策略。
(2)老年代垃圾收集器
- Serial Old收集器:新生代单线程收集器的老年代版本,标记-整理算法
- Paraller Old 收集器:新生代并行收集器的老年代版本,标记-整理算法
- CMS收集器:获取最短回收停顿时间为目标的收集器,标记-清除算法
(3)CMS和G1垃圾收集器的区别
上面没怎么详细说CMS,主要是把它拉过来和G1收集器做一下对比
CMS运作过程分为4个步骤:
- 初始标记 :工作线程停顿
- 并发标记 : 并行执行
- 重新标记:工作线程停顿
- 并发清除 :并行执行
也叫做并发低停顿收集器
缺点:
- 对CPU资源非常敏感
- 无法处理浮动垃圾:在并发清理阶段还会有新垃圾产生,但没有被标记,只能等待下一次GC
- 产生大量的内存碎片:所以产生了两个参数**-XX:+UseCMSCompactAtFullCollection** 用于当顶不住要FullGC时进行内存碎片的整理,-XX:CMSFullGCsBeforeCompaction 用于设置多少次不压缩的Full GC后跟一次带压缩的
G1收集器的运作步骤也大概分为4步:
- 初始标记 :工作线程停顿
- 并发标记
- 最终标记 :工作线程停顿
- 筛选回收
优点:
- 并行于并发 :使用多个CPU缩短工作线程停顿时间
- 分代收集:能采用不同的方式去处理新创建的对象和已经存活了一段时间的对象
- 空间整合:从整体上看是标记整理,所以不会出现CMS那样产生大量的内存碎片
- 可预测的停顿:除了低停顿,还创建了可预测的停顿时间模型,还可以指定毫秒级的停顿时间,不得超过这个时间
G1垃圾收集器不能说它是属于哪一代了,其实看图就看出来、G1是在新生代和老年代的中间线上,它把整个java堆分成了大小相等的独立区域,虽然还保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离了。它们都是一部分Region (不需要连续)的集合
对于可预测的停顿时间模型可以有计划的避免在整个java堆中进行全区域的垃圾收集
注意: 如果你的应用追求低停顿,那现在G1就可以作为一个尝试的选择,如果追求高吞吐量,那G1并不会带来什么特别的好处
7、内存分配和回收策略
(1)Eden区和Survivor区
从上面文章中我们知道了新生代用的是复制算法,但是这样太浪费空间了,IBM公司研究表明,新生代中的对象98%是"朝生夕死"的,所以并不需要按照1:1的比例来划分新生代的内存空间,所以出现了Eden区和Survivor区,
Eden区和Survivor区比例在8:1但是复制算法要求的是两个大小相同的内存区域,所以有两个Survivor区,一个是From Survivor区,一个是To Survivor区,这两个比例为1:1,最终新生代被分为了3份,比例是Eden:From Survivor:To Survivor=8:1:1
(2)对象优先在Eden中分配
从这里开始就简单介绍几种内存分配规则
大多数情况下,对象会在新生代的Eden区进行内存分配,当Eden区没有足够的内存进行分配时,就进行一次Minor GC,通过这次GC把存活的对象放到Survivor 区去,但是如果出现存活的对象太大,Survivor区放不开,那就只能通过分配担保机制直接转移到老年代里去
(3)大对象直接进入老年代
这里的大对象是指需要大量连续内存的java对象,
虚拟机提供了一个参数:-XX:PretenureSizeThreshold,令大于这个值的对象直接放进老年代,防止在Eden和Survivor中频繁的复制
(4)长期存活的对象将进入老年代
这个不是像上面那样直接进入老年代,而是通过在Eden和Survivor 中计算对象的年龄(有一个对象年龄计数器),当达到一定值(默认是15),就会被晋升到老年代,对于老年代的年龄阈值可以用参数:-XX:MaxTenuringThreshold 设置
(5)动态对象年龄判断
上面晋升老年代的方式有些死板,但为了使用不同程序的内存状况,晋升老年代也可以通过另一种方式,但是需要满足特定的条件
- 在新生代某一个年龄的对象相加的和大于Survivor空间的一半
- 想晋升的对象需要大于上面条件1的对象中的年龄
(6)空间分配担保
到这里简单的分配策略就说完了,这里主要讲讲上面的分配担保机制
在发生MinorGC之前,虚拟机会先检查老年代的最大可用的连续空间是否大于新生代的所有对象的总和,如果大于(担心新生代存活的对象太大),那可以确保安全,如果小于,虚拟机会查看HandlePromotionFailure是否允许担保失败(JDK1.6 Update 24以后就不再用HandlePromotionFailure了,相当于把下面的不允许删除了)
允许:检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果小于就Minor GC,出现极端情况,这一次的比以往的都要大,那就Full GC,如果大于就Full GC,虽然绕了一圈但是可以有效减少Full GC的频次()
不允许:那就要进行Full GC