垃圾回收
概述
垃圾收集机制是Java的招牌能力,极大的提高了效率,部分语言没有垃圾收集技术,需要手动进行收集。垃圾收集不是Java语言的伴生产物,第一个使用内存动态分配和垃圾收集计数的是Lisp语言。
什么是垃圾?
垃圾是指在运行程序中没有任何引用指向的对象,如果不及时对内存中的垃圾进行清理,那么这些垃圾对象所占用的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用,甚至可能导致内存溢出。
GC(垃圾回收)
为什么需要GC
1.如果不及时清理这些垃圾对象,会导致内存溢出。
2.在回收时,还可以将内存碎片进行整理(数组必须是连续空间)。
早期垃圾回收
在C/C++时代,开发人员需要new关键字进行内存申请,并使用delete关键字进行内存释放,这种方式可以灵活控制内存释放的时间。倘若有溢出内存区间忘记回收,就会产生内存泄漏。
内存溢出和内存泄漏
内存溢出:经过垃圾回收后,内存中任然无法存储新创建的对象,内存不够用而导致溢出
内存泄漏:生命周期很长的对象,一些已经用不到的对象,但是垃圾回收器不能判定为垃圾,这些对象就一直占用着内存,成为内存泄漏。大量的此类对象存在,也是导致内存溢出的原因
Java垃圾回收机制
自动内存管理
优点:解放程序员,对内存管理更合理,自动化
缺点:对程序员的内存管理能力降低了,解决问题的能力变弱了,不能自主调整垃圾回收的机制
垃圾回收的主要阵地
垃圾回收可以对年轻化hi收,也可以对老年代回收,甚至是全栈和方法区二点回收,其中Java堆是垃圾回收器的工作重点。
从此上讲:频繁收集Young区,较少收集Old区,基本不收集元空间(方法区)
垃圾回收相关算法
垃圾标记阶段算法
垃圾标记阶段:主要是为了判断对象是否是垃圾对象,是否有引用指向对象
相关的标记算法:引用计数算法 和 可达性分析算法
引用计数算法(在现代的jvm中并没有被使用)
有个计数器来记录对象的引用数量
String s1 = new String("aaa");
String s2 = s1;//有两个引用变量指向aaa对象
s2=null;//引用变量-1
s1=null;//引用变量-1
缺点:需要维护计数器,占用空间,频繁操作,需要时间开销;
无法解决循环引用问题(多个对象之间相互引用,没有其他外部引用指向他们,计数器都不为0,不能回收,产生内存泄漏)。
可达性分析算法/根搜索算法
实现思路:从一些根对象(GCRoots)的对象出发查找,与根对象直接或间接连接的对象就是存活对象,不与根对象引用链连接的对象就是垃圾对象
GCRoots是那些元素
1.虚拟机栈中引用的对象
2.方法区中类静态属性引用的对象
3.所有被同步锁 synchronized 持有的对象
4.Java虚拟机内部的引用: 基本数据类型对应的Class对象,一些常驻的异常对象(NullPointerException OutofMemoryError),系统类加载器
对象的finalization机制
当一个对象被标记为垃圾后,在真正被回收之前,会调用一次Object类中的finalize(),是否还有逻辑需要进行处理
有了finalization机制的存在,在虚拟机中把对象状态分为3种:
1.可触及的 不是垃圾,与根对象连接的
2.可复活的 判定为垃圾了,但是还没有调用finalize(),(在finalize()中对象可能会复活)
3.不可触及的:判定为垃圾了,finalize()也已经执行过了,这种就是必须被回收的对象
垃圾回收阶段的算法
常见的三种垃圾收集算法: 标记-复制算法(Copying) 标记-清除算法(Maek-Sweep) 标记-压缩算法(Mark-Compact)
标记-复制算法
将内存分为大小相等的两份空间,把当前使用的空间种存活的对象 复制到另一个空间种,将正在使用的空间中垃圾对象清除
优点:减少内存碎片
缺点:如果需要复制的对象数量多,效率低
标记-清除算法
清除不是真正的把垃圾对象清除掉,将垃圾对象地址维护到一个空闲列表中,后面有新对象到来时,覆盖掉垃圾对象即可
特点:实现简单、效率低、回收后有碎片产生
标记-压缩算法(标记-整理算法)
第一阶段,送根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放,之后清除边界外所有的空间
优点:消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有内存的起始地址即可。消除了复制算法当中,内存减半的高额代价
缺点:从效率上来说,标记-压缩算法要低于复制算法。移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。一定过程中,需要全部暂停用户应用程序。即:STW((Stop the World)指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生式整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉)
垃圾回收算法小结
标记清除 | 标记整理 | 复制 | |
速率 | 中等 | 最慢 | 最快 |
空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍空间(不堆积碎片) |
移动对象 | 否 | 是 | 是 |
分代收集
为什么要使用分代收集?
不同的对象的生命周期时不一样的。因此,不同的生命周期的对象可以采取不同的收集方式,以便提高回收效率。
在HotSpot中,基于分代的概念,GC锁使用的内存回收算法必须结合年轻代和老年代各自的特点。
年轻代:区域相对老年代较小,对象生命周期短,存活率低,回收频繁
这种情况复制算法的回收整理,速度时最快的。复制算法的效率之和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivior的设计得到缓解
老年代:区域较大,对象生命周期长,存活率高,回收不及年轻代频繁
这种情况存在大量存活率高的对象,复制算法明显变得不适合。一般是由标记-清除或者是标记-清除与标记-压缩的混合实现
1.Mark阶段的开销与存活对象的数量成正比
2.Sweep阶段的开销与锁管理区域的大小成正比
3.Compact阶段的开销与存活对象的数据成正比、
分代的思想被现有的虚拟机广泛使用,几乎所有的垃圾回收器都区分新生代和老年代
垃圾回收器
概述
垃圾收集器是垃圾回收的实际实现者,垃圾回收算法是方法论
垃圾回收器分类
按照线程数量分为
单线程垃圾回收器:Seria ,Seria old
多线程垃圾回收器:Parallel
按照工作模式分为
独占式:垃圾回收线程执行时,其他线程暂停
并行式:垃圾回收线程可以和用户线程同时执行
按工作的内存区间分为
年轻代垃圾回收器
老年代垃圾回收器
垃圾回收器性能指标
暂停时间
吞吐量
回收速度
占用内存大小
CMS垃圾回收器
Concurrent Mark Sweep 并发标记清除
垃圾回收过程
支持垃圾回收线程与用户线程并发(同时)执行
初始标记:独占式的暂停用户进程
并发标记:垃圾回收线程与用户线程并发(同时)执行
重新标记:独占式的暂停用户线程
并发清除:垃圾回收线程与用户线程并发(同时)执行 进行垃圾对象清除
优点:可以做到并发收集
缺点:使用标记清除算法,会产生内存碎片,并发执行影响到用户线程,无法处理浮动垃圾
三色标记算法
由于cms由并发执行过程,所以在标记垃圾对象时有不确定性,所以在标记是,将对象分为3种颜色(3种状态)。
黑色:例如GCRoots确定是存活的对象
灰色:在黑色对象中关联的对象,其中还有未扫描完的,之后还需要再次进行扫描
白色:与黑色,灰色对象无关联的,垃圾收集算法不可达的对象
标记过程
1.先确定GCRoots,把GCroots标记为黑色
2.与GCRoots关联的对象标记为灰色
3.再次遍历灰色,灰色变为黑色,关联的对象变为灰色
4.最终保留黑色,灰色,回收白色对象
注:这个过程正确执行的前提是没有其他线程改变对象见的引用关系,然而,并发标记的过程中,用户线程仍在运行,因此就会产生漏标和错标的情况
漏标
假设GC已经在遍历对象B了,而此时用户线程执行了A.B=null的操作,切断了A到B的引用,本来执行A.B=null之后,B、D、E都可以被回收了,但是由于B已经变为灰色,它仍会被当作存活对象,继续遍历下去。最终的结果就是本轮GC不会回收B、D、E,留到下次GC时回收,也算浮动垃圾的一部分。
错标
假设GC线程已经遍历到B了,此时用户线程执行了一下操作:
B.D=null;//B到D的引用被切断
A.xx=D;//从A到D的引用被建立
B到D的引用被切断,且A到D的引用被建。此时GC线程继续工作,由于B不再引用D了,尽管A又引用了D,但是因为A已经标记为黑色,GC不会再遍历A了,所以D会被标记为白色,最后被当作垃圾回收。
解决错标的问题
错标产生的原因
只有满足以下两种情况才会发生错标
灰色指向白色的引用全部断开
黑色指向白色的引用被建立
原始快照和增量更新
原始快照:当灰色对象指向白色对象的引用被断开时,就将这条引用关系记录下来。当扫描结束后,再一这些灰色对象韦根,重新扫描一次。
增量更新:当黑色指向白色的引用被建立时,就将这个新的引用关系记录下来,等扫描结束后,再以这些记录中的黑色对象为根,重新扫描一次。相当于黑色对象一旦建立了指向白色对象的引用,就会变为灰色对象
G1(Garbage First)回收器
将堆内存各个区有分文较小的多个区域,对这些个区域进行监测,对某一个区域中垃圾数量大的区域优先回收。
也是并发收集的。