JVM对象及垃圾回收处理

对象创建

• 给对象分配内存
• 线程安全性问题
• 初始化对象
• 执行构造方法

img

给对象分配内存

  • 指针碰撞

假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离

  • 空间列表

如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录

\(\color{red}{线程安全性问题}\)

在并发情况下,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:

  • 线程同步

对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性

  • 本地线程分配缓冲(TLAB)

把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

对象的结构(对象的内存布局)

  • Header(对象头)

    ​ 非固定的数据结构。一来是用来存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。二来是类型指针(对象指向它的元数据的指针),JVM通过这个指针来确定该对象是哪个类的实例对象,如果对象是一个Java数组,对象头中还需要有一块用来记录数组长度的数据。

    • 自身运行时数据(Mark Word)
    • 哈希值
    • GC分代年龄
    • 锁状态标志
    • 线程持有锁
    • 偏向线程ID
    • 偏向时间戳
    • 类型指针
    • 数组长度(只有数组对象才有)
  • InstanceData(实例数据)

    ​ 存储对象真正有效的数据,也就是程序代码中定义的各种类型的字段内容。不论是从父类继承的,还是类定义的。这部分的存储顺序会受到Java源码中的定义顺序的影响,相同宽度的数据分配到一(long,double)

  • Padding(对齐填充)

    ​ 8个字节的整数倍,不一定必须存在,起到占位符的作用。因为JVM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍。故当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

垃圾回收

判断对象为垃圾对象

  • 引用计数法

    ​ 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就+1;当引用失效时,计数器值就-1;任何时刻计数器为0的对象就是不能再被使用的垃圾对象。

      引用计数算法的实现简单,判定效率高。在大部分情况下它都是一个不错的算法。但是,至少主流的Java虚拟机里面没有选用引用计数算法来管理内存,主要原因是它很难解决对象之间的相互循环引用的问题,这种情况下,即使断开了对象在虚拟机栈中的reference,引用计数器永远都不会为0,这样就会造成内存泄漏

  • 可达性分析

    ​ 基本思路就是通过一系列成为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链”(Reference Chain)。当一个对象到GC Roots没有任何引用链相连(用图论的话说,就是从GC Roots到这个对象不可达)时,则证明这个对象是不可用的,如下图中的object5、object6、object7虽然相互关联,但是它们到GC Roots是不可达的,所以它们将会被判定是可回收对象。

扩展:

输出jvm中gc的详细信息参数配置:-verbose:gc -XX:+PrintGCDetails

[Full GC 168K->97K(1984K), 0.0253873 secs]

箭头前后的数据168K和97K分别表示GC前后所有存活对象使用的内存容量,说明有168K-97K=71K的对象容量被回收,括号内的数据1984K为堆内存的总容量,收集所花费的时间是0.0253873秒(这个时间在每次执行的时候会有所不同)

Note:GC会暂用CPU时间片,有可能造成应用程序在某个时刻极短的停顿(stop the world).

GC Roots

在Java中,可作为GC Roots的对象包括下面几种:

1.虚拟机栈(栈帧中的本地变量表)中引用的对象

2.方法区中类属性引用的对象

3.方法区中常量引用的对象

4.本地方法栈中JNI(即一般说的Native方法)引用的对象

如何回收

回收策略

  • 标记清除

    ​ 标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。

img

  • 复制

    ​ 复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾收集就从根集合(GC Roots)中扫描活动对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。

    img

  • 标记整理

    标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。具体流程见下图:

    img

  • 分代算法

     分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

img

  • 年轻代(Young Generation)的回收算法

    a) 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

    b) 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

    c) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。

    d) 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。

  • 年老代(Old Generation)的回收算法

    a) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

    b) 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

  • 持久代(Permanent Generation)的回收算法

    用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区。

内存分配策略

  • 优先分配Eden区

  • 大对象直接分配到老年代(-XX:PretenureSizeThreshold)

  • 长期存活的对象分配老年代(-XX:MaxTenuringThreshold=15)

  • 空间分配担保(-XX:+HandlePromotionFailure)

    检查老年代最大可用的连续空间是否大于历次晋升到老
    年代对象的平均大小。

  • 动态对象年龄对象(-XX:TargetSurvivorRatio)

    如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一
    半,年龄大于或等于该年龄的对象就可以直接进入老年代

逃逸分析与栈上分配

  • 栈上分配

    ​ 栈上分配主要是指在Java程序的执行过程中,在方法体中声明的变量以及创建的对象,将直接从该线程所使用的栈中分配空间。 一般而言,创建对象都是从堆中来分配的,这里是指在栈上来分配空间给新创建的对象,这样对象内存就可以随栈针出栈而销毁,减少GC压力。 对栈上分配发生影响的参数就是三个,-server-XX:+DoEscapeAnalysis-XX:+EliminateAllocations,任何一个发生变化都不会发生栈上分配,因为启用逃逸分析和标量替换默认是打开的。

  • 逃逸分析

    逃逸是指在某个方法之内创建的对象,除了在方法体之内被引用之外,还在方法体之外被其它变量引用到;这样带来的后果是在该方法执行完毕之后,该方法中创建的对象将无法被GC回收,由于其被其它变量引用。正常的方法调用中,方法体中创建的对象将在执行完毕之后,将回收其中创建的对象;故由于无法回收,即成为逃逸。

    public class EscapeAnalysis {
       public static Object object;
       public void globalVariableEscape(){//全局变量赋值逃逸  
          object =new Object();  
       }  
       public Object methodEscape(){  //方法返回值逃逸
          return new Object();
       }
       public void instancePassEscape(){ //实例引用发生逃逸
         this.speak(this);
       }
       public void speak(EscapeAnalysis escapeAnalysis){
         System.out.println("Escape Hello");
       }
     }
    • 全局变量赋值逃逸
    • 方法返回值逃逸
    • 实例引用逃逸分析

      比如将this给其他线程或者方法使用

    • 线程逃逸

      赋值给类变量或可以在其他线程中访问的实例变量 -server JVM运行的模式之一, server模式才能进行逃逸分析, JVM运行的模式还有mix/client。 -XX:+DoEscapeAnalysis:启用逃逸分析(默认打开)

标量替换和同步消除

  • 标量替换

    ​ 标量就是不可再分解的量,JAVA的基本数据类型就是标量,反之就是聚合量,比如对象。如果逃逸分析确定对象不会被外部使用,并且可以再分。jvm不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替。这些代替的成员变量在栈帧或寄存器上分配空间。
    -XX:+EliminateAllocations:标量替换(默认打开)

  • 同步消除

    ​ 通过逃逸分析,可以确定一个对象是否被其他线程所使用。如果没有,而对象的方法上又有同步锁。jvm会消除对象的同步锁 。

猜你喜欢

转载自www.cnblogs.com/snail-gao/p/11750122.html