理解JVM(4)垃圾回收

常用的垃圾回收算法

引用计数法:

  • 优点:简单
  • 缺点:循环引用问题,引用产生消除时进行加减

标记清除法:

在标记阶段,通过根节点标记所有可达的对象;在清除阶段,清除所有未被标记的对象。
  • 缺点:回收后空间不连续,产生空间碎片,对于大对象会降低工作效率

复制算法:

将内存空间分为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空间。

这里写图片描述
这里写图片描述
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值