相关概述

垃圾:运行程序中没有任何指针指向的对象。这个对象就是需要被回收的垃圾

GC的作用:释放没用的对象,清楚内存里的记录碎片,以便JVM可以将整理出来的内存分配给新的对象,没有GC就不能保证应用程序的正常进行

内存泄露:对象在程序运行期间无法被回收

java自动内存管理:降低内存泄漏和内存溢出的风险

GC的作用范围:方法区和堆区(重点)

分类:频繁收集Young区,较少收集Old区,基本不动Perm区(元空间)


相关算法

注意:在GC执行垃圾回收之前,需要先区分出内存中哪些是存活的对象,哪些是死亡的对象,只有被标记为已经死亡的对象,GC才会在执行垃圾回收的时候,释放掉它的内存空间。当一个对象已经不再被任何存活的对象继续引用时,就可以宣判为死亡

标记阶段:引用计数算法

对每个对象保留一个整型的引用计数器属性。用来记录对象被引用的情况

对于一个对象A,只要任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收

优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性

缺点:
1、需要单独的字段存储计数器,增加存储空间的开销
2、每次赋值都需要更新计数器,增加时间的开销
3、无法处理循环引用(致命缺陷)


标记阶段:可达性分析算法

以根对象集合(GC Roots:一组必须活跃的引用)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达

使用该算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径(引用链),如果目标对象没有任何引用链相连,则不可达,也就意味着该对象已经死亡。只有能够被根对象集合直接或者间接连接的对象才是存活对象

有效地解决了在引用技术算法中循环引用的问题,防止内存泄露的发生

注意:由于栈方式存放变量和指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那么它就是一个Root

扩展:除了固定的GC Roots集合以外,还可以有其他对象”临时性“地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收

1、如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证

2、这也是GC进行时必须STW的一个重要原因,枚举根节点时也是必须要停顿


对象的finalization机制

1、java提供了对象终止机制来允许开发人员提供对象被销毁之前的自定义处理逻辑

2、垃圾回收该对象之前(垃圾回收器发现没有引用指向一个对象),总会先调用这个对象的finalize()方法

3、finalize()方法允许在子类中被重写,用于在对象被回收时进行资源的释放(通常在这个方法内进行一些资源释放和清理工作)

注意:永远不要主动调用某个对象的 finalize方法,应该交给垃圾回收机制调用


理由:

1、在调用时可能会导致对象复活

2、在方法的执行时间是没有保障,它完全由GC线程决定,极端情况下,如果不发生GC,则finalize方法将没有执行机会


对象的三种状态

由于finalize方法的存在,虚拟机中的对象一般处于三种可能的状态

一个无法触及的对象有可能在某一个条件下“复活”自己

1、可触及的:从根节点开始,可以到达这个对象

2、可复活的:对象的所有引用都被释放,但是对象有可能在调用finalize方法后复活

3、不可触及的:对象的finalize方法被调用,并且没有复活,那么就会进入不可触及的状态。不可触及的对象不可能被复活,因为finalize方法只会被调用一次(对象只有在这个状态才能被回收)

判断对象回收的具体过程

1、判断对象到 GC Roots 是否有引用链,没有则进行第一次标记

2、判断对象是否执行finalize方法

1)如果对象没有重写或者finalize方法已经被虚拟机调用过,则该对象判定为不可触及

2)如果对象重写了finalize方法,但是还没有执行过,那么对象将插入到F-Queue队列中,执行方法

3)GC将会对F-Queue队列中的对象进行第二次标记,如果对象在finalize方法中与引用链中的任何一个对象建立了联系,那么在第二次回收时,将会被移除队列。任何对象会再次出现没有引用的情况,直接变成不可触及的状态


清除阶段:标记-清除算法

基础且常见的垃圾收集算法,当堆中有效内存空间被耗尽的时候,就会进行STW,任何进行标记和清除

标记:Collector从引用根节点进行遍历,标记所有被引用的对象,一般是在对象的Header中记录为可达的对象

清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在Header中没有标记为可达对象,则将其回收

缺点:

1、效率不高

2、在进行GC的时候,需要STW

3、清理出来的空间内存是不连续的,产生内存碎片。需要维护一个空闲列表

注意:此处清除的本质是将需要清除的对象地址保存在空闲的地址列表中,当下次有新对象加载时,判断垃圾的位置空间是否够,如果够,就进行存放


清除阶段:复制算法

将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在.使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

优点:

1、没有标记和清除过程,,实现简单,运行高效

2、不会产生内存碎片,且对象完整不丢
缺点:

1、浪费空间

2、对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,内存占用和时间开销大。

注意:如果垃圾对象很多,复制算法则不太理想。所以在 新生代 中使用复制算法是非常好的


清除阶段:标记-压缩算法

