1. Java 对象在虚拟机中的生命周期

在 Java 对象被类加载器加载到虚拟机中后,Java 对象在 Java 虚拟机中有 7 个阶段。

1.1 创建阶段(Created)

创建阶段的具体步骤为:

  • 为对象分配存储空间
  • 构造对象
  • 从超类到子类对 static 成员进行初始化
  • 递归调用到超类的构造方法
  • 调用子类的构造方法

1.2 应用阶段(In Use)

当对象被创建,并分配给变量赋值时,状态就切换到了应用阶段。 这一阶段的对象至少要具有一个强引用,或者显式地使用软引用、弱引用或者虚引用。

1.3 不可见阶段(Invisible)

在程序中找不到对象的任何强引用,比如程序的执行已经超过了该对象的作用域。在不可见阶段,对象仍可能被特殊的强引用 GC Roots 持有着,比如对象被本地方法栈中 JNI 引用或被运行中的线程引用等。

1.4 不可达阶段(Unreachable)

在程序中找不到对象的任何强引用,并且垃圾收集器发现对象不可达。

1.5 收集阶段(Collected)

垃圾收集器已经发现对象不可达,并且垃圾收集器已经准备好要对该对象的内存空间进行重新分配,这个时候如果该对象重写了 finalize 方法,则会调用该方法。

finalize [ˈfaɪnəlaɪz] 终结

1.6 终结阶段(Finalized)

在对象执行完 finalize 方法后仍然处于不可达状态时,或者对象没有重写 finalize 方法,则该对象进入终结阶段,并等待垃圾收集器回收该对象空间。

1.7 对象空间重新分配阶段(Deallocated)

deallocated 解除配置

当垃圾收集器对对象的内存空间进行回收或者再分配时,这个对象就会彻底消失。

被标记为不可达的对象会立即被垃圾回收器回收吗?——不会的。被标记为不可达的对象会进入收集阶段,这时会执行该对象重写的 finalize 方法,如果没有重写 finalize 方法或者 finalize 方法中没有重新与一个可达的对象进行关联才会进入终结阶段,并最终被回收。

2. 对象的创建

通常是通过 new 指令来完成一个对象的创建,当虚拟机接收到一个 new 指令时,它会做如下操作:

2.1 判断对象对应的类是否加载、链接和初始化

虚拟机接收到一条 new 指令时,首先会去检查这个指定的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被类加载器加载、链接和初始化过。如果没有,那必须先执行相应的类加载过程。

2.2 为对象分配内存

类加载完成后,接着会在 Java 堆中划分一块内存分配给对象。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从 Java 堆中划分出来。

内存分配根据 Java 堆是否规整,有两种方式:

bump [bʌmp] 碰撞

  • 指针碰撞(Bump the Pointer):如果 Java 堆的内存是规整的,即所有用过的内存放在一边,而空闲的内存放在一边,中间放着一个指针作为分界点的指示器。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
  • 空闲列表(Free List):如果 Java 堆的内存不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录

选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 因此,在使用 Serial、ParNew 等带 Compact 过程的收集器时,系统采用的分配算法是指针碰撞,而使用 CMS 这种基于 Mark-Sweep 算法的收集器时,通常采用空闲列表。

2.3 处理并发安全问题

allocation [ˌæləˈkeɪʃn] 分配,配置;安置

对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来配内存的情况。

要解决并发的问题,有两种方式:

  • 对分配内存空间的动作进行同步处理——实际上虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性
  • 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,这块内存称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),哪个线程要分配内存,就在哪个线程的 TLAB上 分配,只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁定。 虚拟机是否使用 TLAB,可以通过 -XX:+/-UseTLAB 参数来设定。

2.4 初始化分配到的内存空间

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用 TLAB,这一工作过程也可以提前至 TLAB 分配时进行。这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

2.5 设置对象的对象头

虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息,这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

2.6 执行 init 方法进行初始化

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚刚开始—— init 方法还没有执行,所有的字段都还为零。执行 new 指令之后会接着执行 init 方法,初始化对象的成员变量、调用类的构造方法,这样一个对象就被创建了出来。

3. 对象的内存布局

对象创建完毕,并且已经在 Java 堆中分配了内存,那么对象在内存是如何进行布局的呢?以 HotSpot 虚拟机为例,对象在内存的布局分为三个区域,分别是对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。下面分别来对这三个区域进行简单介绍:

  • 对象头,对象头包括两部分信息:

    • 第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,官方称它为 Mark Word。
    • 另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。 另外,如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据,因 为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中却无法确定数组的大小。
  • 实例数据:是对象真正存储的有效信息, 也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
  • 对齐填充:对齐填充不一定存在,起到了占位符的作用,没有特别的含义。 由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,**换句话说, 就是对象的大小必须是 8 字节的整数倍。**而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍), 因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象的堆内存布局如下所示:

