二十、理解对象与垃圾回收机制

一、虚拟机中的对象

1、对象的创建

我们知道在类加载中经历了加载、验证、准备、解析、初始化、使用、卸载几个阶段。



在初始化阶段中当JVM遇到了一条new指令会经历以下几个阶段:
(1)检查加载
检查类是否被加载,如果加载失败重新加载
(2)分配内存
为新生对象分配内存,其实就是在堆空间中划分一块确定的连续内存区域
(3)内存空间的初始化
为对象分配零值,即默认值。例如int的0,boolean的false。
(4)设置
为对象的请求头设置信息如对象的哈希码、在JVM中的分代年龄、class类信息等
(5)对象的初始化
从Java 程序的角度来说,对象已经创建完成了,从开发者的角度才刚开始,接着就是会调用我们的构造方法完成对象成员变量的赋值。

2、对象的布局结构


一个对象主要包含了对象头、实例数据、对齐填充。

3、对象的访问

我们的Java程序主要是通过栈上的引用来访问操作具体的对象,主要方式就是通过直接指针的方式,引用中存储的就是对象的内存地址,通过引用中的地址直接指向堆中的对象。

4、对象的内存分配策略

我们知道几乎所有的对象都是在堆上分配,说明了也有部分对象不是在堆上分配,而是在栈上分配。

4.1、栈上分配

满足逃逸的对象会在栈上分配。

逃逸分析原理

我们的对象可能通过作为参数调用其他的方法,或者被其他线程共享访问。这种现象我们称之为方法逃逸和线程逃逸。当如果确定了我们的对象不会逃逸到线程之外,那么我们的对象在栈上分配效率会更高。对象的生命周期跟随着我们的线程,也不需要GC进行回收。如下代码
在allocate中只是简单的创建了MyObject 对象,并没有做其他操作。当方法执行完毕之后,对象的就销毁。这种情况对象就是在栈上分配。

public class EscapeAnalysisTest {
   public static void main(String[] args) throws Exception {
       long start = System.currentTimeMillis();
       for (int i = 0; i < 50000000; i++) {
           allocate();
      }
       System.out.println((System.currentTimeMillis() - start) + " ms");
       Thread.sleep(600000);
  }

   static void allocate() {
       MyObject myObject = new MyObject(2020, 2020.6);
  }

4.2、堆上分配

以上说满足逃逸分析的对象会在栈上分配内存,而几乎所有的对象是在堆中分配。而在堆中又划分了不同的区域。主要分为了新生代和老年代,而新生代中又划分了Eden区、From区、To区。From区和To区称为Survivor区。

4.2.1、堆上分配策略

(1)绝大多数的新生对象优先在Eden区分配,当Eden区没有最够空间分配的时候虚拟机将发生一次Minor GC。而如果是大对象直接进入老年代,例如很长的字符串以及数组。
(2)长期存活的对象进入老年代,当对象在Eden区发生了一次Minor GC之后仍然存活下来,并且Survivor区域任然可以容纳的话,则将对象移动到 Survivor区,并将对象的年龄置为1,对象在Survivor区每熬过一次Minor GC对象的年龄就+1。当年龄达到一定的程度(一般的回收器是15,CMS是6)的时候就将这些对象移动到老年代。
(3)除了对年龄判断之外,当Survivor区相同年龄的对象总的大小超过了Survivor区的内存一半,则将大于或者等于该年龄的对象移动到老年代,不需要等待到对象的指定年龄。称为对象年龄动态判定。
(4)在发生Minor GC之前,虚拟机会先检查老年代的最大可用空间是否大于新生代所有对象的大小。如果大于,那么确保了此次Minor GC是安全的。如果不满足条件,则虚拟机检查HandlePromotionFailure是否设置了允许担保失败。如果允许,那么检查老年代最大可用空间是否大于历届从新生代晋升到老年代对象大小的平均值。如果大于,则尝试进行Minor GC,当然此次GC是有风险的,如果担保失败,则会再进行一次Full GC清理堆,栈,方法区等。如果不大于的话,也会发生一次Full GC。

5、判断对象是否为垃圾

我们知道堆空间是有限的,当不同的区域空间不足的时候会发生不同类型的GC,GC的目的就是回收掉不需要的对象,我们称之为垃圾。要回收垃圾之前,JVM要判断哪些对象为垃圾。主要包含两种算法。

5.1、引用计数算法

在对象中添加一个引用计数器,当有一个地方引用时计数器就+1,引用失效时计数器-1。计数器的结果是0的时候,说明没有引用,视该对象为垃圾。但是当对象中包含相互引用的情况下,该方法失效。


5.2、可达性分析算法

该算法的思路是通过GC Roots作为起点,往下开始搜索,搜索走过的路径称为引用链,当一个对象通过引用链无法相连的时候,说明该对象是不可用的,应该视为垃圾。
GC Roots对象主要包含了一下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。
  • JVM的内部引用(class对象、异常对象NullPointException、OutofMemoryError,系统类加载器)。
  • 所有被同步锁(synchronized关键)持有的对象。
  • JVM内部的JMXBean、JVMTI中注册的回调、本地代码缓存等
  • JVM实现中的“临时性”对象,跨代引用的对象(在使用分代模型回收只回收部分代时)
    如下图:



