关于垃圾收集
1、Java 和 C++语言的区别,就在于垃圾收集技术和内存动态分配上,C语言没有垃圾收集技术,需要我们手动的收集。
2、垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。
3、关于垃圾收集有三个经典问题:
哪些内存需要回收?
什么时候回收?
如何回收?
4、垃圾收集机制是Java的招牌能力,极大地提高了开发效率。如今,垃圾收集几乎成为现代语言的标配
什么是垃圾(Garbage)呢?
1、垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
2、外文:An object is considered garbage when it can no longer be reached from any pointer in the running program.
3、如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。
想要学习GC,首先需要理解为什么需要GC?
1、对于高级语言来说,*一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,*因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。
2、除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象(尤其是一些大的对象)。
3、随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序的正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。
Java 垃圾回收机制
Java自动内存管理的优点
1、自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险
2、没有垃圾回收器,java也会和cpp一样,各种悬垂指针,野指针,泄露问题让你头疼不已。
3、自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发
关于自动内存管理的担忧
1、对于Java开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于“自动”,那么这将会是一场灾难,最严重的就会弱化Java开发人员在程序出现内存溢出时定位问题和解决问题的能力。
2、此时,了解JVM的自动内存分配和内存回收原理就显得非常重要,只有在真正了解JVM是如何管理内存后,我们才能够在遇见OutofMemoryError时,快速地根据错误异常日志定位问题和解决问题。
3、当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。
GC 的作用区域
1、频繁在新生区收集,很少在养老区收集,几乎不在方法区(永久区/元空间)收集,其中,Java堆是垃圾收集器的工作重点
2、从次数上讲:
频繁收集Young区
较少收集Old区
基本不收集方法区
3、GC主要关注于方法区和堆中的垃圾收集
引用类型才需要垃圾回收,基本数据类型不需要回收。
垃圾回收相关算法
进行垃圾回收的时候首先要确定哪些是垃圾(判断对象是否可用)?找到垃圾之后怎么清理掉?
分为标记阶段和清除阶段
标记阶段:引用计数器算法和可达性分析算法
标记阶段的目的:判断对象是否存活
1、在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。
2、只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
3、那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
4、判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
标记阶段:引用计数算法
1、引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
2、对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再 被使用,可进行回收。
3、优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
4、缺点:
1、它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
2、每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
3、引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
循环引用举例:
当p的指针断开的时候,内部的引用形成一个循环,这就是循环引用,从而造成内存泄漏
内存泄露:这个对象不再使用,但是GC没法回收
p指向null,后面三个对象都不再使用了,但是引用计数都不是0,就没法GC回收。
如果让你举内存泄露的例子,最好不要举这个例子,因为Java里面没有使用这个例子,如果举这个要指出是引用计数算法的。
标记阶段:可达性分析算法
可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集
在Java中,是通过可达性分析(Reachability Analysis)来判定对象是否存活的。该算法的基本思路就是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
可达性分析算法基本思路:
所谓"GCRoots”根集合就是一组必须活跃的引用,其基本思路如下:
1、可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
2、使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
3、如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
4、在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
散落的葡萄,没有被杆连接。
GC Roots可以是哪些元素?所谓GC Roots,就是一组必须活跃的引用
1、虚拟机栈中引用的对象,比如:各个线程被调用的方法中使用到的参数、局部变量等。
2、静态变量引用的对象,除非类卸载,否则他引用的对象一直存在
3、所有被同步锁synchronized持有的对象(synchronize持有的对象要是被销毁,同步就失效了)
小技巧
所以如果一个引用(指针),它保存了堆内存里面的对象,那它就是一个Root。
栈、方法区、常量池 结构引用堆空间里面的对象,图里面蓝色的,可达对象。红色不可达,是垃圾。
可达性分析算法的注意事项
如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。
这点也是导致GC进行时必须“Stop The World”的一个重要原因。
对象的 finalization 机制
final、finally、finalize
对象销毁前的回调函数:finalize()
1、Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
2、当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
3、finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
用于在对象被回收时进行资源释放
Object 类中 finalize() 源码:
// 等待被重写
protected void finalize() throws Throwable { }
即使重写了这个方法,永远不要主动调用某个对象的finalize()方法应该交给垃圾回收机制调用。
1、在finalize()时可能会导致对象复活。
2、finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。
3、因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收
清除阶段:
垃圾清除算法
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。
目前在JVM中比较常见的三种垃圾收集算法是
1、标记清除算法(Mark-Sweep)
2、标记复制算法(Copying)
3、标记压缩算法(Mark-Compact)
标记-清除(Mark-Sweep)算法:
标记阶段是把所有活动对象(可达对象,reachable)都做上标记的阶段。清除阶段是把那些没有标记的对象,也就是非活动对象回收的阶段
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除
要把用户线程停下来,因为用户线程运行就又会产生垃圾,要保持一致性,就将用户线程先停下来。
标记-清除算法的缺点
1、标记清除算法的效率不算高 (需要进行遍历)
2、在进行GC的时候,需要停止整个应用程序,用户体验较差
3、这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表。
所以现在新的垃圾收集器没有使用这个算法的了,因为产生碎片
标记-压缩算法:
标记-清除-压缩(Mark-Sweep-Compact)算法,是对标记-清除算法的改进
背景:
1、复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。
如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
2、标记-清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM的设计者需要在此基础之上进行改进。
标记-压缩(Mark-Compact)算法由此诞生。
标记-压缩算法的执行流程
1、第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
2、第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
类似于餐厅里面吃饭,还没有吃完的顾客,统一移到餐厅的一个连续的位置。
标记-压缩算法与标记-清除算法的比较:
1、标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。
2、二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。
3、可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,
JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销(标记-清除算法需要空闲列表)。
标记-压缩算法的优缺点
优点
1、消除了标记-清除算法当中,内存区域分散的缺点,有碎片。
2、消除了复制算法当中,内存减半的高额代价。
缺点
1、从效率上来说,标记-整理算法要低于其他算法,因为有碎片的整理过程
2、移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
3、移动过程中,需要全程暂停用户应用程序,时间要长一些。即:STW
标记复制算法
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收
没有标记的过程,把可达的对象,直接复制到内存大小一样的另外一个区域中,而且是连续存放, 复制完成后,A区里面的对象就没有用了,
下一次从B区复制到A区,这样交换使用。
新生代的S0和S1也是使用复制算法。
复制算法的优缺点:
优点
1、没有标记和清除过程,实现简单,运行高效
2、复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点 :此算法的缺点也是很明显的,就是需要两倍的内存空间。
复制算法的应用场景
即特别适合垃圾对象很多,存活对象很少的场景;例如:Young区的Survivor0和Survivor1区
如果活动对象太多,那么每次就需要复制很多才行,效率就低。老年代大量的对象存活,那么复制的对象将会有很多,效率会很低
在新生代,对常规应用的垃圾回收,一次通常可以回收70% - 99% 的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
对比三种清除阶段的算法:
1、效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。
2、而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
3、综合我们可以找到,没有最好的算法,只有最合适的算法
空间开销少。
分代收集算法
为什么要使用分代收集算法
1、分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
2、一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
3、在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关:
比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。
但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
分代收集算法的分代依据:
目前几乎所有的GC都采用分代收集算法执行垃圾回收的
在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。
1、年轻代(Young Gen)
1、年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
2、这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。
而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
2、老年代(Tenured Gen)
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-清除-整理的混合实现。
内存溢出(OOM)、内存泄露:
内存溢出(OOM)
1、由于GC一直在发展,所有一般情况下,除*非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,*否则不太容易出现OOM的情况。
2、大多数情况下,GC会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的Full GC操作,这时候会回收大量的内存,供应用程序继续使用。
3、Javadoc中对OutofMemoryError的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。
内存溢出(OOM)原因分析:说明Java虚拟机的堆内存不够。
1、大量的内存泄露会导致内存溢出
2、代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
3、也很有可能就是堆的大小不合理,我们可以通过参数-Xms 、-Xmx来调整。
说明:
1、在抛出OutofMemoryError之前,通常垃圾收集器会被触发,尽其所能去清理出空间。
2、当然,也不是在任何情况下垃圾收集器都会被触发的
比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutofMemoryError。
内存泄漏(Memory Leak)
1、只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
2、但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”。
静态变量和类的生命周期一样。
3、尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现OutofMemory异常,导致程序崩溃。
内存泄露的举例:
左边的图:Java使用可达性分析算法,最上面的数据不可达,就是需要被回收的对象。
右边的图:后期有一些对象不用了,按道理应该断开引用,但是存在一些链没有断开,从而导致没有办法被回收。
单例模式
单例的生命周期和应用程序是一样长的,所以在单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
一些提供close()的资源未关闭,导致内存泄漏
数据库连接 dataSourse.getConnection(),网络连接socket和io连接必须手动close,否则是不能被回收的。
一个生命周期长的对象引用了一个生命周期短的对象,这个生命周期短的就是可达的,即使不再使用,也不会被GC销毁。