JVM中的各种垃圾回收算法

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

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

JVM使用了一种可达性算法来判断哪些对象可以被回收哪些对象不可以被回收。

这个算法的意思,就是说对每个对象,都分析一下有谁在引用他,然后一层一层去判断,看是否有一个GC Roots

在JVM的规范中,静态变量也可以看做一种 GC Roots,此时只需要一个对象被GC Roots引用了,就不会去回收他

总结:只要你的对象被方法的 局部变量 ,类的静态变量给引用了,就不会回收他们。

Java中对象不同的引用类型

Java中不同的引用类型有强引用,弱引用,软引用,虚引用

复制代码
public class Kafka {
    public static ReplicaManager replicaManager = new ReplicaManager();
}

一个变量引用一个对象,只要是强引用的类型,那么垃圾回收的时候绝对不会去回收这个对象的

复制代码
public class Kafka {
    public static SoftReference<ReplicaManager> replicaManager =
            new SoftReference<ReplicaManager>(new ReplicaManager());
}

正常情况下垃圾回收是不会回收软引用对象的,但是如果你进行垃圾回收之后,发现内存空间还是不够存放新的对象,内存快要溢出,此时就会把这些软引用对象给回收掉,哪怕他被变量引用,但是因为他是软引用,所以还是要回收。

复制代码
public class Kafka {
    public static WeakReference<ReplicaManager> replicaManager =
            new WeakReference<ReplicaManager>(new ReplicaManager());
}

这个软引用就和没有引用类似的,如果发生垃圾回收,就会把这个对象回收掉。

虚引用,这个用的很少暂时可以忽略他。

这些比较常用的就是强引用软引用,强引用代表绝对不能回收的对象,软引用就是说对象可有可无,如果内存实在不够了可以回收他。

finalize()方法的作用

有GC Root用的对象不能被回收,没有GC Roots引用的对象可以回收,如果有GC Roots引用,但是如果是软引用或者弱引用,也有可能被回收掉。

到回收环节,假设没有GC Roots引用的对象,是一定立马被回收吗?

其实不是的,这里有一个finalizie()方法可以拯救他自己

