JAVA对象生命周期(三)-对象的销毁

2021-02-03 01:14

阅读:592

标签:处理   老年   压缩   top   本地   目标   toc   rom   基础   

目录
  • 从引用说起
    • 指针直接引用
    • 句柄引用
    • 优缺点
  • 如何判断对象死亡
    • 引用计数法
    • 可达性分析法
  • 垃圾收集算法
    • 标记-清除算法
    • 复制算法
      • 复制算法——优化
      • 有关年轻代的JVM参数
    • 标记-整理算法
    • 分代收集算法
    • 几种常见的垃圾回收器
      • 串行:Serial 和Serial Old组合收集
      • 串行:ParNew收集器+Serial Old组合收集
      • 并行:Parallel Scavenge收集器+Serial Old(ps marksweep)组合收集
      • 并行:Parallel Scavenge收集器+Parallel Old组合收集
      • 并发:ParNew+标记扫描CMS(Concurrent Mark Sweep)+Serial Old收集器
      • 并发:G1收集器
    • 几种垃圾收集器的组合
  • 总结
  • 参考

从引用说起

Object object = new Object();
  • 假设这句代码出现在方法体中,"Object object” 这部分将会反映到Java栈的本地变量中,作为一个reference类型数据出现。
  • “new Object()”这部分将会反映到Java堆中,形成一块存储Object类型所有实例数据值的结构化内存,根据具体类型以及虚拟机实现的对象内存布局的不同,这块内存的长度是不固定。
  • 另外,在java堆中还必须包括能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些数据类型存储在方法区中。
  • reference类型在java虚拟机规范里面只规定了一个指向对象的引用地址,并没有定义这个引用应该通过那种方式去定位,访问到java堆中的对象位置,因此不同的虚拟机实现的访问方式可能不同,主流的方式有两种:使用句柄和直接指针。

指针直接引用

reference变量中直接存储的就是对象的地址,而java堆对象一部分存储了对象实例数据,另外一部分存储了对象类型数据。

句柄引用

java堆中将划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。

优缺点

这两种访问对象的方式各有优势,使用句柄访问方式最大好处就是reference中存储的是稳定的句柄地址,在对象移动时只需要改变句柄中的实例数据指针,而reference不需要改变。使用指针访问方式最大好处就是速度快,它节省了一次指针定位的时间开销,就虚拟机而言,它使用的是第二种方式(直接指针访问)

如何判断对象死亡

堆中放着几乎所有的对象实例,回收前第一步就要判断对象是否死亡

引用计数法

给对象增加一个引用计数器,每当有一个地方引用他,计数器就加1:当引用失效后,计数器就减1.任何时候计数器为0的对象就是可回收对象。

  • 优点:判定效率很高
  • 缺点:不会完全准确,因为如果出现两个对象相互引用的问题就不行了
/**
 * testGC()方法执行后会不会被GC?  不会!!!!
 *
 * @author TongWei.Chen 2017-09-05 11:15:53
 */
public class ReferenceCountingGC {

    public Object instance = null;

    public static void testGC() {
        
        //step 1
        ReferenceCountingGC objA = new ReferenceCountingGC();
        //step 2
        ReferenceCountingGC objB = new ReferenceCountingGC();
        //相互引用
        //step 3
        objA.instance = objB;
        //step 4
        objB.instance = objA;

        //step 5
        objA = null;
        //step 6
        objB = null;

        //假设在这行发生CG,objA和objB是否能被回收?   不能!!!!
        System.gc();
    }

    public static void main(String[] args) {
        testGC();
    }
}

可达性分析法

通过一系列称为“GC Roots”的对象作为七点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GCRoots没有任何引用链相链时,则证明此对象是不可用的。

可以作为GC Roots的对象包括以下几点
1、虚拟机栈(栈帧中的本地变量表)中引用的对象。
2、方法区中的类静态属性引用的对象或者常量引用的对象。
3、本地方法栈中JNI(就是native方法)引用的对象。

垃圾收集算法

标记-清除算法

最基础的收集算法是“标记-清除”(Mark-Sweep)算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

标记-清除算法是最基础的收集算法,其他的收集算法都是基于这种思路并对其不足进行改进而得到的。

技术图片

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

复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

技术图片

  • 不足:这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。
复制算法——优化

技术图片

  • 现在的商业虚拟机都采用这种收集算法来回收新生代
  • 新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。

当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。

  • HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。

  • 98%的对象可回收只是一般场景下的数据,没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)

有关年轻代的JVM参数
  • -XX:NewSize和-XX:MaxNewSize

用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。

  • -XX:SurvivorRatio

用于设置Eden和其中一个Survivor的比值,这个值也比较重要。

  • -XX:+PrintTenuringDistribution

这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。

  • -XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold

用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。

标记-整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
技术图片

  • 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

分代收集算法

