GC垃圾收集
①垃圾回收概述
-
垃圾:在运行程序中没有任何指针指向的对象(实际上就是无用对象)
-
垃圾回收的必要性:如果不进行垃圾回收,那内存中一直存在这个对象,会可能导致分配给其他进程的内存不够,可能导致内存溢出
-
早期c/c++:垃圾回收手工进行,new申请内存,delete释放内存(比较灵活,但是容易出现内存泄露(可能存在忘记delete的情况))
-
java:自动内存管理,GC主要关注的区域是方法区和堆区(频繁手收集Young区、较少收集Old区、基本不动Perm区)
②垃圾回收相关算法
(1)垃圾标记阶段
这个阶段主要是用于判断对象是否存活(即用于判断对象是否是垃圾,只有标记为死亡的对象(不被任何存活的对象引用)才会被回收)
引用计数算法
原理:为每个对象保存一个整型的引用计数器属性,记录对象被引用的情况(当对象的引用计数器的值为0,就可以进行回收)
优缺点:
-
优点:简单高效,判别效率高,回收没有延迟性
-
缺点:增加了存储开销(每个对象都需要计数器字段);时间开销(计数器的加减)、无法处理循坏引用(这是最致命的缺点,同时也是JVM没有使用该算法的原因)
循环引用:
当p指针断开的时候,后面的实际上就是垃圾可以进行回收,但是因为存在相互引用的情况导致其引用计数器的值不为0不能被回收
可达性分析:
也叫根搜索、追踪性垃圾收集 Tracing Garbage Collection
原理:
存在一个“GC Roots"根集合,从上到下搜索被根集合所连接的目标对象是否可达(可达:存活对象;不可达:死亡对象)
存活对象都会被GC Roots直接或间接相连(搜索走过的路径:引用链)
死亡对象:目标对象没有任何引用链相连
GC Roots:
-
虚拟机栈(栈中的本地变量表)中引用的对象
-
比如:各个线程被调用的方法中使用到的参数、局部变量等。
-
-
本地方法栈内JNI(通常说的本地方法)引用的对象方法区中类静态属性引用的对象
-
比如:Java类的引用类型静态变量
-
-
方法区中常量引用的对象
-
比如:字符串常量池(string Table)里的引用
-
-
所有被同步锁synchronized持有的对象
-
Java虚拟机内部的引用。
-
基本数据类型对应的Class对象,一些常驻的异常对象(如:Nu11PointerException、outofMemoryError),系统类加载器。
-
-
反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。
对象的finalization机制
垃圾回收之前会调用对象的finalize()方法:这个方法是Object类 可以进行重写(进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。)
不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用;原因:
-
在finalize()时可能会导致对象复活。
-
finalize()方法的执行时间是没有保障的,它完全由Gc线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。
-
因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收
-
-
一个糟糕的finalize()会严重影响Gc的性能。
JVM中的对象可能存在的状态:
-
可触及的:被GC Roots直接或间接引用的对象
-
可复活的:对象的所有引用被释放,但是可能在finalize()中复活
-
不可触及的:finalize()被调用且没有复活(不可触及对象不可能被复活,因为finalize()只会被调用一次)
因为finalize()的存在:判断一个对象是否可以进行回收,至少要经历两次判断
可能存在在调用finalize()时与引用链建立了一定的联系
(2)垃圾清除阶段
在成功区分出存活对象和死亡对象后,主要的任务就是执行垃圾回收,释放内存
主要有三种算法:
-
标记-清除算法Mark-Sweep
-
复制算法 Copying
-
标记-压缩算法 Mark-Compact
Mark-Sweep:
-
Mark阶段:标记可达对象被引用的对象,在对象的header中记录
-
Sweep阶段:对堆中的对象进行从到尾的遍历,若header中未被标记,就进行回收处理
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放覆盖原有的地址
优缺点:
-
优点:简单
-
缺点:
-
效率不够高(需要全堆遍历)
-
进行GC时需要STW
-
清理的内存空间不连续,会产生内存碎片
-
Copying:
最好复制较少的存活对象,所以被广泛的运用在新生代中
内存被分为A,B两块,如果A中有存活对象,直接将对象复制到B中,遍历结束后清除A中的对象(就像新生代中的From区和To区)
优缺点:
-
优点:不会产生内存碎片(复制过去的空间连续)、没有标记和清除过程,实现简单,运行高效
-
缺点:需要两倍的内存空间,需要维护对象的引用关系
Mark-Compact:
在Mark-Sweep进行优化形成Mark-Compact
-
Mark阶段:与Mark-Sweep一致
-
Compact阶段:将存活对象压缩到内存的一端,按顺序进行排放,之后清理了边界外所有的空间(主要就是内存空间连续)
优缺点:
-
优点:
-
消除了Mark-Sweep的内存不连续问题
-
消除了Copying内存减半的代价
-
-
缺点:
-
移动对象时需要调整地址
-
效率上看Mark-Compact的效率低于Copy
-
三者对比:
标记清除mark-sweep | 标记整理mark-compact | 复制Copying | |
---|---|---|---|
速率 | 中等 | 最慢 | 最快 |
空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍空间(不堆积碎片) |
移动对象 | 否 | 是 | 是 |
(3)分代收集算法
不同生命周期的对象可以采取不同的收集方式,提高回收效率
-
年轻代:区域小,垃圾较多,对象生命周期短、回收频繁(利用Copying速度最快)
-
老年代:区域大,对象生命周期长、存活率高、回收不频繁(可以利用Mark-Sweep和Mark-Compact混合实现)
开销:
-
Mark阶段的开销与存活对象的数量成正比。
-
Sweep阶段的开销与所管理区域的大小成正相关。
-
compact阶段的开销与存活对象的数据成正比。
(4)增量收集算法
出现算法原因:
上述三种算法处理过程会使应用程序的所有线程挂起(Stop the World),垃圾回收时间过长,会严重影响用户体验,可以利用增量收集算法解决
原理:
让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。
缺点:
线程切换和上下文转换使得GC成本上升,系统吞吐量下降
(5)分区算法
将内存分为一个一个小的region进行GC,每个小region分别进行独立回收
注意分区和分代的区别:
分区是分成一个一个region
分代是分成年轻代和老年代
③垃圾收集相关概念
(1)system.gc()的理解
调用这个方法会显式(程序员自己写的代码)触发FULL GC 同时对新生代、老年代进行回收(但是无法保证对GC的调用 只是提醒需要GC)
(2)内存问题
内存溢出:
没有空闲的内存空间,导致OOM内存溢出(堆内存不够、对象过多而且不能被GC收集)
在OOM之前一般会进行一次GC,有特殊情况当分配较大的对象时直接就空间不足
例子:代码中存在死循环产生过多的对象;集中类中存在对对象的引用使用后没有清零、参数中堆内存设置过小、循环调用方法(StackOverFlow)
内存泄露:
对象不能被程序用到,但没有被GC
长生命周期对象持有短生命周期的引用就很容易发生Memory Leak(长生命周期的对象一直存在,又拥有短生命周期对象导致短生命周期对象不能被回收)
内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现outofMemory异常,导致程序崩溃
例子:
-
静态集合类HashMap、Vector:
static Vector vector=new Vector(10); Object object=new Object; vector.add(object); object=null;// 当object置为空时,object不能被GC
-
单例
单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
-
一些提供close的资源未关闭导致内存泄漏
数据库连接(dataSourse.getConnection() ),网络连接(socket)和io连接必须手动close,否则是不能被回收的。
-
HashMap改变key值:
改变key值后不会被回收(这里还值得仔细看一下)
(3)STW
stop the world 在GC过程中 会停顿整个应用程序,没有任何响应
可达性分析算法中枚举GCRoots会导致Java执行线程停顿
所有的垃圾回收器都有STW
(4)垃圾回收中的并行和并发
并发,指的是多个事情,在同一时间段内同时发生了。
并行,指的是多个事情,在同一时间点上同时发生了。(多个cpu)
并发的多个任务之间是互相抢占资源的。并行的多个任务之间是不互相抢占资源的。
垃圾回收中的:
-
并行(Paralle1):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。如ParNew、Parallel Scavenge、Parallel old;
-
串行(Serial)
-
相较于并行的概念,单线程执行。
-
如果内存不够,则程序暂停,启动JM垃圾回收器进行垃圾回收。回收完,再启动程序的线程。
-
-
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。>用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
(5)安全点和安全区域
安全点:
在特定位置停顿下来开始GC(这个特定位置就是安全点)
安全点的选择:因为安全点选择不当可能会造成程序暂停时间较长,所以以”是否具有让程序长时间执行的特征“来选择安全点,选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转和异常跳转等。
在GC发生时,检查线程在GC处中止:
-
抢先式中断:(目前没有虚拟机采用了)首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
-
主动式中断:设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。(有轮询的机制)
安全区域:
Safepoint 机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。
但是,程序“不执行”的时候呢?例如线程处于sleep-状态或Blocked 状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始Gc都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。
(6)引用
强引用:
StrongReference 无论任何情况下,只要强引用关系还在,GC就不会回收掉被引用的对象(使用new操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。)
造成Java内存泄露的重要原因:即使系统发生OOM,强引用也不会被回收
软引用:
Soft内存不足就回收(不会导致OOM),内存充足时不会被回收
主要是用于描述一些还有用但是非必需的对象,在系统发生内存溢出之前,将对象回收,如果回收后还没有内存就报OOM;
软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
// 声明强引用
Object obj = new Object();
// 创建一个软引用
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; //销毁强引用,这是必须的,不然会存在强引用和软引用
弱引用:
WeakReference:发现就回收
在GC时只要发现弱引用就会被回收(只能生存到下一次垃圾收集发生为止) 可以存放可有可无的缓存数据
WeakHashMap:弱引用的HashMap可以避免OOM(WeakHashMap用来存储图片信息,可以在内存不足的时候,及时回收,避免了OOM)
// 声明强引用
Object obj = new Object();
// 创建一个弱引用
WeakReference<Object> sf = new WeakReference<>(obj);
obj = null; //销毁强引用,这是必须的,不然会存在强引用和弱引用
虚引用:
RhantomReference:完全不会决定对象的生命周期,随时都可能被回收
为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。
虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。
由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。
// 声明强引用
Object obj = new Object();
// 声明引用队列
ReferenceQueue phantomQueue = new ReferenceQueue();
// 声明虚引用(还需要传入引用队列)
PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);
obj = null;
终结器引用
它用于实现对象的finalize() 方法
无需手动编码,其内部配合引用队列使用
在GC时,终结器引用入队。由Finalizer线程通过终结器引用找到被引用对象调用它的finalize()方法,第二次GC时才回收被引用的对象
(7)记忆集和写屏障
记忆集:
从非收集区域指向收集区域的指针集合的抽象数据结构,主要是可以避免全局扫描对象的引用,比如:
G1将堆区划分成多个region,一个region不可能是独立的,它其中存储的对象可能被其他任意region(这些region可能Old区或者Eden区)中的对象所引用。这样一来,在进行YGC的时候,判断Eden区中的一个对象是否存活时,需要去扫描所有的region(包括Old区,Eden区等),导致了在回收年轻代的时候,还需要扫描老年代,同时扫描表示所有Eden区和Old区的region,相当于做了一个全堆扫描,这会大大降低YGC的效率。
写屏障:
一个对象引用进行写操作(即引用赋值)之前或之后附加执行的逻辑。
每次在对一个对象引用进行赋值的时候,会产生一个写屏障中断操作,然后检查将要写入的引用指向的对象是否和该引用当前指向的对象处在不同的region中;如果不同,通过CardTable将相关的引用信息记录到Remembered set中;当进行垃圾收集时,在GC根节点的枚举范围内加入Remembered Set,就可以保证不用进行全局扫描。