本文主要讲授如何判断一个对象是否存活,并举例说明了java虚拟机垃圾回收机制中的垃圾回收算法。一、概述对于java程序员来说,或多或少都听说过GC和垃圾回收机制这两个名词。但是什么是垃圾回收,什么是垃圾,如何回收呢?本文将给出答案。2.垃圾收集机制垃圾收集(英文:GarbageCollection,简称GC)是计算机科学中的一种自动内存管理机制。当不再需要计算机上的动态内存时,应释放它以为内存腾出空间。这种内存资源管理称为垃圾回收。为了让大家更容易理解,我画了一个形象的图。餐厅里有很多张桌子(连续内存区)。顾客(对象)来餐厅就餐,但是这些顾客非常社会化,吃完就不会走。让掌柜开车出去。以前是老板娘干的(手动释放内存),现在引入了出门机器人(垃圾回收机制),告诉顾客吃完就出去。生成:首先,垃圾回收不是java的关联产物。最早使用垃圾收集的语言是诞生于1960年的Lisp,垃圾收集器的目的是减轻程序员的负担,减少程序员犯错的机会。现在,经过半个多世纪的发展,现在的垃圾回收技术已经相当成熟了,大多数语言都支持垃圾回收,比如Python、Erlang、C#、Java等,为什么我们要了解GC和内存分配?当我们需要排查各种内存泄漏和内存溢出时,当垃圾回收成为系统实现高并发的瓶颈时,我们就需要对这种自动技术进行监督和调整。(吃完机器人也不是万能的,还需要老板娘给机器人调参数)3.需要回收哪些内存首先我们知道程序计数器、虚拟机栈、本地这三个区域方法栈是线程私有的。它们与线同生同死;栈帧随着方法的执行而入栈,方法执行完成后弹出栈。类结构确定后,每个栈帧占用多少内存就基本确定了。所以这些区域不需要管理。然后,java堆和方法区是内存共享的。一个接口有多个实现类。不同的类可能需要不同的内存,一个方法的不同分支可能需要不同的内存。我们只能在系统运行时确定需要创建哪些对象。这是垃圾收集器的主战场。垃圾回收策略引用计数算法(ReferenceCounting)为对象添加了一个计数器。每当一个地方引用它时,计数器就加1,当引用失效时就减1。当计数器达到0时,对象不再被使用——对象消亡。引用计数算法实现简单,效率非常好。该算法用于Python、Ruby和其他语言。但是主流的java虚拟机并没有使用这种算法来管理内存,因为它无法解决对象的循环引用问题。publicclassReferenceCounting{publicstaticvoidmain(String[]args){Dogdog1=newDog();Dogdog2=newDog();//狗1和狗2对象互相引用dog1.setSon(dog2);dog2.setSon(dog1);//将两个对象的引用都设置为空启动参数中参数-XX:+PrintGCDetails,打印log[GC7926K->480K(502784K),0.0023280secs][FullGC480K->316K(502784K),0.0098820secs]可以明显看出,虽然两个对象引用彼此,但仍然被回收,所以热点不是引用计数算法。Tracinggarbagecollection目前主流的虚拟机Java和C#都使用Tracinggarbagecollection来判断对象是否存活,以至于一提到垃圾回收就会想到Tracinggarbagecollection。基本思想:定义一些GCRoots对象作为起点,通过引用链跟踪对象是否能到达这些确定的GCRoots对象,那些不能到达这些根对象的对象将被视为死亡。该算法的实际实现将是复杂多变的。开始绘图,现在我们设置GCRoots、碗面和点菜菜单。有空碗没点单名字的会用绿色标注,活下来的,左上角碗里有面条的,等上面的非单身狗,整齐的一家,虽然左右两个人都是空面,菜单上没有,但是中间的人没有引用,而中间的人正好碗里有面!这就是“饭后不走人跟踪法”。在java中,以下对象会被设置为GCRoots:虚拟机栈中引用的对象(栈帧的局部变量表):即对象的方法区中类静态属性引用的对象被局部变量引用:publicstaticDogdog=newDog();方法区常量引用对象:publicstaticfinalHashMapmap=newHashMap();JNI本地方法栈中引用的对象。可达性分析算法(Reachabilityanalysis):如果你看过周志明老师深入理解java虚拟机的文章,就会知道可达性分析这个名词,也就是这里的Tracinggarbagecollection。一开始我以为是两个不同的名字,但是在谷歌搜索Reachabilityanalysis的时候,并没有找到垃圾回收相关的资料。百度查到的可达性分析算法基本都是来自深入理解java虚拟机wiki百科对可达性分析的描述,用来判断一个分布式系统是否可以达到全局状态。java的垃圾回收策略是Tracinggarbagecollection。所以我怀疑可能是对java虚拟机的深入理解用错了术语。逃逸分析(Escapeanalysis)逃逸分析将对象堆分配(heapallocations)转移到栈分配(Stackallocations),从而减少了大量的垃圾收集工作。在编译时判断函数中分配的对象是否被外部方法或线程调用。如果没有,对象将被分配到堆栈以减少垃圾收集工作。引用在jdk1.2之后,java扩展了引用的概念,将引用分为四种:强引用、软引用、弱引用、幻引用。强引用是指程序代码中普遍存在的引用,如“Objectobj=newObject()”。只要强引用仍然存在,垃圾收集器就永远不会回收被引用的对象。软引用用于描述一些仍然有用但不是必需的对象。对于软引用关联的对象,在系统出现内存溢出异常之前,这些对象会被纳入回收范围进行二次回收。如果本次回收内存不足,则会抛出内存溢出异常。JDK1.2之后提供了SoftReference类来实现软引用。弱引用也用于描述非本质对象,但其强度比软引用弱。与弱引用关联的对象只能存活到收集发生之前的下一次垃圾收集。当垃圾回收器工作时,无论当前内存是否充足,只与弱引用相关联的对象都会被回收。JDK1.2之后提供了WeakReference类来实现弱引用。Phantomreferences也变成ghostreferences或phantomreferences,是最弱的引用关系。一个对象是否有虚引用,根本不会影响它的生命周期,也不能通过虚引用来获取对象实例。为对象设置幻影引用关联的唯一目的是在对象被收集器回收时收到系统通知。JDK1.2之后,提供了PhantomReference类来实现幻引用,一个可以忘记的关键字——finalize。当一个对象决定是否需要回收时,它需要经过两个标记过程。第一次是跟踪对象是否连接到GCRoots。如果没有标记,第二次是判断对象是否没有重写finalize方法,或者调用了finalize方法,此时对象就彻底死了。如果重写了finalize方法,没有调用,对象会被放到一个低优先级甚至不执行的队列F-Queue中,然后调用对象的finalize方法。如果该对象在方法中被GCRoots引用,则该对象会成功保存自己。但是F-Queue有可能不执行,所以这种分救的方法并不靠谱。有的教程推荐finalize来释放资源,为什么不试试finally呢?这个关键字可以忘记。4.垃圾收集算法标记清除算法标记清除算法包括两个阶段。首先标记需要回收的对象(标记方法如上),标记完成后统一回收所有标记的对象。明确标注的算法是所有垃圾回收算法的基础,后面的算法都是根据自己的不足改进的。缺点:效率低,打标清零效率不高;空间碎片,标记清除后会产生大量连续的内存碎片,空间碎片过多,当有大对象需要分配空间时,会提前触发gc。空表是未使用的内存,绿色标记的对象是可以清除的对象。这是清理前的状态。整洁的家庭是一个比较大的物体,需要占据一个连续的区域。这是清理后的状态。内存碎片太多。当一个比较大且整洁的家族被分配时,会提前触发新的GC。复制算法为了解决效率问题,出现了复制算法,可以将内存分成大小相等的两块,一次只使用其中一块。当这块内存用完后,将存活的对象复制到另一块内存中。一次清除所有使用的内存。这种算法是高效的,但是浪费了太多的空间。如上图所示,现在使用了内存的下半部分。清理时,将未标记的内存复制到上位内存,然后一次清除下半部分内存。现在大部分商用虚拟机都是使用这种算法来回收新生代。但是内存并不是按照1:1分配的,因为IBM专门研究过,新生代中98%的对象都是born和dield的。将内存分成一个较大的Eden空间和两个较小的Survivor空间。每次使用Eden和其中一个Survivor,回收时将存活的对象复制到另一个Survivor,Eden和使用过的Survivor被清除。一般Eden、Survivor1、Survivor2的比例是8:1:1,所以只会浪费10%的内存。在这里,如果把Eden翻译成伊甸园,对象出生的地方,Survivor幸存者,存活回收的对象,就更容易理解了。如果回收后对象数量超过10%,Survivor空间不够,则需要依赖其他内存(老年代)进行分配保证(HandlePromotion)。markcompaction算法和copycollection算法不适合对象存活率高的情况。当存活的对象过多时,需要复制的对象就会增多,效率就会下降。而如果不想浪费50%的空间,就需要使用额外的空间进行分配保证,所以这个算法不适用于老年代。根据老年代的特点,有人提出了标记算法。标记完对象后,将存活的对象移到一端,然后直接清除边界外的内存。这是回收前,这是回收后。分代收集算法是指根据对象的生命周期将内存分成若干块。一般java堆分为新生代和老年代。对于每次垃圾回收都有大量对象死亡的新生代,采用复制算法;对于高存活代且无额外空间保证的老年代,采用mark-clear或mark-clean算法。增量收集器程序将拥有的内存空间划分为若干个分区。程序运行所需的存储对象会分布在这些分区中,一次只回收其中一个分区,从而避免了程序所有运行线程被挂起进行回收,让部分线程继续运行而不影响回收行为,减少恢复时间,提高程序响应速度。五、总结本文介绍了什么是垃圾回收,java虚拟机的垃圾回收策略,包括引用计数、跟踪垃圾回收和逃逸分析,并以餐厅的形式介绍了几种垃圾回收算法,包括mark-clear和copy算法,标记整理算法。原文:https://icdream.github.io/2019/01/10/jvm03/
