精华内容
下载资源
问答
  • 常见的GC算法(GC的背景与原理) GC 是英文词汇Garbage Collection的缩写,中文一般直译为 “垃圾收集”。当然也会说 “垃圾回收”。 三种垃圾收集器实现(Paraller/CMD/G1) 手动内存管理 之前有C/C++编程经验、...

    常见的GC算法(GC的背景与原理)

    GC 是英文词汇Garbage Collection的缩写,中文一般直译为 “垃圾收集”。当然也会说 “垃圾回收”。

    f37a9c99-3c14-4c8c-b285-0130a598c756.jpg

    三种垃圾收集器实现(Paraller/CMS/G1)

    手动内存管理

    之前有C/C++编程经验、或者了解计算机原理的同学,会很容易理解 “内存分配” 和 “内存释放” 和两个概念。

    计算机程序在执行过程中,需要有地方来存放输入参数、中间变量,以及运算结果,之前的文章有提到,我们知道这些会存放到栈内存中。

    像C/C++的编程,需要使用完资源后,手动调用清除内存操作,这类归属于手动内存管理。

    弊端就是:代码经手人多了,很可能不清楚哪些是需要清理的,造成管理混乱。

    引用计数法

    GC 垃圾收集器就像仓库部门,负责分配内存,负责追踪这些内存的使用情况,并在合适的时候进行释放。

    8442223.png

    但是,如果业务变得更复杂。仓库之间需要协同工作,有了依赖关系之后。

    8648060.png

    这时候单纯的引用计数就会出问题,循环依赖的仓库/对象没办法回收,就像数据库的死锁一样让人讨厌,你没法让它自己变成0。

    这种情况在计算机中叫做 “内存泄漏”, 该释放的没释放,该回收的没回收。

    如果依赖关系更复杂,计算机的内存资源很可能用满,或者说不够用,内存不够用则称为 “内尺寸溢出”。

    这样我们知道了引用计数法的一些缺陷,有没有办法解决呢?办法总比困难多,我找个人专门来排查循环计数行了吧,一个不够就两个…但如果仓库成千上万,或者上亿呢?还是能解决的,最多不就是慢点嘛。

    • 第一代自动垃圾回收算法,使用的是引用计数。针对每个对象,只需要记住被引用的次数,当引用计数变为0时,这个对象就可以被安全地回收了,著名的示例是C++的共享指针;
    • 第二代自动垃圾回收算法,被称为 “引用追踪”,JVM 使用的各种垃圾回收算法都是基于引用追踪方式的算法。

    标记清除算法(Mark and Sweep)

    前面讲了引用计数里需要查找所有的对象计数和对象之间的引用关系。那么如何来查找锁有对象,怎么来做标记呢?

    为了遍历所有对象,JVM明确定义了什么是对象的可达性。

    有一类很明确具体的对象,称为垃圾回收根元素,包括:

    • 局部变量(Local variables)
    • 活动线程(Active threads)
    • 静态域(Static fields)
    • 其他对象

    JVM 使用标记—清除算法,来跟踪所有的可达对象(包括存活对象),确保所有不可达对象占用的内尺寸都能被重用。其中包含两步:

    • Marking(标记):遍历所有的可达对象,并在本地内存中分门别类记下。
    • Sweeping(清除):这一步保证了,不可达对象所占用的内存,在之后进行内存分配时可以重用。

    JVM中包含了多种GC算法,如Parallel Scavenge(并行清除)Parallel Mark + Coyp(并行标记+复制)CMS,他们在实现上略有不同,但理论上都采用了以上两个步骤。

    标记清除算法最重要的优势,就是不再因为循环引用而导致内存泄漏:

    **标记—清除(Mark and Sweep)**是最经典的垃圾回收算法。

    而这种处理方式不好的地方在于:垃圾回收过程中,需要暂停应用程序的所有线程。假如不暂停,则对象间的引用关系会一直不停地发生变化,那样就没法进行统计了。这种情况就做STW停顿(Stop The World pause 全线暂停),让应用程序暂时停止,让JVM进行内存清理工作。

    碎片整理

    每次执行清除(Sweeping),JVM 都必须保证不可达对象占用的内存能被回收重用。这时候,就像是摆满棋子的围棋盘上,一部分位置上棋子被拿掉而产生了一些零散的空位置。但这(最终)有可能会产生内存碎片(类似于磁盘碎片),进而引发两个问题:

    • 写入操作越来越耗时,因为寻找一块足够大的空闲内存会变得困难(棋盘上没有一整片的空地方);
    • 在创建新对象时,JVM 在连续的块中分配内存。如果碎片问题很严重,直至没有空闲片段能存放下新创建的对象,就会发生内存分配错误(allocation error)。

    要避免这类问题,JVM 必须确保碎片问题不失控。因此在垃圾收集过程中,不仅仅是标记和清除,还需要执行“内存碎片整理”过程。这个过程让所有可达对象(reachable objects)依次排列,以消除(或减少)碎片。就像是我们把棋盘上剩余的棋子都聚集到一起,留出来足够大的空余区域。示意图如下所示:

    5160496.png

    说明

    JVM 中的引用是一个抽象的概念,如果 GC 移动某个对象,就会修改(栈和堆中)所有指向该对象的引用。

    移动/拷贝/提升/压缩一般来说是一个 STW 的过程,所以修改对象引用是一个安全的行为。但要更新所有的引用,可能会影响应用程序的性能。

    分代假设

    我们前面提到过,执行垃圾收集需要停止整个应用。很明显,对象越多则收集所有垃圾消耗的时间就越长。但可不可以只处理一个较小的内存区域呢?为了探究这种可能性,研究人员发现,程序中的大多数可回收的内存可归为两类:

    • 大部分对象很快就不再使用,生命周期较短;
    • 还有一部分不会立即无用,但也不会持续太长时间。

    这些观测形成了 弱代假设(Weak Generational Hypothesis),即我们可以根据对象的不同特点,把对象进行分类。基于这一假设,VM 中的内存被分为年轻代(Young Generation)和老年代(Old Generation)。老年代有时候也称为年老区(Tenured)。

    5808335.png

    拆分为这样两个可清理的单独区域,我们就可以根据对象的不同特点,允许采用不同的算法来大幅提高 GC 的性能。

    天下没有免费的午餐,所以这种方法也不是没有任何问题。例如,在不同分代中的对象可能会互相引用,在收集某一个分代时就会成为“事实上的”GC root。

    当然,要着重强调的是,分代假设并不适用于所有程序。因为分代 GC 算法专门针对“要么死得快”、“否则活得长”这类特征的对象来进行优化,此时 JVM 管理那种存活时间半长不长的对象就显得非常尴尬了。

    内存池划分

    堆内存中的内存池划分也是类似的,不太容易理解的地方在于各个内存池中的垃圾收集是如何运行的。请注意:不同的 GC 算法在实现细节上可能会有所不同,但和本章所介绍的相关概念都是一致的。

    5921643.png

    新生代(Eden Space)

    Eden Space,也叫伊甸区,是内存中的一个区域,用来分配新创建的对象。通常会有多个线程同时创建多个对象,所以 Eden 区被划分为多个 线程本地分配缓冲区(Thread Local Allocation Buffer,简称 TLAB)。通过这种缓冲区划分,大部分对象直接由 JVM 在对应线程的 TLAB 中分配,避免与其他线程的同步操作。

    如果 TLAB 中没有足够的内存空间,就会在共享 Eden 区(shared Eden space)之中分配。如果共享 Eden 区也没有足够的空间,就会触发一次 年轻代 GC 来释放内存空间。如果 GC 之后 Eden 区依然没有足够的空闲内存区域,则对象就会被分配到老年代空间(Old Generation)。

    当 Eden 区进行垃圾收集时,GC 将所有从 root 可达的对象过一遍,并标记为存活对象。

    我们曾指出,对象间可能会有跨代的引用,所以需要一种方法来标记从其他分代中指向 Eden 的所有引用。这样做又会遭遇各个分代之间一遍又一遍的引用。JVM 在实现时采用了一些绝招:卡片标记(card-marking)。从本质上讲,JVM 只需要记住 Eden 区中“脏”对象的粗略位置,可能有老年代的对象引用指向这部分区间。更多细节请参考:Nitsan 的博客

    标记阶段完成后,Eden 区中所有存活的对象都会被复制到存活区(Survivor spaces)里面。整个 Eden 区就可以被认为是空的,然后就能用来分配新对象。这种方法称为“标记—复制”(Mark and Copy):存活的对象被标记,然后复制到一个存活区(注意,是复制,而不是移动)。

    读者可以考虑,为什么是复制不是移动?

    存活区(Survivor Spaces)

    Eden 区的旁边是两个存活区(Survivor Spaces),称为 from 空间和 to 空间。需要着重强调的的是,任意时刻总有一个存活区是空的(empty)。

    空的那个存活区用于在下一次年轻代 GC 时存放收集的对象。年轻代中所有的存活对象(包括 Eden 区和非空的那个“from”存活区)都会被复制到 ”to“ 存活区。GC 过程完成后,“to”区有对象,而“from”区里没有对象。两者的角色进行正好切换,from 变成 to,to 变成 from。

    6202084.png

    存活的对象会在两个存活区之间复制多次,直到某些对象的存活 时间达到一定的阀值。分代理论假设,存活超过一定时间的对象很可能会继续存活更长时间。

    这类“年老”的对象因此被提升(promoted)到老年代。提升的时候,存活区的对象不再是复制到另一个存活区,而是迁移到老年代,并在老年代一直驻留,直到变为不可达对象。

    为了确定一个对象是否“足够老”,可以被提升(Promotion)到老年代,GC 模块跟踪记录每个存活区对象存活的次数。每次分代 GC 完成后,存活对象的年龄就会增长。当年龄超过提升阈值(tenuring threshold),就会被提升到老年代区域。

    具体的提升阈值由 JVM 动态调整,但也可以用参数 -XX:+MaxTenuringThreshold 来指定上限。如果设置 -XX:+MaxTenuringThreshold=0 ,则 GC 时存活对象不在存活区之间复制,直接提升到老年代。现代 JVM 中这个阈值默认设置为 15 个 GC 周期。这也是 HotSpot JVM 中允许的最大值。

    如果存活区空间不够存放年轻代中的存活对象,提升(Promotion)也可能更早地进行。

    老年代(Old Gen)

    老年代的 GC 实现要复杂得多。老年代内存空间通常会更大,里面的对象是垃圾的概率也更小。

    老年代 GC 发生的频率比年轻代小很多。同时,因为预期老年代中的对象大部分是存活的,所以不再使用标记和复制(Mark and Copy)算法。而是采用移动对象的方式来实现最小化内存碎片。老年代空间的清理算法通常是建立在不同的基础上的。原则上,会执行以下这些步骤:

    • 通过标志位(marked bit),标记所有通过 GC roots 可达的对象;
    • 删除所有不可达对象;
    • 整理老年代空间中的内容,方法是将所有的存活对象复制,从老年代空间开始的地方依次存放。

    通过上面的描述可知,老年代 GC 必须明确地进行整理,以避免内存碎片过多。

    永久代(Perm Gen)

    在 Java 8 之前有一个特殊的空间,称为“永久代”(Permanent Generation)。这是存储元数据(metadata)的地方,比如 class 信息等。此外,这个区域中也保存有其他的数据和信息,包括内部化的字符串(internalized strings)等等。

    实际上这给 Java 开发者造成了很多麻烦,因为很难去计算这块区域到底需要占用多少内存空间。预测失败导致的结果就是产生 java.lang.OutOfMemoryError: Permgen space 这种形式的错误。除非 OutOfMemoryError 确实是内存泄漏导致的,否则就只能增加 permgen 的大小,例如下面的示例,就是设置 perm gen 最大空间为 256 MB:

    -XX:MaxPermSize=256m
    复制
    

    元数据区(Metaspace)

    既然估算元数据所需空间那么复杂,Java 8 直接删除了永久代(Permanent Generation),改用 Metaspace。从此以后,Java 中很多杂七杂八的东西都放置到普通的堆内存里。

    当然,像类定义(class definitions)之类的信息会被加载到 Metaspace 中。元数据区位于本地内存(native memory),不再影响到普通的 Java 对象。默认情况下,Metaspace 的大小只受限于 Java 进程可用的本地内存。这样程序就不再因为多加载了几个类/JAR 包就导致 java.lang.OutOfMemoryError: Permgen space.。注意,这种不受限制的空间也不是没有代价的 —— 如果 Metaspace 失控,则可能会导致严重影响程序性能的内存交换(swapping),或者导致本地内存分配失败。

    如果需要避免这种最坏情况,那么可以通过下面这样的方式来限制 Metaspace 的大小,如 256 MB:

    -XX:MaxMetaspaceSize=256m
    复制
    

    垃圾收集

    各种垃圾收集器的实现细节虽然并不相同,但总体而言,垃圾收集器都专注于两件事情:

    • 查找所有存活对象
    • 抛弃其他的部分,即死对象,不再使用的对象。

    第一步,记录(census)所有的存活对象,在垃圾收集中有一个叫做 标记(Marking) 的过程专门干这件事。

    标记可达对象(Marking Reachable Objects)

    现代 JVM 中所有的 GC 算法,第一步都是找出所有存活的对象。下面的示意图对此做了最好的诠释:

    6696297.png

    首先,有一些特定的对象被指定为 Garbage Collection Roots(GC 根元素)。包括:

    • 当前正在执行的方法里的局部变量和输入参数
    • 活动线程(Active threads)
    • 内存中所有类的静态字段(static field)
    • JNI 引用

    其次,GC 遍历(traverses)内存中整体的对象关系图(object graph),从 GC 根元素开始扫描,到直接引用,以及其他对象(通过对象的属性域)。所有 GC 访问到的对象都被标记(marked) 为存活对象。

    存活对象在上图中用蓝色表示。标记阶段完成后,所有存活对象都被标记了。而其他对象(上图中灰色的数据结构)就是从 GC 根元素不可达的,也就是说程序不能再使用这些不可达的对象(unreachable object)。这样的对象被认为是垃圾,GC 会在接下来的阶段中清除他们。

    在标记阶段有几个需要注意的地方:在标记阶段,需要暂停所有应用线程,以遍历所有对象的引用关系。因为不暂停就没法跟踪一直在变化的引用关系图。这种情景叫做 Stop The World pause全线停顿),而可以安全地暂停线程的点叫做安全点(safe point),然后,JVM 就可以专心执行清理工作。安全点可能有多种因素触发,当前,GC 是触发安全点最常见的原因。

    此阶段暂停的时间,与堆内存大小,对象的总数没有直接关系,而是由存活对象(alive objects)的数量来决定。所以增加堆内存的大小并不会直接影响标记阶段占用的时间。

    标记 阶段完成后,GC 进行下一步操作,删除不可达对象。

    删除不可达对象(Removing Unused Objects)

    各种 GC 算法在删除不可达对象时略有不同,但总体可分为三类:清除(sweeping)、整理(compacting)和复制(copying)。[下一小节] 将详细讲解这些算法。

    清除(Sweeping)

    **Mark and Sweep(标记—清除)**算法的概念非常简单:直接忽略所有的垃圾。也就是说在标记阶段完成后,所有不可达对象占用的内存空间,都被认为是空闲的,因此可以用来分配新对象。

    这种算法需要使用空闲表(free-list),来记录所有的空闲区域,以及每个区域的大小。维护空闲表增加了对象分配时的开销。此外还存在另一个弱点 —— 明明还有很多空闲内存,却可能没有一个区域的大小能够存放需要分配的对象,从而导致分配失败(在 Java 中就是 OutOfMemoryError)。

    6898662.png

    整理(Compacting)

    标记—清除—整理算法(Mark-Sweep-Compact),将所有被标记的对象(存活对象),迁移到内存空间的起始处,消除了“标记—清除算法”的缺点。

    相应的缺点就是 GC 暂停时间会增加,因为需要将所有对象复制到另一个地方,然后修改指向这些对象的引用。

    此算法的优势也很明显,碎片整理之后,分配新对象就很简单,只需要通过指针碰撞(pointer bumping)即可。使用这种算法,内存空间剩余的容量一直是清楚的,不会再导致内存碎片问题。

    7068361.png

    复制(Copying)

    **标记—复制算法(Mark and Copy)**和“标记—整理算法”(Mark and Compact)十分相似:两者都会移动所有存活的对象。区别在于,“标记—复制算法”是将内存移动到另外一个空间:存活区。“标记—复制方法”的优点在于:标记和复制可以同时进行。缺点则是需要一个额外的内存区间,来存放所有的存活对象。

    7149973.png

    展开全文
  • 常见的gc算法有哪些? java garbage collection是一个自动进程,用于管理程序使用的运行时内存。通过自动执行JVM,可以减轻程序中分配和释放内存资源的开销。 垃圾回收机制是由垃圾回收器Garbage Collection来...

    常见的gc算法有哪些?

     java garbage collection是一个自动进程,用于管理程序使用的运行时内存。通过自动执行JVM,可以减轻程序中分配和释放内存资源的开销。

     

    垃圾回收机制是由垃圾回收器Garbage Collection来实现的。GC是后台的守护进程,它的特别之处是它是一个低优先级进程。但是可以根据内存的使用情况动态的调整他的优先级,因此,它是内存中低到一定程度时,才会自动运行,从而实现对内存的回收,这就是垃圾回收的时间不确定的原因。这个服务不是我们启动的是自动启动的。

     

    程序运行期间,所有对象实例存储在运行时数据区域的heap中,当一个对象不再被引用(使用),他就需要被回收,在GC过程中,这些不需要被使用的对象从heap中回收,这样就会有空间循环被利用。

    引用计数法

    简单但是速度很慢,缺陷是不能处理循环引用的情况。 
    原理:此对象有一个引用,既增加一个计数器,删除一个引用减少一个计数器,垃圾回收时,只回收计数器为0的对象,此算法最致命的是无法处理循环引用的情况

    3.标记-清除算法

    标记清除算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

    之所以说他是最基础的收集算法,是因为后续的收集算法都是基于这种思路并且对其不足进行改进而得到的。

    它的主要不足有两个:
    1. 一个是效率问题,标记和清除两个过程的效率都不高
    2. 另一个是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续的内存而不得不提前触发另一次垃圾收集动作。

    4.复制算法

    为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用的内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。只要移动堆指针,按照顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半。

    5. 标记-整理算法

    复制收集算法在对象存活率较高的时候,就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有对象都是100%存活的极端情况,所以在老年代一般不能直接选用这种算法。根据老年代的特点。有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与”标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界意外的内存。

    6.分代收集算法

    这种算法并没有什么新的思路,只是根据对象的存活周期的不同,将内存划分为几块。一般是把java堆分成新生代和老年代,这样就可以根绝各个年代的特点采取最适当的收集算法。在新生代中,每次垃圾回收时都发现大批对象的死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用”标记-清理”或者“标记-整理”算法来进行回收。

    7.系统线程划分

    串行收集器

    使用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高。但是,也无法使用多处理器的优势。所以此收集器适合单处理器的机器。当然,此收集器也可以用在小数量(100M左右)情况下的多处理器机器上,可以使用-XX:+UseSerialGC打开。

    并行收集器

    对年轻代进行并行垃圾回收,因此可以减少垃圾回收的时间,一般在多线程多处理器上使用,使用-XX:+UseParallelGC打开。年老代进行并行收集。如果年老代不使用并发收集的话,是使用单线程进行垃圾回收,因此会制约扩展能力,使用-XX:+UseParallelOldGC打开。

    使用-XX:ParallelGCThreads=设置并行垃圾回收的线程数,此值可以设置与机器处理数量相等。
    此收集器可以进行如下配置:
    最大垃圾回收暂停:指定垃圾回收时的最大暂停时间,通过-XX:MaxGCPauseMillis=指定。为毫秒数,如果指定了这个值的话,堆大小和垃圾回收相关参数会进行调整以达到指定值。此值可能会减少应用的吞吐量。吞吐量:吞吐量为垃圾回收时间与非垃圾回收时间的比值。通过-XX:GCTimeRatio=来设定,公式为1/(1+N),例如,-XX:GCTimeRatio=19时,表示5%的时间用于垃圾回收,默认情况为99,既1%的时间用于垃圾回收。

    并发收集器

    可以保证大部分工作都并发执行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中,大型应用,使用-XX:+UseConcMarkSweepGC打开。

    并发收集器主要减少年老代的暂停时间,他在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象,在每个年老代垃圾回收周期中,在收集初期并发收集器会对整个应用进行简短的暂停,在收集中还会再暂停一次,第二次暂停会比第一次长,在此过程中多个线程同时进行垃圾回收工作。

    并发收集器使用处理器换来短暂的停顿时间,在一个N个处理器的系统上,并发收集部分使用K/N个可用处理器进行回收,一般情况下1<=K<=N/4

    在只有一个处理器的主机上使用并发收集器,设置为incremental mode模式也可以获得较短的停顿时间。

    浮动垃圾:由于在应用运行的同时进行垃圾回收,所以有些垃圾回收可能在垃圾回收完成时产生,这样就造成了“floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉,所以并发收集器一般需要20%的预留空间用于这些浮动垃圾。

    Concurrent Mode Failure:并发收集器在应用程序运行时进行收集,所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先充满了,这种情况下将会发生“并发模式失败”,此时整个应用将会暂停。进行垃圾回收。

    启动并发收集器:因为并发收集在应用运行时进行收集,睡衣必须保证收集完成之前有足够的内存空间供程序使用,否则会出现“Concurrent Mode Failure”,通过设置-XX:CMSinitiatingOccupancyFraction=指定还有多少剩余堆时开始执行并发收集。

    并行:多个事件同一时间发生,同时做多件事
    并发:多个事件在同一个时间间隔内发生。每年11.11日狂欢节,一天内接受的最大人数。

     

    来源自:
    ---------------------
    作者:九师兄-梁川川
    来源:CSDN
    原文:https://blog.csdn.net/qq_21383435/article/details/80473540
    版权声明:本文为博主原创文章,转载请附上博文链接!

    转载于:https://www.cnblogs.com/Tpf386/p/11210483.html

    展开全文
  • JVM常见的GC算法

    2020-07-18 08:52:46
    如今,垃圾收集几乎称为现代语言标配,及时经过如此长时间发展,Java垃圾收集机制仍然在不断演进中,不同大小设备、不同特征应用场景,堆垃圾收集提出了新挑战。 什么是垃圾 垃圾是指在运行程序中...

    JVM 垃圾回收概述

    垃圾收集机制是Java的招牌能力,极大的提高了开发效率。如今,垃圾收集几乎称为现代语言的标配,及时经过如此长时间的发展,Java的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战。

    在这里插入图片描述

    什么是垃圾

    • 垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
    • 如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一致保存到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出

    早期垃圾回收

    在早期的C/C++时代,垃圾回收基本上是手工进行的。开发人员可以使用new关键字进行内存申请,并使用delete关键字进行内存释放。

    垃圾回收算法

    在这里插入图片描述
    在这里插入图片描述

    垃圾标记阶段:引用计数法

    在堆中存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是死亡对象。只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉起所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。

    引用计数法每个对象保存一个整形的引用计数器属性。用于记录对象被引用的情况。

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

    • 优点:实现简单,垃圾对象便于表示,判定效率高,回收没有延迟性

    • 缺点:

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

    在这里插入图片描述

    引发内存泄漏

    public class RefCountGC{
        private byte[] bigSize = new byte[5*1024*1024];
        Object reference = null;
        public static void main(String[] args){
            RefCountGC obj1 = new RefCountGC();
            RefCountGC obj2 = new RefCountGC();
            obj1.reference = obj2;
            obj2.reference = obj1;
            obj1 = null;
            obj2 = null;
            System.gc();
        }
    }
    

    python 如何解决引用计数法的循环引用

    • 手动解除,就是在合适的实际,解除引用关系
    • 使用弱引用weakref,weakref是python提供的标准库,旨在解决循环引用。

    可达性分析算法(跟搜索算法,追踪性垃圾收集)

    • 相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效的解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。

    • 基本思路

      • 可达性分析算法是以根对象集合为起始点,按照从上到下的方式搜索被跟对象集合所连接的目标对象是否可达。
      • 使用可达性分析算法后,内存中存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链。
      • 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。
      • 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
    • 在Java语言中,GC Roots包括以下几类元素:

      • 虚拟机栈中引用的对象
        • 各个线程被调用的方法中使用到的参数、局部变量等
      • 本地方法栈内JNI引用的对象
      • 方法区中类静态属性引用的对象(JDK7之后放在堆空间中)
      • 方法区中常量引用的对象
      • 所有被同步锁synchronized持有的对象
      • Java虚拟机内部的引用。
      • 反应java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
    • 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须要一个能保证一致性的快照中进行,这点不满足的话分析结果的准确性就无法保证。

    • 枚举根节点是必须要停顿。

    对象的finalization机制

    • Java语言提供了对象终止机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
    • 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会调用这个对象的finalize()方法
    • finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源和清理工作,比如关闭文件,套接字和数据库连接等。
    • 不要主动调用finalize()方法。
      • 在finalize()是可能会导致对象复活
      • finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()没有执行机会
      • 一个糟糕的finalize会严重影响GC的性能
    • 从功能上来说,finalize()方法与C++中的析构函数比较类似,但是Java采用的是基于垃圾回收器的自动内存管理机制,所以finalize()方法本质上不同于c++中的析构函数
    • 由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了,但是,该对象并非非死不可,这时候他们暂时处于缓刑阶段。一个无法触及的对象有可能在某一个条件下复活自己,如果这样,那么对他的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。
      • 可触及的,从根节点开始,可以到达这个对象。
      • 可复活的,对象的所有引用都被释放,但是对象有可能在finalize()中复活。
      • 不可触及的,对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。
    • 以上3中状态中,由于finalize()方法的存在进行的区分。只有在对象不可触及是才可以被回收。

    具体过程

    判断一个对象obj是否可回收,至少要经历两次标记过程:

    1. 如果对象objA到GC Roots没有引用链,则进行第一次标记。
    2. 进行筛选,判断此对象是否有必要执行finalize()方法。
      1. 如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判为不可触及的。
      2. 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发器finalize()方法执行。
      3. finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记的时候,objA会被溢出即将回收集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接编程不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。
    public class CanReliveObe{
        public static CanReliveObj obj;
        @Override
        protected void finalize() throws Throwable{
            super.finalize();
            System.out.println("调用当前类重写finalize()方法");
            obj = this;
        }
    }
    //复活过程
    

    清除阶段:标记-清除算法(Mark-Sweep)

    当区分出内存中存活对象和死亡对象之后,GC接下来的任务就是执行垃圾回收,释放掉无用的对象所占用的内存空间,以便由足够的可用内存空间为新对象分配内存。

    目前在JVM中比较常见的三种垃圾收集算法是标记清除算法,复制算法,标记压缩算法

    执行过程

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

    • 标记:Collertor从引用根节点开始遍历,标记所有被引用的对象。一般是对象的Header中记录为可达对象。
    • 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在Header中没有标记为可达对象,就将其回收。

    在这里插入图片描述

    • 缺点
      • 效率不高
      • 在进行GC的时候,需要停止整个应用程序,导致用户体验差
      • 这种方式清理的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表
    • 什么是清除
      • 这里所谓的清除并不是真的置空,而是把需要清楚的对象地址保存在空闲的地址列表里面。下次由新对象需要加载时,判断垃圾的位置空间是否足够,如果够,就存放。

    复制(Copying)算法

    核心思想

    将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的对象,交换两个内存的角色,最后完成垃圾回收。

    在这里插入图片描述

    • 优点
      • 没有标记和清除过程,实现简单,运行高效
      • 复制以后保证空间的连续性,不会出现“碎片问题”。
    • 缺点
      • 此算法的缺点也是很明显的,就是需要两倍的内存空间。
      • 对于G1这种分拆称为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用而且时间开销也不小。

    在这里插入图片描述

    标记-压缩算法(Mark-Compact)

    执行过程

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

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

    在这里插入图片描述

    标记-压缩算法的最终效果等同于标记-清除算法执行完成后,在进行一次内存碎片整理,因此,也可以把他称为标记-清除-压缩算法

    二者的本质差异在于标记清除算法是一种非移动式的回收算法,标记压缩式移动式的,是否移动回收后的存活对象式一项优缺点并存的风险决策。

    可以看到,标记的存活对象将会被整理,按照内存地址一次排列,而未被标记的内存会被清理掉,入磁依赖,当我们需要给新对象分配内存时,JVM只需要持有一个内存的其实地址即可,这比维护一个空闲列表显然少了许多开销。

    标记清除算法需要使用空闲列表,复制算法和标记压缩算法可以使用指针碰撞。

    小结

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-igZvWuIY-1595033219707)(C:\Users\lym\AppData\Roaming\Typora\typora-user-images\image-20200705183637373.png)]

    从效率上将,复制算法是最快的,但是浪费了太多内存

    为了兼顾上面提到的三个指标,标记整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记清除多了一个整理内存的阶段。

    分代收集算法

    把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。

    目前几乎所有的GC都是采用分代收集算法执行垃圾回收的。

    在这里插入图片描述

    CMS是针对老年代的垃圾收集算法,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高,而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器作为补偿措施,当内存回收不佳时,将采用SerialOld执行Full GC以达到对老年代内存的整理。

    在这里插入图片描述
    在这里插入图片描述

    分区算法

    在这里插入图片描述

    展开全文
  • java之常见的gc算法

    千次阅读 2019-05-27 23:13:59
    引用计数法 Reference Counting ...根搜索算法 GC Roots Tracing 以一系列叫“GC Roots”对象为起点开始向下搜索,走过路径称为引用链(Reference Chain),当一个对象没有和任何引用链相连...

    引用计数法 Reference Counting

    给对象添加一个引用计数器,每过一个引用计数器值就+1,少一个引用就-1。当它的引用变为0时,该对象就不能再被使用。它的实现简单,但是不能解决互相循环引用的问题。

    根搜索算法 GC Roots Tracing

    以一系列叫“GC Roots”的对象为起点开始向下搜索,走过的路径称为引用链(Reference Chain),当一个对象没有和任何引用链相连时,证明此对象是不可用的,用图论的说法是不可达的。那么它就会被判定为是可回收的对象。

    JAVA里可作为GC Roots的对象
    虚拟机栈(栈帧中的本地变量表)中引用的对象
    方法区中的类静态属性引用的对象
    方法区中的常量引用的对象
    本地方法栈中JNI(即Native方法)的引用的对象

    标记-清除算法 Mark-Sweep

    这是一个非常基本的GC算法,它是现代GC算法的思想基础,分为标记和清除两个阶段:先把所有活动的对象标记出来,然后把没有被标记的对象统一清除掉。但是它有两个问题,一是效率问题,两个过程的效率都不高。二是空间问题,清除之后会产生大量不连续的内存。
    在这里插入图片描述

    复制算法 Copying

    复制算法是将原有的内存空间分成两块,每次只使用其中的一块。在GC时,将正在使用的内存块中的存活对象复制到未使用的那一块中,然后清除正在使用的内存块中的所有对象,并交换两块内存的角色,完成一次垃圾回收。它比标记-清除算法要高效,但不适用于存活对象较多的内存,因为复制的时候会有较多的时间消耗。它的致命缺点是会有一半的内存浪费。
    在这里插入图片描述

    标记整理算法 Mark-Compact

    标记整理算法适用于存活对象较多的场合,它的标记阶段和标记-清除算法中的一样。整理阶段是将所有存活的对象压缩到内存的一端,之后清理边界外所有的空间。它的效率也不高。
    在这里插入图片描述
    参考链接:
    https://blog.csdn.net/weixin_42045591/article/details/80629449

    展开全文
  • JVM6:常见的GC算法

    2021-02-19 18:48:10
    标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行实现是,在标记阶段,首先通过根节点,标记所有从根节点开始可达对象。因此,未被标记对象...
  • 1、复制算法 将内存平分为两部分(from(a)和to(b),左a右b),假设最开始是使用a,预留b,当a内存不够时,就将a中不可回收对象复制到b中,然后a中所有对象清空,此时内存使用情况变成使用b。预留a。 图解 ...
  • 1. 标记清除算法:将所有需要回收对象进行标记,标记结束以后对标记对象进行回收,但是效率低,会造成大量碎片。 2. 复制算法:复制算法将空间分为两部分,每次只使用一部分。当一块空间用完了,就将这块还存活...
  • 一、在java中如何判断对象是否死亡?死亡意味着可以被回收 1、引用计数法算法: 给每一个对象添加一个引用计数器,当一个引用指向对象,计数器值...java语言在可作为GC Roots对象包括以下几种:为什么? 1、虚拟机栈
  • 常见GC算法

    2018-06-03 15:20:31
    下面介绍几种常见的GC算法。引用计数法 Reference Counting给对象添加一个引用计数器,每过一个引用计数器值就+1,少一个引用就-1。当它的引用变为0时,该对象就不能再被使用。它的实现简单,但是不能解决互相循环...
  • 今天和大家一起解析下常见的GC算法设计。 什么是GC GC是一种软件进行自动的内存回收的方式。 如果软件运行过程中,发现某些对象没有了引用(或者称之为不可达)的状态时,就会启动GC过程。将这部分内存进行释放。以...
  • 常见GC算法介绍

    2020-09-01 17:34:05
    GC四大算法概述引用计数法复制算法(Copying)标记清除(Mark-Sweep)标记压缩算法(Mark-Compact)总结 ...普通GC(minor GC):只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活
  • 常见gc算法

    2017-05-19 08:30:29
    引用计数算法 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0对象就是不可能再被使用。 缺点:引用和去引用伴随加法和减法,影响性能。 ...
  • 用任何带 GC 的语言最后都要直面 GC 问题。在以前学习 C# 的时候就被迫...趁着这个机会我总结了一下常见的 GC 算法。分别是:引用计数法、Mark-Sweep法、三色标记法、分代收集法。   1. 引用计数法 原理是在每个...
  • 几种常见GC算法介绍

    万次阅读 多人点赞 2019-02-21 23:49:16
    本文主要是对常用的GC算法(引用计数法、标记-清除法、复制算法、标记-清除算法)作出相关的说明,并对相关知识做简单的介绍。 一、什么是堆?  堆指用于动态(即执行程序时)存放对象的内存空间。而这个对象,在...
  • gc算法与种类

    2017-11-21 11:45:23
    介绍常见的gc算法引用计数法: 标记清除 标记压缩 复制算法 原理讲解
  • JVM常见4种GC算法

    2020-05-12 10:03:20
    JVM常见GC算法1.标记-清除算法(Mark-Sweep)2.标记-整理(压缩)算法(Mark-Compact)3.复制算法(Copying)4.分代收集(Generational Collecting)算法 1.标记-清除算法(Mark-Sweep) 算法分为标记和清除两个...
  • GC算法介绍

    2018-08-15 10:11:09
    本文将为你介绍常见的GC算法。 概述 常见的GC算法主要有三种:标记-清除算法、标记-整理算法、复制算法。还有一种分代收集算法,这种算法无非就是对内存的不同区域使用前面三种不同的算法。 这三种GC算法总体而言,...
  • JVM中的GC算法

    2019-06-05 17:26:12
    下面介绍几种常见的GC算法。 引用计数法 Reference Counting 给对象添加一个引用计数器,每过一个引用计数器值就+1,少一个引用就-1。当它的引用变为0时,该对象就不能再被使用。它的实现简单,...
  • GC常见的算法

    万次阅读 热门讨论 2018-05-27 22:22:19
    1.GC (Garbage Collection) java garbage collection is an automatic process to manage the runtime memory used by programs.by doing it automatic JVM relieves the programer of the overhead of assigning ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 761
精华内容 304
关键字:

常见的gc算法