JVM垃圾回收
Contents
1. 内存分配和回收原则
- 创建对象优先在新生代的Eden区进行分配内存,Eden区满了会触发一次Minor GC,将Eden区的对象移动到S区,如果S区仍然不足以存放该对象则证明该对象是大对象,会直接放到老年代
- 长期存活的对象也会进入老年代,长期存活的标准是对象头的GC年龄信息达到阈值。在Eden区时年龄为0,第一次进入S区后年龄为1,往后每次新生代发生GC时,存活的对象会在S0区和S1区之间来回移动,每次移动GC年龄就会加1。
- 年龄阈值的判断方法是,虚拟机会将对象的大小按照年龄从小到大进行的顺序进行累加,当累加到某个年龄时对象大小的总和超过的S区的一半,就取当前年龄和设置的年龄阈值之中的最小值
- Minor GC主要作用与新生代的Eden区,每次Eden区满了就会触发,Full GC是对整个堆空间和方法区进行GC
2. 死亡对象的判断方法
- 引用计数法,为对象创建一个引用计数器,每当有地方引用这个对象计数器就加一,引用失效就减一,计数器为0则代表该对象死亡。但是该方法无法解决对象之间循环引用问题,当两个对象之间除了互相引用对方再也无其他地方引用,就会导致引用计数器一直不为0无法回收
- 可达性分析法,通过一系列可以作为GC Roots的节点,向该对象进行搜索,如果可以搜索到该对象则证明该对象存活。可以作为GC Roots的有虚拟机栈中局部变量表引用的对象,本地方法栈中引用的对象,方法区常量引用的对象,所有被同步锁持有的对象,方法区类静态字段引用的对象。
- 可达性分析中被标记为无法到达的对象不会被立即回收,如果重写了finalize方法就会会放入一个队列中,并新开一个线程对队列中的对象进行二次扫描,第二次仍然被标记为不可达才会回收。对象在这个队列中主要是执行finalize方法,并且在这个方法中可能会使得对象重新被引用从而复活
3.引用类型总结
- 强引用:Java中普遍存在的引用,是必不可少的。例如
String str = new String("abc");就是强引用,强引用是无论无何都不会被GC回收的,即便抛出OOM异常。 - 软引用:软引用是可有可无的引用,在内存不足时进行回收。JVM不保证只在内存不足时进行回收软引用,但是在抛出OOM之前所有的软引用一定会被回收
- 弱引用:和软引用类似是可有可无的引用,区别在于无论内存是否充足,JVM扫描到弱引用就会进行回收
- 虚引用: 形同虚设的引用,一般用于跟踪垃圾回收过程,必须配合一个引用队列使用,当JVM回收虚引用的对象时,必须先将该对象放入与之关联的队列中
4. 如何判断一个字符常量是否是废弃常量
- 在字符串常量池中主要回收的就是废弃的常量,当一个字符常量没有被任何一个String的对象进行引用时就表明该对象废弃了
5. 如何判断一个类是否无用
- 该类的实例对象被全部回收
- 该类的 Classloader 被回收
- 该类的class文件没有在任何地方被引用,也就是无法在任何地方通过反射访问到该类
- 满足这三个条件的类是可以被回收但是不是一定会被回收
6. 垃圾清理算法
- 标记-清理算法:两次扫描,第一次将存活的对象打上标记,第二次则清理未被标记的可回收对象。效率较低并且会产生大量内存碎片,如果新对象没有内存碎片可以容纳就是提前触发一次新的GC。
- 标记-复制算法:将对象区域分为两块,将标记的存活对象直接复制到空间的那一块空间,然后直接对原来的内存空间全部回收。新生代的Minor GC中,两个S区之间就是用的标记复制算法。Eden:S0:S1 的 比例是 8:1:1。老年代不适合标记复制的原因是老年代的对象较大且存活时间较长,如果采用复制则复制的对象又多又大,降低GC效率
- 标记-整理算法:将存活的对象向一端移动,回收掉另一端全部的内存空间,老年代的算法就是使用的标记-整理
- 分代收集算法:为了提高GC效率,对不同的空间使用不同的回收算法,新生代使用标记复制,老年代使用标记整理
7. 垃圾收集器
7.1 基础垃圾收集器
- Serial收集器:串行收集器,是单线程的收集器,在进行垃圾收集时会停止其他所有的工作进程,直到垃圾收集完毕
- ParNew收集器:是Serial的多线程版本,进行垃圾收集时仍需要停止其他所有的工作进程,但是在收集垃圾时会创建多个线程进行并行收集。并且是除Serial外唯一可以和CMS收集器进行配合的。ParNew注重于降低STW的时间,为此会提高GC的频率。
- Parallel Scavenge收集器:同样也是多线程并行收集器,与ParNew类似,唯一的不同点就是Parallel Scavenge更加注重提高CPU的利用率,也就是提高单位时间内用户代码运行的时间,它不会在乎一次GC停顿的时间,只在乎全天运行下来GC停顿时间占比较少。
- 补充:Serial收集器适用于Client模式的服务,也就是客户端服务,此类型的服务启动快占用堆空间小。ParNew收集器使用于Server模式的服务,启动慢,在启动时会进行大量的代码优化,但是服务启动后吞吐量很高。
7.2 CMS收集器
- 并发收集器,采用的是标记-清理算法,收集经历四个阶段
- 初始标记,直接标记和GC Root直接相连的对象
- 并发标记,同时启动GC线程和用户线程,在一个闭包环境种进行可达性分析,但是不保证这个闭包环境中包含所有的可达对象,因为用户线程会不断更新对象引用,所以算法会将对象的引用变更存下来。
- 重新标记,为了修正并发标记时对象引用的变更导致的可达对象变更的问题,这个阶段标记时间一般比初始标记较长,但是远远比并发标记时间短
- 并发清除,同时启用用户线程和GC清除线程对未标记的区域进行回收
- 优点是并发收集,STW时间短
- 缺点是CPU敏感、标记-清除算法会产生大量的内存碎片、无法处理浮动垃圾,浮动垃圾就是并发标记期间由于用户线程导致的某些对象有可达的变成不可达的,此时本次GC无法处理,只能再下一次GC才能处理
- 三色标记,白色是未被收集器访问的对象,初始都是这个颜色;灰色是已经被收集器访问,但是它引用的其他对象未被全部访问,中间状态;黑色是已经被收集器访问且它引用的其他所有对象也被访问过,最终状态。并发标记的记录对象引用变更就是当一个黑色对象重新引用的一个白色对象,那就会将这个黑色对象变为灰色对象。
7.2 G1收集器
- 也是并发收集器,具有以下四个特点:
- 并发收集,可以同时运行Java程序和GC程序
- 分代收集,保留了分代收集的特征
- 从整体上看是标记-整理算法,从部分上看是标记-复制算法
- 可预测的停顿时间,G1与CMS都是可以减低SWT的时间,G1有个不同的点就是可以确定在一个长度为M毫秒的时间范围内,GC的时间不能超过N毫秒
- 收集过程同样也是四步
- 初始标记,标记直接和GC Root相连的对象
- 并发标记,同时启动用户线程和GC线程,标记所有可达的对象
- 最终标记,产生STW,处理并发标记中用户线程导致的对象引用变更
- 筛选回收,产生STW,使用标记-复制算法将选定区域的存活对象全部复制到空间区域局,然后直接回收选定区域的内存。G1会维护一个优先队列,每次回收会根据预测的时间上限选择价值最高的区域进行回收。
- Java9开始被设置为默认收集器
7.3 ZGC收集器
- 进一步减低STW的时间,但是牺牲了一些吞吐量
- 是未来的发展趋势,在Java11中引入