    上图可以通过GC Roots相连的对象我们当之为应该存活对象,而无法相连的对象我们视为垃圾。

6、垃圾回收算法

当判断了对象为垃圾之后,JVM就通过垃圾回收算法进行回收。主要的垃圾回收算法包括如下

6.1、复制算法

将内存区域分为两部分相同的区域,一部分是空的,一部分用来存对象。当发生GC的时候,将存活的对象复制到另外一块内存区域,然后清理掉原来的区域。优点就是保持了内存的连续性,而缺点就是需要浪费一块内存,并且对对象进行复制移动,如果存活对象比较多,则性能比较低。
在新生代中通常存活的对象比较少,一般就是采用了复制算法,不需要复制移动太多对象,而在新生代中不是将整个新生代划分成两半,而是将新生代划分成了Eden区,From区、To区。它们的比例通常是8:1:1。而From区和To区称之为Survivor区,所以是将Survivor区一分为二。因为研究表明百分之98的对象都是朝生夕死的。这样对新生代的划分,可以提高JVM堆空间的利用率。所以当Eden区发生MinorGC的时候,将Eden区以及Survivor区的对象复制到Survivor中的空闲部分,例如From区,然后清理掉其他区域。下一次Minor GC的时候,又将Eden区和From区存活的对象复制到To区,再清理掉其他的区域。


6.2、标记清除算法

通过可达性分析算法标记可达的对象,不可达的对象则是需要回收的对象,然后清除。优点就是不需要浪费内存,缺点就是会产生内存碎片。导致内存空间不连续。
一般使用在老年代中,因为老年代中的对象一般都是比较难回收的,所以需要回收的对象比较少,因此清理少部分对象,不会造成很多内存碎片。

6.3、标记整理算法

标记所有需要回收的对象,将存活的对象移动到了另外一端,将外界的对象直接清理。虽然没有内存碎片,但是效率比较低,一般也是用在老年代。

二、JVM中常见的垃圾回收器

1、分代收集的思想

  • 在新生代中,每次垃圾收集的时候都发现有大量的对象死去,少部分的对象存活。因此使用复制算法,只需要复制移动极少部分对象,然后清理掉剩余垃圾对象。
  • 而在老年代中对象的存活率比较高,没有额外的空间担保,因此一般采用标记清除和标记整理算法。因为存活的对象比较多,而垃圾比较少,所以只需标记清理少部分对象。
  • 以上说的存活对象指的是非垃圾对象,死去对象则是通过可达性分析算法分析出的垃圾对象。

2、常见垃圾回收器

Serial/Serial Odl

新生代和老年代的单线程串行收集器

ParNew、ParallelScavenge/Perallel Old

新生代和老年代多线程并行收集器

CMS

多线程并发收集器,主要用于老年代,基于标记清除算法。
垃圾回收器工作的时候所有的用户线程会停掉,这就是所说的Stop The Worl现场,如果用户线程停止太久,那么就会造成用户体验卡顿。并发收集器则指的是垃圾回收线程和用户线程并发执行。
但是在CMS进行垃圾回收的时候也不是所有的阶段都和用户线程并发执行。如下图:


(1)初始标记
这个阶段只是通过可达性分析算法标记GC Roots直接关联的对象,这部分对象比较少。速度很快。这个阶段暂停了用户线程。
(2)并发标记
标记GC Roots所有关联的对象,花费时间比较久,用户线程和GC线程同时执行
(3) 重新标记
修正在并发标记中用户线程运行有可能产生的垃圾对象,所以需要重新标记。这个阶段时间也比较短,暂停了用户线程。
(4) 并发清除
标记完之后清除垃圾,用户线程和GC线程并发工作。

浮动垃圾

在CMS工作中,在并发标记和并发清除阶段GC线程和用户线程并发工作,因此避免不了这个过程用户线程产生了新的垃圾,这个垃圾称之为浮动垃圾。下一次GC的时候进行回收。

3、Stop The Worl现象

当垃圾回收器开始工作进行垃圾回收的时候,所有的用户线程会停掉,只有垃圾回收线程在工作。这种现场称之为Stop The World。因此频繁的垃圾回收会造成频繁的Stop The World。我们的用户线程频繁的停掉和恢复,这样就会造成应用的卡顿。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章