1、从根节点开始标记所有被引用对象.

2、将所有的存活对象压缩到内存的一端,按顺序排放。

3、清理边界外所有的空间。

优点:

消除了标记一清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可

消除了复制算法当中,内存减半的代价

缺点:

从效率上来说,标记一整理算法要低于复制算法。

移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。移动过程中需要进行STW


总结清除阶段

Mark-Sweep Mark-Compact Copying
速度 中等 最慢 最快
空间开销 少(会存在堆积碎片) 少(不堆积碎片) 通常需要或对象的两倍大小(不堆积碎片)
移动对象

分代收集算法

分代算法是针对对象的不同特征,而使用合适的算法,实际上没有新算法产生,而是对前三个算法的实际应用,在新生代使用复制算法,老年代使用标记清除/标记压缩算法清除

老年代中:

Mark阶段的开销与存活对象的数量成正比

Sweep阶段的开销与所管理区域的大小成正相关

Compact阶段的开销与存活对象的数据成正比


增量收集算法

为了避免STW影响用户体验或者系统的稳定性,让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,直到垃圾收集完成

注意:增量收集算法基础还是传统的标记—清除和复制算法,只是通过对线程冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作

缺点:因为线程切换和上下文转换的小号,使得垃圾回收的总体成本上升,造成系统吞吐量下降


分区算法

一般堆空间越大,一次GC的时间就越长,GC产生的停顿就越长,为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据停顿时间去回收小区间,而不是整个堆空间,从而减少一个GC所产生的停顿。


相关概念

System.gc()

通过System.gc()或Runtime.getRuntime().gc()的调用,会显式触发Full GC同时对新生代和老年代进行回收(无法保证马上执行GC)

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
new SystemGCTest();
System.gc();//无法保证马上执行GC
//System.runFinalization();//强制调用使用引用的对象的finalize()方法
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("SystemGCTest 重写了finalize()");
}

内存溢出

1)java虚拟机堆内存设置不够

2)代码中创建了大量对象,并且长时间不能被垃圾收集器收集(存在或被引用)

3)在OOM之前,通常垃圾收集器会被触发,尽可能去清理出空间


内存泄露

严格来说:只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄露

实际上:一些操作会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义的内存泄露

发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现0utOfMemory异常,导致程序崩溃。


比如:

单例模式的生命周期和应用程序是一样的,如果在单例程序中持有堆外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄露的产生


STW

在GC事件发生的过程中会产生应用程序的停顿。停顿产生时整个应用程序都会被暂停,没有任何响应

所有GC都有这个事件,是由JVM在后台自动发起和完成的


程序的并行和并发

并发

一个时间段中有几个程序都是在同一个处理器上运行,并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段,然后在这几个时间区间之间来回切换。


并行

当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行

取决于CPU的核心数量


垃圾回收的并行和并发

并行:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态

串行:单线程执行。如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收。回收完,再启动程序的线程。

并发:用户线程和垃圾收集线程同时执行(不一定是并行的,可能会交替执行)垃圾回收线程在执行时不会停顿用户程序的运行


安全点与安全区域

安全点

程序执行时只有在特定位置才能停顿下来开始GC,这些位置被称为安全点

安全点如果太少可能会导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。

如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来? (*)

  • 抢先式中断(没有虚拟机采用):中断所有线程,如果还有线程不在安全点,就恢复线程,让线程跑到安全点

  • 主动式中断:设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起


安全区域

在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。


引用

强引用

最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值。即类似“0bject obj=new object( )”这种引用关系。只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。(强引用可以直接访问目标对象)

StringBuffer str = new StringBuffer("hello");

局部变量str指向Stringbuffer实例所在堆空间,通过str可操作该实例,那么str就是StringBuffer实例的强引用


软引用

在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常内存不足即回收

内存足够时,不会回收软引用的可达对象;内存不足时,会回收软引用的可达对象

1
2
3
Object obj = new Object();	//声明强引用
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; //销毁强引用

弱引用

只被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。但是由于垃圾回收器的线程通常优先级很低,因此不一定能很快发现持有弱引用的对象,这种情况下,弱引用对象可以存在较长时间。

1
2
3
Object obj = new Object();	//声明强引用
WeakReference<Object> wr = new WeakReference<Object>(obj);
obj = null; //销毁强引用

虚引用

一个对象是否有虛引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虛引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知(跟踪垃圾回收过程)

虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虛引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。

1
2
3
4
object obj = new object();
ReferenceQueuephantomQueue = new ReferenceQueue( ) ;
PhantomReference<object> pf = new PhantomReference<object>(obj, phantomQueue);
obj = null;

终结器引用(*)

用以实现对象的finalize方法,无需手动编码,内部配合引用队列使用。在GC时,终结器引用入队。由Finalize线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象