jvm之垃圾回收

1.什么是垃圾

垃圾是指在运行程序没有任何指针指向的对象,这个对象就需要被回收的垃圾。

如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。

2.为什么需要GC

对高级语言来说,一个基本认知如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样,除了释放没有的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将会所占用堆内存移到堆内存的一端,以便jvm将整理出来的内存分配给新的对象。

随着应用程序所应付业务越来越大,复杂,用户越来越多,没有GC就不能保证应用程序的正常运行。而经常造成STW又跟不上实际的需求,所以会不断地尝试对GC进行优化。

3.垃圾标记阶段的算法之引用计数算法

引用计数算法比较简单,对每个对象保存一个整型的引用计数器属性,用于记录的情况。

对于一个对象A,只要有任何对象引用了A,则A 的引用计数器加1,当引用失效时,引用计数器就减1,只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。

优点:实现简单,垃圾对象便于辨识,判定效率高,回收没有延迟。

缺点:它需要单独的字段存储计数器,这样的做法增加了存储空间的开销,每次赋值都需要更新计数器,伴随着加法和减法操作,着增加了时间开销,引用计数器有一个严重的问题,即无法处理循环的情况,这是一条致命缺陷,导致再java的垃圾回收没有使用这类算法。如下图所示:

 Java没有选择引用计数,是因为器存在一个基本的难题,也就是很难处理循环引用关系,但Python,它支持引用计数和垃圾回收机制,Python是如何解决循环引用的呢,它手动解除,就是在合适的时机,解除引用关系,使用弱引用weakref,weakref是Python提供的标准库,旨在解决循环引用。

4.垃圾标记阶段的算法之可达性分析算法

基本思路:

①可达性算法是以根对象集合为起点,按照从上至下的方式搜索根对象集合所连接的目标对象是否可达。

②使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链。

③如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。

④在可达性分析算法中,只有能够被根对象集合直接或间接连接的对象才是存活对象。

下图是根可达分析示意图:

 在Java语言中,GC Roots包括以下几类元素:

①虚拟机栈中引用的对象,比如线程被调用到的参数、局部变量等。

②本地方法栈内JNI(通常说的本地方法)引用的对象。

③方法区中类静态属性引用对象,比如Java类的引用类型静态变量。

④方法区中常量引用的对象,比如:字符串常量池(String Table)里的引用

⑤所有被同步锁synchronized持有的对象。

⑥Java虚拟机内部引用,基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerRxcetion、OutOfMemoryError),系统类加载器。

⑦反映java虚拟机内部情况的JMXBean、JVMTI中注册回调、本地代码缓存等。

示意图:

 除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收(Partial GC)。如果只针对Java堆中的某一块区域进行垃圾回收(比如典型的只针对新生区),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全被其他区域的对象引用,这时候就需要一并管理的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性。

辨识小技巧:由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。

5.对象的finalization机制

Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的子定义处理逻辑。当垃圾回收器发现没有被引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象finalize()方法。finalize()方法进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。

永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由如下:

①在finalize()时可能会导致对象复活。

②finalize()方法的执行时间没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。

③一个糟糕的fianlize()会严重影响GC的性能。

一个无法触及的对象有可能在某一条件下“复活“自己,如果这样,那么对它的回收就是不合理,为此,定义虚拟机中的对象的三种状态。如下:

①可触及的:从根节点开始,可以到达这个对象。

②可复活的:对象的所有引用都被释放,但是对象有可能在fianlize()中复活。

③不可触及的:对象finalize()被调用,并且没有复活,那么就会进入不可 触及状态,不可触及对象不可能被复活,因为fianlize()只会被调用一次。

以上三种状态中,由于fianlize()方法的存在,进行区分。只有对象不可触及时才可以被回收。

6.垃圾清除阶段算法之标记-清除算法

执行过程:

当堆中有效内存空间被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。

标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Head中记录为可达对象。

清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记可达对象,则进行回收。

示意图:

缺点:效率不算高,在进行GC的时候,需要停止整个应用程序,导致用户体验差,这个方式清理出来的空闲内不连续,产生内存碎片。需要维护一个空闲列表。

何为清除:这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。

7.垃圾清除阶段算法之标记-复制算法

核心思想:将存活的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象赋值到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存角色,最后完成垃圾回收。

示意图:

 优点:没有标记和清除过程,实现简单,运行高效。复制过程以后保证空间的连续性,不会出现”碎片“问题。

缺点:此算法的缺点是很明显的,就是需要两倍 的空间。对于G1这种拆分成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管内存占用或时间开销也不小。特别的如果系统的垃圾对象很多,复制的存活对象数量并不会太大,或者说非常低才行。

 8.垃圾清除阶段算法之标记-标记-压缩算法

