文章目录
前言
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。
垃圾回收的作用:
-
对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。
-
除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象。
垃圾回收主要针对堆区以及方法区线程共享的部分,因为线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁。而方法的栈帧在执行完方法之后就会自动弹出栈并释放掉对应的内存。所以这一部分不需要垃圾回收器负责回收。
一、方法区的回收
方法区中能回收的内容主要就是不再使用的类。
判定一个类可以被卸载。需要同时满足下面三个条件:
- 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
这段代码中就将局部变量对堆上实例对象的引用去除了,所以对象就可以被回收。
- 加载该类的类加载器已经被回收。
这段代码让局部变量对类加载器的引用去除,类加载器就可以回收。
- 该类对应的
java.lang.Class
对象没有在任何地方被引用。
二、垃圾判别阶段算法
在堆里存放着几乎所有的java对象实例,在GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象, GC 才会在执行垃圾回收时,释放掉其所占用的内存空间, 因此这个过程我们可以称为垃圾标记阶段。
简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。(如果要让对象回收,必须将局部变量到堆上的引用去除。)
判断对象是否可以回收,主要有两种方式:引用计数法和可达性分析法。
1、引用计数法
引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1。
优点: 实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。当计数为0的时候就说明不存在引用关系,可以回收。
缺点:
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
- 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。对性能有所影响。
- 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java 的垃圾回收器中没有使用这类算法。
2、可达性分析算法
Java使用的是可达性分析算法来判断对象是否可以被回收。可达性分析将对象分为两类:垃圾回收的根对象(GC Root
)和普通对象,对象与对象之间存在引用关系。相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
基本思路:
- 可达性分析算法是以根对象集合(
GC Roots
)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。 - 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(
Reference Chain
) - 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
GC Roots 对象
在Java语言中, GC Roots
包括以下几类元素:
- 虚拟机栈中引用的对象
- 比如:各个线程被调用的方法中使用到的参数、局部变量等。
- 本地方法栈内
JNI
(通常说的本地方法)引用的对象 - 类静态属性引用的对象
- 比如:Java类的引用类型静态变量
- 方法区中常量引用的对象
- 比如:字符串常量池(
String Table
)里的引用
- 比如:字符串常量池(
- 所有被同步锁
synchronized
持有的对象 - Java虚拟机内部的引用。
- 基本数据类型对应的Class对象,一些常驻的异常对象(如:
NullPointerException
、OutOfMemoryError
),系统类加载器。
- 基本数据类型对应的Class对象,一些常驻的异常对象(如:
- 反映java虚拟机内部情况的
JMXBean
、JVMTI
中注册的回调、本地代码缓存等。
总结:由于Root
采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root
。
三、常见的引用对象
可达性算法中描述的对象引用,一般指的是强引用,即是GC Root
对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收。除了强引用之外,Java中还设计了几种其他引用方式:
- 软引用
- 弱引用
- 虚引用
- 终结器引用
1、强引用
当在Java语言中使用new
操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null
,就是可以当做垃圾被收集了,当然具体回收时机还是要看垃圾收集策略。
相对的, 软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的,在一定条件下,都是可以被回收的。所以,强引用是造成Java内存泄漏的主要原因之一。
强引用具备以下特点:
- 强引用可以直接访问目标对象。
- 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会回收强引用所指向对象。
- 强引用可能导致内存泄漏。
2、软引用
软引用是用来描述一些还有用,但非必需的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
如下图中,对象A被GC Root对象强引用了,同时我们创建了一个软引用SoftReference对象(它本身也是一个对象),软引用对象中引用了对象A。
接下来强引用被去掉之后,对象A暂时还是处于不可回收状态,因为有软引用存在并且内存还够用。
如果内存出现不够用的情况,对象A就处于可回收状态,可以被垃圾回收器回收。
特别注意:
软引用对象本身,也需要被强引用,否则软引用对象也会被回收掉。
软引用的使用方法
- 将对象使用软引用包装起来,
new SoftReference<对象类型>(对象)
。 - 内存不足时,虚拟机尝试进行垃圾回收。
- 如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。
- 如果依然内存不足,抛出
OutOfMemory
异常。
public class SoftReferenceDemo2 {
public static void main(String[] args) throws IOException {
byte[] bytes = new byte[1024 * 1024 * 100];
SoftReference<byte[]> softReference = new SoftReference<byte[]>(bytes);
System.out.println(softReference.get());
}
}
3、弱引用
弱引用也是用来描述那些非必需对象,只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。
但是,由于垃圾回收器的线程通常优先级很低,因此, 并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。
弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。
弱引用非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。
弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。
/**
* 弱引用案例 - 基本使用
*/
public class WeakReferenceDemo2 {
public static void main(String[] args) throws IOException {
byte[] bytes = new byte[1024 * 1024 * 100];
WeakReference<byte[]> weakReference = new WeakReference<byte[]>(bytes);
bytes = null;
System.out.println(weakReference.get());
System.gc();
System.out.println(weakReference.get());
}
}
执行之后发现gc执行之后,对象已经被回收了。
4、虚引用和终结器引用
这两种引用在常规开发中是不会使用的。
- 虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。Java中使用
PhantomReference
实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现。 - 终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在
Finalizer
类中的引用队列中,在稍后由一条由FinalizerThread
线程从队列中获取对象,然后执行对象的finalize
方法,在对象第二次被回收时,该对象才真正的被回收。在这个过程中可以在finalize
方法中再将自身对象使用强引用关联上,但是不建议这样做。
四、垃圾回收算法
Java是如何实现垃圾回收的呢?简单来说,垃圾回收要做的有两件事:
1、找到内存中存活的对象
2、释放不再存活对象的内存,使得程序能再次利用这部分空间
1、垃圾回收算法的评价标准
Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为Stop The World
简称STW
,如果STW
时间过长则会影响用户的使用。
判断GC算法是否优秀,可以从三个方面来考虑:
1. 吞吐量
吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即吞吐量 = 执行用户代码时间 /(执行用户代码时间 + GC时间)。吞吐量数值越高,垃圾回收的效率就越高。
2.最大暂停时间
最大暂停时间指的是所有在垃圾回收过程中的STW时间最大值。比如如下的图中,黄色部分的STW就是最大暂停时间,显而易见上面的图比下面的图拥有更少的最大暂停时间。最大暂停时间越短,用户使用系统时受到的影响就越短。
3.堆使用效率
不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。
上述三种评价标准:堆使用效率、吞吐量,以及最大暂停时间不可兼得。
一般来说,堆内存越大,最大暂停时间就越长。想要减少最大暂停时间,就会降低吞吐量。
没有一个垃圾回收算法能兼顾上述三点评价标准,所以不同的垃圾回收算法它的侧重点是不同的,适用于不同的应用场景。
2、标记清除算法
当堆中的有效内存空间(available memory
)被耗尽的时候,就会停止整个程序(也被称为stop the world
),然后进行两项工作,第一项则是标记,第二项则是清除。
- 标记:
Collector
从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header
中记录为可达对象。 - 清除:
Collector
对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header
中没有标记为可达对象,则将其回收。
缺点:
- 效率比较低:递归与全堆对象遍历两次
- 在进行GC的时候,需要停止整个应用程序,导致用户体验差
- 这种方式清理出来的空闲内存是不连续的,产生内存碎片。
3、复制算法
核心思想:
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点:
- 吞吐量高,复制算法只需要遍历一次存活对象复制到To空间即可,比标记-整理算法少了一次遍历的过程,因而性能较好,但是不如标记-清除算法,因为标记清除算法不需要进行对象的移动
- 不会发生碎片化,复制算法在复制之后就会将对象按顺序放入To空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间。
缺点:
内存使用效率低,每次只能让一半的内存空间来为创建对象使用。
3、标记整理算法
标记整理算法也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案。
核心思想分为两个阶段:
1.标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root
开始通过引用链遍历出所有存活对象。
2.整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间。
优点:(此算法消除了“标记-清除”和“复制”两个算法的弊端。)
- 消除了标记/清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
- 消除了复制算法当中,内存减半的高额代价。
缺点:
- 从效率上来说,标记-压缩算法要低于复制算法。
- 效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。
- 对于老年代每次都有大量对象存活的区域来说,极为负重。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
- 移动过程中,需要全程暂停用户应用程序。即:
STW
4、分代垃圾回收算法
现代优秀的垃圾回收算法,会将上述描述的垃圾回收算法组合进行使用,其中应用最广的就是分代垃圾回收算法(Generational GC
)。
分代垃圾回收将整个内存区域划分为年轻代和老年代:
1、分代回收时,创建出来的对象,首先会被放入Eden
伊甸园区。
2、随着对象在Eden区
越来越多,如果Eden区
满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC
或者Young GC
。
Minor GC
会把需要eden
中和From
需要回收的对象回收,把没有回收的对象放入To
区。
3、接下来,S0会变成To区,S1变成From区。当eden区满时再往里放入对象,依然会发生Minor GC。
此时会回收eden区
和S1(from)
中的对象,并把eden
和from区
中剩余的对象放入S0
。
注意:每次Minor GC
中都会为对象记录他的年龄,初始值为0,每次GC完加1。
4、如果Minor GC
后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代。
当老年代中空间不足,无法放入新的对象时,先尝试Minor GC
如果还是不足,就会触发Full GC
,Full GC
会对整个堆进行垃圾回收。
如果Full GC
依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory
异常。