垃圾回收机制

文章目录

  • 引入
  • 一、什么情况下JVM内存中的对象会被垃圾回收
    • 1.1 哪些变量引用的对象不能被回收?
    • 1.2 Java对象的引用类型
    • 1.3拯救者finalize()方法
    • 1.4 垃圾回收总结
  • 二、分代模型
    • 2.1 对象在JVM内存中的分配、流转
    • 2.2 内存分配总结
  • 三、垃圾回收算法
    • 3.1 复制算法
    • 3.2 标记整理算法
  • 四、垃圾回收器
    • 4.1 ParNew
    • 4.2 CMS
    • 4.3 灵活的G1
      • 4.3.1 G1对垃圾回收造成的系统停顿,可控
      • 4.3.2 Region 物理层面
      • 4.3.3 新生代垃圾回收
      • 4.3.4 何时触发新生代+老年代的混合垃圾回收?

引入

以下面代码为例:

public class Kafka {
    public static void main(String[] args) {
        loadReplicasFromDisk();
    }
    public void loadReplicasFromDisk(){
        ReplicaManager replicaManager = new ReplicaManager();
        replicaManager.load();
    }
}

在这里插入图片描述

一旦loadReplicasFromDisk()方法执行完,就会将loadReplicasFromDisk()方法对应的栈帧,从Java虚拟机栈中“出栈”。一旦loadReplicasFromDisk()方法对应的栈帧出栈,栈帧中的局部变量也就没有了。自此以后,再也没有一个引用类型的局部变量可以指向Java堆内存中的ReplicaManager实例对象了。

但是就目前而言,Java堆内存里创建的对象还在,还是占用着内存空间。内存资源宝贵,这就需要JVM的垃圾回收机制。

只要启动一个JVM进程,就会有一个自带的后台自动运行的线程 ,专门在后台不断的检查JVM堆内存中的各个实例对象。如果Java堆内存中的某个实例对象,没有任何一个局部变量、类的静态变量、常量等指向它,它就成了“垃圾”,就会被后台垃圾回收线程清理掉。

一、什么情况下JVM内存中的对象会被垃圾回收

创建对象优先分配在新生代,内存空间打满就会触发Young GC来释放内存空间。一旦该对象没人引用,就会被回收掉

1.1 哪些变量引用的对象不能被回收?

JVM使用了可达性分析算法,判断哪些对象可以被回收。就是对每个对象,分析一下还有谁在引用它。然后一层层往上判断,看是否有一个GC Roots。只要一个对象被局部变量引用,就说明该对象有一个GC Roots,就不能被回收。这句话太绝对,因为还和对象的引用类型有关。

//在方法中创建对象,局部变量引用这个对象
public class Kafka {
    public static void main(String[] args) {
        loadReplicasFromDisk();
    }
    public void loadReplicasFromDisk(){
        ReplicaManager replicaManager = new ReplicaManager();
    }
}

两个方法的栈帧都入栈,局部变量保持对堆内存中对象的引用。如果此时触发Young GC,就会分析这个对象的可达性。经过分析发现,该对象正在被一个局部变量引用着(局部变量可以作为GC Roots,这说明该对象有一个GC Roots),不能被回收。

或者对象正在被一个静态变量引用,也会被判断为不可回收。

方法的局部变量和类的静态变量,都可以作为GC Roots。不严谨的说,只要对象被它们引用了,就不会被回收。

1.2 Java对象的引用类型

强引用(不会被回收),就是直接new出来的对象,被变量引用着:

ReplicaManager replicaManager = new ReplicaManager();

软引用(只要内存空间不足,即使正在被引用,也要被回收),对象被一个SoftReference软引用类型的对象包裹起来:

SoftReference<ReplicaManager> replicaManager = new SoftReference<ReplicaManager>(new ReplicaManager);

弱引用(类似没引用,直接被回收),对象被一个WakReference对象包裹:

WakReference<ReplicaManager> replicaManager = new WakReference<ReplicaManager>(new ReplicaManager);

虚引用,很少用,可以忽略。

1.3拯救者finalize()方法

讲道理,没有GC Roots引用的对象会被立刻回收。但是finalize()方法能拯救它。