对象的 hashCode 不是一开始就有的,是调用 hashCode 方法的时候添加的。

问:一个 Java 没有属性的对象占多大内存?

Object obj = new Object();,在 Java 中空对象占 8B/8 byte ,对象的引用占 4B,所以一个上面那条语句占用的空间是 4B + 8B = 12B,因为对象的大小必须是 8 的倍数,所以分配 16B。

4. 对象的访问定位

建立对象是为了使用对象,Java 程序需要通过栈上的 reference 数据来操作堆上的具体对象。 由于 reference 类型在 Java 虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种。

如果使用句柄访问的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息, 如图所示:

如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象地址, 如图所示:

这两种对象访问方式各有优势:

  • 使用句柄来访问的最大好处就是 reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要修改;
  • 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在 Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本;

5. 怎样判断对象已死/垃圾标记算法

垃圾收集器(Garbage Collection),通常被称作 GC。GC 主要做两个工作,一个是内存的划分和分配,另一个是对垃圾进行回收。

  • 关于内存的划分和分配,目前 Java 虚拟机内存的划分是依赖于 GC 设计的。现在 GC 都采用了分代收集算法来回收垃圾的,Java 堆作为 GC 主要管理的区域,被细分为新生代和老年代,再细致一点新生代又可以划分为 Eden 空间、From Survivor 空间、To Survivor 空间等,这样划分是为了更快得进行内存分配和回收。空间划分后,GC 就可以为新对象分配内存空间。
  • 关于对垃圾的回收,被引用的对象是存活的对象,而不被引用的对象是死亡的对象(也就是垃圾),GC 要区分出存活的对象和死亡的对象(也就是垃圾标记),并对垃圾进行回收。 在对垃圾进行回收前,GC 要先标记出垃圾,目前有两种标记垃圾的算法,分别是引用计数算法和根搜索算法。

