对象的创建与死亡以及内存分配、回收机制.md

本文详细探讨了Java对象的创建过程,包括类加载检查、内存分配(指针碰撞、空闲列表、并发分配、本地线程分配缓冲)、对象初始化、对象头设置、执行方法等步骤。此外,介绍了对象的内存布局,如OOP-Klass模型、对象头的Mark Word、实例数据、对齐填充等。同时讨论了对象的访问定位,包括直接指针和句柄访问两种方式。文章还涉及了对象内存分配,如栈上分配、对象在Eden区的分配、大对象直接进入老年代、长期存活对象的处理,以及内存回收机制,如引用计数和可达性分析算法,以及finalize方法的作用。
摘要由CSDN通过智能技术生成

对象的创建

对象创建的主要流程

对象创建的流程

类加载检查

当虚拟机遇到new指令时,首先要检查这个指令的参数是否能在常量池中找到类的符号引用,并检查这个符号引用的类是否已经被加载,验证、解析、初始化过,如果没有那必须先执行相应的类加载过程。

分配内存

类加载检查通过后,接下来虚拟机要为新生对象分配内存。对象所需要的内存大小在类加载后便可确定。为对象分配空间的任务等同于把一块大小的确定的内存从堆内存中划分出来。

对象内存分配中有两个问题:

  1. 如何进行内存划分
  2. 并发情况下如何保证多个对象同时内存分配成功。
内存分配方式
指针碰撞(bump the Pointer)

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

空闲列表(Free List)

如果 Java 堆内存不是规整的,已使用的内存和未使用的内存空间交错,此时无法使用指针碰撞分配方式,则需要由虚拟机维护一个内存列表,记录哪些内存时可用的,在分配的时候找到一块足够大的空间分配给对象,并更新列表。

并发分配
线程同步处理

虚拟机通过 CAS + 失败重试机制将内存分配空间的动作进行同步处理。

本地线程分配缓冲

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

对象初始化

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

基本数据类型的零值:

数据类型零值
byte(byte)0
char'\u0000'
short(short)0
booleanfalse
int0
long0l
float0.0f
double0.0
referencenull

设置对象头

在对象初始化之后,虚拟机还需要对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码、对象的分代年龄等。这些信息存放在对象的对象头(Object Header)中。

执行方法

从虚拟机角度来讲,上面的步骤执行完之后,一个新的对象就已经产生。但是从程序员角度来说还需要执行<init>方法和构造方法为对象的属性赋值。至此一个完整的对象才算生产出来。

对象的内存布局

OOP-Klass Model

在理解对象的内存布局之前,可以总体了解一下在 Hotspot 虚拟机中对象的 OOP-Klass 模型,这里的 OOP 指的是Oridinary Object Pointer(普通对象指针),他用来表示对象的实例信息,实际上是隐藏在在指针里面的对象。而 Klass 则包含元数据和方法信息,用来描述 Java 类。

JVM 使用 Oop 来表示一个对象,在 Java 程序运行过程中,每创建一个新的对象,在 JVM 内部都会在 JVM 内部创建一个对象类型的 oop 对象。各种 oop 类的共同基类为 oopDesc 类。oop 的继承体系如下:

// hotspot/src/share/vm/oops/oopsHierarchy.hpp
...
// Oop的继承体系
typedef class oopDesc*                            oop;
typedef class   instanceOopDesc*            instanceOop;
typedef class   arrayOopDesc*                    arrayOop;
typedef class     objArrayOopDesc*            objArrayOop;
typedef class     typeArrayOopDesc*            typeArrayOop;
...

oop 继承体系

oop 的子类有两个,分别是 instanceOop 和 arrayOop。前者表示 Java 中普通的对象,后者表示数组对象。oop 的储存结构主要由对象头和对象体组成。

oop 内存结构

Oop 的主要的两个成员属性:

