常用的垃圾回收算法
引用计数法:
- 优点:简单
- 缺点:循环引用问题,引用产生消除时进行加减
标记清除法:
在标记阶段,通过根节点标记所有可达的对象;在清除阶段,清除所有未被标记的对象。
- 缺点:回收后空间不连续,产生空间碎片,对于大对象会降低工作效率
复制算法:
将内存空间分为2块,每次使用一块,在垃圾回收时,将正在使用内存中的存活对象复制到未使用的一块中,清除这块的所有对象
- 缺点:内存被折半了
在Java的新生代串行垃圾回收器中,使用Eden,from,to。其中from,to进行角色互换实现复制算法。因为新生代对象死得块,用很小的一块的内存就能放得下存活的对象。但出现放不下的情况,会将对象直接放进老年代。此外,大对象,老年对象,一块survivor区域放不下的对象,都是直接进入老年代。
标记压缩算法:
适用于存活对象多,垃圾对象少的地方,即老年代。首先也是标记,然后将存活对象压缩整理到内存的一端,之后清除边界外的所有空间。
分代算法
分代算法基于对象的存活率不同,分为新生代和老年代,根据上述算法中,复制算法适合新生代,标记压缩算法适合老年代。
卡表的数据结构:老年代以512字节(或4K)为块,分为若干张卡(Card)。卡表为单字节数组,每个数组元素对应堆中的一张卡,每次老年代对象引用新生代对象时,该卡对应的卡表中的元素会设置为脏,垃圾收集器只会扫描脏卡。
分区算法
分区算法将整个堆空间划分为连续的不同小区间。每个小区间都独立使用,独立回收。这样的好处是可以控制一次回收内存的大小,不至于回收整个堆内存而产生大量的停顿时间。
判断可触性
强引用
- 强引用直接访问对象
- 强引用指向的对象不会被回收
- 强引用会造成内存泄露
软引用:内存不足会自动回收
class User(var name: String, var id: Int) {
var data = ByteArray(6 * 1000 * 1000)
override fun toString(): String {
return "User(name=$name)"
}
}
fun main(args: Array<String>){
var u: User? = User("Owen", 1)
val soft = SoftReference(u)
u = null
println(soft.get())
System.gc()
println("After GC:" + soft.get())
val b = ByteArray(6 * 1000 * 1000)
println("After big data:" + soft.get())
}
/*
User(name=Owen)
After GC:User(name=Owen)
After big data:null
*/
引用队列
PS:在创建一个引用对象,传入一个引用队列,这个对象死后会进入引用队列
import java.lang.ref.ReferenceQueue
import java.lang.ref.SoftReference
//要弱引用的类
class User(var name: String, var id: Int) {
var data = ByteArray(6 * 1000 * 1000)
override fun toString(): String {
return "User(name=$name)"
}
}
//强化的弱引用,记录一个UID
class UserSoftReference(p0: User?,
p1: ReferenceQueue<in User>?,
val uid: Int = p0?.id!!)
: SoftReference<User>(p0, p1) {}
//引用队列
var softQueue: ReferenceQueue<User>? = null
//一个守护线程
class CheckRedQueue : Thread() {
override fun run() {
//不断查看删除的引用
while (true){
softQueue?.let {
val obj: UserSoftReference? =
softQueue?.remove() as? UserSoftReference
println("User Id ${obj?.uid} is deleted")
}
}
}
}
fun main(args: Array<String>){
val t = CheckRedQueue();
t.isDaemon = true
t.start()
var u:User? = User("Owen",1)
softQueue = ReferenceQueue<User>()
val userSoftRef = UserSoftReference(u, softQueue)
u = null
println(userSoftRef.get())
System.gc()
println("After GC :${userSoftRef.get()}")
val daa = ByteArray(6*1000*1000)
println("After big data:${userSoftRef.get()}")
Thread.sleep(1000)
}
/*
User(name=Owen)
After GC :User(name=Owen)
User Id 1 is deleted
After big data:null
*/
弱引用:发现即回收
class User(var name: String, var id: Int) {
var data = ByteArray(6 * 1000 * 1000)
override fun toString(): String {
return "User(name=$name)"
}
}
fun main(args: Array<String>){
var u: User? = User("Owen",12)
val weak = WeakReference<User>(u)
u = null
System.gc()
println("After GC:${weak.get()}")
}
虚引用
随机可能被垃圾收集器回收,虚引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况
Stop-The-World
在垃圾回收时,应用程序会无响应,来保持系统状态在某一瞬间的一致性。
class MyThread : Thread(){
val map: HashMap<Long,ByteArray> = HashMap()
override fun run() {
while (true){
if (map.size*512/1024/1024 >= 900){
map.clear()
println("clean map")
}
var b1: ByteArray?
for (i in 0..100){
b1 = kotlin.ByteArray(512)
map.put(System.nanoTime(),b1)
}
Thread.sleep(1)
}
}
}
class PrintThread : Thread(){
companion object {
val startTime = System.currentTimeMillis()
}
override fun run() {
while (true){
val t = System.currentTimeMillis() - startTime
println("${t/1000}.${t%1000}")
Thread.sleep(100)
}
}
}
fun main(args: Array<String>){
val t = MyThread()
val p = PrintThread()
t.start()
p.start()
}
垃圾收集器种类
串行回收器
是Client下默认的垃圾回收器,仅仅使用单线程回收,会产生Stop-The-World.
新生代日志[GC ….[DefNew: xxxxK -> xxxK…]]
老年代日志[Full GC [Tenured: xxxxK -> xxxK…]]
- -XX:+UseSerialGC:新生代,老年代都使用串行回收器
- -XX:+UseParNewGC: 新生代使用ParNew回收器,老年代串行收集器
- -XX:+UseParallelGC: 新生代使用ParallelGC回收器,老年代串行收集器
并行回收器
新生代ParNew回收器:
工作在新生代,只是将串行程序并行化,策略,算法不变。在并行能力强的CPU上,产生的停顿时间要短于串行回收器,但单CPU上可能表现更差。
日志[GC ….[ParNew: xxxxK -> xxxK…]]
- -XX:+UseParNewGC: 新生代使用ParNew回收器,老年代串行收集器
- -XX:+UseConcMarkSweepGC: 新生代使用ParNew回收器,老年代使用CMS
- -XX:ParallelGCThreads指定多少线程参与并发收集,小于8CPU,则等于CPU数量,大于8CPU,则3+((5*CPU)/8)
新生代ParallelGC回收器:
使用复制算法的收集区,非常关注系统的吞吐量
日志[GC [PSYoungGen …]]
- -XX:+UseParallelGC: 新生代使用ParallelGC回收器,老年代串行收集器
- -XX:+UseParallelOldGC: 新生代使用ParallelGC回收器,老年代ParallelOldGC收集器
- -XX:MaxGCPauseMillis: 设置最大垃圾收集器停顿时间
- -XX:GCTimeRatio: 设置吞吐量大小。它的值为1-100,系统会使用1/(1+n)的时间来用于垃圾收集。如默认为19,则会使用1/(1+19) = 5%的时间来垃圾收集
- -XX:UseAdaptiveSizePolicy: 打开自适应GC策略,会自动调整新生代大小,Eden区和Survivor区比例,进入老年代的年纪等参数
老年代ParallelOldGC回收器:
使用标记压缩算法,也是非常关注吞吐量。也是Stop-The-World后,使用多线程并发收集垃圾。
日志[Full GC [PSYoungGen …]]
CMS回收器:
Concurrent Mark Sweep,并发标记清除,使用标记清除算法
工作流程:初始标记 -> 并发标记 -> 预清理 -> 重新标记 -> 并发清除 -> 并发重置
- 初始标记:STW: 标记根对象
- 并发标记:和应用程序并发:标记所有对象
- 预清理:和应用程序并发:清理前准备,并控制停顿时间
- 重新标记:STW: 修正并发标记的数据
- 并发清理:和应用程序并发:清理垃圾
- 并发重置:和应用程序并发:恢复第1步
预清理会刻意等待一次新生代GC, 然后预测下一次新生代GC,在当前时间和预测时间的中间时间,进行重新标记。使用-XX:-CMSPrecleaningEnabled,不进行预清理。
- -XX:-CMSPrecleaningEnabled,不进行预清理。
- -XX:+UseConcMarkSweepGC,启用CMS回收器
- -XX:+ConcGCThreads或-XX:ParallelCMSThreads,设置并发线程数量,当CPU紧张时,收到CMS影响,垃圾收集会很糟糕
- -XX:CMSInitiatingOccupancyFraction,回收阀值,默认68,即当老年代内存使用率达到68%后,会进行CMS回收。如果回收时出现内存不足,会CMS失败,然后启动老年代串行收集器进行垃圾回收,这样的话,会STW
- -XX:+UseCMSCompactAtFullCollection,由于是标记清除算法,可以整理空间,启用内存碎片的整理,STW
- -XX:CMSFullGCsBeforeCompaction,进行多少次CMS, 才进行一次内存整理
- -XX:+CMSClassUnloadingEnabled,会使用CMS的机制回收Perm区的Class数据
G1回收器:
- 并行性:利用多核进行回收
- 并发性:部分工作会和应用程序并发
- 分代GC:兼顾新生代和老年代的回收
- 空间整理:每次回收都会复制对象,减少空间碎片
- 可预见性:由于分区,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,对STW的时长进行很好的控制。
阶段1:新生代
新生代GC主要收集eden和survivor区。回收后,所有eden区全部清空,survivor区会收集一部分数据,但至少存在一个survivor区。并且老年代的区域会增加。
阶段2:并发标记周期
初始标记=>根区域扫描=>并发标记=>重新标记=>独占清理=>并发清理
标记为G的区域,只是内部垃圾比例比较高
阶段3:混合回收
针对含垃圾比例高的区域进行回收,这是Garbage First Garbage Collector名字的由来,回收时优先选择垃圾比例最高的区域
该阶段,既会执行正常的新生代GC,也会选取一些老年代区域进行回收,同时处理新生代和老年代。因为新生代GC会清空Eden区域,此外两块标记为G的老年代也被清理,存活对象被移动到其他区域,减少空间碎片。
阶段4:可能的Full GC
在回收时,出现内存不足,会转入Full GC。
命令
- -XX:UseG1GC,打开G1收集器
- -XX:MaxGCPauseMillis,指定最大停顿时间
- -XX:ParallelGCThreads,设置并行回收的工作线程数量
- -XX:InitiatingHeapOccupancyPercent,指定堆使用率达到多少后,触发并发标记周期,默认为45。这个值不会修改,如果该值偏大,会导致回收时内存不足,Full GC可能性增大,如果该值偏小,会频繁GC,应用程序性能降低
日志
其他参数
- -XX:+DisableExplicitGC,禁言线性System.gc()
- -XX:+ExplicitGCInvokesConcurrent,显性调用System.gc()使用并发收集
- -XX:MaxTenuringThreshold,进入老年代的年纪
- -XX:+PrintHeapAtGC,打印堆日志
- -XX:TargetSurvivorRatio,设置Survivor区的目标使用率,默认为50。当超过该比率时,会减少进入老年代的年纪。
- -XX:PretenureSizeThreshold,设置对象多大,会直接进入老年代,单位为字节。默认为0,即看情况
- -XX:-UseTLAB,禁用TLAB,即线程本地分配缓存。这个区域为了加速对象分配,对象默认分配在堆上,多线程时分配空间必须同步,就降低性能。使用TLAB,能避免多线程冲突,TLAB本身占用eden空间,虚拟机会为每一个线程分配一块TLAB空间。