一)什么是垃圾?
垃圾指的是在应用程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾,如果不及时的针对内存中的垃圾进行清理,那么这些垃圾对象所占用的内存空间可能一直保留到应用程序结束,被保留的空间无法被其他对象使用,甚至有可能导致内存溢出
JVM规范说了并不需要必须回收方法区,不具有普遍性,元空间使用的是JVM之外的内存
二)如何判断一个对象是否是垃圾:
2.1)引用计数器:
引用计数器优点:实现简单,垃圾对象便于标识,判断效率高,回收没有延迟性
引用计数器缺点:效率要比可达性分析要强,随时发现,随时回收,实现简单,但是可能存在内存泄漏
1)它需要单独的字段存储计数器,这样的做法增加了存储空间的开销
2)每一次赋值都是需要更新计数器,伴随着加法和减法的操作
3)引用计数器又一个严重的问题,就是无法处理循环引用的问题,这是一条致命缺陷
虽然在JAVA中没有使用循环引用但是Python中使用了两种方法解决了这个问题:
1)手动解除,很好理解,就是在合适的时机,接触引用关系
2)使用弱引用weakref,
2.2)可达性分析:
局部变量表,静态引用变量,通过引用链关联的引用链是不会被回收,局部变量表天然作为GCROOTS,就是只是进行新生代回收的时候老年代的引用也可以作为GCROOTS
1)虚拟机栈中引用的对象(栈帧中的本地方法表) 2)方法区中(1.8称为元空间)的类静态属性引用的对象 一般指被static修饰的对象,加载类的时候就加载到内存中 3)方法区中的常量引用的对象。 4)本地方法栈中的JNI(native方法)引用的对象。 注意即使可达性算法中不可达的对象也不是一定要马上被回收还有可能被抢救一下 要真正宣告对象死亡需经过两个过程: 1)可达性分析后没有发现引用链 2)查看对象是否有finalize方法,如果有重写且在方法内完成自救[比如再建立引用],还是可以抢救一下,注意这边一个类的finalize只执行一次,这就会出现一样的代码第一次自救成功第二次失败的情况。[如果类重写finalize且还没调用过,会将这个对象放到一个叫做F-Queue的序列里,这边finalize不承诺一定会执行,这么做是因为如果里面死循环的话可能会时F-Queue队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。
**如果要是使用可达性分析算法来判断内存是否要进行回收,那么分析工作必须要在一个能够保持一个一致性的快照来进行,这一点不满足的话分析结果的准确性就无法保证,这一点也就是GC必须进行STW的一个重要原因,**即使是号称几乎不会发生停顿的CMS垃圾回收器枚举根节点的时候也是必须要停顿的
三)对象的finalize方法详解:
1)JAVA语言提供了对象终止机制来允许开发人员提供对对象销毁之前的自定义处理逻辑,当垃圾回收器发现没有引用指向一个对象的时候,就是垃圾回收器在进行回收此对象之前,总是会调用这个对象的finalize方法,当垃圾回收器发现没有任何一个引用指向该对象的时候,总是会调用这个对象的finalize()方法,finalize()方法允许在子类中被重写,用于在垃圾回收时进行资源释放和垃圾清理的工作,关闭文件,套接字和数据库连接等等
2)永远不要试图调用某一个对象的finalize方法,应该交给垃圾回收器来调用
2.1)finalize方法可能会导致对象复活
2.2)finalize()方法执行时间是没有保障的,他完全由GC线程所决定,极端情况下,如果不发生GC,那么一个糟糕的finalize()方法会影响程序的执行性能
如果说所有的根节点都无法访问到某一个对象,说明该对象已经不再被使用了,一般来说,此对象需要被回收,但事实上,也并非是非死不可的,这个时候他们暂时处于唤醒状态,一个无法触及的对象很有可能在某一个条件下复活自己,如果这样没那么对于他的回收就是极其不合理的,为此,定义虚拟机中的对象可能的三种状态:
1)可触及的:从根节点开始可以到达这个对象
2)可复活的:对象的所有引用都被释放,但是对象很有可能在finalize()中复活
3)不可触及的:对象的finalize()方法被调用并且没有复活,那么就会进入到不可及状态,不可触及的对象不可能复活,因为finalize()方法只会被调用一次;以上三种状态中,是由于finalize()方法的存在进行的区分,只有在对象不可触及的时候才可以被回收
3)所以说判断一个对象是否可以进行回收至少要经历两次标记过程
3.1)如果说对象A到Gcroots不存在引用链,那么就进行第一次标记
3.2)如果筛选,进行判断对象是否执行了finalize()方法
a)如果对象没有重写finalize()方法或者是finalize()方法已经被虚拟机调用过,那么虚拟机不会再重新调用该方法,直接该对象就被标记成不可达的
b)如果对象A重写了finalize()方法,还没有被执行过,那么该对象会被插入到一个队列中,这是由虚拟机自动创建的低优先级的finalizer线程触发其finalizer方法执行
c)finalize()方法是对象进行逃脱死亡的最后机会,稍后GC就会对队列中的对象做第二次标记, 如果该对象和引用链上面的任意一个对象建立了联系,那么在第二次标记的过程中此对象会被移出即将回收的集合,之后,对象会再次出现没有引用存在的情况,在这种情况下fnalize()方法不会被再次调用,对象会直接变成不可触及的状态
代码执行两次,一次分为finalize()方法没有被注释,一种有注释
javapublic class Test { public static Test obj;//这是一个类变量 // @Override // protected void finalize() throws Throwable { // System.out.println("调用当前链上的finalize方法"); // obj=this;//当前带回收的对象在finalize方法上和一个引用链上面的对象建立了联系 // } public static void main(String[] args) throws InterruptedException { obj=new Test(); //对象第一次拯救自己 obj=null; System.gc();//调用垃圾回收器 System.out.println("第一次GC"); //因为finalizer线程优先级很低,主线程暂停2s来等待他 Thread.sleep(3000); if(obj==null){ System.out.println("对象已经死了"); }else{ System.out.println("对象还活着"); } obj=null; System.gc();//调用垃圾回收器 System.out.println("第二次GC"); //因为finalizer线程优先级很低,暂停2s来等待他 Thread.sleep(3000); if(obj==null){ System.out.println("对象已经死了"); }else{ System.out.println("对象还活着"); } } }
四)垃圾回收算法:
垃圾回收任何时候都可能,当系统觉得你内存不足了就会开始回收常见的比如分配对象内存不足时这里的内存不足有可能 不是占用真的很高,可能是内存足够,但是没有连续内存空间去放这个对象,当前堆内存占用超过阈值时,手动 调用 System.gc() 建议开始GC时,系统整体内存不足时等
4.1)标记清除算法:
标记是非垃圾的对象就是可达的对象,然后清除的是垃圾对象,要先递归进行遍历所有可达对象,然后清除的时候需要再开始遍历一遍整个内存空间,还需要进行维护空闲列表
就比如说我们的硬盘,只要你不小心点击了格式化,此时也不是真正的进行格式化,只是标记性删除,但是千万不要再向里面存放数据,因为数据会覆盖,就不好恢复了
当堆中有效的空间被耗尽的时候就会停止整个程序进行STW,首先进行标记,然后进行清除
1)标记:从引用根节点进行标记,标记所有可达的对象,也就不是垃圾的对象,一般是在对象的header头里面标记成可达的对象;
2)清除:对整个堆内存进行从头到尾的进行线性的遍历,如果发现某一个对象在header中没有被标记,那么直接将其回收
缺点:效率不算太高,在进行GC的时候需要终止整个应用程序,用户体验差,还有就是这种方式进行清理出来的空闲内存不是连续的,而是会产生内存碎片,还需要维护一个空闲列表
注意这里面的清空并不是真正的清空,而是需要把消除的对象的地址保存在一个空闲列表里面,下次有新的对象需要加载的时候,要判断垃圾的位置空间是否足够,如果够就进行存放
**4.2)标记整理算法:**内存利用率贼低
首先经过可达性分析在A区找到可达的对象,一旦找到了可达的对象就不需要进行标记,直接将可达的对象进行复制算法放到另一块区域B,另一块空间的所有区域B的对象都是连续的
将活着的内存空间分为两块,每一次只是用其中一块,再进行垃圾回收的时候将正在使用的内存中的存活对象复制到还没有被使用到的内存快里面,然后最后清楚正在使用到的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收
1)因为在新生代,对于常规应用的垃圾回收,一般情况下是可以回收很多的内存空间的,回收性价比就比较高
2)没有标记和清除过程,实现简单,运行高效
3)复制过去以后保证空间的连续性,不会出现内存碎片问题
缺点:维护引用和对象的地址映射
1)需要两倍的内存空间
2)回收的对象比较少,剩余存活的对象比较多,那么移动的对象比较多,但是还要大量维护指针和对象的关系,老年代不适合使用复制算法,因为很多对象都不死,老年代复制对象开销太大
3)**对于G1这种拆分成大量的Regin的GC,复制而不是移动,意味着GC需要维护Regin之间的对象引用关系,不管是内存占用还是空间开销也不少,**如果系统中的垃圾对象很多,复制算法需要复制的存活的数量不算太大,或者说非常低才可以
4.3)标记整理算法:
从根节点标记所有被根节点引用的对象,将所有的存活对象压缩到内存的一端,按照顺序进行存放,最后清除所有边界以外的空间,还要移动位置,还要修改引用对象关系很麻烦,这个算法比标记清除算法效率还低
标记压缩算法的最终效果就是等同于标记清除算法执行完成以后,再来进行一次碎片整理,二者的本质差异就是标记清除算法是一种非移动式的回收算法,标记压缩算法是非移动式的,是否移动回收后的存活对象是一项优缺点并存的风险策略,还可以看到标记的存活对象会被清理,需要按照内存地址进行依次排列,而没有被标记的内存会被回收掉清理掉,如此一来JVM在进行分配内存空间的时候,JVM只是需要维护一个内存的起始地址就可以了,这笔维护一个空闲列表节省了很多开销
优点:消除了标记清楚算法中的的内存区域分散的特点,我们需要给新对象分配内存的时候,JVM只是需要持有一个内存的起始地址即可,消除了复制算法中内存减半的高额代价
缺点:从效率上来说,标记整理算法要低于复制算法,移动对象的时候如果对象被其他引用所指向,还需要调整引用的地址,移动过程中,需要全程暂停用户应用程序;
4.4)分代回收:
1)在前面的这些算法中,没有一个算法可以完全代替其他算法,它们都具有自己独特的优势和特点,分代收集算法应运而生,分代收集算法是基于这样一个事实,不同对象的生命周期是不一样的,因此不同生命周期的对象可以采取不同的收集方式,一边用力啊提升回收效率,一般是吧JAVA堆分成新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,来提升垃圾回收的效率;
2)在JAVA程序运行的过程中会产生大量的对象,其中有一些对象是和业务信息相关,比如说Http请求的session对象,线程,Socket连接,这类对象和业务直接挂钩,因此生命周期比较端,比如说String对象,由于不可变的特性,系统会产生大量的这些对象,甚至有的对象只使用一次就被回收,新生代:老年代=1:2,edin区:幸存者1区:幸存者2区;
3)目前几乎所有的GC都是采用粉黛收集算法来执行垃圾回收的,在HotSpot虚拟机中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点
年轻代:
区域相比于老年代较小,对象的生命周期比较短,存活率低,回收比较频繁,这种情况复制算法的回收整理,速度是最快的,渎职算法的效率值和当前存活对象有关,因此很是适合年轻代的回收,而复制算法解决的是内存利用率不高的问题,通过两个幸存者区得到缓解
老年代:
**区域比较大,对象的生命周期比较长,存活率高,回收不及年轻代频繁,这种情况存在大量存活度高的对象,复制算法明显是非常不合适的,一般是由标记整理或者是标记清楚算法的混合实现,**标记阶段的开销和存活对象的数量成正比,清除阶段的开销和所管理区域的大小成正比,整理阶段的开销和存活对象的数据成正比;
标记的开销和存活的对象成正比,因为标记只能标记存活的对象
清除阶段要进行全堆空间线性的遍历,压缩或者是整理和存活对象的大小成正比
以HotSpot中的CMS垃圾回收器为例,CMS是基于标记压缩清除来实现的,对于对象的回收效率很高,但是对于碎片问题,CMS会使用基于标记压缩算法的Serial Old回收器作为补偿机制,当内存回收不佳的时候,将采用Serial Old执行Full GC来达到对于老年代内存的管理
对于STW的理解:
先确定GCROOTS,枚举根节点,此时要进行Stop The World,确保数据的一致性
stop the world停止的是用户线程,就是为保证一致性
可达性分析算法中枚举根节点会导致所有Java执行线程停顿
衡量一个垃圾回收器的标准就是吞吐量和低延迟
4.5)增量收集算法:
用户暂停时间/垃圾回收时间
吞吐量:工作线程一共的执行业务的时间
比如说我现在有一个房子,我一直不进行清理,一直制造垃圾,直到三个月之后才清理一次,此时清理的时间就比较长,阻隔用户线程的时间就比较长,但是如果说隔一会清理一会效果就会比较好,用户线程和回收线程协调交替执行,看样子就是并发的执行从而到达一种低延迟的行为,就是为了让用户感觉好一点;
被STW中断的应用程序线程会在完成GC之后恢复,频繁的中断会让用户感觉像是网速不快造成的电影卡顿一样,CMS自称低延迟,开发中不要显示的进行GC,导致STW
就是类似于洗衣服,假设现在我只是做两件事情:在宿舍给别人讲题和在卫生间洗衣服
工作线程:在宿舍给别人讲题
GC线程:在卫生间洗衣服
吞吐量:给宿舍讲题时间越长,吞吐量越高
停顿时间:在卫生间洗衣服越长,STW时间就越长
如果想要达到极高的吞吐量,那么就少去卫生间洗衣服,一次洗的多一点,这样吞吐量就特别高
停顿时间:多去洗衣服,每一次一会就回来,这样会使停顿时间最短,不让舍友等待时间过长, 但是存在着从宿舍去卫生间和从卫生间回到宿舍时间的开销,会降低讲题总时间
4.5)分区算法降低停顿时间,主要是保证低延迟而不是吞吐量