Java虚拟机的垃圾回收主要从三个方面去概括:
- 垃圾判断算法
- GC(垃圾回收)算法
- 垃圾回收器的实现和选择
垃圾判断算法
垃圾判断算法,用于确定什么是垃圾内存。主要包括引用计数算法和根搜索算法。
- 引用计数算法
引用计数算法,给对象添加一个引用计数器,当有一个地方引用它,计数器加1,当引用失效了,计数器减1,当计数器的值为0,表示这个对象不再被使用,内存可以回收。有点:算法简单,缺点是无法解决循环引用问题。
- 根搜索算法
通过一系列称为GC Roots的点作为搜素的起始点,如果一个对象到GC Roots节点没有任何引用链,则表示这个对象不可用了。
GC Roots包括:
- 栈帧中的局部变量表中引用
- 方法区中的静态引用
- JNI中的引用
方法区的垃圾回收
JVM规范并不要求回收方法区中的垃圾内存,因为性价比极低。在堆中,尤其在新生代,一次GC通常可以回收70%~95%的空间,GC在方法区中回收的比例远低于此。
当前商业JVM都实现了方法区的GC,主要回收废弃常量和无用类,要求非常苛刻。
垃圾回收算法
- 标记-清除算法(Mark-Sweep)
算法分为标记和清除两个阶段,首先标记出所有需要回收的对象,然后回收所有标记的对象。存在的问题: 1. 效率问题:标记和清除的效率都不高。需要扫描所有对象,堆越大,GC越慢 2. 空间问题:标记清除之后会产生大量不连续的内存碎片,内存碎片太多会导致后续无法找到足够的连续内存而提前触发一次GC。GC次数越多,内存碎片越严重。
原始
标记
清除
- 标记-整理算法(Mark-Compact)
标记过程和上面一样,清除时令存活对象的一段移动,然后清除指针边界一侧的内存区域。
这种算法的特点:
- 没有内存碎片
- 比标记清除算法花更多时间进行compact
- 复制算法(Copying)
将可用内存分成两块,每次只使用其中的一块,当半区内存用完了,就把当前存活的对象复制到另外一块中去,然后将原来的那一块内存整体一次性清除掉。这样每次回收内存都是对整个半区回收,不用考虑内存碎片问题,分配内存时候只需要移动堆的指针,按顺序分配即可,实现简单,运行高效。缺点是内存变为原来的一半,空间代价太高。
现代商业虚拟机都是采用复制算法来回收新生代,将新生代划分为eden和2个survivor区,真正只使用eden和其中一块survivor区,另一块survivor区保持空闲,GC时,会将eden和survivor中存货的对象一次性拷贝到空闲的那个survivor中,然后一次性清除eden和原来的survivor区。
复制算法特点:
- 只需要扫描存活的对象,效率更高
- 不会产生内存扫描
- 需要浪费额外的内存空间作为复制区域
- 复制算法适合生命周期非常短的对象,这样每次能回收大部分对象,存活的对象少,相应的复制空间开销就少
- 分代算法(Generational)
商业虚拟机都是采用分代收集算法,根据对象的存活周期将对象划分为新生代和老年代,各个年代根据自己的特点采用合适的收集算法。比如,新生代大量的对象生命周期很短,存活对象少,只需要付出少量对象的复制空间成本即可。对于老年代可以采用标记清除或标记整理算法回收。 年轻代包括Eden Space,From Space和To Space这三部分,From Space和To Space又叫Survivor,默认情况下他们的大小比例是8:1:1。
对象一般在Eden Space中生成,当Eden Space满了时,存活的对象被复制到From Space,当Eden Space再次满了,Eden Space中存活的对象和From Space对象中存活的对象一并被复制到To Space,如果To Space对象满了,多余的对象或者存活超龄的对象,就会被复制到老年代。From Space和To Space完全对称,轮流替换。
大多数对象在Eden Space上分配内存,对于一些大对象,直接在老年代分配内存。
GC时的引用处理
GC要做的事情是将那些dead对象所占的内存回收。Hotspot认为没有引用的对象是dead的。Hotspot的引用分为4种:
- Strong 通过Object o = new Object()创建的引用
- Soft、Weak和Phantom都是继承自Reference 当Full GC时候,对Reference引用特殊处理:
- Soft:内存不够或长期不用时会被GC
- Weak:一定被GC
- Phantom:本来就没引用,当从JVM 堆中释放内存时会通知
GC的时机
在分代收集的基础上,GC分为Minor GC和Full GC。
- Minor GC
触发条件:新对象生成时,Eden 满了 特点:大多数对象在Minor GC时回收,效率高,执行的时间比较短
- Full GC
对整个JVM进行整理,包括Young,Old和Metaspace。
触发时机:a. Old满了 b. Metaspace满了 c. 调用System.gc()
效率低,尽量少执行。一旦出现Full GC,就会出现Stop The World,业务线程暂停执行。
垃圾回收器
分代模型是GC的宏观愿景,而垃圾回收器是GC算法的具体实现。Hotspot提供了多种垃圾回收器,应当根据具体的业务场景采用不同的垃圾回收器,没有一种垃圾回收器是万能的。
GC时并行和并发的概念:
- 并行(Parallel):多个GC线程同时工作,而用户线程处于等待状态。
- 并发(Concurrent):垃圾回收的同时,运行用户线程工作。
单线程收集器(Serial):收集时会暂停所有的工作线程,使用复制算法它是JVM在Client模式下的默认新生代垃圾回收器。主要特点:
- 最早的收集器,单线程
- 新生代和老年代都可以使用
- 复制算法 + 标记整理算法
ParNew收集器:Serial收集器的多线程版本,算法、STW等都和Serial一样,它是JVM在Server模式下默认的新生代垃圾回收器。
Parallel Scavenge收集器:多线程收集器,复制算法,它主要特点是吞吐量高,也就是GC的总运行时间短。STW不理想。
Serial Old:单线程版本收集器,老年代版本。
Parallel Old:Parallel Scavenge的老年代实现,多线程的标记清除算法,吞吐量好。Parallel Scavenge + Parallel Old=高吞吐量,但是STW不理想。
CMS(Concurrent Mark Sweep):很复杂的老年代收集器,主要特点:追求最短停顿时间,并发(GC和用户线程同时进行),标记清除算法,一般结合ParNew收集器。它通过占用很多CPU资源来减少用户线程停顿。