Eden [ˈiːdn] 伊甸园(《圣经》中亚当和夏娃最初居住的地方 survivor [sərˈvaɪvər] 幸存者;生还者;残存物

5.1 引用计数算法(Reference Counting)

引用计数算法的基本思想:为每个对象添加一个引用计数器,每当有 一个地方引用它时,计数器就加 1,引用失效就减 1。当引用计数器中的值变为 0,则该对象就不能被使用,变成了垃圾。

客观地说,引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法。但是,目前主流的 Java 虚拟机没有选择引用计数算法来为垃圾标记,主要原因是引用计数算法没有解决对象之间相互循环引用的问题。

举个例子,在下面代码的注释 1 和注释 2 处,d1 和 d2 相互引用,除此之外这两个对象无任何其他引用,实际上这两个对象已经死亡,应该作为垃圾被回收,但是由于这两个对象相互引用,引用计数器就不会为 0,如果 Java 虚拟机采用了引用计数算法,垃圾搜集器就无法回收它们。

class _2MB_Data {public Object instance = null;private byte[] data = new byte[2 * 1024 * 1024]; // 用来占内存,测试垃圾回收
}class ReferenceGC {public static void main(String[] args) {_2MB_Data d1 = new _2MB_Data();_2MB_Data d2 = new _2MB_Data();d1.instance = d2; // 1d2.instance = d1; // 2d1 = null;d2 = null;System.gc();}
}

在 Android Studio 中的 Edit Configurations 中的 VM options 加入 -verbose:gc 来输出 GC 日志:

运行程序,GC 日志为:

内存的变化:回收前的内存的大小是 11M,回收后的内存大小 3M,17M 代表内存总大小。因此可以得知内存回收大小为 8M。这就说明虚拟机并没有应用计数算法来标记内存,它对上述代码中的两个死亡对象进行了回收。

在线分析GC工具:
https://www.cnblogs.com/twyong/p/14057809.html
https://gceasy.io/gc-index.jsp

5.2 根搜索算法/可达性分析算法

这个算法的基本思想就是选定一些对象作为 GC Roots,并组成根对象集合,然后以这些 GC Roots 的对象作为起始点,向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连,就是 GC Roots 到这个对象不可达,则证明此对象是不可用的,是可以被回收的对象, 如下所示:

从上图中可以看出,Obj5、Obj6、Obj7 都是不可达的对象,其中 Obj5 和 Obj6 虽然互相引用,但是因为它们到 GC Roots 是不可达的,所以它们仍旧被判定为可回收的对象,这样根搜索算法就解决了引用计数算法无法解决的问题:已经死亡的对象应为相互引用而不能被回收。

在 Java 中,可以作为 GC Roots 的对象主要有以下几种:

  • Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
  • 方法区中运行时常量池引用的对象
  • 方法区中静态属性引用的对象

HotPot 中采用的是可达性分析算法。

6. 垃圾收集算法

垃圾被标记后,GC 就会对垃圾进行收集,垃圾收集有很多种算法。 以下就是常用的垃圾收集算法。

6.1 标记—清除算法(Mark-Sweep)

sweep [swiːp] 清除

标记—清除算法是最基础垃圾收集算法,它将垃圾收集分为两个阶段:

  • 标记阶段:标记出可以回收的对象
  • 清除阶段:回收被标记的对象所占用的空间

标记—清除算法之所以是基础的,是因为后面讲到的垃圾收集算法都是在此算法的基础上进行改进的。标记—清除算法的执行过程如下所示:

标记—清除算法主要有两个缺点:一个是标记和清除的效率不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,碎片太多可能会导致后续没有足够的连续内存分配给较大的对象(可能导致 OOM),无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

6.2 复制算法(Copying)

为了解决标记—清除算法的效率不高的问题,产生了复制算法。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。 复制算法的执行过程如下所示:

这种算法每次都对整个半区进行内存回收,不需要考虑内存碎片的问题,代价就是将内存缩小为了原来的一半。复制算法的效率与与对象的存活率有很大的关系,如果对象的存活率较低,复制算法的效率就会很高。 由于绝大多数对象的生命周期很短,并且这些生命周期很短的对象都存于新生代中,所以复制算法被很广泛应用于新生代中。

6.3 标记—压缩算法(Mark-Compact)

compact [kəmˈpækt;ˈkɑːmpækt] 紧凑的

在新生代中可以使用复制算法,但是在老年代中就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。更关键是,如果不想浪费 50% 的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100% 存活的极端情况,所以在老年代一般不能直接选用这种算法。

因此就出现了一种标记—压缩算法,与标记—清除算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑地排列在一起,然后对边界以外的内存进行回收,回收后,已用和未用的内存都各自一边。 标记—压缩算法的执行过程如下所示:

6.4 分代收集算法

分代收集算法会结合不同的收集算法来处理不同的空间,因此在学习分代收集算法之前要了解 Java 堆区的空间划分。

Java 堆区的空间划分在 Java 虚拟机中,各种对象的生命周期会有较大的差别,大部分对象生命周期很短暂,少部分对象生命周期很长,有的甚至与应用程序以及 Java 虚拟机的运行周期一样长。因此,应该对不同生命周期的对象采取不同的收集策略,根据生命周期长短可以将他们分别放到不同的区域,并在不同的区域采用不同的收集算法,这就是分代的概念。

generational [ˌdʒenəˈreɪʃənl] 一代的;生育的 tenured [ˈtenjərd](美)享有终身职位的

按照主流的 Java 虚拟机的垃圾收集器都采用分代收集算法(Generational Collection)。Java 堆区基于分代的概念,分为新生代(Young Generation)和老年代(Tenured Generation),这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

根据 Java 堆区的空间划分,垃圾收集的类型分为两种,它们分别如下:

minor ['mainə] 未成年的;较小的 major 主要的;重要的;较多的

  • Minor Collection:新生代的垃圾收集
  • Full Collection:对老年代进行收集,又可以称为 Major Collection,Full Collection 通常情况下会伴随至少一次的 Minor Collection,它的收集频率较低,耗时较长。

当执行一次 Minor Collection 时,Eden 空间的存活对象会被复制到 To Survivor 空间,并且之前经历一次 Minor Collection 并在 From Survivor 空间存活的仍年轻的对象也会复制到 To Survivor 空间。当所有存活的对象被复制到 To Survivor 空间,或者晋升到老年代,也就意味着 Eden 空间和 From Survivor 空间剩下的都是可回收的对象。如下图所示:

这个时候 GC 执行 Minor Collection,Eden 空间和 From Survivor 空间都会被清空,新生代中存活的对象都放在 To Survivor 空间。接下来将 From Survivor 空间和 To Survivor 空间互换位置,也就是此前的 From Survivor 空间成为了现在 To Survivor 空间,每次 Survivor 空间互换都要保证 To Survivor 空间是空的,这就是复制算法在新生代中的应用。在老年代则会采用标记—压缩算法或者标记—清除算法。

参考

https://www.cnblogs.com/chenyangyao/p/5303462.html
https://zhuanlan.zhihu.com/p/45354152
https://blog.csdn.net/weixin_43194122/article/details/88890663?utm_term=xloggc参数的使用&utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2allsobaiduweb~default-4-88890663&spm=3001.4430
https://blog.csdn.net/baidu_22254181/article/details/82555485
https://blog.csdn.net/qq_39192827/article/details/85611873
https://www.cnblogs.com/shoshana-kong/p/10571982.html
https://blog.csdn.net/alvin_csdn_blog/article/details/78313996
https://zhidao.baidu.com/question/139667452916106845.html

Java虚拟机(四)—— Java虚拟机中的对象相关推荐

  1. 深入JVM虚拟机(四) Java GC收集器

    转载自  深入JVM虚拟机(四) Java GC收集器 1 GC收集器 1.1 Serial串行收集器 串行收集器主要有两个特点:第一,它仅仅使用单线程进行垃圾回收:第二,它独占式的垃圾回收. 在串行 ...

  2. java流的序列化_Java中的对象流和序列化介绍

    最近,在讲流的使用,其中对象流的作用其实就是将自定义类的对象与流之间相互转换的流. 看起来还是挺简单的,那么看下面的例子: public class Student{ private int id; ...

  3. java null 写前面_java中判断对象为null时,null在前面还是后面

    因为目前只学习并使用java语言,所以这里主要是根据java来说的 Java中对null进行判断放在前后没有什么区别,只是为了代码规范,为了避免写代码时书写错误. 下面面两个测试Demo都没有报错.n ...

  4. Java Servlet(三):Servlet中ServletConfig对象和ServletContext对象

    本文将记录ServletConfig/ServletContext中提供了哪些方法,及方法的用法. ServletConfig是一个抽象接口,它是由Servlet容器使用,在一个servlet对象初始 ...

  5. java 垃圾回收 null_java方法中把对象置null,到底能不能加速垃圾回收

    今天逛脉脉,看见匿名区有人说java中把对做置null,这种做法很菜,不能加速垃圾回收,但是我看到就觉得呵呵了,我是觉得可以加速置null对象回收的. 测试的过程中,费劲的是要指定一个合理的测试堆大小 ...

  6. java 获取两个List 中 不同对象

    User user=new User();user.setUsername("张三");User user1=new User();user1.setUsername(" ...

  7. java基础(四) java运算顺序的深入解析

    1. 从左往右的计算顺序   与C/C++不同的是,在Java中,表达式的计算与结果是确定的,不受硬件与环境的影响.如: int i = 5; int j = (i++) + (i++) +(i++) ...

  8. 三角形网格 四方形网格_HTML5中3D对象的三角形网格

    三角形网格 四方形网格 Triangle mesh for 3D objects in HTML5 Today's lesson is a bridge between two-dimensional ...

  9. 【重难点】【JVM 01】OOM 出现的原因、方法区、类加载机制、JVM 中的对象

    [重难点][JVM 01]OOM 出现的原因.方法区.类加载机制.JVM 中的对象 文章目录 [重难点][JVM 01]OOM 出现的原因.方法区.类加载机制.JVM 中的对象 一.OOM 出现的原因 ...

  10. List中根据对象字段快速查找对象

    import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util. ...

最新文章

  1. Exception在语义上的处理。在系统中的意义。
  2. 空间索引不能用analyze进行分析
  3. Live预告 | 地平线李星宇:智能汽车电子构架如何变革迎接数字化重塑?...
  4. C#LeetCode刷题之#54-螺旋矩阵(Spiral Matrix)
  5. Part1 R语言的基本操作
  6. html5 标签大写还是小写,html5中有没有规定字母标签是用大写还是小写?
  7. 丢失更新的问题产生和解决
  8. [转]vs2010 crystal report使用
  9. mac推箱子c语言,c语言写的推箱子源码,非常适合新手学习
  10. 禁用微信浏览器的下拉_解决微信浏览器禁止下拉查看真实域名网址的问题
  11. 图像生成质量fid、inception score、KID计算
  12. [linux内核] 3.系统调用处理过程
  13. 还原魔方的软件(十月三十日更新)
  14. 从钉钉后台对接考勤打卡信息(仅供参考)
  15. Robocup 2D新手导读(入门总结)
  16. 测试用例模板(通用)
  17. 手机蓝牙和蓝牙模块进行通信
  18. matlab循环语句详解
  19. 看完这妹纸的日更作业,网友直呼:中国计算机界的神!
  20. Ajax页面缓存问题分析与解决办法

热门文章

  1. ☀️前端基础—【HTML⚡】
  2. 工具箱@CMD实用命令
  3. 虚拟机中的Linux系统如何联网?
  4. Oracle Reports 6i培训教程 - 百度文库
  5. Kotlin入门第四节
  6. Spring Cloud Hoxton 版本微服务项目搭建 admin 监控客户端
  7. BCD码的作用和实现
  8. 视觉与智能学习近期期刊阅读与相关知识学习
  9. Exception in thread main java.sql.SQLException: Access denied for user ''@'localhost' (using passw
  10. JavaWeb-smbms项目