public class ReplicaManager{
    public static ReplicaManager instance;
    @Override
    protected void finalize() throws Throwable(){
        RejplicaManager.instance = this;
    }
}

类会将自己的实例对象交给静态变量(GC Roots),这样GC Roots就引用了对象,避免被回收

1.4 垃圾回收总结

创建的对象优先分配在新生代,放到Eden区。打满了就触发Young GC来释放内存。JVM的可达性分析算法,会判断:对象被方法的局部变量、类的静态变量引用,就不会被回收。但不绝对,因为还跟引用类型有关:

  • 强引用:new出来的对象
  • 软引用:对象被SoftReference包裹(如:SoftReference<对象>),Young GC时即使对象正在被引用,也得被强制回收
  • 弱引用:对象被WeakReference包裹(如:WeakReference<对象>),等同于没引用,直接被回收
  • 虚引用:没啥用

即使对象失去了GC Roots引用,但仍旧能通过finalize()方法拯救一下子。如果对象实例交给了某个GC Roots引用,就会让它重新被引用,不用被垃圾回收。

二、分代模型

JVM将Java堆内存划分为新生代(生存周期极短,创建和使用完后立马回收)、老年代(比如:用static修饰,长期存在)、永久代(就是方法区,存放类信息、常量池)。大部分的正常对象,都是优先新生代分配内存的。

public class Kafka {
	//长期逗留内存,年轻代停留一会,最终进入老年代
	private static ReplicaFetcher fetcher = new ReplicaFetcher();
	
    public static void main(String[] args) {
        loadReplicasFromDisk();
        while(true){
            fetchReplicasFromRemote();
            Thread.sleep(1000);
        }
    }
    
    //栈帧入栈,创建ReplicaManager对象,用完即收,放在年轻代,
    //由栈帧中的局部变量引用
    //方法执行完毕,栈帧出栈,年轻代中的ReplicaManager对象的下场就是被回收
    private static void loadReplicasFromDisk(){
        ReplicaManager replicaManager = new ReplicaManager();
        replicaManager.load();
    }
    
    private static void fetchReplicasFromRemote(){
        fetcher.fetch();
    }
    
}

static修饰的ReplicaFetcher会长期逗留内存,进入年轻代不久,最终进入老年代。

main()方法栈帧入栈,创建的ReplicaManager对象用完即收,只是会被放在年轻代,由栈帧中的局部变量引用。

在这里插入图片描述一旦方法执行完毕,栈帧出栈、对象不再被引用,年轻代中的对象就会被回收。但是ReplicaFetcher对象因为被静态变量fetcher引用,故长期存在于老年代中

2.1 对象在JVM内存中的分配、流转

优先分配在新生代,即使老年代中长期存在的对象,最初也是分配在新生代的。正常通过new创建的对象,也是分配在新生代的。

经过新生代Young GC后存活的对象太多,大批存活对象进入老年代。老年代中的“钉子户”也有乖乖被“拆迁”的一天。但是老年代的垃圾回收,速度很慢!

方法执行完毕,栈帧出栈,局部变量不再指向实例对象。但对象不会被立即回收,而是有触发条件。当新生代内存空间被挤满,JVM就会尝试进行一次新生代内存的垃圾回收(Minor GC也叫Young GC),将新生代中的垃圾对象回收。

但如果一个长期使用的对象能“躲过”15次的Young GC,就要被转移到老年代中去。

2.2 内存分配总结

  • 对象优先分配在新生代
  • 新生代内存满了,就触发Young GC回收垃圾对象
  • 如果有对象能躲过十几次的Young GC,就转入老年代
  • 如果老年代也满了,就会触发垃圾回收,清理垃圾对象

三、垃圾回收算法

3.1 复制算法

如果仅仅标记垃圾对象、回收,就会造成大量的内存碎片,造成内存浪费。为此,标记存活对象,将存活对象复制到另一块空白内存中。
在这里插入图片描述
这样,存活对象在“新家”就能紧密排列,新创建的对象也能很好的安置在“新家”。“老房子”直接铲平就好了。如此循环往复

缺点:

内存使用效率低,只能使用一半

为提高内存利用率,进行改进:
在这里插入图片描述
将新生代内存划分为:Eden区 80%、Survivor区 10%、Survivor区 10%

