zoukankan      html  css  js  c++  java
  • 2、带着问题学习JVM_对象存活_垃圾回收的内容_理论_收集算法

      带着问题学习:深入理解Java虚拟机 JVM高级特性与最佳实践第3版,本篇博文从为什么要学习垃圾回收?如何判断对象是否为垃圾?垃圾回收都发生在哪些地方?从不同维度学习垃圾回收理论?当前的垃圾收集算法有哪些?垃圾收集器有哪些?及垃圾收集器优劣的衡量标准是什么?为了避免本篇篇幅过长后两个问题将在下一篇进行学习。
    1、内存动态分配与内存回收技术已经相当成熟,为什么我们还要去了解垃圾收集和内存分配?
    1)当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。
    2)运行时数据区域中的线程私有区域的内存分配和回收都具备确定性,随线程而生随线程而亡,不需要过多的关注垃圾回收问题。
    3)Java堆和方法区这两个区域则有着很显著的不确定性只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理

    2、如何判断对象是否存活?
    1)引用计数算法
    2)可达性分析算法
    这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

    固定可作为GC Roots的对象包括以下六种:
    a.虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
    b.本地方法栈中JNI引用的对象
    c.方法区中类静态属性引用的对象、常量引用的对象
    d.虚拟机内部的引用(如基本数据类型对应的Class对象、一些常驻的异常对象、系统类加载器)
    e.所有被同步锁(synchronized关键字)持有的对象
    f.反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

    3、引用的含义及分类:
    1)在JDK 1.2版之前,如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。
    2)在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。
    a.强引用:
    是在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回
    收掉被引用的对象。

    b.软引用:
    是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
    在JDK 1.2版之后提供了SoftReference类来实现软引用。

    c.弱引用:
    也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
    在JDK 1.2版之后提供了WeakReference类来实现弱引用。

    d.虚引用:
    也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能
    在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用

    4、谈谈方法区中的垃圾回收内容:
    《Java虚拟机规范》中提出不要求虚拟机在方法区中实现垃圾收集,事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期的ZGC收集器就不支持类卸载)
    方法区中的垃圾回收:主要包含两部分内容废弃的常量和不再使用的类型。
    1)废弃常量如何回收:(以常量池中的java 字符串为例)
    已经没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似。
    2)不再被使用的类如何回收:(至少需要满足一下三个条件)
    a.该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
    b.加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
    c.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    规范规定对满足上述三个条件的无用类是允许进行回收,而不是没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class、-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类加载和卸载信息。
    在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

    5、你知道的垃圾收集理论有哪些?
    1)从如何判定对象消亡的角度出发,垃圾收集算法的分类
    a.“引用计数式垃圾收集”(Reference Counting GC)被称为“直接垃圾收集”,其在主流的Java虚拟机中均未涉及。
    b.“追踪式垃圾收集”(Tracing GC)也被称为“间接垃圾收集”

    2)分代收集理论
    分代收集是建立在以下两个理论基础之上的
    a.弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
    b.强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
    c.跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。这其实是可根据前两条假说逻辑推理得出的隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。如果某个新生代对象存在跨代引用引用某个老年代的对象,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时跨代引用也随即被消除了。

    问题1:谈谈什么是跨代引用?
    所谓跨代引用就是老年代的对象引用了新生代的对象,或者新生代的对象引用了老年代的对象。那对于这种情况我们的GC在进行扫描的时候不可能直接把我们的整个堆都扫描完,那这样效率也太低了。所以这时候就需要开辟了一小块空间,维护这种引用,而不必让GC扫描整个堆区域,而这种小空间就叫做记忆集。

    问题2:谈谈什么是记忆集?
    记忆集也叫rememberSet,垃圾收集器在新生代中建立了记忆集这样的数据结构,用来避免把整个老年代加入到GC ROOTS的扫描范围中。对于记忆集来说,我们可以理解为他是一个抽象类,那么具体实现它的方法将由子类去完成。
    依据跨代引用假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生MinorGC时,只有包含了跨代引用的小块内存里的对象才会被加入到GCRoots进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。
    公司项目组大佬对记忆集的理解:记忆集是非收集区域指向收集区域的指针的集合的数据结构抽象

    问题3:实现记忆集的方式有那些?包含以下三种:
    a.字长精度
    b.对象精度
    c.卡精度(卡表) 卡表(Card Table)是一种对记忆集的具体实现。主要定义了记忆集的记录精度、与堆内存的映射关系等。卡表中的每一个元素都对应着一块特定大小的内存块,这个内存块我们称之为卡页(card page),当存在跨带引用的时候,它会将卡页标记为dirty,那么JVM对于卡页的维护也是通过写屏障的方式。

    3)不同分代垃圾收集的概念划分
    a.部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,部分收集分为以下三种收集:
    新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
    老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
    混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
    b.整堆收集(Full GC): 收集整个Java堆和方法区的垃圾收集


    6、垃圾收集算法:
    1)标记-清除算法(Mark-Sweep)是最基础的收集算法,是因为后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改进而得到的。
    问题1:标记-清除算法如何实现的?
    分为“标记”和“清除”两个阶段实现
    a.首先通过可达性分析标记出所有需要回收的对象;
    b.在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象;

    问题2:标记-清除算法的缺点?
    a.执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
    b.内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

    2)标记-复制算法/半区复制算法(Semispace Copying):简称为复制算法,为了解决标记-清除算法面对大量可回收对象时执行效率低的问题。
    问题1:标记-复制算法如何实现的?
    它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

    问题2:标记-复制算法的缺点?
    a.如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销
    b.将可用内存缩小为了原来的一半,空间浪费未免太多了一点;

    问题2:标记-复制算法的使用?
    现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代。Andrew Appel针对具备“朝生夕灭”特点的对象(98%对象朝夕灭),提出了一种更优化的半区复制分代策略,现在称为“Appel式回收”。HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局。Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间

    3)标记-整理算法(Mark-Compact):是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
    问题1:标记-整理算法的缺点:
    a.移动使的内存回收时会更复杂
    b.如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行,可能会导致“Stop The World”的停顿现象

    问题2:站在标记-整理算法的角度进一步理解标记-清除算法的缺点:
    如果跟标记-清除算法那样完全不考虑移动和整理存活对象的话,弥散于堆中的存活对象导致的空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决。譬如通过“分区空闲分配链表”来解决内存分配问题(计算机硬盘存储大文件就不要求物理连续的磁盘空间,能够在碎片化的硬盘上存储和访问就是通过硬盘分区表实现的)。内存的访问是用户程序最频繁的操作,甚至都没有之一,假如在这个环节上增加了额外的负担,势必会直接影响应用程序的吞吐量。

    备注:针对以上三种垃圾收集算法的思考?
    思考1:基于移动对象使的内存回收时更复杂,不移动则内存分配时会更复杂,那么我们可以从以下停顿时间和吞吐量两个角度来分析:
    a.从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿
    b.但是从整个程序的吞吐量来看,移动对象会更划算。吞吐量的实质是赋值器(“用户程序”或“用户线程”代替)与收集器的效率总和。不移动对象会使得收集器的效率提升一些,但因内存分配和访问相比垃圾收集频率要高得多,这部分的耗时增加,总吞吐量仍然是下降的。

    思考2:不同收集器使用的垃圾回收算法应用实例?
    a.HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的。
    b.关注延迟的CMS收集器则是基于标记-清除算法的
    c.还有一种“和稀泥式”解决方案可以不在内存分配和访问上增加太大额外负担,做法是让虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。基于标记-清除算法的CMS收集器面临空间碎片过多时采用的就是这种处理办法。

    参看书籍:深入理解Java虚拟机:JVM高级特性与最佳实践第3版.pdf,如有需要欢迎留言!
  • 相关阅读:
    技术人生:码农必读
    DDD:子龙关于聚合的总结
    DDD:DomainEvent、ApplicationEvent、Command
    VisualStudio:【外部工具】之代码生成器
    技术人生:为你的决定负责
    DDD:通过四色原型来理解聚合
    DDD:贫血模型和领域模型的一些思考
    TDD:第一次真正使用TDD的感受
    DDD:领域层服务的设计原则
    技术人生:大出着眼 小处着手
  • 原文地址:https://www.cnblogs.com/jiarui-zjb/p/14301646.html
Copyright © 2011-2022 走看看