Code前端首页关于Code前端联系我们

Java虚拟机如何垃圾回收内存?

terry 2年前 (2023-09-25) 阅读数 49 #后端开发

我们来谈谈垃圾收集器,也称为gc。顾名思义,它是一个收集垃圾的容器。那么什么是垃圾呢?我们这里指的是没人想要的那堆东西。

1。垃圾收集器的由来

为什么会有垃圾收集器?不知道你有没有想过这个问题。运行程序需要什么样的垃圾收集器?

看一下下面两行代码:

User user = new User("root","123456")
user = new User("lisi","123123")

画一个简单的内存图。可以看到局部变量user原本指向对象root,现在指向对象lisi。那么这个root对象此时就没有了。使用,如果像root一样对象很多的话,JVM性能会越来越低,最终创建一个对象可能要十几秒,而且有一天堆内存就满了,会报内存溢出异常;

所以我们需要想办法像root一样清理对象,保证jvm的高效运行;

java虚拟机怎么去收集对内存的垃圾?

如果虚拟机不提供gc,你认为会发生什么?其实没问题,但是每次都必须使用代码手动释放不需要的对象。这样做有优点也有缺点。好处是可以帮助我们控制堆内存。缺点是我们处于一些更复杂的程序中。由于手动释放内存必然会导致错误,所以错误不是很明显,可能需要很长时间调试才能看到!

所以Java自己处理这种工作,留下一个GC线程始终在后台运行,准备清理未使用的对象。虽然一定程度上会对jvm性能产生一些影响,但是gc就是这么好用。 ,我们不再需要担心垃圾对象的释放,这就简化了编写程序的难度,所以这种影响是完全可以接受的!

顺便说一下两个基本概念,内存泄漏和内存溢出:

内存溢出很容易理解。说明我们需要存储对象的空间太大,但是请求的内存比较小,所以我们安装的时候会报内存溢出异常。例如,如果您查询一个整数,但将其存储为只能使用 long 存储的数字,则出现内存溢出。更专业的说就是:你请求分配的内存超过了系统的容量。给你,系统无法满足需求,所以发生溢出。

内存泄漏是指我们新的对象存放在堆中却没有被释放,所以堆中的内存会越来越小,从而导致系统变慢,严重时程序会卡住。 ;专业的解释是:你使用了malloc或者ny申请了一块内存,但是没有通过free或者erase释放这块内存,导致这块内存一直被占用。

对于我们的jvm来说,我们通常不用担心内存泄漏,因为我们的程序背后有一个强大的gc在默默的为我们清理,但是也会有特殊的情况,比如分配的对象被available 但不再有用时会导致(零未分配给过时的数据存储单元)。至于这个可用性意味着什么,我们稍后再讲;

相对来说,内存溢出比较常见,而gc只能回收堆内存,所以静态变量不会被回收;

再提一下另外两个小概念,非守护线程(也叫用户线程)和守护线程。下面让我们看看这个讨厌的程序运行时会发生什么。有多少线程?

public class User{
 public static void main(String[] args){
  ("我是java小新人");
 }
}

两个线程,一个是运行main方法的线程,也是一个在后台运行gc的线程。这里,用户线程就是运行main方法的线程,而运行gc的线程就是守护线程,默默守护着jvm,如果jvm是雅典娜,那么守护线程就是黄金圣斗士;

当用户线程停止时,整个程序直接停止,守护线程也会终止;但如果黄金圣斗士死了,雅典娜依然可以好好地生活,继续幸福下去。只是为了好玩;

2。堆内存结构

哎,如果真的要通过源码看内存的结构,那是很沮丧的。除非你是这个领域的专业人士,否则真的很难理解。本来想自己画一张草图,但觉得太丑了,就借了一张图:

java虚拟机怎么去收集对内存的垃圾?

路上可以清晰地看到整个土堆记忆分为年轻人聚集的地方和老人聚集的地方人们聚集。年轻人往往占据1/3的空间(新生代),而老年人往往占据2/3的空间(老一代)。但是,年轻人必须分门别类。 Eden区占新生代的8/3。 10、From Survivor区占新生代的1/10,To Survivor区占新生代的1/10,ummm。 。 。我特地查了百度翻译,伊甸园---->天堂,幸存者----->幸存者;哦~~~我好像明白起名者的用意了!

那么新生代和老一代做什么呢?我们制作的物品位于哪里?

新生代:Java对象适用于内存和存储对象,存储的对象是那种消亡比较快的对象。在许多情况下,它们在创建后不久就会被删除。寿命较长的对象会被移至老年代。

老年代:存储长字符串、数组等大对象需要大量连续内存空间,可以直接进入老年代;长期项目也会进入老一代。具体时间是多长?其实,标准。这些是经过 15 对新生代清理(Minor Gc)后仍然存活的对象。

