什么是垃圾?
运行程序中没有任何指针指向的对象。
为什么要gc?
不进行垃圾回收,内存迟早会被消耗殆尽,没有gc不能保证引用程序的正常优化。
内存溢出:需要创建了新的对象,没有空闲空间,并且在垃圾收集后也无法提供更多内存
。意思是在报OOM时之前会执行一次gc。但也不绝对,比如new一个对象的内存超过了堆空间直接报OOM。
内存泄漏:严格来说,只有对象不再被程序使用,但是gc又不能回收他们的情况,叫内存泄漏
。一、io流、jdbc连接、ThreadLocal使用后未关闭释放内存空间,导致可用内存变少。二、单例模式的生命周期和程序是一样长的,如果在此程序中持有对外部对象的引用的话,那么这个外部对象是不能被回收的,导致内存泄漏。
标记阶段:引用计数算法
在GC执行垃圾回收之前需要判定那些是存活对象,那些是死对象(当一个对象已经不再被任何存活的对象继续引用时,宣判死亡)。
- 引用计数器的属性,用于记录对象被引用的情况。
对于一个对象A,只要任何一个对象引用了A, 则A的计数器就加1,当引用失效时计数器就减1.当对象A的计数器为0时即表示对象A不可能被使用,可回收。
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟。
缺点: - 它需要单独的字段存储计数器,增加了储存空间开销。
- 每次赋值都需要重新更新计数器,增加时间开销
- 存在无法处理循环引用的情况,这是致命缺陷,是java垃圾回收器没有采用的原因。
标记阶段:可达性分析算法
又名:根搜索算法,追踪性垃圾收集
相比引用计数算法而言,可达性分析算法解决了循环引用问题,防止内存泄漏。
基本思路:
- 可达性分析算法是以根对象集合为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
- 内存中存活的对象都会被根对象集合直接或间接连接着,搜索路径称为引用链
GC Roots元素:
- 虚拟机栈中引用的对象
- 本地方法栈内JNI引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 所有同步锁synchronized持有的对象
- java虚拟机内部引用
- 反映java虚拟机内部情况JMXBean、
对象的finallization机制
在垃圾回收这个对象之前,先调用这个对象的finalize()方法。
允许子类重写,用于对象被回收时进行资源释放。(比如关闭文件、套接字和数据库连接)
不要主动调用某个对象得finalize()方法,应交给垃圾回收机制调用,原因:
- 在finalize()时可能会导致对象复活
- finalize()方法的执行时间是没有保障的,他完全由GC线程决定,在极端情况下,若不放生gc,则finalize()方法将没有执行机会
- 一个糟糕的finalize()会严重影响GC性能
由于finalize()方法,对象有三种状态:
- 可触及的:从根节点开始,可以达到这个对象
- 可复活的:对象的所有引用都被释放,但是对象又可能在finalize()中复活
- 不可触及的:对象的finalize()被调用,并且没有复活,就会进入不可触及状态。不可触及对象是无法复活的。
因为finalize()只能被调用一次
。
判定对象是否可回收(2次标记)
1、如果objA对象 到GC Roots 没有引用链,则进行第一次标记
2、进行筛选,判断此对象是否有必要执行finalize()方法
- 一、objA没有重写finalize()方法,或则finalize()方法已经被虚拟机调用过,则认为没有必要执行,objA被判定是不可触及的
- 二、如果objA重写了finalize()方法,且未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的finalize线程触发finalize()方法执行。
- 三、
finalize()方法是对象逃脱死亡的最后机会
,如果objA在finalize()方法中和任何对象建立联系,那么在第二次标记时,objA将被“移除即将回收”集合。但是finalize()方法只能执行一次,下次就必定死亡了。
清楚阶段:标记-清除算法(Mark Sweep)
- 标记:collector从根节点开始遍历,标记所有被引用的对象。标记可达对象。
- 清楚:collector对堆内存从头到尾进行线性遍历,发现没有被标记的对象将其回收
缺点:
1、效率一般(原因第二次线性遍历)
2、在进行GC时,需要停止整个应用程序,用户体验差
3、这个清理出来的内存空间是不连续的,产生内存碎片。
清楚阶段:复制算法(Copying)
这里的复制算法在方法区中的年轻代的survivor区中应用,to、from,谁空谁是to。
为了解决标记-清楚算法在垃圾回收效率的问题。
优点:
- 没有标记和清楚过程,实现简单,
运行高效
。 - 复制过去以后保证空间的连续性,不会出现“碎片”问题
缺点:
- 需要两倍的内存空间
- 复制而不是移动,意味GC需要维护region之间对象引用关系。
特别的:
- 如果系统中存活对象很多,那么复制对象数量就会影响性能。
清楚阶段:标记压缩算法(Mark Compact)
- 标记:从根节点开始遍历标记所有被引用的方法
- 压缩:将所有存活的对象压缩到内存的一端
标记-清楚-压缩(碎片整理)
缺点:
- 从效率上来说,标记-整理算法要低于复制算法
- 移动对象的同时,如果对象被其它对象引用,则还需要调整引用的地址
- 移动过程中,需要全程暂停用户应用程序。STW
小结
复制算法效率最高,但消耗量太多的内存
分代收集算法(Generation Collecting)
不同对象的生命周期长短不一,分代回收提高效率。
目前所有的GC都是采用分代收集算法执行垃圾回收。
- 年轻代:区域相对于老年代较小,对象生命周期短、存活率低,回收频繁。(复制算法)
- 老年代:区域较大,对象生命周期长、存活率搞,回收频率低。(标记清楚与标记压缩混合使用)
增量收集算法,分区算法
以上算法在垃圾回收过程中都会停止其他线程运行(stop the world)。
增量收集算法:垃圾收集线程只收集一小片区域的内存空间,接着切换到引用程序线程。依次反复。
增量收集算法的基础还是标记清除、标记压缩算法。着重点在于处理线程间冲突的妥善处理,允许垃圾收集线程以分断的方式完成标记、清理、复制工作。
**缺点:**垃圾收集过程中间断性执行应用程序代码,能减少系统的停顿时间。但是由于线程切换和上下文的消耗使得总体成本上升,造成系统吞吐量下降
相关概念
System.gc()理解
调用System.gc()提醒jvn的垃圾回收器执行full gc
,不确定是否马上执行gc。但是再调用System.runFinalZation()方法会强制执行finalize()方法。
垃圾回收的并行与并发
并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且者几个程序都是在同一个cpu上运行
。并发不是真正意义上的“同时执行”,只是把cpu一个时间段划分成几个片段,然后在这几个片段中来回切换运行。(交替执行)
并行:系统有多个cpu,当一个cpu执行一个进程时,另一个cpu可以执行另外一个进程,两个进程互不抢占资源,同时进行称为并行。
两者对比:
并发:指多个事件,在同一个时间段
同时发生,互相抢占资源,一个cpu
并行:指多个事件,在同一个时间点
同时发生,不互相抢占资源,多个cpu
安全点与安全区域
safe point
程序执行时并非所有地方都能停顿下来开始 gc,只有在特定位置才能停顿下来,这些位置称为安全点。
safe point 的选择非常重要,如果太少可能会导致gc等待时间过长,如果太多可能会导致运行时性能问题。
根据让程序长时间执行的特征
选着执行时间较长的指令作为safe point,如方法的调用、循环跳转、异常跳转。
如何在发生GC时,检查所有线程都跑到最近的安全点停顿下来呢?
- 抢断式中断:(目前虚拟机没有采用了)首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
- 主动式中断:设置一个中断标志,各个线程运行到safe point的时候主动轮询这个标志,如果中断标志为真则将自己进行中断挂起。
safe region
当有线程sleep状态,短时间不能走到safe point, JVM就不能进行gc,安全区域就来解决这类问题。
safe region是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的
安全区域发生gc依然会继续执行,要出安全区域了但gc没有结束才会等待
引用
以下引用都能触及(可达),强度依次减弱。
强引用
不回收
Object 0 = new Object();
强引用可触及(可达的),垃圾收集器永远不会回收的对象。
造成内存泄漏的原因之一。
软引用
在内存不足即回收
,
内存足够时不会回收
弱引用
发现即回收
WeakHashMap
虚引用
用于对象回收跟踪
由于虚引用可以将跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中去执行。