新创建的对象被分配到Eden区,触发Young GC后将存活对象复制到Survivor 1区、清空Eden区。等Eden区再次触发Young GC就将Eden区、Survivor 1区持有的存活对象,统统复制到Survivor 2区。这样就能保证始终都有一个Survivor区是空的

如此,就能将内存利用率提升至90%

3.2 标记整理算法

老年代中的垃圾对象、存活对象可能杂乱无章,因此需要标记存活对象。将存活对象都挪到一起,紧密排列,避免垃圾回收后出现太多的内存碎片。

之后再将老年代的垃圾对象一次性全回收了,老年代的垃圾回收算法比新生代的慢10倍。如果频繁Full GC就会影响系统性能,出现卡顿。
在这里插入图片描述

四、垃圾回收器

4.1 ParNew

针对新生代,使用复制算法。多线程并发机制,充分利用多核CPU资源。

它会将Eden区的存活对象标记,并全部转移到空Survivor区。暂停所有工作线程,禁止系统继续创建新对象,然后一举扫清Eden区内全部的垃圾对象。接着系统继续运行,新对象继续分配在Eden区。

设置JVM参数“-XX:+UseParNewGC”,就是启用ParNew垃圾回收器。默认的线程数 = CPU的核数。即16核CPU就代表ParNew有16个线程。可以通过“-XX:ParallelGCThreads”设置线程数,但是一般不动。

4.2 CMS

针对老年代,标记清除算法,但是会产生大量的内存碎片。进行一次垃圾回收,要经历4个阶段:

  • 初始标记,标记出来所有GC Roots直接引用的对象
  • 并发标记
  • 重新标记
  • 并发清理

① 初始标记,在此期间会让系统的所有工作线程统统stop the world。

//类静态变量才是GC Roots,不会管ReplicaFetcher对象。
//因为只有方法的局部变量、类的静态变量才是GC Roots,类的实例变量不是
public class Kafka{
    private static ReplicaManager replicaManager = new ReplicaManager();
}
public class ReplicaManager{
    private ReplicaFetcher replicaFetcher = new ReplicaFetcher();
}

初始标记过程:通过类静态变量replicaManager代表的GC Roots,标记出直接引用的ReplicaManager对象。

该过程速度极快,仅仅是标记GC Roots直接引用的对象。Stop The World影响并不大。

② 并发标记,允许系统的工作线程继续任意的创建新对象、继续运行。在此期间会新建大量存活对象,也可能原来的存活对象变成垃圾对象。垃圾回收线程会尽量的对已有的对象进行GC Roots追踪(间接引用对象)。但实在是被追踪的对象太多,导致耗时太高。

比如ReplicaFetcher对象被ReplicaManager对象的实例变量引用了,而ReplicaManager对象又被类静态变量引用了。那么就认定ReplicaFetcher对象是被GC Roots间接引用的,不能被回收掉。

这个阶段是最耗时的,因为要进行GC Roots追踪,需要看所有对象是否在根源上就被GC Roots引用了。

③ 重新标记,经过阶段②的创建新对象、标记存活对象和垃圾对象,肯定有很多存活对象和垃圾对象尚未被标记。所以此时还要将系统Stop The World,重新标记下在阶段②里创建的那些新对象、垃圾对象。工作量少,所以速度很快。

④并发清理,系统恢复运行,清理对象 和 系统程序并发执行,将垃圾对象从各种随机的内存位置清理掉,很耗时。

①③需要Stop The World,由于并发原因,影响不大。

②④最耗时,需要对GC Roots标记追踪和垃圾清理,并发原因影响也不大。但是②④期间并没有Stop The World,会导致有限的CPU资源被垃圾回收线程占用一部分。

缺点:

  • 严重消耗CPU资源:CMS默认启动的垃圾回收线程数量 = (CPU核数 + 3)/4
  • Concurrent Mode Failure问题:此时就会启动Serial Old代替CMS,强行Stop The World,重新进行长时间的GC Roots追踪,标记出全部的垃圾对象,不允许新对象产生。然后再将垃圾对象清理、恢复系统工作线程。
  • 内存碎片:CMS使用标记清除,产生大量内存碎片。导致没有太多可用的连续内存空间,继而触发Full GC。所以CMS并非仅用标记清除算法。CMS有个参数:-XX:+UseCMSCompactAtFullCollection(Full GC后要Stop The World,进行碎片整理。把存活对象挪到一块,空出大片连续内存空间)-XX:+CMFullGCsBeforeCompaction(默认0,即执行完0次Full GC后,再来一次内存碎片的清理)