垃圾收集器对于这两个内存有两种操作模式。一种是清理新生代,称为Minor Gc,一种是清理老年代,称为Major Gc。

顺便说一下:很多博客都将Major GC 和Full GC 描述为同一类型。事实上,是有差异的。由于很多Java虚拟机的实现方式不同,所以有不同的叫法,例如Minor GC也可以称为Young GC,Major GC也可以称为Old GC,但Full GC则略有不同。 Full GC 清理整个堆区域——包括年轻代、老年代和永久代(也称为方法区)。因此,Full GC可以说是Minor GC和Major GC的结合体。当然,在我们的例子中,为了更好地理解,我们只将Full GC视为Major GC。

3。筛选和清理对象

如果GC想要工作,它首先必须知道哪些对象需要清理。想一想,新生代和老年代的对象那么多,如何快速方便的过滤掉呢?有两种方法可用

1。引用计数算法,相当于给你创建的对象偷偷添加了一个计数器。每次引用该对象时,计数器都会加一,引用失败时,计数器会减一。当计数器到0的时候,说明这个对象没有变量引用了,所以我们可以说这个对象可以被清理了

2.根搜索算法(这是jvm使用的),这个怎么理解?你可以想象现在有一场争吵。该数组包含对某些事物的引用。我们称这个矩阵为“GC Root”。然后我们根据这个数组中的引用找到对应的对象,看看这个对象中是否有引用。哪些物体,继续往下看,就形成了这么多的线条。这条线上的对象称为“可达对象”。不在这一行的对象就是不可达对象,不可达对象就是我们要清理的对象;

其中,可以作为GC Root的对象:

(1)。类中的静态变量,当它有一个指向对象的引用时,它的作用就像root

(2)。当前线程可以作为root

(3)的参数。 Java 方法或方法中的局部变量。这两个对象可以用作root

(4).JNI方法中的局部变量或参数。这两个物体可以作为root

(5)。其他的。

这种根搜索算法更专业的解释是:以一系列称为“GC Root”的对象为起点,从这些节点开始向下搜索,搜索到的所有走过的路径称为参考链(reference chain) ))Chain),当一个对象没有连接到GC root的引用链时(用图论的术语来说,就是当GC root无法被该对象访问时),就证明该对象是可以被回收的。

4。进行垃圾收集

我们之前已经筛选出了要清理的物品,但是如何更快地清理它们呢?我们需要慢慢地把对象一一删除吗?就像你想清理手机里的垃圾一样,你想慢慢地一次清理一个应用程序的数据吗?当然不可能,浪费时间!当然,我们是用移动管家或者360管家把要清洗的东西收集起来放在一起,然后一起洗。一言以蔽之,太棒了!

好吧,这里也是一样。我们需要找到一种方法,将所有需要清洁的物品一起清理。有什么办法吗?

1。标记-----清除算法:该方法分为两步,先标记,再删除。其实就是对要回收的物品进行标记,然后将标记的物品全部清理掉;这种类型的方法比较适合对象比较少的内存。如果对象太多,标记它们会花费很长时间,更不用说删除它们了。而且,通过这种方法删除的内存空间会分散到各处。如果下次创建大对象可能会出现问题。问题1

2。复制算法:根据内存容量将内存分成两个相等的块。一次仅使用一个块。当该内存块满时,将存活的对象复制到另一个块中,并直接删除已使用的内存块。这种方法最大的缺陷是它使用内存。只能使用总内存的一半,如果复制的对象很多,会花费很多时间。

3。标记----组织算法:结合了上述两种方法的优缺点的改进方法。标记方法与第一种方法相同。标记要清理的对象,然后将所有标记的对象移动到内存的某个小角落,最后集中销毁该小角落

4. 生成收集算法:这是结合了以上三种优点的最佳方法方法。这是目前最好的方法。某些 JVM 使用的方法。该算法的核心思想是根据对象生存时间的不同,将内存划分为不同的域。一般来说,GC堆分为新生代和老年代;新生代的特点是每次都有垃圾,回收时有大量的垃圾需要回收,并且存活下来的对象很少,所以可以使用复制算法;老年代的特点是每次垃圾回收时只需要回收少量对象,所以可以选择“标记--清除方法”或者“标记--排序算法”

所以目前大多数JVM都使用GCs Generation收集算法。

5。执行GC的步骤

到目前为止我所说的无非就是介绍堆的内部结构,然后如何找到要清理的对象,然后如何最快的清理以提高效率!

现在简单说一下GC清理步骤(详细版):

1.当我们创建对象时,我们会进行评估。极少数非常大的对象会直接放入老年代。另外,所有新创建的对象都会插入新生代的Eden区;

