Java垃圾回收机制

时间:2022-10-08 00:02:09

前言

Java语言比C/C++语言更加用于友好重要的一点就是垃圾回收机制,C/C++语言在堆内存分配的资源必须开发这手动删除,如果开发者忽略了这一点就会导致内存泄漏,程序可用的内存空间越来越少,最后没有内存资源可用导致应用失败。Java的自动垃圾回收功能会始终在后台有一个垃圾回收线程,过一段时间检查内存中不再使用的垃圾内存将它们释放返回到堆内存中,这一特性将程序开发者从内存管理中解脱出来,可以将更多的精力放到业务逻辑中。现在就来探讨一下Java内存垃圾回收实现的方法。

朴素回收算法

垃圾回收算法不是从一开始就非常高效和智能,最开始的算法都是一些很简单的方法,这里介绍常见的简单垃圾回收算法。

标记清除算法

整个回收过程分成两步首先需要标记出所有需要回收的垃圾对象,之后在对所有垃圾对象做释放回收。不过标记和回收效率都不是很高,而且在回收过程中会产生大量的内存碎片,如果需要一块大内存可能需要在触发垃圾回收。

拷贝算法

将内存分为两块,一块专门用来负责申请新内存,另外一块空闲,当需要垃圾回收时将负责内存申请的内存块里所有还活着的对象直接复制到空闲内存里,然后将之前负责分配内存全部释放掉,拷贝后的空闲内存作为新的申请内存块。拷贝算法能够解决之标记清除算法出现大量内存碎片的问题,不过它始终需要一块空闲内存,内存利用率比较低。

标记整理算法

前面的标记清除算法假定在回收过程中大部分的对象都只存活很短的时间,如果有很多对象存活的的话使用标记清理需要移动大量的对象,所有标记整理算法就将所有活动的对象移动到内存的一端,在活动对象边界之外的所有对象全部都释放。

分代算法

对回收的对象分为不同的代,根据不同代的特点对它们分别使用不同的回收策略,现代的很多Java回收算法都采用这种算法。

垃圾判定

要做垃圾回收很重要的一点是判断哪些内存对象是程序运行还需要的,而哪些对象用户不再需要,不再被程序使用的对象就可以被判定为垃圾对象,它们就应该被系统回收。常用的垃圾判定有两种方式引用计数和GCRoot算法。

引用计数

通常一个对象被另外一个对象引用我们就可以认为这个对象是可用的,一个对象被别的对象引用一次就将它的引用计数加1,如果引用他的对象减少一个引用计数就减1,每次做垃圾回收的时候判断它的引用计数是否为零,如果为零代表没有被任何对象引用,就判定为垃圾,如果大于零代表被其他对象引用就需要保存在内存中。

不过如果在内存中两个对象互相引用对方,而他们又都没有被其他对象引用,可见它们是垃圾对象,不过由于它们的引用计数都不是零就导致它们不会被垃圾回收,这就导致了内存泄漏。为此在Java中采用的是GCRoot算法,这个算法就可以避免循环引用的问题。

GCRoot算法

Java中采用GCRoot算法来判定内存对象究竟是否是垃圾,该算法的基本思想是找到GCRoot对象,可以肯定这些对象一定是程序需要保留的对象,通过这些对象的引用找到所有它们可以到达的对象,所有无法到达的对象就都可以认为是垃圾。通常作为GCRoot的对象包括如下的这些对象:

  • Java运行时栈局部变量
  • 静态变量
  • Native方法所引用的对象
  • 活动线程
  • 等待中的monitor

Java垃圾回收实现

Java采用GCRoot算法判定内存中的对象是否是需要回收,为了能够充分利用前面提到的朴素算法的优点,采用了分代回收算法。首先内存被分成了新生代、老年代和持久代。
Java垃圾回收机制
新生代里面生成大部分对象只存在很短的时间,里面的内存分成了三大块Eden、Survivor1和Survivor2,Eden主要负责为新生成的对象提供内存,如果Eden无法再提供申请的内存空间就会触发GC,这时Eden中所有活动的对象都会被拷贝到Survivor1的内存中,Eden空间被释放,之后如果Eden再被申请完毕就会将(Eden + Survivor1)中存活的对象拷贝到Survivor2,再将Eden里的内存释放掉,随着程序的运行Eden空间很快又被申请完毕需要执行GC,会把
(Eden + Survivor2)中存活的对象拷贝到Survivor1中,这样不断的拷贝和释放,如果某个对象在Survivor中存活超过某个阈值就会被放入老年代。

老年代的对象说明它存活的声明周期特别长,不必经常做垃圾回收操作,如果老年代执行GC通常都是用标记清理或者标记整理算法。

最后是持久代,它里面存放的对象很多声明周期都是和JVM相同的对象,主要放置classLoader读进来的Class,放置String.intern字符串常量池。不过PermGen持久代在Java8中称作MetaSpace,默认不设置限制,使用系统内存,而字符串常量池被放入堆。