系列文章:
JVM一:JVM内存区域划分
JVM二:JVM类加载机制
JVM三:JVM垃圾回收机制(GC)
目录
1.什么是垃圾?
指的是不再使用的内存。
2.垃圾回收
将不用的内存,自动释放,解决内存泄露问题。
3.GC主要针对堆进行释放
GC是以"对象"为基本单位,进行回收,而不是字节。
垃圾回收(GC)主要处理三种情况:
-
完全不使用的对象:当一个对象不再被任何其他对象或程序部分引用时,GC会将其识别为垃圾并回收。这意味着,只有在对象完全没有被引用时,才会成为垃圾回收的目标。
-
部分引用的对象:如果一个对象仅部分属性或方法不再被使用,但整体上仍有引用存在,GC不会回收该对象。只有当对象作为一个整体不再需要时,才会考虑回收。
-
回收策略:在执行垃圾回收时,GC会回收整个对象,而不会只回收对象的某一部分。即,垃圾回收器是基于对象整体来判定是否需要回收,而不是基于对象的部分属性或方法。
这种回收机制确保了内存的有效管理和程序的稳定性,避免因部分回收导致的程序错误或内存泄漏。
4.实际工作过程
1.判定垃圾方法
如何判断哪个对象是垃圾,哪个对象不是垃圾,关键思路:抓住这个对象,查看它到底有没有"引用"指向它?那应该如何具体知道对象是否有引用指向?
1.1死亡对象判定方法
1.1.1 引用计数
引用计数机制是一种垃圾回收策略,它通过给每个对象分配一个引用计数器来跟踪对象的引用情况。具体流程如下:
-
初始化引用计数器:当创建一个新的对象时,为其分配一个初始值为0的引用计数器。
-
增加引用计数:每当有一个引用指向该对象时,将该对象的引用计数器加1。这可以通过赋值操作、参数传递等方式实现。
-
减少引用计数:每当某个引用被销毁或不再指向该对象时,将该对象的引用计数器减1。
-
检查引用计数:当对象的引用计数器变为0时,意味着没有任何引用指向该对象,因此可以认为它是垃圾。此时,垃圾回收器会释放该对象所占用的内存空间。
引用计数机制的局限性可以从以下几个方面进行详细分析:
-
内存空间利用率低
- 每个对象都需要一个额外的引用计数器来记录其被引用的次数。这个计数器会随着对象的创建而分配,无论对象的大小如何。
- 假设每个引用计数器占用4个字节的存储空间,对于一个体积为1KB的对象,附加的空间消耗相当于增加了约4%的额外空间。而对于更小的对象,比如4字节的对象,引用计数器的体积与对象本身的体积相等,这意味着整体空间占用翻了一倍。
- 这种空间的额外消耗在处理大量小型对象时尤为明显,可能导致显著的内存效率降低。
-
循环引用问题
- 当两个或多个对象彼此之间相互引用,但没有其他活动路径可以访问这些对象时,就形成了所谓的循环引用。此时,即使这些对象不再被程序的其他部分使用,它们的引用计数也不会归零。
- 例如,考虑两个对象(1号和2号)互相引用,但没有任何外部引用指向它们。如果销毁它们之间的相互引用,每个对象的引用计数将从1减到0。然而,由于它们彼此之间的存在引用,引用计数器不会归零。
- 这种情况下,即使这两个对象实际上已经无法被程序访问,它们仍然不会被标记为垃圾,从而导致内存泄露。
1.1.2 可达性分析
在Java中,对象之间通过引用相互联系,形成了复杂的链式或树形结构。
以二叉树为例,一个对象的引用可能指向另一个对象,而这个对象的成员又可能指向其他对象,这样的关系构成了一个整体的网络。
为了有效地管理内存,Java使用了可达性分析来标记和清除不再使用的对象。
可达性分析的基本概念:
- 将所有Java对象视为通过引用连接成的树结构。
- 从根节点(称为GC roots)开始遍历,标记所有可达的对象。
- GC roots包括栈上的局部变量、常量池中的对象以及静态成员变量。
- 不可达的对象,即从GC roots无法遍历到的对象,被视为垃圾,将被回收。
可达性分析的缺点:
- 速度较慢:与引用计数相比,可达性分析需要遍历整个对象树,这通常是一个较慢的过程。
- 间歇性执行:尽管速度较慢,但可达性分析不需要持续执行。它通常会在特定的时间间隔内进行,以减少对系统性能的影响。
2.对象释放方法
2.1 标记-清除算法
标记-清除(Mark-and-Sweep)算法分为“标记(Mark)”和“清除(Sweep)”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。
如下图,灰色部分,为标记的对象。
但是这种算法,会产生明显的两个问题:
1.效率问题:标记和清除两个过程效率都不高;
2.内存碎片问题:标记清除后,被释放的空闲空间,是零散的,不是连续的,会有内存碎片问题。
2.2 复制算法
复制(Copying)算法为了解决内存碎片问题而出现的,它将内存分成大小相同的两块,然后将"不是垃圾"的对象复制到另一半,然后再把使用的空间一次性清理掉。
虽然复制算法解决了内存碎片问题,但是它也存在两个明显的问题:
1.空间效率:空间利用率低,可用内存缩小为原来的一半;
2.复制成本问题:如果要清除的垃圾比较少,有效对象比较多,复制成本比较大。
2.3 标记-整理算法
标记-整理算法(Mark-and-Compact)是一种解决复制算法缺点的内存回收方法。它与"标记-清理"算法在标记过程上相同,但不同之处在于处理存活对象的方式。标记-清理算法会直接回收垃圾对象,而标记-整理算法则是将所有存活对象向一端移动,然后直接清理掉垃圾对象。这种方法类似于顺序表中删除中间元素时的元素搬运操作。
虽然标记-整理算法解决了内存碎片问题,同时也保证了空间利用率问题,但是它也存在明显的问题:
效率不高,如果要搬运的空间比较大,此时开销比较大
2.4 分代回收算法
上述算法各有优缺点,我们可以根据不同的场景应用不同的算法,从而得出了一个复合算法——分代回收算法。该算法将垃圾回收分为不同的场景,根据对象的生命周期长短来选择使用哪种算法。
Java的对象要么生命周期特别短,要么生命周期特别长。我们可以引入一个概念,即对象的年龄。年龄是指对象熬过垃圾回收(GC)的轮次。经过一次垃圾回收的扫描,如果发现这个对象还不是垃圾,就为它增加一轮年龄。
对于年轻代的对象,我们可以使用复制算法或标记-整理算法进行垃圾回收;而对于老年代的对象,我们可以使用标记-清理算法或标记-整理算法进行垃圾回收。
如上图,根据对象的年龄,我们将对象分成不同的代。新创建的对象,其年龄为0,被放置在伊甸区(Eden Space)。在年轻代中,我们使用复制算法来进行垃圾回收。当对象在垃圾回收过程中存活下来,即熬过一轮GC后,它会被移动到幸存区(Survivor Space)。
在幸存区,对象会继续经历周期性的垃圾回收扫描。如果对象被识别为垃圾,则会被释放;如果不是垃圾,它会被拷贝到另一个幸存区。注意,两个幸存区不会同时使用,而是交替使用。由于幸存区的空间相对较小,这种来回拷贝导致的空间浪费是可以接受的。
如果一个对象在两个幸存区之间多次来回拷贝,它的年龄会逐渐增加,最终会被移至老年代(Old Generation)。老年代中的对象通常具有较长的生命周期。对于老年代,我们也需要进行周期性的垃圾回收扫描,但相比年轻代,频率更低。在老年代,我们采用标记-整理算法来释放空间。
通过这种分代回收的方式,我们可以更高效地管理内存,对不同生命周期的对象采用适当的垃圾回收策略,从而优化程序的性能和内存使用效率。