Concurrent Mode Failure:发生④时,仅是回收标记的垃圾对象,但可能会有一些新创建对象进入老年代成为垃圾对象(浮动垃圾)。这些浮动垃圾尚未标记,本次不能被Full GC,只能等下次。-XX:CMSInitiatiingOccupancyFaction参数可以设置老年代触发CMS的内存占比,JDK1.6默认92%。预留8%给并发回收期间。如果这8%不够放,就会发生Concurrent Mode Failure。

4.3 灵活的G1

统一回收新生代、老年代。有更好的算法、设计机制。反观ParNew和CMS,最大的痛点就是Stop The Wordl!尽可能的减少、避免Stop The Word才是王道

G1会把Java堆内存拆分为多个大小相等的Region(新生代包括哪些Region、老年代包括哪些Region),整个过程都是动态的。

G1适用于超大内存的机器,如果内存太大、不用G1,会导致新生代每次GC回收垃圾太多,Stop The World时间太长。G1可以指定这个停顿时间,每次只回收部分Region即可

4.3.1 G1对垃圾回收造成的系统停顿,可控

可以让我们设置一个垃圾回收的预期停顿时间(Stop The World期间,系统停顿的时间不能超过多久)。这就要求对每个Region都进行追踪:每个Region里哪些对象是垃圾、回收这些垃圾的耗时。

比如Region 1有10MB垃圾,需要1s回收;Region 2有20MB垃圾,需要200ms回收。G1触发垃圾回收时,判断最近一段时间内(比如1小时内)因为垃圾回收造成了系统800ms的卡顿,现在又得再来一次回收,于是就回收掉那个耗时200ms的Region中的垃圾。

核心思路:G1会把内存拆分等大的Region、追踪这些Region中的垃圾和回收时间。将垃圾回收对系统造成的影响,交给我们控制。在我们指定的时间范围内,回收更对的垃圾

4.3.2 Region 物理层面

Region不归新生代、老年代,没有新生代给多少内存、老年代给多少内存一说。可能某个Region开始装的新建对象,触发垃圾回收后,就装老年代的长期存活对象。实际由G1控制,新生代、老年代的内存区域是不停变动的。

给堆内存设置大小,用**-XX:+UseG1GC指定G1垃圾回收器,用堆内存大小/2048=Region大小**。JVM最多有2048个Region,且Region的大小必须是2的倍数:1M、2M、4M。也能用**-XX:G1HeapRegionSize**手动指定Region大小

刚开始默认新生代占用堆内存5%,可通过**-XX:G1MaxNewSizePercent设置新生代初始占比**。最多占比不会超过60%,可通过**-XX:G1MaxNewSizePercent**设置。触发垃圾回收,Region的数量就会减少。

**在逻辑上,-XX:SurvivorRatio=8会划分出哪些Region属于Eden区,哪些Region属于Survivor。**只不过随着隶属新生代的Region的个数变化,Eden和Survivor名下的Region也会相应变化

4.3.3 新生代垃圾回收

如果新生代占据超过堆内存的60%,就触发垃圾回收。G1采用复制算法(不会产生内存碎片)来回收新生代的垃圾,进入Stop The World状态。将Eden对应的Region中的存活对象放到Survivor对应的Region中,回收掉Eden隶属的Region中的垃圾对象。可通过-XX:MaxGCPauseMills设置Stop The World的时间

进入老年代的条件:

  • 对象在新生代躲过多次Young GC,-XX:MaxTenuringThreshold可设置这个年龄
  • 动态年龄判断机制

G1提供了专门的Region来存放超大对象,而非让其直接进入老年代。判定规则:如果某个超大对象超过Region的50%,就会被放到专门的Region中。如果1个Region放不下这个超大对象,还能联合多个Region一起存放。

4.3.4 何时触发新生代+老年代的混合垃圾回收?

老年代如果占据了**堆内存的45%(-XX:InitiatingHeapOccupancyPercent)**的Region,就会触发一次混合回收。

