理解JVM(4)垃圾回收

来源:互联网 发布:匹配滤波器 矩阵算法 编辑:程序博客网 时间:2024/05/16 12:54

常用的垃圾回收算法

引用计数法:

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

标记清除法:

在标记阶段,通过根节点标记所有可达的对象;在清除阶段,清除所有未被标记的对象。

  • 缺点:回收后空间不连续,产生空间碎片,对于大对象会降低工作效率

复制算法:

将内存空间分为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.ReferenceQueueimport 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)"    }}//强化的弱引用,记录一个UIDclass 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 deletedAfter 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空间。

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

原创粉丝点击