Java 虚拟机的语境下,垃圾指的是死亡的对象所占据的堆空间
。
垃圾回收的两种方式
- 引用计数法
弊端:无法处理循环引用 - 可达性分析
这个算法的实质在于将一系列 GC Roots(暂时理解为由堆外指向堆内的引用
) 作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。
Stop-the-world以及安全点
JVM中的Stop-the-world是通过安全点机制来实现的。当JVM收到stop-the-world的命令时,它便会等待所有线程都到达安全点,才允许请求stop-the-world的线程进行独占的工作。
安全点的初始目的并不是让其他线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java 虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析。(安全点意味着在这个点时,所有工作线程的状态是确定的,JVM 就可以安全地执行 GC 。)
安全点检测:
1.执行JNI本地代码
由于本地代码需要通过 JNI 的 API 来完成三个操作(访问Java对象、调用Java方法、返回原Java方法),因此 Java 虚拟机仅需在 API 的入口处进行安全点检测(safepoint poll),测试是否有其他线程请求停留在安全点里,便可以在必要的时候挂起当前线程。
2.解释执行字节码
对于解释执行来说,字节码与字节码之间皆可作为安全点。Java 虚拟机采取的做法是,当有安全点请求时,执行一条字节码便进行一次安全点检测。
3.执行即时编译器生成的字节码
执行即时编译器生成的机器码则比较复杂。由于这些代码直接运行在底层硬件之上,不受 Java 虚拟机掌控,因此在生成机器码时,即时编译器需要插入安全点检测,以避免机器码长时间没有安全点检测的情况。HotSpot 虚拟机的做法便是在生成代码的方法出口以及非计数循环的循环回边(back-edge)处插入安全点检测。
4.线程阻塞:
阻塞的线程由于处于 Java 虚拟机线程调度器的掌控之下,因此属于安全点
除了垃圾回收之外,Java 虚拟机其他一些对堆栈内容的一致性有要求的操作也会用到安全点这一机制。
垃圾回收的三种方式
1.清除(Sweep)
- 把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表中;
- 当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,并划分给新建的对象;
- 缺点
内存碎片:JVM堆中的对象必须是连续分布的 分配效率低下:逐个访问列表中的项,来查找能够放入新建对象的空闲内存
2.压缩(Compact)
- 把存活对象聚集到内存区域的起始位置,从而留下一段连续的内存空间
- 能解决内存碎片的问题,代价为
压缩算法的性能开销
3.复制(Copy)
- 把内存区域划分为两等分,分别用from和to指针来维护,from指针指向的内存区域用来分配内存
- 当发生垃圾回收时,便把存活的对象复制到to指针指向的内存区域,并且交换from指针和to指针的内容
- 同样能解决内存碎片的问题,代价为
堆空间的使用效率极其低下
- 压缩也需要复制数据
- 压缩:需要复杂的算法保证引用能够正确更新
- 复制:可以在复制完成后统一更新引用
分代回收
-
新生代
Java对象只存活很短时间,因此可以频繁地采用耗时较短的垃圾回收算法 -
老年代
JVM将做一次全堆扫描,耗时可能将不计成本
如果触发老年代回收,说明
1.新生代并没有回收大部分本该回收的垃圾
2.堆空间已经耗尽
JVM堆分配
JVM将堆分为新生代和老年代。
新生代分为Eden区和两个大小相同的Survivor区
-
默认情况下,JVM采取动态分配的策略(-XX:+UsePSAdaptiveSurvivorSizePolicy):
(1)依据生成对象的速率,以及Survivor区的使用情况动态调整Eden区和Survivor区的比例
(2)也可以通过-XX:SurvivorRatio=8来固定这个比例
(3)其中一个Survivor区会一直为空,比例越低堆空间浪费越严重 -
调用new指令时,会在Eden区划出一块作为存储对象的内存
(1)由于堆空间是线程共享的,因此需要同步
(2)JVM采用的技术为TLAB(Thread Local Allocation Buffer),-XX:+UseTLAB,默认开启
TLAB
-
每个线程可以向JVM申请一段连续的内存,作为线程私有的TLAB
-
这个操作需要加锁,线程需要维护两个指针,一个指向TLAB中空余内存的起始位置,一个指向TLAB的末尾
-
new指令,直接通过指针加法来实现,即把指向空余内存位置的指针加上所请求的字节数
- 如果加法后空余内存指针的值仍然小于等于指向末尾的指针,代表分配成功
- 否则TLAB已经没有足够的空间来满足本次新建操作,这个时候需要当前线程重新申请新的TLAB
Minor GC
-
GC算法
标记-复制
将 Survivor 区中的老存活对象晋升到老年代,然后将剩剩下的存活对象和 Eden 区的存活对象复制到另一个 Survivor 区中 -
MinorGC的复制过程
当发生Minor GC时,Eden区和from指向的Survivor区中的存活对象会被复制到to指向的Survivor区,然后交换from和to指针 -
MinorGC的标记场景
- 当一个对象被复制的次数为-XX:+MaxTenuringThreshold=15时,那么该对象将被晋升到老年代(JVM会记录Survivor区中的对象一共被来回复制了几次)
-
触发时机
当Eden区的空间被耗尽,JVM会触发一个MinorGC,来回收新生代的垃圾 -
晋升到老年代的条件
- 当一个对象被复制的次数为-XX:+MaxTenuringThreshold=15时,那么该对象将被晋升到老年代
- 如果Survivor区已经被占用-XX:TargetSurvivorRatio=50%的时候,那么较高复制次数的对象也会被晋升到老年代
6.MinorGC 的优势
- 理想情况下,Eden区中的对象都基本死亡了,那么需要复制的数据是非常少的,效果将很好
- Minor GC无需对整个堆进行回收(借助卡表,无需全堆扫描,规避老年代的对象引用新生代的对象,该引用作为GCROOTS时进行全堆扫描的情况)
卡表
将整个堆划分为大小为512Bytes的卡,并且维护一个卡表,用来存储每张卡的标识位(对应的卡是否可能存在有指向新生代对象的引用,若存在,即认为这张卡是脏的)
- 如何避免全堆扫描?
在进行Minor GC的时候,便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到Minor GC的GC Roots里,当完成所有脏卡的扫描后,JVM会将所有的脏卡的标识位清零