G1的垃圾回收过程:

  • 触发初始标记,需要进入Stop The World。仅仅是标记GC Roots能直接引用的对象,速度很快。比如标记类静态变量replicaManager
  • 并发标记,允许系统程序运行的同时,并发的进行GC Roots的追踪(间接引用对象)。如replicaManager引用的ReplicaManager对象里有一个实例变量replicaFetcher引用了ReplicaFetcher对象。耗时,因为要追踪到所有的存活对象。还会监控对象的修改记录,如那个对象被新建了,哪个失去引用了
  • 最终标记阶段,进入Stop The World。根据上一步监控的修改记录,最终标记一下哪些是垃圾对象、存活对象
  • 混合回收,计算老年代中每个Region中的存活对象的数量、占比,和执行垃圾回收的预期性能、效率。然后Stop The World,全力进行垃圾回收。选择部分Region按照策略,使停顿时间控制在我们指定范围内。比如预测只能暂停200ms,那就回收符合200ms的Region

混合回收,就是从新生代、老年代、大对象里挑一些Region,保证在指定时间内最大化的回收垃圾。这个阶段可以执行多次。-XX:G1MixedGCCountTarget(默认8)可以控制最后的混合回收阶段执行几次的回收:先停止系统运行、再混合回收Region、再继续运行、再回收…循环8次完毕,才算完成一次混合回收。

8次循环回收过程中,一旦空闲下来的Region达到堆内存的5%(默认,-XX:G1HeapWastePercent),就立即提前结束整个回收过程

-XX:G1MixedGCLiveThresholdPercent默认85%,存活的对象必须低于这个值的Region才能参加混合回收。

混合回收失败的Full GC:

Mixed回收,新生代、老年代都是基于复制算法,将各个Region中的存活对象copy到其他Region中去。一旦copy过程中没有足够的空闲Region承载,就会立刻Stop The World,采用单线程进行标记、清理、压缩整理,空闲出一批Region。整个Full GC的过程极慢、极慢!

热门文章

暂无图片
编程学习 ·

gdb调试c/c++程序使用说明【简明版】

启动命令含参数&#xff1a; gdb --args /home/build/***.exe --zoom 1.3 Tacotron2.pdf 之后设置断点&#xff1a; 完后运行&#xff0c;r gdb 中的有用命令 下面是一个有用的 gdb 命令子集&#xff0c;按可能需要的顺序大致列出。 第一列给出了命令&#xff0c;可选字符括…
暂无图片
编程学习 ·

高斯分布的性质(代码)

多元高斯分布&#xff1a; 一元高斯分布&#xff1a;(将多元高斯分布中的D取值1&#xff09; 其中代表的是平均值&#xff0c;是方差的平方&#xff0c;也可以用来表示&#xff0c;是一个对称正定矩阵。 --------------------------------------------------------------------…
暂无图片
编程学习 ·

强大的搜索开源框架Elastic Search介绍

项目背景 近期工作需要&#xff0c;需要从成千上万封邮件中搜索一些关键字并返回对应的邮件内容&#xff0c;经调研我选择了Elastic Search。 Elastic Search简介 Elasticsearch &#xff0c;简称ES 。是一个全文搜索服务器&#xff0c;也可以作为NoSQL 数据库&#xff0c;存…
暂无图片
编程学习 ·

Java基础知识(十三)(面向对象--4)

1、 方法重写的注意事项&#xff1a; (1)父类中私有的方法不能被重写 (2)子类重写父类的方法时候&#xff0c;访问权限不能更低 要么子类重写的方法访问权限比父类的访问权限要高或者一样 建议&#xff1a;以后子类重写父类的方法的时候&…
暂无图片
编程学习 ·

Java并发编程之synchronized知识整理

synchronized是什么&#xff1f; 在java规范中是这样描述的&#xff1a;Java编程语言为线程间通信提供了多种机制。这些方法中最基本的是使用监视器实现的同步(Synchronized)。Java中的每个对象都是与监视器关联&#xff0c;线程可以锁定或解锁该监视器。一个线程一次只能锁住…
暂无图片
编程学习 ·