执行过程:

第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象。

第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界所有的空间。

示意图:

 优点:消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。消除了复制算法当中,内存减半的高频代价。

缺点:从效率上说,标记-整理算法要求低于复制算法。移动对象的同时,如果被其他对象引用,则还需要调用引用的地址,移动过程中,需要全程暂停用户程序。即:STW。

9.分代收集算法

分代收集算法,是基于这样的一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的算法,以提高垃圾回收效率。

年轻代特点:区域相对老年代较小,对对生命周期短、存活率低,回收频繁。这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象有关,因此很适用于年轻代的回收。而复制算法内存利用率不高,通过HotSpot中的两个survivor的设计得到缓解。

老年代特点:区域大,对象生命周期长、存活率高,回收不及年轻代频繁。这种情况存在大量存活率高的对象,复制算法变得不适合。一般由标记-清除与标记-整理的混合实现。

10.增量收集算法

基本思想:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序。依次反复,直到垃圾收集完成。总的来说,增量收集算法的基础任是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或是复制工作。

缺点:使用这种方式,由于在垃圾回收过程中间断性地执行应用程序代码,所有能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

11.System.gc()的理解

在默认情况下,通常System.gc或者Runtime.getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。然而System.gc调用附带一个声明,无法保证垃圾收集器的调用。

jvm实现者可以通过System.gc()调用来决定JVM的GC行为,而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过麻烦了。在一些特殊情况下,如果正在编写一个性能基准,我们可以在运行之间调用Sytem.gc().

12.内存溢出与内存泄漏

内存溢出造成原因:①Java虚拟机的堆内存设置不够。比如:可能内存泄漏问题,也很有可能就是大小不合理,比如我们比较可观的数据量,但是没有显式指定JVM堆大小或者指定数值偏小。我们可以通过参数-Xms、-Xmx来调整。②代码中创建了大量大对象,并且长时间不能被垃圾收集器收集。

内存泄漏:严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。但是实际情况很多时候一些不太好的实践会导致对象的生命周期变得很长甚至导致OOM,也可以叫宽泛意义上的”内存泄漏“。尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就被逐步蚕食,直至耗尽所有内存,最终出现OutIOfMemory异常,导致程序崩溃。注意,这里的存储空间并不是物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区指定的大小。

内存泄漏例子:

①单例模式:单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对象外部对象的引用的话,那么这个外部对象不能被回收的,则会导致内存泄漏的产生。

②一些提供close的资源未关闭导致内存泄漏,比如数据库连接,网络连接(socket)和io连接必须手动close,否则是不能被回收的。

13.stop The World

stop The World,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿生产时整个应用程序线程都会被暂停,没有任何响应,有点卡死的感觉,这个停顿称为STW。

可达性分析算法枚举根节点会导致所有Java执行线程停顿,分析工作必须在一个能确保一致性的快照中进行,一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上,如果出现整个分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证。

哪怕时G1也不能完全避免Stop-the-world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂替时间。

STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉,开发中不要用System.gc();会导致Stop-the-world的发生。

14.安全点与安全区域

安全点:程序执行时并非在所有地方都停下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点”。安全点的选择很重要,如果太少可能导致GC等待时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都 非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。

如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来 :①抢占式中断(目前没有虚拟机采用了):首先中断所有线程,如果还没有线程不在安全点,恢复线程,让线程跑到安全点。

②主动式中断:设置一个标志,各个线程运行到Safe Point的时候主动轮询这个标志。如果中断标志为真,则将自己进行中断挂起。

安全区域:Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”的时候呢?,例如线程处于Sleep状态或Blocked状态,这个时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况就需要安全区域来解决。

安全区域时指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是开始GC都是安全的。我们也可以把Safe Region 看作是被扩展的Safepoint。

15.引用

Reference子类中只有终端器引用是包内可见的,其他3种引用类型均为public,可用在应用程序中直接使用。

强引用:最传统的“引用”的定义,指在程序代码之中普遍存在的引用赋值,即类似“Object obj =new Object()”这种引用关系。无论任何情况下,只要强调引用关系还存在,垃圾收集器就永远不会回收被引用的对象。

软引用:在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会发生内存溢出异常。

弱引用:被弱引用关联的对象只能生存到下一次收集之前。当垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够都会回收掉被引用关联的对象。

虚引用:一个对象是否虚引用的存在,完全不会对其生存时间构成影响,无法通过虚引用来获得一个对象的实例。为一个对象设置引用关联的唯一目的就是在这个对象被回收时收到一个系统通知。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值