上面在讲复制算法优化的时候也提到了年轻代使用的垃圾收集算法。

  • 当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,根据对象存活周期的不同将内存划分为几块。
  • 把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
  • 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

几种常见的垃圾回收器

串行:Serial 和Serial Old组合收集
  • 串行垃圾回收器在进行垃圾回收时,它会持有所有应用程序的线程,冻结所有应用程序线程,使用单个垃圾回收线程来进行垃圾回收工作。
  • 串行垃圾回收器是为单线程环境而设计的,如果你的程序不需要多线程,启动串行垃圾回收。
  • 串行收集器是最古老,最稳定以及高效的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)
  • 使用方法:-XX:+UseSerialGC 串联收集
    技术图片
串行:ParNew收集器+Serial Old组合收集
  • ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩
  • ParNew是并行收集器,不是并发收集器。并行收集器只是串行的多线程版本而已,此时用户线程仍然处于等待状态。并发是指用户线程和垃圾收集线程可以同时执行,在不通的cpu上。
  • 使用方法:-XX:+UseParNewGC ParNew收集器
    技术图片
并行:Parallel Scavenge收集器+Serial Old(ps marksweep)组合收集
  • 跟ParNew类似,是一种吞吐量优先收集器,即目标是达到一个可控制的吞吐量
吞吐量 = 运行用户代码时间 / (运行用户代码时间) + 垃圾收集时间
  • 新生代复制算法、老年代标记-压缩
  • 使用方法:-XX:+UseParallelGC Parallel Scavenge收集器
并行:Parallel Scavenge收集器+Parallel Old组合收集
  • Parallel Old是Parallel Scavenge收集器的老年代版本
  • 使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供
  • 使用方法:-XX:+UseParallelOldGC Parallel Scavenge Old收集器
    技术图片
并发:ParNew+标记扫描CMS(Concurrent Mark Sweep)+Serial Old收集器
  • 以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
  • CMS收集器是基于“标记-清除”算法实现的
  • 4个步骤,包括:
    • 初始标记(CMS initial mark):暂停所有其他线程,并记录下直接和roots相连的对象,速度很快;
    • 并发标记(CMS concurrent mark):同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象,因为用户线程可能会不断的更新引用域,所以GC无法保证可达对象分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
    • 重新标记(CMS remark):就是为了修正并发标记期间因为用户程序继续运行而导致标记变动的那一部分对象,这个阶段时间会比初始标记时间长,比并发标记时间短。
    • 并发清除(CMS concurrent sweep):开启用户线程,同时GC线程开始对标记的区域进行清扫。
  • Serial Old作为cms收集器出现Concurrent Mode Failure失败后的后备收集器使用
  • 使用方法:-XX:+UseConcMarkSweepGC cms收集器
  • 优点:并发收集、低停顿。
  • 缺点:
    • 对cpu资源敏感
    • 无法处理大量的浮动垃圾
    • “标记清除算法”产生大量空间碎片、并发阶段会降低吞吐量
-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量) 

技术图片

并发:G1收集器

G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:

  • 空间整合:G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
  • 可预测停顿:这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了
  • 分代收集:上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。
  • 可预测的停顿:这是G1相对CMS的另一大优势,除了可以降低停顿时间外,还能建立可预测的停顿时间模型,让使用者明确指定在一个为ms的时间片段内。
  • G1还维护了一个优先队列,每次根据允许的时间,优选选择回收价值最大的Region(这也是G1名字的由来)。
  • 收集步骤
    • 标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),并且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark)
    • 并发标记:从GC Roots中对堆对象进行可达性分析,找出存活对象
    • 最终标记:修改并发期间变动的标记记录
    • 筛选回收:根据用户指定的停顿时间制定回收计划
    • 复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。唯一和串行垃圾回收器不同的是,并行垃圾回收器是使用多线程来进行垃圾回收工作的。
      技术图片
G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。和CMS类似,G1收集器收集老年代对象会有短暂停顿。
G1收集器的核心思想是在CMS基础上增加了在有限的时间内尽可能高的收集效率。

几种垃圾收集器的组合

技术图片

总结

垃圾收集的核心还是从“标记-清理”算法基础上的各种优化版本,每一种算法的都是从时间和空间两个角度出发来达到最高效率,请根据个人应用特性来选择最适合的垃圾收集器,好啦本文就说这么多,希望大家多思考多练习,欢迎留言讨论。

参考

https://blog.csdn.net/high2011/article/details/80177473
https://blog.csdn.net/u011130752/article/details/50886939
https://www.cnblogs.com/grey-wolf/p/9217497.html
https://www.sohu.com/a/217151448_812245

JAVA对象生命周期(三)-对象的销毁

标签:处理   老年   压缩   top   本地   目标   toc   rom   基础   

原文地址:https://www.cnblogs.com/jimoliunian/p/12806537.html


评论


亲,登录后才可以留言!