栈、方法区、常量池 结构引用堆空间里面的对象,图里面蓝色的,可达对象。红色不可达,是垃圾。
可达性分析算法的注意事项
如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。
这点也是导致GC进行时必须“Stop The World”的一个重要原因。
对象的 finalization 机制:
对象销毁前的回调函数: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)
标记-清除(Mark-Sweep)算法:
标记阶段是把所有活动对象(可达对象,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、综合我们可以找到,没有最好的算法,只有最合适的算法。
分代收集算法:
为什么要使用分代收集算法?
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销毁。