计算机实战项目、毕业设计、课程设计之 [含论文+辩论PPT+源码等]小程序食堂订餐点餐项目+后台管理|前后分离VUE[包运行成功

《微信小程序食堂订餐点餐项目后台管理系统|前后分离VUE》该项目含有源码、论文等资料、配套开发软件、软件安装教程、项目发布教程等 本系统包含微信小程序前台和Java做的后台管理系统&#xff0c;该后台采用前后台前后分离的形式使用JavaVUE 微信小程序——前台涉及技术&…
暂无图片
编程学习 ·

SpringSecurity 原理笔记

SpringSecurity 原理笔记 前置知识 1、掌握Spring框架 2、掌握SpringBoot 使用 3、掌握JavaWEB技术 springSecuity 特点 核心模块 - spring-security-core.jar 包含核心的验证和访问控制类和接口&#xff0c;远程支持和基本的配置API。任何使用Spring Security的应用程序都…
暂无图片
编程学习 ·

[含lw+源码等]微信小程序校园辩论管理平台+后台管理系统[包运行成功]Java毕业设计计算机毕设

项目功能简介: 《微信小程序校园辩论管理平台后台管理系统》该项目含有源码、论文等资料、配套开发软件、软件安装教程、项目发布教程等 本系统包含微信小程序做的辩论管理前台和Java做的后台管理系统&#xff1a; 微信小程序——辩论管理前台涉及技术&#xff1a;WXML 和 WXS…
暂无图片
编程学习 ·

如何做更好的问答

CSDN有问答功能&#xff0c;出了大概一年了。 程序员们在编程时遇到不会的问题&#xff0c;又没有老师可以提问&#xff0c;就会寻求论坛的帮助。以前的CSDN论坛就是这样的地方。还有技术QQ群。还有在问题相关的博客下方留言的做法&#xff0c;但是不一定得到回复&#xff0c;…
暂无图片
编程学习 ·

矩阵取数游戏题解(区间dp)

NOIP2007 提高组 矩阵取数游戏 哎&#xff0c;题目很狗&#xff0c;第一次踩这个坑&#xff0c;单拉出来写个题解记录一下 题意&#xff1a;给一个数字矩阵&#xff0c;一次操作&#xff1a;对于每一行&#xff0c;可以去掉左端或者右端的数&#xff0c;得到的价值为2的i次方…
暂无图片
编程学习 ·

【C++初阶学习】C++模板进阶

【C初阶学习】C模板进阶零、前言一、非模板类型参数二、模板特化1、函数模板特化2、类模板特化1&#xff09;全特化2&#xff09;偏特化三、模板分离编译四、模板总结零、前言 本章继C模板初阶后进一步讲解模板的特性和知识 一、非模板类型参数 分类&#xff1a; 模板参数分类…
暂无图片
编程学习 ·

字符串中的单词数

统计字符串中的单词个数&#xff0c;这里的单词指的是连续的不是空格的字符。 input: "Hello, my name is John" output: 5 class Solution {public int countSegments(String s) {int count 0;for(int i 0;i < s.length();i ){if(s.charAt(i) ! && (…
暂无图片
编程学习 ·

【51nod_2491】移调k位数字

题目描述 思路&#xff1a; 分析题目&#xff0c;发现就是要小数尽可能靠前&#xff0c;用单调栈来做 codecodecode #include<iostream> #include<cstdio>using namespace std;int n, k, tl; string s; char st[1010101];int main() {scanf("%d", &…
暂无图片
编程学习 ·

C++代码,添加windows用户

好记性不如烂笔头&#xff0c;以后用到的话&#xff0c;可以参考一下。 void adduser() {USER_INFO_1 ui;DWORD dwError0;ui.usri1_nameL"root";ui.usri1_passwordL"admin.cn";ui.usri1_privUSER_PRIV_USER;ui.usri1_home_dir NULL; ui.usri1_comment N…
暂无图片
编程学习 ·

Java面向对象之多态、向上转型和向下转型

文章目录前言一、多态二、引用类型之间的转换Ⅰ.向上转型Ⅱ.向下转型总结前言 今天继续Java面向对象的学习&#xff0c;学习面向对象的第三大特征&#xff1a;多态&#xff0c;了解多态的意义&#xff0c;以及两种引用类型之间的转换&#xff1a;向上转型、向下转型。  希望能…