问题引入
什么是GC垃圾回收(Garbage Collection),为什么需要GC回收(Garbage Collection)?
什么是垃圾?
垃圾(Garbage)是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
为什么需要GC?
- 如果不及时的对垃圾进行清理,那么这些垃圾对象所占用的对象,一直会保留到应用程序结束,并且垃圾对象所占用的这部分空间不能被其它对象所使用,这样如果程序中的对象比较多的时候,可能会导致内存溢出,并且这样也是比较浪费空间的。
- Java程序的内存,不需要且不支持开发者手动回收(例如,C++的析构函数等功能),全部交由JVM进行管理,降低程序编程的门槛!
回收内存的类型
手动回收
在早期的C/C++时代,垃圾回收基本上是手工进行的。开发人员可以使用new关键字进行内存申请,并使用delete关键字进行内存释放。
存在的问题
这种方式可以灵活控制内存释放的事件,但是会给开发人员带来频繁申请和释放内存的管理负担。
倘若有一处内存区间由于程序员编码的问题忘记被回收,那么就会产生内存泄漏,垃圾对象永远无法被清除,随着系统运行时间的不断增长,垃圾对象所耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃。
自动回收
自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险。
自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发。
没有垃圾回收器,java也会和cpp一样,各种悬垂指针,野指针,泄漏问题让你头疼不已。
存在的问题
- Java开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于”自动”,那么这将会是一场灾难,最严重的就会弱化Java开发人员在程序出现内存溢出时定位问题和解决问题的能力。
学习的原因
了解JVM的自动内存分配和内存回收原理就显得非常重要,只有在真正了解JVM是如何管理内存后,我们才能够在遇见OutOfMemoryError时,快速地根据错误异常日志定位问题和解决问题。
当需要排查各种内存溢出,内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些”自动化”的技术实施必要的监控和调节。
垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区的回收,其中Java堆是垃圾收集器的工作重点。
从次数上讲:
- 频繁收集Young区
- 较少收集Tenure区
- 基本不动Perm区(或元空间)
现在,除了Java以外,C#,Python,Ruby等语言都使用了自动垃圾回收的思想,也是未来发展趋势。可以说,这种自动化的内存分配和垃圾回收的方式已经成为现代开发语言必备的标准。
如何定位垃圾?
在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有标记那些对象是存活的,GC回收才会回收不会存活的对象(垃圾),释放掉所占用的内存空间。
那么在JVM中究竟是如何标记一个死亡对象呢?当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
如何判断对像是否存活,一般有两种算法,分别是引用计数法和可达性分析算法,而引用计数法是很老的一种算法,先简单介绍这两种算法。
引用计数算法
主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是单纯的引用计数很难解决对象之间相互循环引用的问题;
基本思路
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就 +1;当引用失效时,计数器值就 -1;任何时刻计数器为 0 的对象就是不可能再被使用的。
例如
objA.instance = objB 及objB.instance = objA
,导致objA与objB互相引用着对方,导致被引用计数都不为 0,引用计数算法就无法回收它们。
引用计数算法的优缺点
优点
实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
- 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
- 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷。导致在Java的垃圾回收器中没有使用这类算法。什么叫做无法处理循环引用呢?如下图:
内存中的对象不再使用,但是这段内存的空间又释放不掉,这中情况叫做内存泄漏。
可达性分析算法
当前主流的商用程序语言(Java、C#)的内存管理子系统,都是通过可达性分析算法来判定对象是否存活。
每次都会从根部进行扫描,如果发现一个对象没有直接或间接的和根部相连,那么这个对象就会被标记为死亡。
在GC垃圾回收的时候,就会把这个对象回收掉。只有和根部直接或间接相连的对象才是存活的对象。
可达性分析算法有叫做根搜索算法或追踪性垃圾收集算法
- 相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
基本思路:
可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
使用可达性分析算法后,内存中的存活对象都会被跟对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以属于为垃圾对象
在可达性分析算法中,只有能够被跟对象集合直接或者间接连接的对象才是存活对象。
所谓GC Roots跟集合就是一组必须活跃的引用。
GC Roots
在Java语言中,GC Roots包括以下几类元素:
虚拟机栈中引用对象,比如:各个线程被调用的方法中使用到的参数,局部变量等
本地方法栈内JNI(通常说的本地方法)引用的对象
方法区中类静态属性引用的对象,比如:Java类的引用类型静态变量
方法区中常量引用的对象,比如:字符串常量池(String Table)里的引用
所有被同步锁synchronized持有的对象
Java虚拟机内部的引用,基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException,OutOfMemoryError),系统类加载器。
反映java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等
回收的过程
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。
当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。程序中可以通过覆盖finalize()来一场”惊心动魄”的自我拯救过程,但是,这只有一次机会呦。
即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。
第一次标记:如果对象在进行可达性分析后发现与GC Roots相连接的引用链,那它将会被第一次标记;
第二次标记:第一次标记后会对所有需要回收对象进行一次筛选,筛选的条件是此对象是否有重写finalize()方法,将所有这类待回收且覆盖finalize()方法进行存放到Finalize-Queue中,之后将被进行第二次标记筛选过滤。
- 第二次筛选后仍在GC-Queue队列的对象将真的会被回收,如果对象在finalize()方法中重新与引用链建立了关联关系,那么会从GC队列中移除,将会逃离本次回收,继续存活。
引用介绍
GC垃圾回收器回收对象时,对象的有效性分析不仅仅是需要考虑对象可达性,还需要考虑对象的引用强度,从而使程序可以更加灵活地控制对象的生命周期。
可以用一个公式概括:对象的有效性=可达性+引用类型。
Java的设计人员把对象的引用细分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种级别,细分的准则是体现在被GC回收的优先级上:强引用<软引用<弱引用<虚引用。
强引用(Strong Reference)
强引用表示一个对象处在【有用,必须】的状态,是使用最普遍的引用。如果一个对象具有强引用,那么垃圾回收器绝不会回收它。就算在内存空间不足的情况下,Java虚拟机宁可抛出OutOfMemoryError错误,使程序异常终止,也不会通过回收具有强引用的对象来解决内存不足的问题。
软引用(Soft Reference)
软引用表示一个对象处在【有用,但非必须】的状态。在内存空间足够的情况下,如果一个对象只具有软引用,那么垃圾回收器就不会回收它,但是如果内存空间不足,垃圾回收器就会回收这个对象(回收发生在OutOfMemoryError错误之前)。只要垃圾回收器没有回收它,这个对象就能被程序使用。
- 弱引用(Weak Reference)
弱引用表示一个对象处在【可能有用,但非必须】的状态。类似于软引用,但是强度比软引用更弱一些:只具有弱引用的对象拥有更短暂的生命周期。GC线程在扫描它所管辖的内存区域的过程中,一旦发现只具有弱引用的对象,就会回收掉这些被弱引用关联的对象。也就是说,无论当前内存是否紧缺,GC都会回收被弱引用关联的对象。不过,由于GC是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
- 虚引用(Phantom Reference)
虚引用表示一个对象处在【无用】的状态,这意味着虚引用等同于没有引用,在任何时候都可能被GC回收。设置虚引用的目的是为了被虚引用关联的对象在被垃圾回收器回收的时候,能够收到一个系统通知(用来跟踪对象被GC回收的活动)。虚引用和弱引用的区别在于:虚引用的使用必须和引用队列(Reference Queue)联合使用。