ClassLoader 类加载器
什么是类加载器
- 用于将class文件将文件读取到内存中
读取class文件的来源:
1、自己写的java代码 编译成class文件
2、通过网络方式下载class文件
3、war、jar包解压之后的class文件
4、从数据库中读取class文件
5、Java动态代理模式,反射/cglib 生成代理class
类加载的流程
类加载的步骤
其中loadClass的类加载过程有如下几步:
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载
- 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的 java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
- 验证:校验字节码文件的正确性
- 准备:给类的静态变量分配内存,并赋予默认值
- 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用
- 初始化:对类的静态变量初始化为指定的值,执行静态代码块
类被加载到方法区中后主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。
注意,主类在运行过程中如果使用到其它类,会逐步加载这些类。
懒加载方式:jar包或war包里的类不是一次性全部加载的,是使用到时才加载
类加载器的分类
1、启动(引导)类加载器 (使用C++编写)
- 读取 $JAAVA_HOME/jre/lib 下的文件
- 注意:当获取类加载器信息为null时,代表在启动类加载器中(因为是C++写的,C++中不可能有Java对象信息)
2、扩展类加载器
- 读取 $JAAVA_HOME/jre/lib/ext 下的文件
3、应用类加载器
- 读取当前项目所引用或定义的jar包和class和文件
4、自定义类加载器
- 读取class文件范围,都是属于自定义
每个类加载器的职责(读取范围)不一样
注意:这几种类加载器,并不是继承关系
1、这几种类加载器,并不是继承关系,而是ClassLoader中有一个属性,parent,此属性中设置了类加载器的上级加载器
2、如果为扩展类加载器,则上级加载器为null。因为引导类加载器是C++对象,Java中读取不到,所以为null
3、当parent为空时,则会去引导类加载器中尝试加载
双亲委派机制
- 加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。说简单点就是,先找父亲加载,不行再由儿子自己加载
当类加载器收到请求时
首先会依次向上查找最顶层的父类加载器(启动加载器)
再由上而下依次读取class文件
如果该类加载器已经读取到class文件,那么子节点不会再读取
为什么设计双亲委派机制
- 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
- 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
打破双亲委派机制
- 方法:创建自定义MyClassLoader类,继承ClassLoader,重写loadClass()方法,修改下图中向上委托的代码
Tomcat打破双亲委派机制
我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:
1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的 不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是 独立的,保证相互隔离。
2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程 序,那么要有10份相同的类库加载进虚拟机。
3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的 类库和程序的类库隔离开来。 4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中 运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。
Tomcat 如果使用默认的双亲委派类加载机制行不行?
答案是不行的。为什么?
第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认 的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
第三个问题和第一个问题一样。
第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp 文件其实也就是class文 件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp 是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想 到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件
Java内存结构
堆
- 可见范围:所有线程共享
- 存放 new实例、数组,对线程共享(会产生线程安全问题)
- 在JMM中,表示主内存
- 内存释放:当数组和对象在没有引用变量指向它的时候,才变成垃圾,不能再被使用,但是仍然占着内存,在随后的一个不确定的时间被垃圾回收器GC释放掉。
栈(线程栈)
- 生命周期:Java虚拟机栈是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭),每个线程都有自己独立的栈空间,相互间不影响
- 存放内容:基本数据类型与对象引用(内存地址)
- 内存释放:当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。
- 可见范围:线程独享
栈贞
- 栈帧:线程中,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)
- 栈帧内容:局部变量表、操作数栈、动态链接、方法返回地址和一些附加信息
局部变量表
- 局部变量表(Local Variables)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了方法所需的分配的局部变量表的最大容量。
操作数栈
- 操作数栈(Operand Stack)也常被成为操作栈,是一个后入先出栈,用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。
1、将局部变量的value压入操作数栈
2、从操作数栈中取值,并在cpu中计算
3、将计算后的值存入操作数栈
4、操作数栈出栈,重新赋值给局部变量
动态链接
- 动态链接:如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期间调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
- 静态链接:当一个字节码文件被装载进jvm内部时,如果被调用的目标方法在编译期可知,且运行期保持不变。为静态链接。
方法返回地址
- 退出方法的两种方式:
- 1、正常退出:引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层方法的调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定
- 2、异常退出:在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,就会导致方法退出。一个方法使用异常完成出口的方式退出,是不会给上层调用者产生任何返回值的。
- 无论采用何种退出方式,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法返回地址则是记录方法被调用的位置
附加信息
- 虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。
元空间(方法区)
- 可见范围:所有线程共享
- 存储内容:是用来存放有关 类、方法 信息的一块逻辑上的区域,除此之外,还用来存储常量、静态变量,以及一些代码缓存等数据。这些信息几乎不变;不怎么依赖于对象,独立。
- 在JMM中,表示工作内存
存放内容:
1、局部变量
2、栈贞 记录方法的信息
3、栈操作
栈贞:
每个方法,都会有一个栈贞,生命周期与方法一致
当方法结束时,或者方法出现异常停止时,栈贞销毁
本地方法栈
- Java代码调用C语言的代码 gni
方法区(元空间)
- 存放类的信息、常量、静态变量、运行时常量,对线程共享
程序计数器
- 每个线程中,都有独立的程序计数器
- 记录当前线程执行的行号,只有在多线程中,才存在作用
在多线程的情况下,记录多线程之间切换上下文的位置
线程切换时,通过程序计数器来确定从哪一行继续执行
类加载器
- 用于将class文件将文件读取到内存中
创作不易,你的支持,我的动力
更多IT知识,技术内容,技术书籍,IT体系网课,请关注vx公众号:
豆萌萌 网课大咖
对象的创建
步骤
- 1.类加载检查:虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
- 2.分配内存 :在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
- 划分内存的方法:
- “指针碰撞”(Bump the Pointer)
- (默认用指针碰撞) 如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。.
- “空闲列表”(Free List)
- 如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录
- “指针碰撞”(Bump the Pointer)
- 内存划分时的并发问题:可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
- 划分内存的方法:
解决分配内存并发问题:
1、CAS
2、本地线程分配缓冲,每个线程预分配一块空间进行内存分配,不会影响到其他线程,故解决并发问题。
- 3.初始化 :内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
- 4.设置对象头 :初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
- 5.执行<init>方法:即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值,执行其构造方法。
对象分配
对象栈上分配
对象逃逸分析
- 就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。
- 无返回值的方法,可以确定对象的作用域
- 有返回值的方法,则该返回对象的作用域不确定
标量替换
- 当对象经过逃逸分析确定不会被外部访问,并且对象可以进一步分解时,JVM不会创建该对象,而是将所有成员变量拆分成该对象的若干代替成员,为这些代替成员分配内存空间,避免因没有一大块连续内存空间导致对象不够分配,JDK7默认开启。
标量与聚合量
- 标量:Java中不可被进一步分解的量,如基本数据类型;
- 聚合量:Java中还可以被进一步分解的量,如Java对象。
经测试,开启标量替换有助于减少GC频率,提高程序性能
栈上分配依赖于逃逸分析和标量替换
对象Eden区分配
年轻代
- Eden:伊甸园区
- Survivor:幸存者区
- 有两个Survivor区,分别为 from 与 to
- Eden与Survivor比例:默认 8:1:1
- 因为新生代的对象都是朝生夕死的,存活时间很短,所
以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可
- 大部分情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
Minor GC
- 指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
Full GC
- 一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上。
当Eden对象满时,会触发Minor GC,将所有存活的对象放入一个Survivor中,清空Eden与另一个Survivor,当下一次Eden满了,触发Minor GC时,同样将Eden与Survivor存活对象,放入空的Survivor中,清空Eden与另一个Survivor。
- 存活的对象在两个Survivor中来回存放
大对象直接进入老年代
- 原因:为了避免为大对象分配内存时的复制操作而降低效率。
- 大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
设置大对象的大小(Serial 和ParNew两个收集器下有效)
-XX:PretenureSizeThreshold
- 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代。
长期存活对象进入老年代
- Age:对象年龄计数器(默认超过15岁放入老年代)
对象每经过一次Minor GC后仍然存活,则Age年龄+1
设置晋升年龄阈值参数:
-XX:MaxTenuringThreshold
对象动态年龄判断
当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的 50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了, 例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会 把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年 龄判断机制一般是在minor gc之后触发的。
老年代空间分配担保机制
- 年轻代进行Minor GC前,JVM会计算老年代剩余可用空间,如果剩余可用空间小于年轻代所有对象大小之和(包括垃圾对象),则会触发一次Full GC
-XX:-HandlePromotionFailure
设置此参数情况下:
老年代剩余空间 > 历次Minor GC时存入老年代对象空间大小平均值
则不会Full GC
- 如果设置了上述参数(JDK1.8默认设置),则会比较老年代可用内存大小,是否大于之前每次Minor GC后进入老年代对象的平均大小
创作不易,你的支持,我的动力
更多IT知识,技术内容,技术书籍,IT体系网课,请关注vx公众号:
豆萌萌 网课大咖
对象内存回收
引用计数法
- 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就+1;当引用失效,计数器就-1,任何时候计数器为0的对象就是不可能再被使用的。
- 优点:简单、效率高
- 缺点:循环依赖情况下,计数器不会为0,则不会通知GC回收它们
可达性分析算法
- 将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。
GC Roots
- 根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
finalize()方法最终判定对象是否存活
- 即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
- 1. 第一次标记
- 第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法,对象将直接被回收。
- 2、第二次标记
- 如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可移除“即将回收”集合中,如果这时对象未逃脱,基本上它就真的被回收了。
注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。
如何判断一个类是无用类
需同时满足以下三个条件,才可以算“无用类”
1、该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
2、加载该类的 ClassLoader 已经被回收。
3、该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾收集
垃圾收集算法
分代收集理论
- 根据年轻代和老年代的特点,来选择合适的收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。
一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
1、比如在新生代中,每次收集都会有大量对象(近99%)死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可 以完成每次垃圾收集。
2、而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选 择“标记-清除”或“标记-整理”算法进行垃圾收集。
注意,“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上
标记-复制 算法
- 为了解决效率问题而出现,它将内存一分为二,每次只使用其中的一块,当这一块空间用完后,将存活的对象复制到另一块中,然后将空间清理掉,每次垃圾回收都是对内存的一半空间进行回收。
- 优点:效率高
- 缺点:空间利用低,只能使用内存一半的空间
标记-清除 算法
- 算法分为“标记”、“清除”两个阶段:标记存活的对象,统一回收未被标记的对象(或者标记垃圾对象,再将标记的所有对象清除),这种算法比较常见。
- 优点:空间利用率相比标记复制高
- 缺点:
- 效率问题(如果需要标记的对象太多,效率不高)
- 空间问题(会产生大量不连续的内存碎片)
标记-整理 算法
- 根据老年代的特点特出的一种标记算法,标记过程与“标记-清除”算法一致,但后续步骤不是直接对可回收对象回收,而是让所有存活对象移动向一端,然后清理边界以外的内存。
垃圾收集器
- 如果说垃圾收集算法是垃圾收集的理论,那么垃圾收集器就是内存回收的具体实现,需要根据不同的场景,来选择不同的垃圾收集器
Serial收集器 (串行收集器)
- 开启参数:
- 开启年轻代:-XX:+UseSerialGC
- 开启老年代:-XX:+UseSerialOldGC
- Serial(串行)收集器是最基本、历史最悠久的垃圾收集器。它的 “单线程” 的意义不仅仅意味着它只会用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。
新生代采用复制算法
老年代采用标记-整理算法。
在单线程垃圾收集器中,效率比较高
- Serial Old收集器,是Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。
Parallel Scavenge 收集器 (并行收集器)
- Parallel收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器类似。默认的收集线程数跟cpu核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。
CMS等垃圾收集器的关注点更多的是用户线程的停 顿时间(提高用户体验)。
所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。
Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,可以 选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用复制算法,老年代采用标记-整理算法。
ParNew 收集器
- ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。
- 开启参数:-XX:+UseParNewGC
CMS 收集器
- CMS(Concurrent Mark Sweep 并发标记清除)收集器是一种以获取最短回收停顿时间为目标的收集器,注重用户体验。它是HotSpot虚拟机第一款真正意义上的并发收集器,第一次实现垃圾收集器与用户线程同时工作。
- 垃圾清理步骤
- 初始标记:暂停所有其他线程(Stop The World),记录下GC ROOT能直接引用的对象,速度快
- 并发标记:从GC ROOT的直接关联对象开始遍历整个对象图,过程耗时较长,但并不需要停顿用户线程。同时,因为用户线程继续运行,有可能导致标记过的对象状态发生改变
- 重新标记:暂停所有其他线程(Stop The World),修正并发标记期间产生变动的对象标记记录,时间略长于初始标记,但远远短于并发标记。主要用到三色标记里的增量更新算法做重新标记
- 并发清理:开启用户线程,同时GC线程开始对未标记区域清扫,如果有新增对象标记为黑色,则不作任何处理。
- 并发重置:重置GC过程中的标记数据。
- 核心参数
- 1. -XX:+UseConcMarkSweepGC:启用cms
- 2. -XX:ConcGCThreads:并发的GC线程数
- 3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
- 4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
- 5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
- 6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-X:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
- 7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
- 8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
- 9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
- 缺点
- 1、对CPU资源敏感(会和服务抢资源);
- 2、无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了);
- 3、它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数可以解决;
- 4、上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收 。
三色标记
三色标记颜色
- 黑色:已经被垃圾收集器访问过,且这个对象的引用对象都已经扫描过。如果有其他对象引用指向了黑色对象,无需重新扫描一遍。
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象至少存在一个引用还没有被扫描过。
- 白色:表示对象尚未被垃圾收集器访问过。最开始对象默认为白色。若在分析结束阶段,仍然是白色对象,则代表不可达,会被回收。
多标-浮动垃圾
- 在并发标记过程中,如果方法结束,导致部分局部变量(GC ROOT)被销毁,这个GC ROOT引用的对象之前又被扫描过(标记为垃圾对象),那么本轮不会回收这部分内存。这部分本应回收但并未回收到的内存,被称为“浮动垃圾”,需要等待下一轮再进行回收。
漏标-读写屏障
- 漏标会导致被引用的对象被当成垃圾误删除,这是严重的Bug,有两种解决方案。增量更新与原始快照
增量更新
- 当黑色对象插入新的指向白色对象的引用关系式,就将这个新的引用记录下来,装进集合中,并将这个黑色对象标记为灰色。等并发扫描结束后,再将集合中记录的引用源头对象,作为扫描起点,重新扫描一次。
原始快照
- 原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色,目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾) ,以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。
创作不易,你的支持,我的动力
更多IT知识,技术内容,技术书籍,IT体系网课,请关注vx公众号:
豆萌萌 网课大咖