目录
概述:
什么是垃圾(Garbage)?
想要学习GC,首先需要理解为什么需要GC?
Java 垃圾回收机制:
Java自动内存管理的优点:
关于自动内存管理的担忧:
GC 的作用区域:
垃圾回收相关算法(重要):
标记阶段:引用计数器算法和可达性分析算法
标记阶段:引用计数算法
标记阶段:可达性分析算法
可达性分析算法的注意事项
对象的 finalization 机制:
清除阶段:
标记-清除(Mark-Sweep)算法:
标记-清除算法的缺点:
标记复制算法:
复制算法的优缺点:
标记-压缩算法:
标记-压缩算法的执行流程:
标记-压缩算法与标记-清除算法的比较:
标记-压缩算法的优缺点:
对比三种清除阶段的算法:
分代收集算法:
为什么要使用分代收集算法?
内存溢出(OOM)、内存泄露:
1、Java 和 C++语言的区别,就在于垃圾收集技术和内存动态分配上,C语言没有垃圾收集技术,需要我们手动的收集。
2、垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。
3、关于垃圾收集有三个经典问题:
哪些内存需要回收?
什么时候回收?
如何回收?
4、垃圾收集机制是Java的招牌能力,极大地提高了开发效率。如今,垃圾收集几乎成为现代语言的标配。
1、垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
外文:An object is considered garbage when it can no longer be reached from any pointer in the running program.
2、如果不及时对内存中的垃圾进行清理,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。
1、对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。
2、除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象(尤其是一些大的对象)。
3、随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序的正常进行。而经常造成STW(Stop the World)的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。
1、自动内存管理无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险
2、没有垃圾回收器,java也会和cpp一样,各种悬垂指针,野指针,泄露问题让你头疼不已。
3、自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发
1、对于Java开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于“自动”,那么这将会是一场灾难,最严重的就会弱化Java开发人员在程序出现内存溢出时定位问题和解决问题的能力。
2、了解JVM的自动内存分配和内存回收原理显得非常重要,只有在真正了解JVM是如何管理内存后,我们才能够在遇见OutofMemoryError时,快速地根据错误异常日志定位问题和解决问题。
3、当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。
1、频繁在新生区收集,很少在养老区收集,几乎不在方法区(永久区/元空间)收集,其中,Java堆是垃圾收集器的工作重点
2、从次数上讲:
频繁收集Young区
较少收集Old区
基本不收集方法区
3、GC主要关注于方法区和堆中的垃圾收集
引用类型才需要垃圾回收,基本数据类型不需要回收。
进行垃圾回收的时候首先要确定哪些是垃圾(判断对象是否可用)?找到垃圾之后怎么清理掉?
分为标记阶段和清除阶段
内存泄露:这个对象不再使用,但是GC没法回收
p指向null,后面三个对象都不再使用了,但是引用计数都不是0,就没法GC回收。
如果让你举内存泄露的例子,最好不要举这个例子,因为Java里面没有使用这个例子,如果举这个要指出是引用计数算法的。
Java中的内存泄露问题_Neon Zhou的博客-CSDN博客_java内存泄漏
所以如果一个引用(指针),它保存了堆内存里面的对象,那它就是一个Root。
栈、方法区、常量池 结构引用堆空间里面的对象,图里面蓝色的,可达对象。红色不可达,是垃圾。
如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。
这点也是导致GC进行时必须“Stop The World”的一个重要原因。
对象销毁前的回调函数:finalize()
1、Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
2、当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
3、finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
// 等待被重写
protected void finalize() throws Throwable { }
即使重写了这个方法,永远不要主动调用某个对象的finalize()方法应该交给垃圾回收机制调用。
1、在finalize()时可能会导致对象复活。
2、finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。
3、因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收。
目前在JVM中比较常见的三种垃圾收集算法是
1、标记清除算法(Mark-Sweep)
2、标记复制算法(Copying)
3、标记压缩算法(Mark-Compact)
标记阶段是把所有活动对象(可达对象,reachable)都做上标记的阶段。清除阶段是把那些没有标记的对象,也就是非活动对象回收的阶段。
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。要把用户线程停下来,因为用户线程运行就又会产生垃圾,要保持一致性,就将用户线程先停下来。
1、标记清除算法的效率不算高 (需要进行遍历)
2、在进行GC的时候,需要停止整个应用程序,用户体验较差
3、这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表。
所以现在新的垃圾收集器没有使用这个算法的了,因为产生碎片
没有标记的过程,把可达的对象,直接复制到内存大小一样的另外一个区域中,而且是连续存放, 复制完成后,A区里面的对象就没有用了,下一次从B区复制到A区,这样交换使用。
没有标记的过程,把可达的对象,直接复制到内存大小一样的另外一个区域中,而且是连续存放, 复制完成后,A区里面的对象就没有用了,下一次从B区复制到A区,这样交换使用。
新生代的S0和S1也是使用复制算法。
优点
1、没有标记和清除过程,实现简单,运行高效
2、复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点 :此算法的缺点也是很明显的,就是需要两倍的内存空间。
复制算法的应用场景
即特别适合垃圾对象很多,存活对象很少的场景;例如:Young区的Survivor0和Survivor1区
如果活动对象太多,那么每次就需要复制很多才行,效率就低。老年代大量的对象存活,那么复制的对象将会有很多,效率会很低
在新生代,对常规应用的垃圾回收,一次通常可以回收70% - 99% 的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
标记-清除-压缩(Mark-Sweep-Compact)算法,是对标记-清除算法的改进
背景:
1、复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
2、标记-清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM的设计者需要在此基础之上进行改进。
标记-压缩(Mark-Compact)算法由此诞生。
1、第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
2、第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
1、标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。
2、二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。
3、可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销(标记-清除算法需要空闲列表)。
优点
1、消除了标记-清除算法当中,内存区域分散的缺点,有碎片。
2、消除了复制算法当中,内存减半的高额代价。
缺点
1、从效率上来说,标记-整理算法要低于其他算法,因为有碎片的整理过程
2、移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
3、移动过程中,需要全程暂停用户应用程序,时间要长一些。即:STW
1、效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。
2、而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
3、综合我们可以找到,没有最好的算法,只有最合适的算法。
内存溢出(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销毁。