有些内存区域因为方法结束或者线程结束时就自动回收了,所以并不需要考虑它们的回收。主要关注的是对java堆和方法区的回收。(方法区怎么回收?)
对象存活分析
判断对象是否存活的算法有引用计数算法和可达性分析算法。java用的是可达性分析算法。
引用计数算法原理是,给对象添加一个计数器,引用一次加一,引用失效减一。当计数器为0时对象就不可能被使用,所以可以回收该对象。
这个算法简单高效但是有一个缺点就是不能处理对象引用循环的情况,即对象A引用对象B,B引用A,除此之外没有任何其他对象引用A和B,这样子AB永远不可能被使用,因为计数器值为1也不会被回收。
可达性分析算法,让一些自动回收(我还不确定选择的标准)的对象作为GCRoot,每个被引用的对象都会有一条到GCRoot的引用链。回收时通过GCRoot来搜索对象的引用,若找不到这个对象就说明这个对象没有被引用是可回收的(GCRoot到这个对象不可达)。
可作为GCRoot的对象有:
- 虚拟机栈的局部变量表中引用的对象(栈的)(执行上下文)
- 本地方法引用的对象
- 方法区的静态属性引用的对象(方法区的)(全局性的引用)
- 方法区中常量引用的对象
引用
引用分为强引用、软引用、弱引用、虚引用。
- 强引用,new出来的一类引用,只要引用还在就不会被回收。
- 软引用,内存不足时会强制回收这类引用,SoftReference类实现。
- 弱引用,活不到下一次gc,WeakReference类实现。
- 虚引用,虚的,不能取得对象实例,唯一作用是在对象被gc时能收到一个系统通知,PhantomReference类实现(在对象被回收前将虚引用加入ReferenceQueue中,通过这个队列可以知道那些对象被回收了)。
关于finalize()方法
Object的protected方法,可在gc回收前调用这个方法,实现资源清理或者自救。
若gc第一次标记时,判断到对象需要执行finalize方法,就将它加入一个队列中,另开线程执行这个方法,但不会等它执行完。
如果对象在finalize方法中完成了自我拯救,在第二次标记时它会被移除出“即将回收”集合。
- finalize方法被覆盖了才会被执行
- 只能执行一次
- 运行代价高昂且没必要,最好忘了它。
垃圾收集算法
主要有 标记-清除算法、标记-整理算法、复制算法、分代收集算法。
标记-清除
先标记出所有要清除的对象,然后再统一回收这些对象。
不足:
效率低。
空间碎片多,给大对象分配内存时找不到足够的连续内存,会提前触发另一次gc。
复制
将内存分成两半,每次只使用其中一半,在用完时将存活对象复制到另一半上去,再将那用完的半部分空间都清理掉。
不足:可用内存减少了
虚拟机将新生代分为了Eden区和两个Survivor区,大小 8:1:1。每次只用Eden和一块Survivor,回收时将存活对象复制到另一块Survivor中。
可以这样分是因为新生代中大多是朝生夕死的对象。但存活对象也有可能会多余10%,所以需要分配担保机制,在Survivor空间不够时,让其他空间比如老年代提供分配担保,把存活对象复制到这些空间里。(是部分还是全部复制过去?咦担保是用的复制吗)
标记-整理
标记所有可回收对象,然后将存活对象全部向一端移动,最后清除掉边界外的内存。
老年代的选择。
分代收集算法
根据对象存活时间来划分内存。
一般将java堆分为新生代和老年代,然后可以根据不同年代的特点采用不同的收集算法。新生代朝生夕死存活对象少,所以可用复制算法。老年代顽强存活对象多,又没有其他空间给它担保所以只能选标记清除和标记整理。
枚举根结点、安全点和安全区域
枚举根结点时为了防止引用关系变化,需要阻塞所有运行java code的线程(stop the world),VM操作相关的线程不会被阻塞,运行native code的线程如果不与javacode交互也不会被阻塞。
另外不需要检查所有引用位置来找全GCRoot,数据结构OopMap记录了引用的位置。
但是因为很多指令会导致引用关系变化,即OopMap内容改变,又不可能给每条指令后面都生成对应的OopMap。所以OopMap只在特定的位置生成,这些位置就是安全点。
安全点是指一些特殊的位置,在这些位置线程状态可以被确定,使jvm安全的进行一些操作(比如gc,程序运行时并非在所有地方都能停顿下来进行gc,到达安全点才能停下来)。
安全点的选定不能太少,让gc等待时间太长(自己的理解:假设只有一个安全点,每次gc都要等待线程运行到这个安全点才能开始,安全点多点的话,线程短时间内有很大几率碰到一个安全点,gc就不用等那么久才能开始),也不能过于频繁增加负荷。
所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等。
(这一段看不懂)
这些特定的位置主要在:
1、循环的末尾
2、方法临返回前 / 调用方法的call指令后
3、可能抛异常的位置
线程自己响应中断:在安全点设置一个标志,轮询这个标志,若为真就自己中断挂起。
若线程阻塞,无法轮询,就需要安全区域了,安全区域可以看作是安全点的扩展。线程执行到安全区域时标示自己已经进了safe region,gc时不用管这些线程,当线程要出这个区域时查看gc是否已经完成,没完成需要等到gc完成才能出去。
垃圾收集器
Serial收集器
单线程,需要stoptheworld,简单高效,适用于client模式的VM,新生代的选择,SerialOld可用于老年代。
新生代复制算法,SerialOld老年代标记-整理算法
ParNew收集器
多线程,Serial的多线程版,需要暂停用户线程,server模式VM中首选的新生代收集器。
除了Serial唯一能与CMS收集器配合工作的收集器。
Parallel Scavenge收集器
多线程,新生代收集器,复制算法。吞吐量优先收集器。吞吐量是代码运行时间和cpu总运行时间的比值(即代码时间+gc时间)。
要减少停顿时间就要牺牲新生代空间,新生代空间小了,停顿时间也就变小了,但是相应的gc次数变多了,吞吐量也降了下来。
-XX:GCTimeRatio gc时间占总时间的比值
-XX:MaxGCPauseMillis 最大停顿时间
-XX:+UseAdaptiveSizePolicy 自适应策略
Serial Old收集器
jdk1.5 前与parallel搭配使用
作为CMS的后备预案
Parallel Old 收集器
1.5后 与Parallel搭配使用。
CMS收集器
以最短停顿回收时间为目标,标记清除算法,并发低停顿收集器
只有初始标记和重新标记需要阻塞用户线程
- 初始标记:标记GCRoots直接关联的对象(stop the word)
- 并发标记:GCRoots Tracing过程,用户线程可并发执行
- 重新标记:修改并发标记阶段,对象引用改变了的那一部分标记对象的标记(stop the world),时间比并发标记短的多
(有新的引用即新的对象出现怎么办怎么确定它与垃圾对象的区别??再tracing一次吗??
标记是怎么标记的??三色标记法了解一下) - 并发清除
缺点:
- 对CPU资源敏感。
并发时会占用一部分线程,默认启动的垃圾收集线程数是(cpu数量+3)/4。 - 无法处理浮动垃圾。(并发时,标记的对象用户线程不要了)
并发时用户线程会产生新的垃圾,这一部分垃圾只能在下次gc时处理。所以gc时需要预留一部分内存来给用户使用,如果预留的内存不够会出现“Concurrent Mode Fail”失败导致另一次full gc,此时会临时启用SerialOld收集器重新对老年代进行收集。
默认老年代使用了92%空间后激活CMS收集器。 - 大量空间碎片产生。
-XX:+UseCMSCompactAtFullCollection 默认开启,在没足够内存需要full gc时进行碎片整理
-XX:CMSFullGCsBeforeCompaction 默认0,在进行多少次不压缩的fullgc后,跟着来一次压缩(碎片整理)的fullgc
G1收集器
它java堆的内存分布与其他收集器不一样,它将java堆划分成了很多个相同大小的区域(region)。
- 空间整合:不会产生空间碎片,整体是标记-整理算法(以区域为基础进行操作),局部是复制算法(将区域内的存活对象复制到另一个区域)。
-
分代收集:各个年代是一部分region的集合。
- 并行
- 可预测的停顿:能指定垃圾收集时间,因为G1会在后台根据每个区域垃圾的价值(回收所需时间和回收获得的空间的经验值)维护一个优先列表,每次根据允许的垃圾收集时间优先回收最大的区域。
老年代引用新生代的对象的情况:
老年代有一个CardTable,若老年代中有引用新生代的对象,就将cardtable对应的索引记录到新生代对象所属的RememberSet中,这样子MinorGC就不用搜索全堆,只需将RSet加入根结点枚举范围扫描特定的地方。
运行步骤:
- 初始标记,停顿线程,但时间很短
- 并发标记,耗时长
- 最终标记,应该还是有浮动垃圾?
- 筛选回收,将各个region根据价值进行排序,根据用户要求的回收时间制定回收计划。停顿线程,并行执行(真并行?)
(JIT编译后被拆散为标量类型栈上分配??)
对象的内存分配规则
- 优先在Eden区分配,没有足够内存会发起MinorGC
-
大对象直接进入老年代(长字符串或数组)
-XX:PretenureSizeThreshold 大对象大小,大于这个值的对象直接分配在老年代 -
长期存活的对象进入老年代,熬过一次MinorGC,年龄加1,默认15岁进老年代
-XX:MaxTenuringThreshold 设置进入老年代的年龄阀值 - 动态对象年龄判定, 如果survivor中相同年龄的所有对象大小总和超过survivor的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
-
空间分配担保 ,gc时,若survivor空间不足,将对象放进老年代。
(jdk6)MinorGC前,需要判断是否安全,先检查新生代对象总大小是否小于老年代连续空间的大小或者历次晋升到老年代的对象大小的平均值,是的话可进行minorGC(若minorGC时老年代空间不足,担保失败,只好重新发一次fullGC),不是的话改为fullGC。