复制代码
public class ReplicaManager {
    public static ReplicaManager instance;

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

如果重新让GC Root变量引用了自己,那么就不用被垃圾回收了。

JVM中有哪些垃圾回收算法

复制算法的背景引入

针对新生代的垃圾回收算法,他叫做复制算法

首先把新生代的内存分为两块

复制代码
public class Play{
    public static void main(String[] args) {
        loadFile();
    }
    public static loadFile() {
        Manager manager = Manager();
    }
}

假设一下,代码在不停的运行,大量的对象都分布在了新生代内存中的一块区域,也只会分布在这块区域

而分配过后,很快就失去了局部变量或者静态变量的引用,成为了垃圾对象

这个时候,新生代内存那块被分配对象的内存区域基本上都要满了,要再次分配对象的时候,发现里面内存空间不足 ,那么此时就会触发Minor GC去回收掉新生代那块被使用的内存空间垃圾对象。

一种不好的垃圾回收思路

假设我们现在要进行垃圾回收,就是直接对上图被使用的那块内存区域中的垃圾对象进行标记

也就是标记出来哪些对象是可以被垃圾回收的,然后就直接对那块内存区域中的对象进行垃圾回收,把内存空出来

回收完后的内存区域会如图所示,回收掉了大量的垃圾对象,但是保留了一些被人引用的存活对象

但是存活对象在内存区域里东一个西一个,非常的凌乱,而且造成了大量的内存碎片

那么什么是内存碎片呢?

这些红色区域就是内存碎片,这些内存碎片有的大有的小

内存碎片大多会造成什么问题呢?

内存浪费:比如现在打算分配一个新的对象,尝试在上图那块被使用的内存区域里去分配

可能因为为内存碎片太多的缘故,虽然所有的内存碎片加起来其实有很大的一块内存,但是因为这些内都是碎片式分散的,所有导致没有一块完整的足够的内存空间来分配新的对象

所有这种直接对一块内存空间回收掉垃圾对象,保留存活对象的方法,绝对不可取

因为内存碎片太多,就是他最大的问题,会造成大量内存浪费,很多内存碎片压根是没有办法使用的

一个合理的垃圾回收思路

这种思想不像上面的直接对已经使用的那块区域把垃圾对象全部回收掉,然后保留全部存活对象

而是先对那块在使用的内存空间标记出里面有哪些对象是不能进行垃圾回收的,就是要存活的对象

然后先把哪些存活的对象转移到另外一块空白的内存中

通过把存活对象先转移到另外一块空白内存区域,我们可以把这些对象都比较紧凑的排列在内存里

这样就可以让被转移的那块内存区域几乎没有什么内存碎片,对象都是按顺序排列在这块内存里的

然后那块被转移的内存区域,是不是多出来一大块连续的可用的内存空间

此时就可以将新对象分配在那块连续内存空间里了

这个时候,再一次把原来使用的那块内存区域中的垃圾对象全部一扫而空,全部给回收掉,空出来一块内存区域

所谓的"复制算法 ",把新生代内存划分分为两块内存区域,然后只使用其一块内存,待那块内存快满的时候,就会把里面的存活对象一次性转移到另外一块内存区域,保存没有内存碎片

接着一次性回收原来那块内存区域的垃圾对象,再次空出来一块内存区域,两块内存区域就这么重复循环使用

复制算法有什么缺点?

复制算法的缺点其实非常明显,假设我们给新生代1G内存,那么只要512MB内存是可以用的

另外512MB的内存空间是要一直再那边空着,然后512MB内存满了,就把存活对象转移到另外512MB内存空间去

从始至终,就有一半内存可以用,这样的算法对内存的使用效率太低了

复制算法的优化:Eden区和Survivor区

经过一次垃圾回收后,新生代99%的对象都被回收掉了,就1%的对象存活下来了

所有实际上真正的复制算法会做出一下优化,把新生代内存区域划分为三块:

1个Eden区,2个Survivor区,其中 Eden区占80%内存空间 ,每块Survivor区各占10%内存空间

刚开始的对象都是分配在Eden区,如果满了此时会触发垃圾回收

这时Eden区所有的存活对象都会一次性转移到一块空着的Survivor区,接着Eden区就会被清空,然后再次分配再Eden区,如果下次Eden区再满,再次发生Minor GC,就会把Eden区和上次Minior GC后存活对象的Survivor区内的存活对象,转移到另一块Survivor区去。

年轻代和老年代分别适合什么样的垃圾回收算法

年轻代对象进入老年代

默认情况下,当对象年龄达到15岁的时候,也就是躲过15次GC后,他会转移到老年代,我们可以通过调整JVM参数设置"-XX:MaxTenuringThreshold"来设置,默认是15岁

动态对象年龄判断

这里跟这个对象有另一个规则可以让对象进入老年代,不用等15次GC之后才可以

假如说当前对象的Survivor区域里,一批对象的总大小大于了这块Survivor区域的内存大小50%,那么此时大于等于这批年龄的对象,就可以直接进入老年代了

说白了就是无论是15岁规则,还是动态年龄判断的规则,都是希望那些可能长期存活的对象,尽早进入老年代

大对象进入老年代

我们可以使用这个参数"-XX:PretenureSizeThreshold",可以把他的值设置为字节数。比如"1048576"字节,就是1MB

意思就是,如果你要创建一个大于这个的对象,比如一个超大的数组,此时就直接把这个大对象放到老年代去

这是为了避免新生代出现那种超大对象,然后多次躲过GC,还得把他再Survivor区域里来回复制多次才能进入

这么大的对象再内存中复制来复制去也浪费时间

Minor GC后的对象太多无法进入Survivor区怎么办?

这个时候就必须将这些对象直接转移到老年代去

老年代空间分配担保规则

如果新生代里面存活大量对象,确实Survivor都存放满了,必须转移到老年代去,但是 如果老年代里面的内存空间也不够放这些对象呢?该怎么办?

首先在执行一次Minor GC之前,JVM会检查一下老年代可用的内存空间,是否大于新生代所有对象的总大小

因为最极端的情况下,可能新生代Minior GC过后,所有对象都存活下来,新生代所有 对象全部要进入老年代

如果老年代的内存大小是新生代所有对象的,此时就可以对新生代发生一次Minor GC,即使Minor GC后所有对象都存活,Survivor区放不下,也可以转移到老年代去

那如果此时老年代空间也不够了怎么办?

假如在Minor GC之前,发现老年代的可用内存已经小于新生代的全部对象大学里,就会 看一个"-XX:-HandlePromotionFailure"的参数设置了

如果有这个参数,那么就会进行下一步判断,看老年代的内存大小是否大于之前的Minor GC后进入老年代对象的平均大小。

如果上面的步骤失败了,此时就会发生一次"Full GC"就是对老年代对象进行垃圾回收,尽量腾出空间,再执行Minor GC

老年代垃圾回收算法

在Minor GC之前,检查发现可能Minor GC之后要进入老年代的对象太多,老年代放不下,此时需要提取触发Full GC然后再带着执行Minor GC

对老年代采用的是标记整理算法

首先标记出来老年代当前存活的对象,这些对象一般是东一个西一个的,接着会让这些存活对象在内存里面进行移动,把存活对象尽量都挪到一边去,避免垃圾回收后出现过多的内存碎片,然后再一次性把垃圾对象都回收掉

老年代的垃圾回收算法速度比新生代垃圾回收算法速度慢10倍,如果系统频繁出现老年代Full GC垃圾 回收,会导致系统性能被严重影响,出现频繁卡顿现象。

常见的垃圾回收器有哪些?有什么特点?

背景引入

现在我们团队研发了一个数据计算系统,日处理数据量上亿规模

这个数据计算系统会不停通过SQL语句和其他方式从各种数据存储中提取数据到内存中来计算,大致生产负载时每分钟大概需要执行500次数据提取和计算任务,我们分配了多台机器,所以每台机器负责执行100次数据提取和计算

每次会提取1w左右的数据到内存中,平均每次计算大概耗费10s时间

每台机器是4核8G内存配置,JVM内存给了4G,其中新生代和老年代分别为1.5G的内存空间

每条数据大概有20个字段,平均每条数据在1kb左右,计算1w条就有10MB左右大小

Eden区占80%大概是1.2GB,Survivor为100MB

一分钟大概计算100次任务,一次任务10MB,基本上一分钟后Eden区满了

这时候触发Minor GC,在进行此操作的时候,还要进行以下操作