// hotspot/src/share/vm/oops/oop.hpp
class oopDesc {
 ...
 private:
  // 用于存储对象的运行时记录信息,如哈希值、GC分代年龄、锁状态等
  volatile markOop  _mark;
  // Klass指针的联合体,指向当前对象所属的Klass对象
  union _metadata {
    // 未采用指针压缩技术时使用
    Klass*      _klass;
    // 采用指针压缩技术时使用
    narrowKlass _compressed_klass;
  } _metadata;
 ...
}	

_mark_metadata被称为对象头,其中前者存储对象的运行时记录信息;后者是一个指针,指向当前对象所属的Klass对象。

和 oop 一样 Klass 也有一个继承体系:

Klass 继承体系

// hotspot/src/share/vm/oops/oopsHierarchy.hpp
...
class Klass;  // Klass继承体系的最高父类
class   InstanceKlass;  // 表示一个Java普通类,包含了一个类运行时的所有信息
class     InstanceMirrorKlass;  // 表示java.lang.Class
class     InstanceClassLoaderKlass; // 主要用于遍历ClassLoader继承体系
class     InstanceRefKlass;  // 表示java.lang.ref.Reference及其子类
class   ArrayKlass;  // 表示一个Java数组类
class     ObjArrayKlass;  // 普通对象的数组类
class     TypeArrayKlass;  // 基础类型的数组类
...

不同于Oop,Klass在InstanceKlass下又设计了3个子类,其中InstanceMirrorKlass用于表示java.lang.Class类型,该类型对应的oop特别之处在于其包含了static field,因此计算oop大小时需要把static field也考虑进来;InstanceClassLoaderKlass主要提供了遍历当前ClassLoader的继承体系;InstanceRefKlass用于表示java.lang.ref.Reference及其子类。

总体来说在 Hotspot 虚拟机中存在对象、类的元数据(InstanceKlass)、类的 Java 镜像是那种对象。三者之间的关系是这样的:

Hotspot Klass

每个Java对象的对象头里,_klass字段会指向一个VM内部用来记录类的元数据用的InstanceKlass对象;InsanceKlass里有个_java_mirror字段,指向该类所对应的Java镜像——java.lang.Class实例。HotSpot VM会给Class对象注入一个隐藏字段“klass”,用于指回到其对应的InstanceKlass对象。这样,klass与mirror之间就有双向引用,可以来回导航。
这个模型里,java.lang.Class实例并不负责记录真正的类元数据,而只是对VM内部的InstanceKlass对象的一个包装供Java的反射访问用。

在JDK 7或之前的HotSpot VM里,InstanceKlass是被包装在由GC管理的klassOopDesc对象中,存放在GC堆中的所谓Permanent Generation(简称PermGen)中。从JDK 8开始的HotSpot VM则完全移除了PermGen,改为在native memory里存放这些元数据。新的用于存放元数据的内存空间叫做Metaspace,InstanceKlass对象就存在这里。至于java.lang.Class对象,它们从来都是“普通”Java对象,跟其它Java对象一样存在普通的Java堆(GC堆的一部分)里。

对象头

对象头

Hotspot 虚拟机对象头分为两部分,一部分是用来储存对象自身运行时数据,如 hash 码、GC 分代年龄、锁状态标志、偏向线程 ID、线程持有的锁等,称为Mark Word 即 JVM 中的 markOop。

另一部分为对象指向它自身类元数据的类型指针。虚拟机通过这个指针来确定对象是哪个类的实例及 _metadata。

对于数组对象,在对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。

Mark Word

Mark Word 用来储存对象自身运行时数据,如hash码,GC 分代年龄、锁状态、偏向线程 ID、线程持有的锁、偏向时间戳等,这部分数据在 32 位虚拟机和 64 位虚拟机(未开启指针压缩)分别为32bit 和 64 bit。Mark Word 在 32 位虚拟机中存储内容如下:

对象头 Mark Word

