什么是垃圾?
垃圾是指在程序运行中没有任何指针指向的对象,这些对象就是需要被回收的垃圾,如果不及时对内存中的这些”垃圾”进行清理,那么垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间将无法被其他对象使用,甚至可能导致内存溢出.
为什么要需要GC?
Java 的垃圾回收机制
java的垃圾回收机制:自动内存管理机制
优点:降低内存泄露和内存溢出的风险,让java开发人员更专注于业务的开发
缺点:弱化了java程序员在内存溢出定位和解决的能力
Java堆是垃圾收集器的工作重点:频繁收集年轻代,较少收集老年代,基本不动Perm区/元空间
垃圾回收相关算法
在堆中存放着几乎所有的java对象实例,在GC执行垃圾回收之前,首先需要区分内存中哪些对象是存活对象,哪些对象是已经死亡的对象.只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉所占用的内存空间.
那么在JVM中究竟如何标记一个死亡对象呢,简单来说,当一个对象已经不再被任何存活着的对象继续引用时,我们可以宣判其已经死亡.
判断对象存活一般有两种方式:
1.引用计数算法
2.可达性分析算法
垃圾标记阶段———引用计数算法&可达性分析算法
该算法实现较为简单:对每个对象保存一个整型的引用计数器属性,用来记录对象被引用的情况
对于一个对象A,只要任何对象引用了A,则A的引用计数器就会加1,当引用失效时,引用计数器就会减1.只要对象A的引用计数器的值为0,则表示对象A不可能再被使用,可进行回收.
该算法的优点:实现简单,垃圾对象便于识别;判定效率高,回收没有延迟性
缺点:
1.它需要单独的字段存储计数器,这样的做法会增加存储空间的开销
2.每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销
3.引用计数器有一个严重的问题,即无法处理循环引用的问题.这是一个致命的缺陷,导致Java的垃圾回收器没有使用该算法
总结:
1.引用计数算法,是很多语言的资源回收选择,例如Phython
2.Java并没有选择引用计数算法,是因为其存在一个基本的难题,也就是很难处理循环引用的问题
3.Python是如何解决循环引用问题的?
1.手动解除:很好理解,就是在合适的时机,解除引用关系
2.使用弱引用Weakref,Weakref是Python提供的标准库,旨在解除循环引用
可达性分析算法 (或者称为根搜索算法,追踪性垃圾收集)
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法解决了引用计数算法的循环引用的问题
Java选择了该算法作为垃圾标记的默认算法
首先我们需要引入一个概念———GC Roots(根集合就是一组必须活跃的引用)
基本思路:
1.可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达
2.使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或者间接连接着,搜索所走过的路径称为引用链(Reference Chain)
3.如果目标对象没有任何引用链相连,则认为是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象
4.在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象
在Java语言中,GC Roots包括以下元素:
注意::
1.如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行.
2.上面的要求也是导致GC进行时必须”Stop The World”的一个重要原因
PS:即使是号称几乎不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的
对象的finalization机制
Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
当垃圾收集器发现没有一个引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法;
finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放,通常这个方法中进行一些资源释放和清理的工作,比如关闭文件,套接字和数据库连接等等;
永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用!理由如下所示:
1.在finalize()时可能会导致对象复活
2.finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生,则finalize()方法将没有执行机会;
3.一个糟糕的finalize()会严重影响GC的性能
由于finalize()方法的存在,导致虚拟机的对象一般处于三种可能的状态:
垃圾清除阶段算法———标记-清除算法
目前在JVM中比较常见的三种垃圾算法是标记-清除算法(Mark-Sweep),复制算法(CopIng),标记-压缩算法(Mark-Compact)
标记-清除(Mark-Sweep)算法
执行过程:
当堆中的有效空间(available memory)被耗尽的时候,就会停止整个程序(STW),然后进行两项工作。
第一项是标记,第二项是清除;
标记:Collector从引用根节点开始遍历,标记所有被引用的对象.一般是在对象的Header中记录为可达对象.
清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在Header中没有标记为可达对象,则将其回收.
标记-清除算法的缺点:
1.效率不算高
2.在进行GC的时候,需要停止整个应用程序,导致用户体验差
3.清理出来的空闲内存是不连续的,产生内存碎片.需要维护一个空闲列表
注意:何为清除?
这里所谓的清除并不是真正的置空,而是把需要清除的对象地址保存在空闲的地址列表里.下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放.
垃圾清除算法——-复制算法
为了解决标记-清除算法在垃圾收集效率方面的缺陷,复制算法(Copying)应运而生;
复制算法思想:
将活着的内存空间分为两块,每次只使用其中的一块,在垃圾收集时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾的回收;
复制算法的特点
优点:
1.没有标记和清除过程,实现简单,运行高效
2.复制过去以后保证空间的连续性,不会出现”碎片”问题
缺点 :
1.此算法需要两倍的内存空间
2.对于G1这种拆分成大量region的GC来说,复制而不是移动,意味着GC需要维护region之间对象的引用关系,不管是内存占用还是时间开销也不小
注意:
如果系统的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,或者说非常低才行(例如:新生代的垃圾回收,新生代中的Survivor0和Survivor1区的垃圾回收思想)
垃圾清除算法——-标记-压缩(整理)算法
复制算法虽然高效,但是必须建立在存活对象少,垃圾对象多的前提之下,这种情况在新生代比较适合.但是对于老年代来说,大部分对象都是存活的对象,则不再适用于复制算法;
而标记-清除算法虽然可以应用于老年代的垃圾收集,但是这种算法执行效率过低,并且执行完垃圾收集以后会产生内存碎片,故垃圾-压缩(Mark-Compact)算法由此诞生.
标记-压缩算法执行过程:
第一阶段:和标记-清除算法发一样,Collector从引用根节点开始遍历,标记所有被引用的对象.一般是在对象的Header中记录为可达对象.
第二阶段:将所有存活的对象压缩到内存的一端,按照顺序进行排放
之后,清理边界外所有的空间
标记-压缩算法的最终效果等同于标记-清除算法执行完成以后,再进行一次内存碎片的整理工作,因此,也可以把它称为标记-清除-压缩算法(Mark-Sweep-Compact)
对于标记-清除算法来说:标记清除算法是一种非移动式的回收算法,标记压缩 是移动式的.是否移动回收后的存活对象是一项优缺点并存的风险决策.
可以看到:标记的存活对象将会被整理,按照内存地址一次进行排列,而未被标记的内存会被清理掉,如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然要方便很多.
引入知识点:指针碰撞(Bump the Pointer)
如果内存空间以规整和有序的方式进行分布,即已用和未用的内存都各在一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上,这种分配方式叫做指针碰撞(Bump the Pointer)
标记-压缩算法的特点
优点:
1.消除了标记-清除算法当中,内存区域分撒的缺点,我们需要给新对象分配内存时,只需要持有一个内存的起始地址即可;
2.消除了复制算法当中,内存减半的高额代价
缺点:
1.从效率上来说,标记-整理算法要低于复制算法
2.移动对象时,如果该对象被其他对象引用,则还需要调整引用的地址
3.移动过程中,需要全称暂停用户的应用程序,即STW
分代收集算法
目前几乎所有的垃圾收集器都是采用分代收集(Generational Collecting)算法执行垃圾收集的;
增量收集算法
上述的算法中,在垃圾收集过程中,应用软件将处于一种Stop the World的状态,在Stop the World状态下,应用程序的所有线程都会挂起,暂停一切正常工作,等待垃圾回收的完成.如果垃圾回收的时间过长,应用程序会被挂起很久,将严重影响用户体验.
此时提出一种增量收集算法(Incremental Collecting)
基本思想:
如果一次性将所有的垃圾进行收集,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序交替执行,每一次,垃圾收集值收集一小片区域的内存空间,接着切换到应用程序线程,依次反复,知道垃圾收集完成.
总的来说,增量收集算法的基础仍然是传统的标记-清除和复制算法.增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方法完成标记,清理或者复制工作.
增量收集算法的缺点:
使用这种方式,由于在垃圾收集过程中,间断性还执行了应用程序代码,所以能减少系统的停顿时间,但是,由于线程切换和上下文转换的销毁,会使得垃圾回收的总成本上升,造成系统的吞吐量的下降;
分区算法
一般来说,在相同的条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长,为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿.
分区算法:将按照对象的生命周期长短划分为两个部分,分区算法将真个堆空间分为连续的不同的小区间.每个小区间都独立使用,这种算法的好处是可以控制一次回收多少个小区间:其中,G1 GC就采用这种分区算法思想.
垃圾回收相关概念
System.gc()的理解
内存溢出(OOM)
没有空闲内存指的就是没有空闲的堆内存,而导致Java虚拟机堆内存不足的原因主要有两点:
在抛出OutOfMemoryError之前,通常垃圾收集器会被触发,尽其所能去清理出空间.
例如: 在引用机制分析中,涉及到JVM会去尝试回收软引用指向的对象等
在java.nio.BIts.reserveMemeory()方法中,我们可以清楚的看到System.sc()会被调用,以清理空间.
当然,也不是在任何情况下垃圾收集器都会被触发的
比如:我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutOfMemoryError异常.
内存泄露(Memory Leak)
STW(Stop The World)
垃圾回收的并行和并发
并发(Concurrent):
并行(Parallel):
并发VS并行:
对于垃圾回收来说:
并行(Parallel):指的是多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态.
比如:ParNew,Parallel Scavenge,Parallel Old
串行(Serial):单线程执行:如果内存不够,暂停程序,启动垃圾收集器进行垃圾回收.等垃圾收集完毕,再启动程序的线程.
并发(Concurrent):指的是用户线程与垃圾收集器线程同时执行(但不一定是并行的,可能交替执行)垃圾收集线程在执行时不会停顿用户程序的运行.