  • 首先会对Minor GC之前进行检查,先看老年代的可用内存空间是否大于新生代全部对象

这时候再进行一次Minor GC后200MB的对象能放入Survivor区吗?不能因为其内存不够,就会触发空间担保机制,让其200MB对象进入老年代,然后再清空Eden区

这样计算下来每分钟会有200MB数据进入老年代,当第三分钟后再进行Minor GC前会检查新生代对象是否大于老年代对象,这是老年代1.1GB,新生代1.2GB,此时需要查看 "-XX:HandlePromotionFailure"这个参数是否打开,默认打开,此时进入第二步检查,看老年代可用空间是否大于历次Minor GC过后进入老年代的对象平均大小

大概执行7次MinorGC后,老年代只有100MB内存,几乎要满了,这时候就需要发生一次Full GC,回收全部对象

按这样子推算七八分钟就需要进行一次Full GC,会十分严重系统性能

如何进行JVM调优?

这个案例下我们可以增加新生代的内存占比,3GB左右的堆内存,其中2GB分配给新生代,1GB给老年代

每次Minor GC后200MB存活对象可以放到Survivor,下一次Minor GC的时候,这个Survivor计算任务应该已经完成了,可以回收了。这样就不会有多少对象进入老年代了,此时Full GC频率可能从几分钟一次降低到几小时一次。

但是需要注意的是,前面我们也提到了动态年龄判断升入老年代规则,就是S区的同龄对象超过S区的一半,就直接升入老年代,这里的优化方法只是一个示例,意思是要我们增加S区大小

为了避免动态年龄判断规则,我们可以调整 "-XX:SurvivorRation=8",默认Eden区域为80%,可以减低Eden区域,多给S区域更多空间。

"Stop the World"问题分析

如果程序一直运行将Eden区塞满了怎么办,这时候必定发生Minor GC

针对新生代我们会使用ParNew垃圾回收器进行回收,采用的是复制垃圾算法来回收垃圾

此时会把Eden区的存活对象标记出来,然后转移到S1区去,接着一次性清空Eden区垃圾对象

系统继续运行,会将Eden区放满对象,然后发送Minor GC后会将所有存活对象放到S2区中

但是我们在发送GC的时候,难道就不会创建对象吗?

如果这些垃圾对象,有些还是有引用的存活对象怎么弄?现在我们需要考虑如何去让垃圾回收器去持续追踪这些新对象的状态

简单介绍垃圾回收器

Serial和Serial Old:分别用来回收新生代和老年代垃圾对象

工作原理就是单线程运行,垃圾回收的时候会停止我们写的系统的其他工作线程,让我们的系统卡死,然后在进行垃圾回收

ParNew和 CMS 垃圾回收 :ParNew现在一般是新生代垃圾回收器,CMS是用在老年代垃圾回收器,他们都是多线程并发执行,性能更好

G1 垃圾回收 :统一收集新生代和老年代,采用了更优秀的算法和设计机制

Stop the World

在继续垃圾回收的时候,要尽可能让垃圾回收器专心干活,不能随便让Java系统创建对象,就会进入这个状态

如果这个状态持续100ms,可能导致我们的系统100ms不会创建新的对象了,这是十分不好的事情

相关推荐
小王不爱笑1322 小时前
G1 GC 的核心基础:Region 模型的补充细节
java·jvm·算法
wangchunting5 小时前
Jvm-垃圾收集器
java·开发语言·jvm
鹤旗7 小时前
While语句,do-while语句,for语句
java·jvm·算法
wuqingshun3141598 小时前
谈谈你对springAop动态代理的理解?
java·jvm
gelald8 小时前
JVM - 运行时内存模型
java·jvm·后端
2501_924952699 小时前
自动化机器学习(AutoML)库TPOT使用指南
jvm·数据库·python
专注_每天进步一点点13 小时前
方法 A 用线程池 1,方法 A 内部的方法 B 也用同一个线程池 1提交任务。导致的线程死锁
jvm
小王不爱笑13213 小时前
深度剖析:synchronized 底层实现原理(JVM 视角)
jvm
庞轩px13 小时前
线程池核心参数与拒绝策略深度解析
java·jvm·数据库