实例数据

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。这部分的存储顺序会受到虚拟机分配策略参数和字段在源码中定义顺序的影响。HotSpot 虚拟机默认的分配策略为 longs/doubules、ints、shorts/chars、bytes/booleans、oops。从分配策略中可以看出,相同宽度的字段总是被分配在一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果 CompactFields 参数值为 true(默认为 true),那么子类中较窄的变量也可能会插入到父类变量的空隙中。

对齐填充

第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。这种排布方式可以让原始类型字段最大限度地紧凑排布在一起,减少字段间因为对齐而带来的空隙;同时又让引用类型字段尽可能排布在一起,减少OopMap的开销。

举例来说,对于下面的类 C

class A {
  boolean b;
  Object o1;
}

class B extends A {
  int i;
  long l;
  Object o2;
  float f;
}

class C extends B {
  boolean b;
}

它的实例对象布局就是:(假定是64位HotSpot VM,开启了压缩指针的话)

-->  +0 [ _mark     ] (64-bit header word)
     +8 [ _klass    ] (32-bit header word, compressed klass pointer)
    +12 [ A.b       ] (boolean, 1 byte)
    +13 [ (padding) ] (padding for alignment, 3 bytes)
    +16 [ A.o1      ] (reference, compressed pointer, 4 bytes)
    +20 [ B.i       ] (int, 4 bytes)
    +24 [ B.l       ] (long, 8 bytes)
    +32 [ B.f       ] (float, 4 bytes)
    +36 [ B.o2      ] (reference, compressed pointer, 4 bytes)
    +40 [ C.b       ] (boolean, 1 byte)
    +41 [ (padding) ] (padding for object alignment, 7 bytes)

所以C类的对象实例大小,在这个设定下是48字节,其中有10字节是为对齐而浪费掉的padding,12字节是对象头,剩下的26字节是用户自己代码声明的实例字段。

留意到C类里字段的排布是按照这个顺序的:对象头 - Object声明的字段(无) - A声明的字段 - B声明的字段 - C声明的字段——按继承深度从浅到深排布。而每个类里面的字段排布顺序则按前面说的规则,按宽度来重排序。同时,如果类继承边界上有空隙(例如这里A和B之间其实本来会有一个4字节的空隙,但B里正好声明了一些不宽于4字节的字段,就可以把第一个不宽于4字节的字段拉到该空隙里,也就是 B.i 的位置)。

同时也请留意到A类和C类都声明了名字为b的字段。它们之间有什么关系?——没关系。
Java里,字段是不参与多态的。派生类如果声明了跟基类同名的字段,则两个字段在最终的实例中都会存在;派生类的版本只会在名字上遮盖(shadow / hide)掉基类字段的名字,而不会与基类字段合并或令其消失。上面例子特意演示了一下A.b 与 C.b 同时存在的这个情况。

使用 JOL工具查看对象的信息:

# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
tech.stack.C object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
     12     1            boolean A.b                                       false
     13     3                    (alignment/padding gap)                  
     16     4   java.lang.Object A.o1                                      null
     20     4                int B.i                                       0
     24     8               long B.l                                       0
     32     4              float B.f                                       0.0
     36     4   java.lang.Object B.o2                                      null
     40     1            boolean C.b                                       false
     41     7                    (loss due to the next object alignment)
Instance size: 48 bytes
Space losses: 3 bytes internal + 7 bytes external = 10 bytes total

指针压缩

什么是对象的指针压缩
  • jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩

  • jvm配置参数:UseCompressedOops,compressed­­压缩、oop(ordinary object pointer)­­对象指针

  • 启用指针压缩:-­XX:+UseCompressedOops(默认开启),禁止指针压缩:-­XX:-­UseCompressedOops

为什么要进行指针压缩
  • 在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据, 占用较大IO,同时GC也会承受较大压力

  • 为了减少64位平台下内存的消耗,启用指针压缩功能

  • 在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得 jvm 只用32位地址就可以支持更大的内存配置(小于等于32G)

  • 堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间

  • 堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内存不要大于32G为好