2。此时,新生代中只有Eden区有对象,两个Survivor区都是空的;当我们创建很多对象时,Eden区快满的时候,就会发生第一次GC(即进行一次Minior GC)。 Eden区和“From”区中幸存的对象(此时“From”区为空)将被移动到Surviver区的“To”区。 ”区域,并为每个对象设置一个计数器,记录其年龄,初始值为1;每次GC时,都会对存活的对象设置年龄+1操作。默认是当年龄达到时15年,下一次GC会把这个“老油条”直接扔到老年代

3。Minior GC之后,会进行更厉害的操作,就是把“To”区和“是的,只要改个名字,然后执行下一次Minior GC即可。

4.由于创建了很多对象,Eden区快满了,所以再次进行Minior GC。Eden中幸存的对象区域会被直接移动到 Surviver 区域中的“To”区域,此时“From”区域(这里指交换名称之前“To”区域中的对象有两个地方可以去,要么去旧的区域) 15岁以上的一代,或者搬到“To”区

5 我们看看这个时候,只有“To”区的物品是活的,伊甸园区全是垃圾物品并且可以直接清理,“From”-该区域为空;无论如何,请确保在下一次 Minior GC 之前名为“Two”的 Survivor 区域是空的。没关系

6。当老年代快满的时候,会进行Major GC。这个清理事件非常慢,比Minior GC至少慢十倍,甚至更多,所以我们尽量少执行Major GC

注意:如果移动过程中“To”区域被填满,剩余的项目将直接移至老年代。另外,每次Mi​​nior GC之前都会对晋级进行判断。只要老年代中的连续空间大于新生代对象的总大小或之前活动的平均大小,就会执行Minor GC。否则,将执行Major GC。

简化版:

(1) 将Eden区的存活对象+From Survivor保存的对象复制到To Survivor;

(2) 将 Eden 和 Survivor 移除;

(3) Invert from Survivors To 幸存者的逻辑关系:From 变为To,To 变为From。

(4)老年代Major GC的执行时间很长,所以尽量少执行

Minor GC只有在Eden空间快满的时候才会触发。 Eden空间占新生代的大部分,因此可以降低Minor GC的频率。当然,我们使用两个survivor也付出了一定的代价,比如10%的空间浪费、复制对象的开销等等

6。补充知识点

通过查看很多大佬的博客看到很多相关的东西还蛮有趣的,所以就简单的留下了一个小备注:

。新创建的对象在堆新生代的Eden区中,由于堆中的内存是所有线程共享的,所以需要加锁来分配堆中的内存。为了提高效率,Sun JDK在Eden上为每个新创建的线程分配独立的空间,供该线程独占使用。这个空间称为TLAB(线程本地分配缓冲区)。在TLAB上分配内存不需要加锁,因此JVM在给线程中的对象分配内存时会尝试在TLAB上分配内存。如果对象太大或者TLAB耗尽,仍然会分配在Eden区或者堆上的老年代。如果Eden区的内存也用完了,就会进行一次Minor GC。

。许多人认为方法区(或者HotSpot虚拟机中的永久代)没有垃圾回收。 Java虚拟机规范规定,虚拟机不需要在方法空间中实现垃圾收集,而是在方法空间中完成。垃圾收集的“成本效率”普遍较低:在堆中,尤其是新生代,典型应用一次垃圾收集一般可以回收70%到95%的空间,而永久代的垃圾收集效率远低于这个。

对象调用.finalize方法后,对象会被回收吗?

经过可访问性分析后,GC Roots无法访问的对象是可以被回收的(但不一定会被回收,必须至少被标记两次)。此时,该对象被第一次标记,并被标记一次。判断,如果对象没有调用或者没有重写finalize()方法,则在第二次标记后可以回收;否则,该对象将进入FQueue并转到JVM稍后创建的Finalizer线程。进行回收利用。此时,如果该对象在finalization中“自保存”,即与引用链中的任意对象建立了引用关系,并且GC Roots可达,那么当它第二次被标记为“时,就会被移除”正在进行回收”集合;如果最终处理没有逸出,则进行回收。因此,调用finalize方法后,对象可能不会被回收。

。如果survivor空间中所有相同年龄的对象大小之和大于survivor空间的一半,则年龄大于或等于该年龄的对象将直接进入老年代。不用等到15。

总结

这篇文章讲的是Java虚拟机垃圾回收内存的方式。第一步是通过可达性分析来确定哪些对象是可达的,哪些是不可达的,哪些是不可达的。这就是我们正在处理的事情!这些遥不可及的东西可以存在于新生代和老年代。在新生代中,使用复制算法来处理垃圾,在老年代中,使用标记和排序算法来处理垃圾。这种处理方法也可以称为生成集合算法!并且还简单讲了Minor GC和Major GC的释放方法!

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门