GC Root是啥,怎么通过GC Root查找
GC Root是用来确认对象是否已经不被使用,如果没有被使用了就可以进行回收了。CG Root的对象包括下面几种:
- java虚拟机栈中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- JNI(java native interface)中引用的对象。(java调用JNI的时候会传入对象参数)
引用计数
引用计数顾名思义就是只要对象引用一次就+1,退出了不使用了就-1。但是当循环引用时就无法处理,另外计数需要额外的共享空间进行存储以及需要繁琐的更新动作,所以java虚拟机使用的是GC Root进行可达性分析。如果可达GC Root就说明不能回收,不可达就可以进行回收。
Stop-the-word
因为判断对象是否可以被回收需要进行检查对象是否可达GC Root,所以如果在回收的时候对象的可达性还在变化可能会误杀(比较严重,幸运的话可以重新创建)和漏杀(会增加GC次数)对象,这都是不大友好的,所以比较方便的是Stop-the-word这样对象之间的引用没有变化,所以JVM选择在安全点和安全区(堆栈没啥变化)内进行Stop-the-word。
安全点
确保线程在安全点暂停恢复后不会导致引用关系发生变化,比如方法调用、循环跳转、异常跳转等。当需要进行gc时,线程一旦碰到安全点就会主动式中断进行gc。
安全区
安全区是对安全点的补充,如果线程已经处于阻塞或等待的状态就没办法进入安全点了,所以一旦线程处于这些状态垃圾回收器是可以进行gc的,因为这时对象的引用也不会发生变化。当线程恢复的时候还在gc的话就需要继续等待了。
三种回收算法
标记-清除
通过在空闲列表中记录已死亡的对象的位置,然后在gc的时候进行清除,比较简单。
缺点
容易产生碎片,新的对象可能比之前清理的小。同时因为对象需要连续的空间,比较极端的情况下后面新建的对象都比较大,存在空闲的空间利用不上。
标记-压缩
压缩可以看的出来清理之后留下的空间比较连续,不会产生碎片。
缺点
比较消耗性能,需要移动对象,引用也需要改变。
标记-复制
可以看到,复制算法比较简单不需要标记再清除,同时不需要移动对象,只需要把存活的对象复制到新的空间中原来的空间直接进行格式化,也能也不会产生碎片,能够解决清除算法的缺点和部分压缩算法的缺点。
缺点
浪费一半空间,也需要改变索引。
总结
下面通过一个表格来对比这几种算法。
| 算法 | 优点 | 缺点 |
|---|---|---|
| 标记-清除 | 速度快 | 存在碎片、大的对象利用不上小的空闲空间 |
| 标记-压缩 | 通过压缩实现了空间的连续 | 需要移动对象和对象引用的改变,比较耗时。 |
| 标记-复制 | 空间连续、速度比压缩快 | 浪费一半的空间、对象引用会改变 |
我们该怎么选择呢?具体看看下面对堆的划分。
堆划分
根据对java对象生命周期的动态分析,大部分java对象只存活很小一段时间,小部分会存活较长时间。所以我们可以把堆大致划分为年轻代和老年代,因为大部分新生的对象会死亡,那么是不是意味着如果使用“标记-复制”算法只需要复制少量的对象?那既然是少量的对象,为啥需要浪费一半的空间?我们是不是可以只用少量空间存放活下来的对象?所以jvm对堆的划分如下图:
新产生的对象先放在Eden区,当需要gc的时候把少量的对象复制在to区,再把from和to进行调换,保证to区一直是空的,gc的时候把Eden区和form区进行格式化。可以设置-XX:SurvivorRatio进行划分比例,默认为8,Eden区占8/10。-XX:+UserPSAdaptiveSurvivorSizePolicy默认开启,根据情况动态划分比例。
那么什么时候Survivor区的对象会进入老年代区呢?如果被来回复制15次(可以通过-XX:+MaxTenuringThreshold进行配置,默认15)会进入老年区。或者当Survivor区已经被占用了(可以通过-XX:TargetSurvivorRatio进行配置,默认50)那么较高复制次数的对象会进入老年区。
老年代的对象除了来自新生代,还有当一个对象过大时会直接进入老年代,可以通过-XX:PretenureSizeThreshold指定(默认为0,指任何对象直接在新生代分配内存)
TLAB
因为堆空间是共享的,那么如果多线程的情况下怎么处理呢?怎么避免不会写串呢?答案是TLAB技术(Thread Local Allocation Buffer,使用-XX:+UserTLAB,默认开启)。就是当需要新建对象的时候,会单独划分一块给创建线程独占(占用的过程加锁),如果划分的不够会重新进行申请更大的TLAB。
卡表
Minor GC的好处在于如果新的对象之间的引用都在新生代就好了,因为这样GC Root也在新生代,这样在gc的时候只需要扫描新生代堆中的对象。那么老年代中的对象引用了新生代中的对象咋办呢?
HotSpot给出的解决方案就是卡表(Card Table)技术,该技术将整个堆划分为一个大小为512字节的卡(目的),并维护一个卡表,用来存储每张卡的一个标识位。
就是在新生代的对象进行复制的时候对象的引用也会改变,这个时候可以判断持有要改变的对象的引用是否被老年代的对象引用,如果有就在卡表中进行标记,等下次GC的时候就把卡表中标记的老年代对象加入新生代的GC Roots中,同时卡表的标记就可以清除了。
垃圾收集器
新生代
parnew
parnew为serial的多线程版本。
parallel scavenge
parallel scavenge和parnew类似,但更加注重吞吐率(比如让长时间的任务先让出来给短时间的)。parallel scavenge不能和CMS一起使用。
目标是达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))。比较适合后台运算而不需要太多交互的任务。
-
-XX:MaxGCPauseMillis:默认200ms,收集器将尽可能地保证内存回收花费时间不超过设定值。避免parallel为了达到吞吐量导致YGC时间过长。
设置越小GC停顿越少,是因为把新生代空间设置更小,300M比收集500M更快,这也将导致GC更频繁,比如原来10秒收集一次停顿100ms,现在变成5s一次,每次70ms,虽然快但是吞吐量下降了。
-
-XX:GCTimeRatio:垃圾收集的时间占时间的比率,默认为12(即1/(1+12))。
-
-XX:+UseAdaptiveSizePolicy:默认开启,指定后不需要手动配置-Xmn、-XX:SurvivorRatio、+XX:PretenureSizeThreshold了,垃圾收集器会根据吞吐量这个目标进行动态调整,只需要设置一个-Xmx(默认为2G,最好根据情况设置下)就好了。
老年代
CMS(在java9中已经废弃)
目标是缩短垃圾收集器时用户线程的停顿时间。
G1
G1(Garbage First)把堆化整为零,把堆划分为小堆,小堆里面放上面的分区 Eden Survivor Tenured,便于多线程并行回收,同时死亡最多的小区先回收。
内空间配担保(jdk8弃用)
担保就是当新生代的对象存活对象survivor中存放不下,但是老年代也存放不下,又不想进行full gc,看下一下最大可用连续空间的对象年龄是否大于老年代的平均年龄,如果小于就再进行minor gc,如果大于就进行full gc。
不确定jdk8废除的原因,但是感觉这个担保机制没啥用,但是感觉段时间内进行一次minor gc没啥用。