CompressedOops的原理

32位内最多可以表示4GB,64位地址分为堆的基地址+偏移量,当堆内存<32GB时候,在压缩过程中,把偏移量/8后保存到32位地址。在解压再把32位地址放大8倍,所以启用CompressedOops的条件是堆内存要在4GB*8=32GB以内。

所以压缩指针之所以能改善性能,是因为它通过对齐(Alignment),还有偏移量(Offset)将64位指针压缩成32位。换言之,性能提高是因为使用了更小更节省空间的压缩指针而不是完整长度的64位指针,CPU缓存使用率得到改善,应用程序也能执行得更快。

  • 零基压缩优化(Zero Based Compressd Oops):

    零基压缩是针对压解压动作的进一步优化。 它通过改变正常指针的随机地址分配特性,强制堆地址从零开始分配(需要OS支持),进一步提高了压解压效率。要启用零基压缩,你分配给JVM的内存大小必须控制在4G以上,32G以下。如果GC堆大小在4G以下,直接砍掉高32位,避免了编码解码过程 如果GC堆大小在4G以上32G以下,则启用UseCompressedOop 如果GC堆大小大于32G,压指失效,使用原来的64位(所以说服务器内存太大不好…)。

案例
// -XX:-UserCompressedOops
public class App {
    public static void main(String[] args) {
        ClassLayout classLayout = ClassLayout.parseInstance(new C());
        System.out.println(classLayout.toPrintable());
    }
}
// ---------------
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
tech.stack.C object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           30 fd ca 9d (00110000 11111101 11001010 10011101) (-1647641296)
     12     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     16     1            boolean A.b                                       false
     17     7                    (alignment/padding gap)                  
     24     8   java.lang.Object A.o1                                      null
     32     8               long B.l                                       0
     40     4                int B.i                                       0
     44     4              float B.f                                       0.0
     48     8   java.lang.Object B.o2                                      null
     56     1            boolean C.b                                       false
     57     7                    (loss due to the next object alignment)
Instance size: 64 bytes
Space losses: 7 bytes internal + 7 bytes external = 14 bytes total

使用-XX:-UseCompressedOops关闭指针压缩,再次使用 JOL 工具查看对象 C 内存分配可以看出对象 C 占用了 64 字节,明显要比开启指针压缩时 48 字节大得多。

对象的访问定位

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

直接指针

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

直接引用

直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销,由于对象的访问在 Java 中十分频繁,因此此类开销极少成多后也是一项十分可观的成本。HotSpot 虚拟机使用的就是直接指针访问方式。

句柄访问

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

句柄访问

句柄访问最大的好处就是 reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。

对象的内存分配

对象的内存分配一般来说都是堆上分配,但是也可能经过 JIT 编译后被拆散为标量类型并间接地在栈上分配。下面将通过一些代码来理解对象内存分配的规则。

对象栈上分配

逃逸分析

逃逸分析(Escape Analysis)的基本行为是就分析对象动态作用域:当一个对象在方法中被定义后,它可以能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。

public A escape() {
    A a = new A();
    a.b = false;
    a.o1 = new Object();
    return a;
}

public void noEscape(){
    A a = new A();
    a.b = false;
    a.o1 = new Object();
}

如上例代码 escape 方法中的 a对象返回了,这个对象的作用域范围就无法确定,因此a对象在方法escape中逃逸了。 noEscape方法中的 a 对象我们可以确定当方法结束,这个对象就可以认为是无效对象了,对象a 没有逃逸出 noEscape 方法。

JVM 对于这种情况可以通过参数-XX:+DoEscapeAnalysis来开启逃逸分析。开启后可以通过-XX:PrintEscapeAnalysis来查看分析结果。JDK7 之后默认开启逃逸分析,如果要关闭使用参数-XX:-DoEscapeAnalysis

标量替换

