分代搜集算法
上面三种算法都是有明显的缺陷,但是都是一步一步发展来的,分代搜集算法不能称为新的算法只是对上面三个算法的使用方式不同。分代搜集算法是针对对象不同的特性,来进行GC的。
对象分类:
- 朝生夕死对象,生存周期不长
列如:方法的局部变量,循环内的临时变量
- 生存周期长但是最终还是会死的对象,这类对象虽然生存周期很长,但是几乎要死的。
例如:缓存对象,数据库连接对象,单例对象
- 对象一般一旦出生几乎不会死
例如:运行时常量池对象,加载过的类信息
对象对应的内存
朝生夕死/生存周期长但是最终还是会死的对象,这类对象都在JAVA堆
几乎不会死对象在方法区
在java虚拟机规范中要求对JAVA堆要求必须实现GC,所以对于朝生夕死/生存周期长但是最终还是会死的对象,被GC掉是必然的结局
Java堆的对象回收(朝生夕死/生存周期长但是最终还是会死的对象)
朝生夕死对象:这种对象存活率很低,刚好适合用复制算法,但是复制算法的内存浪费太大了,所以有特殊处理,将堆先非常两个部门年轻代和年老代。先介绍一下年轻代,年轻代分为两个部门两个10%的内存区域,一个80%的内存区域,这三块区域怎么相互配合呢?画一个图就明白了
为什么会有年老代呢?从上面知道GC之后存活的区域只占10%内存,那么如果存活对象的大小超过10%呢,超过的部分按照这个逻辑是会丢失的。所以第二部分年老代起作用了,什么情况下对象会从年轻代转入到年老代呢?
- 在新生代中,每个对象都会有‘年龄’(每熬过一次GC年龄就+1),当对象达到年龄达到了,JVM设置的年龄值,就会从新声代转入到年老代
- 新生代存活的对象超过了10%时,多余的对象会放入年老代,相当于年老代是新生代的备用仓库
很显然对于年老代来说,对象的特性是生存周期长但是最终还是会死的对象,所以适合用标记-整理/标记-清除算法
JAVA方法区的对象回收(对象一般一旦出生几乎不会死)
方法区的GC不是分代搜集算法的内容。但是很显然方法区的内存区域是没有备用仓库的,所以不能使用复制算法。
触发GC的时机
- 当年轻代或者老年代满了,JVM无法为新的对象分配内存空间了,那么JVM就会触发一次GC回收掉哪些已经不会再被使用到的对象
- 手动调用System.gc()方法,通常这样会触发一次Full GC以及至少一次的Minor GC
- 程序运行的时候有一条低优先级的GC线程,GC线程是一条守护线程,当GC线程处于运行状态的时候,自然就触发了一次GC。
mionr GC/full GC/major GC
JVM每一次GC不一定是对三部分内存(方法区,新生代,老年代)同时进行回收的。
GC分为两种mionr GC(普通GC),full GC/major GC(全局GC)
mionr GC:针对新生代进行GC
full GC/major GC:针对年老代,偶尔会对方法区,新生代GC
很显然,对于年老代和方法区的GC收益往往是不大的(因为对象很难死嘛),所以经过好几次mionr GC之后才会触发full GC
对象内存分配的原则
- 长期存活的对象进入老年代。Eden区中的对象再一次Minor GC后如果没有被回收那么对象年龄就会+1,当对象年龄达到-XX:MaxTenuringThreshold设置的值的时候,对象进入到老年代
- Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到-XX:MaxTenuringThreshold设置的值
- 对象优先分配在Eden区(可能不同的垃圾收集器不一样,我测试用的是Server模式下默认的垃圾收集器)
- 大对象直接进入老年代
通过-XX:PretenureSizeThreshold=3145728设置,设置这个参数可以避免大对象在Eden和Survivor之间来回复制。但是对于Server默认的垃圾回收器Parellel Scavenge+Parallel Old来说是无效的
- 空间分配担保机制
我们都知道新生代采用的是复制算法,有一个Survivor区域来存储每次Minor GC存活下来的对象,我们都知道Survivor区域默认是占用新生代的10%空间,如果Minor GC
之后存活的对象大于Survivor的内存空间了怎么办呢?如果老年代剩余的连续空间大于Minor GC 后存活的对象所占用的空间那么存活的对象会进入老年代,这就是空间分配担保机制。那么如果老年代剩余连续空间大小不够了,JVM就会查看HandlePromotionFailure设置值时否允许担保失败,如果允许,那么就会继续检查老年代最大可用的连续空间时否大于历次晋升到老年代对象的平均大小,如果大于尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于或者设置不允许冒险,那这是就要进行一次Full GC