如果把一个对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换(Scalar Replacement)。标量(Scalar)是指一个数据已经无法再分解成更小的数据来表示了,Java 虚拟机中的原始数据类型(int、long 等数值类型以及 reference 类型等)都不能进一步分解,它们就可以成为标量。相对的,如果一个数据可以继续分解,那它就称作聚合量(Aggregate),Java 中的对象就是最典型的聚合量。

如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不会创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认开启。

栈上分配示例
/**
 * 逃逸分析 标量替换 -> 栈上分配
 * 调用一亿次 alloc() 如果分配到堆上 大约需要 48B x 1000000 ≈ 45m 的空间 如果堆空间小于该值, 必然会触发 GC
 * 使用如下参数不会发生 GC:
 * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+EliminateAllocations  -XX:+PrintGCDetails
 * <p>
 * 使用如下参数都会发生大量 GC
 * 关闭逃逸分析 开启标量替换
 * -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+EliminateAllocations  -XX:+PrintGCDetails
 * <p>
 * 开启逃逸分析 关闭标量替换
 * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:-EliminateAllocations  -XX:+PrintGCDetails
 */
public class AllocateOnStack {
    private static void alloc() {
        C c = new C();
        c.b = true;
        c.i = 1;
        c.f = 2.0F;
        c.l = 1L;
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println("time used: " + (end - start));
        try {
            TimeUnit.SECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//  -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+EliminateAllocations  -XX:+PrintGCDetails
time used: 11
[GC (Allocation Failure) [PSYoungGen: 4096K->480K(4608K)] 4096K->488K(15872K), 0.0027909 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 4608K, used 1487K [0x00000007bfb00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 4096K, 24% used [0x00000007bfb00000,0x00000007bfbfbcb0,0x00000007bff00000)
  from space 512K, 93% used [0x00000007bff00000,0x00000007bff78000,0x00000007bff80000)
  to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
 ParOldGen       total 11264K, used 8K [0x00000007bf000000, 0x00000007bfb00000, 0x00000007bfb00000)
  object space 11264K, 0% used [0x00000007bf000000,0x00000007bf002000,0x00000007bfb00000)
 Metaspace       used 3641K, capacity 4606K, committed 4864K, reserved 1056768K
  class space    used 406K, capacity 430K, committed 512K, reserved 1048576K

//  -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+EliminateAllocations  -XX:+PrintGCDetails                                
[GC (Allocation Failure) [PSYoungGen: 4096K->464K(4608K)] 4096K->472K(15872K), 0.0023558 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 4560K->480K(4608K)] 4568K->488K(15872K), 0.0016919 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 4576K->464K(4608K)] 4584K->480K(15872K), 0.0012115 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 4560K->416K(4608K)] 4576K->440K(15872K), 0.0014123 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 4512K->432K(4608K)] 4536K->456K(15872K), 0.0007827 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 4528K->448K(4608K)] 4552K->472K(15872K), 0.0014156 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 4544K->64K(4608K)] 4568K->469K(15872K), 0.0011911 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 4160K->0K(3584K)] 4565K->429K(14848K), 0.0008173 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 3072K->64K(4096K)] 3501K->493K(15360K), 0.0008980 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 3136K->0K(4096K)] 3565K->429K(15360K), 0.0008181 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 3072K->0K(4096K)] 3501K->429K(15360K), 0.0009510 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 3072K->96K(4096K)] 3501K->525K(15360K), 0.0010059 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 3168K->96K(4096K)] 3597K->549K(15360K), 0.0008356 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
time used: 32
Heap
 PSYoungGen      total 4096K, used 1733K [0x00000007bfb00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 3072K, 53% used [0x00000007bfb00000,0x00000007bfc996f8,0x00000007bfe00000)
  from space 1024K, 9% used [0x00000007bfe00000,0x00000007bfe18000,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 11264K, used 453K [0x00000007bf000000, 0x00000007bfb00000, 0x00000007bfb00000)
  object space 11264K, 4% used [0x00000007bf000000,0x00000007bf071498,0x00000007bfb00000)
 Metaspace       used 3635K, capacity 4606K, committed 4864K, reserved 1056768K
  class space    used 406K, capacity 430K, committed 512K, reserved 1048576K

// -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:-EliminateAllocations  -XX:+PrintGCDetails
[GC (Allocation Failure) [PSYoungGen: 4096K->464K(4608K)] 4096K->472K(15872K), 0.0012666 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 4560K->496K(4608K)] 4568K->504K(15872K), 0.0007431 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 4592K->480K(4608K)] 4600K->496K(15872K), 0.0005317 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 4576K->432K(4608K)] 4592K->448K(15872K), 0.0005055 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 4528K->448K(4608K)] 4544K->464K(15872K), 0.0004695 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 4544K->400K(3584K)] 4560K->416K(14848K), 0.0005831 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 3472K->96K(4096K)] 3488K->497K(15360K), 0.0005897 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 3168K->96K(4096K)] 3569K->513K(15360K), 0.0006654 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 3168K->96K(4096K)] 3585K->513K(15360K), 0.0006979 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 3168K->0K(4096K)] 3585K->433K(15360K), 0.0011842 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 3072K->64K(4096K)] 3505K->497K(15360K), 0.0004890 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 3136K->32K(4096K)] 3569K->465K(15360K), 0.0010702 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 3104K->0K(4096K)] 3537K->449K(15360K), 0.0010044 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
time used: 25
Heap
 PSYoungGen      total 4096K, used 2704K [0x00000007bfb00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 3072K, 88% used [0x00000007bfb00000,0x00000007bfda4070,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 11264K, used 449K [0x00000007bf000000, 0x00000007bfb00000, 0x00000007bfb00000)
  object space 11264K, 3% used [0x00000007bf000000,0x00000007bf0704a8,0x00000007bfb00000)
 Metaspace       used 3086K, capacity 4502K, committed 4864K, reserved 1056768K
  class space    used 339K, capacity 390K, committed 512K, reserved 1048576K                                

由上运行结果可看出开启逃逸分析和标量替换后除了启动 jvm 时发生了一次 GC 其余时间都没有发生 GC,而关闭逃逸分析或者标量替换都会发生大量 GC,同时如果想完成程序栈上分配,则需要同时开始逃逸分析和标量替换。把运行内存参数调整放大+Xms500m +Xmx500m ,让程序睡眠 20s,此时都没有发生 GC,通过 jmap 查看程序的内存分布:

开启逃逸分析和标量替换:

逃逸分析

此时程序中只存在 68768 个 C 对象。关闭逃逸分析再次查看:

逃逸分析

此时 C 实例对象有 1000000 个。

对象有现在 Eden 区分配

大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

  • Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。

  • Major GC/Full GC:一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上。

大量的对象被分配在 Eden 区,Eden 区满了之后会触发 MinorGC,可能会有 99% 以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到 survivor1 区,下一次eden区满了后又会触发minor gc,把eden区和survivor1区垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空的survivor2区,因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可

JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy

示例如下:

/**
 * -Xmx30m -Xms30m -XX:+PrintGCDetails 
 */
public class GCTest {

    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] allocation1 = new byte[2 * _1MB];
        byte[] allocation2 = new byte[2 * _1MB];
        byte[] allocation3 = new byte[_1MB];
        byte[] allocation4 = new byte[_1MB];
//        alloc();
//        byte[] allocation5 = new byte[_1MB];
    }


//    private static void alloc() {
//        byte[] allocation1 = new byte[2 * _1MB];
//        byte[] allocation2 = new byte[2 * _1MB];
//        byte[] allocation3 = new byte[_1MB];
//        byte[] allocation4 = new byte[_1MB];
//    }
}
--------------------------------------------------
Heap
 PSYoungGen      total 9216K, used 7816K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 95% used [0x00000007bf600000,0x00000007bfda22d8,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
  to   space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
 ParOldGen       total 20480K, used 0K [0x00000007be200000, 0x00000007bf600000, 0x00000007bf600000)
  object space 20480K, 0% used [0x00000007be200000,0x00000007be200000,0x00000007bf600000)
 Metaspace       used 2973K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 328K, capacity 388K, committed 512K, reserved 10

上述代码设置参数-Xmx30m -Xms30m根据虚拟机老年代和新生代默认比例为 2:1 可得知年轻代大小为 10MB,老年代为20MB。新生代中 Eden:s1:s1 = 8:1:1,其中 Eden 为 8MB,s1 为 1MB,另外s2 也为 1MB。在 allocation 1,2,3,4 申请内存时都在 eden 区进行分配。接下来再为 allocation5 申请内存,输出 GC 日志如下:

[GC (Allocation Failure) [PSYoungGen: 7652K->592K(9216K)] 7652K->6744K(29696K), 0.0049781 secs] [Times: user=0.03 sys=0.01, real=0.01 secs] 
Heap
 PSYoungGen      total 9216K, used 1782K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 14% used [0x00000007bf600000,0x00000007bf7297f0,0x00000007bfe00000)
  from space 1024K, 57% used [0x00000007bfe00000,0x00000007bfe94010,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 20480K, used 6152K [0x00000007be200000, 0x00000007bf600000, 0x00000007bf600000)
  object space 20480K, 30% used [0x00000007be200000,0x00000007be802040,0x00000007bf600000)
 Metaspace       used 3024K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 329K, capacity 388K, committed 512K, reserved 1048576K

此时 jvm 发生了一次 Minor GC,但是注意到老年代使用了 30% 的内存空间。因为给allocation5分配内存的时候eden区内存几乎已经被分配完了,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,GC期间虚拟机又发现allocation1,2,3,4无法存入Survior空间,所以只好把新生代的对象提前转移到老年代中去,老年代上的空间足够存放allocation1-4,所以不会出现Full GC。如果把 allocation1-4 的内存分配动作封装到 alloc() 方法中,再次运行程序:

[GC (Allocation Failure) [PSYoungGen: 7652K->592K(9216K)] 7652K->600K(29696K), 0.0011807 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 1782K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 14% used [0x00000007bf600000,0x00000007bf729838,0x00000007bfe00000)
  from space 1024K, 57% used [0x00000007bfe00000,0x00000007bfe94010,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 20480K, used 8K [0x00000007be200000, 0x00000007bf600000, 0x00000007bf600000)
  object space 20480K, 0% used [0x00000007be200000,0x00000007be202000,0x00000007bf600000)
 Metaspace       used 2973K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 328K, capacity 388K, committed 512K, reserved 1048576K

可以发现之前的 allocation1-4 并没有进入老年代,已经被 GC 掉了。这也是为什么我们在写代码中尽量封装方法减少方法体大小的原因。

大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 SerialParNew 两个收集器下有效。

/**
 * -Xmx30m -Xms30m -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC -XX:PretenureSizeThreshold=3145728
 */
public class BigObj {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] b = new byte[3 * _1MB ];
    }
}
---------------------------------------------------------
Heap
 par new generation   total 9216K, used 1673K [0x00000007be200000, 0x00000007bec00000, 0x00000007bec00000)
  eden space 8192K,  20% used [0x00000007be200000, 0x00000007be3a2458, 0x00000007bea00000)
  from space 1024K,   0% used [0x00000007bea00000, 0x00000007bea00000, 0x00000007beb00000)
  to   space 1024K,   0% used [0x00000007beb00000, 0x00000007beb00000, 0x00000007bec00000)
 concurrent mark-sweep generation total 20480K, used 3072K [0x00000007bec00000, 0x00000007c0000000, 0x00000007c0000000)
 Metaspace       used 2972K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 328K, capacity 388K, committed 512K, reserved 1048576K

-XX:+UseConcMarkSweepGC参数意味着在老年代使用 cms 垃圾收集器,此时 jvm 默认的年轻代垃圾收集器为ParNew收集器,可以省略-XX:+UseParNewGC参数。通过上述代码示例可以看出 byte[] b 直接进入了老年代,占用了 3072KB的空间,正好与我们 new 出来的 3M 大小相同。

大对象直接在老年代分配的目的是避免在 Eden区和两个 survivor 区进行大量的复制。

长期存活的对象进入老年代

虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应该放在新生代,哪些对象应该放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。如果在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。对象在 Survivor 中没熬过一次 Minor GC, 年龄就增加一岁,当它的年龄增加到一定程度(默认 15 岁,CMS 收集器默认为 6 岁,不同的垃圾收集器会稍微有些不同),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过-XX:MaxTenuringThreshold来设置。

动态对象年龄判断

当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的

老年代空间分配担保机制

在每次 Minor GC 之前,虚拟机会检查老年代最大连续可用空间是否大于新生代所有对象(包括垃圾对象)总空间,如果大于则说明此次 Minor GC 是安全的。如果不大于,虚拟机会检查 HandlePromotionFailure 设置的值是否允许担保失败,如果不允许,则进行一次 Full GC,如果允许,则检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,尝试进行一次 Minor GC,如果小于改为进行一次 Full GC。

空间分配担保机制

内存分配流程

内存分配流程

对象的内存回收

对象创建使用完成之后,需要被垃圾收集进行回收以释放内存空间。在回收之前,第一件事就是要确定这些对象中哪些还“存活着”,哪些已经“死去”。

引用计数算法

给对象添加一个引用计数器,每当一个地方引用它时,计数器的值就加 1;当引用失效时,计数器的值就减1;任何时刻计数器为 0 的对象就是不可能在被使用的。

**引用计数算法(Reference Counting)的实现简单,判定效率也很高,但是目前主流虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之前相互循环引用的问题。**所谓对象之间的相互引用问题,如下面代码所示:除了对象a 和 b 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知 GC 回收器回收他们。

public class ReferenceCounting {
    Object instance = null;

    public static void main(String[] args) {
        ReferenceCounting a = new ReferenceCounting();
        ReferenceCounting b = new ReferenceCounting();
        a.instance = b;
        b.instance = a;
        a = null;
        b = null;
    }
}

可达性分析算法

可达性分析(Reachability Analysis)的基本思路就是通过一系列的成为GC Roots的对象为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Refernce Chain),当一个独享到 GC Roots 没有任何引用链相连(用图论的话来说,就是从 GC Roots到这个对象不可达)时,则证明此对象是不可用的。

可达性分析

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

  • 虚拟机栈(栈中变量表)中的引用对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(Native)方法引用的对象。

对象引用类型

java的引用类型一般分为四种:强引用软引用、弱引用、虚引用

强引用

普通的变量引用,类似Object obj = new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用

用来描述一些还有用但并非必需的对象。该类对象正确情况下不会被回收,但是 GC 做完后发现释放不出空间存放新的对象,则会把这些对象回收掉。软引用可用来实现内存敏感的高速缓存。在JDK 1.2之后,提供了SoftReference类来实现弱引用。

public static SoftReference<User> user = new SoftReference<User>(new User());
弱引用

弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

public static WeakReference<User> user = new WeakReference<User>(new User());
虚引用

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用

finalize 方法

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。

标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。

  1. 第一次标记并进行一次筛选。

    筛选的条件是此对象是否有必要执行finalize()方法。 当对象没有覆盖finalize方法,对象将直接被回收。

  2. 第二次标记

    如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。

如何判断一个类是无用的类

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?

类需要同时满足下面3个条件才能算是 “无用的类”

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。

  • 加载该类的 ClassLoader 已经被回收。

  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

参考文章:

Java的对象模型——Oop-Klass模型(一)

Java的对象模型——Oop-Klass模型(二)

JVM符号引用转换直接引用的过程?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值