垃圾收集分析(1)-Java对象结构(上)

    xiaoxiao2021-03-25  59

    GC(Garbage Collection)是目前很多编程语言自带的特性,例如Java,Python;GC是一个很好的特性,能让使用这个语言编程的程序员不去关心内存回收,并且降低内存泄漏和内存溢出发生的概率。 我们以Java语言JVM为例,从其对象结构和JVM运行时内存结构出发,针对其GC算法思路和实现进行分析,同时类比其他GC算法。 首先,在Java 8中,Java对象在内存中结构包括: 1. 类型指针:一个指向类信息的指针,描述了对象的类型。 2. 标记字(Mark Word):一组标记,描述了对象的状态,包括对象散列码(如果有)、对象的形状(是否是数组)、锁状态、数组长度(如果标记显示这个对象是数组,描述了数组的长度) 3. 对齐性填充:所有对象都是8字节对齐的 -> 也就是说,所有对象的起始位置都是满足A(A%8==0),所以对于有的对象需要这个对齐性填充来满足这个规则。 4. 域变量区域:这个对象的域变量所占用的内存。Java域变量存在两类:原始类型(primitive type)和普通对象指针(ordinary object pointer)。

    同时,Java对象内存分布还有一些规则,通过openjdk的jol(http://openjdk.java.net/projects/code-tools/jol/)工具我们来查看下这些规律: Maven引入包:

    <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.7.1</version> </dependency>

    1.对齐性填充在域变量区域之前或者末尾(全为8字节长度类型时,补位在前,否则补位在后),测试代码:

    public class MainTest { public static void main(String[] args) throws Exception { out.println(VM.current().details()); out.println(ClassLayout.parseClass(A.class).toPrintable()); } public static class A { long f; } }

    结果:

    # Running 64-bit HotSpot VM. # Using compressed oop with 0-bit shift. # Using compressed klass with 3-bit shift. # Objects are 8 bytes aligned. # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] test.MainTest$A object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 (alignment/padding gap) 16 8 long A.f N/A Instance size: 24 bytes Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

    由于整体对象的纯大小为20bytes,不能满足8bytes对齐,所以需要补位。补位4bytes。 2.原始类型域会重排序,按照长度大小从大到小排列(64位机器上reference类型占用8个字节,开启指针压缩后占用4个字节。其他原始类型如下例子所示):

    public static class B{ boolean bo1, bo2; byte b1, b2; char c1, c2; double d1, d2; float f1, f2; int i1, i2; long l1, l2; short s1, s2; }

    内存中结构:

    OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 (alignment/padding gap) 16 8 long A.f N/A Instance size: 24 bytes Space losses: 4 bytes internal + 0 bytes external = 4 bytes total test.MainTest$B object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 float B.f1 N/A 16 8 double B.d1 N/A 24 8 double B.d2 N/A 32 8 long B.l1 N/A 40 8 long B.l2 N/A 48 4 float B.f2 N/A 52 4 int B.i1 N/A 56 4 int B.i2 N/A 60 2 char B.c1 N/A 62 2 char B.c2 N/A 64 2 short B.s1 N/A 66 2 short B.s2 N/A 68 1 boolean B.bo1 N/A 69 1 boolean B.bo2 N/A 70 1 byte B.b1 N/A 71 1 byte B.b2 N/A Instance size: 72 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

    3.子类的域排列在父类的域之后,但是注意,如果父类需要后置补位,则会将子类的某些域提前,来补位,但是整体上还是满足子类的域在父类的域后面,只是之前的1号规则域变量按长度从大到小排序的规则就不满足了:

    public static class A{ boolean bo1, bo2; byte b1, b2; char c1, c2; double d1, d2; float f1, f2; int i1, i2; long l1, l2; } public static class B extends A{ boolean bo1, bo2; byte b1, b2; char c1, c2; double d1, d2; float f1, f2; int i1, i2; long l1, l2; } public static class C extends B{ boolean bo1, bo2; byte b1, b2; char c1, c2; double d1, d2; float f1, f2; int i1, i2; long l1, l2; }

    对于C,内存中结构为:

    test.MainTest$C object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 float A.f1 N/A 16 8 double A.d1 N/A 24 8 double A.d2 N/A 32 8 long A.l1 N/A 40 8 long A.l2 N/A 48 4 float A.f2 N/A 52 4 int A.i1 N/A 56 4 int A.i2 N/A 60 2 char A.c1 N/A 62 2 char A.c2 N/A 64 1 boolean A.bo1 N/A 65 1 boolean A.bo2 N/A 66 1 byte A.b1 N/A 67 1 byte A.b2 N/A 68 4 float B.f1 N/A 72 8 double B.d1 N/A 80 8 double B.d2 N/A 88 8 long B.l1 N/A 96 8 long B.l2 N/A 104 4 float B.f2 N/A 108 4 int B.i1 N/A 112 4 int B.i2 N/A 116 2 char B.c1 N/A 118 2 char B.c2 N/A 120 1 boolean B.bo1 N/A 121 1 boolean B.bo2 N/A 122 1 byte B.b1 N/A 123 1 byte B.b2 N/A 124 4 float C.f1 N/A 128 8 double C.d1 N/A 136 8 double C.d2 N/A 144 8 long C.l1 N/A 152 8 long C.l2 N/A 160 4 float C.f2 N/A 164 4 int C.i1 N/A 168 4 int C.i2 N/A 172 2 char C.c1 N/A 174 2 char C.c2 N/A 176 1 boolean C.bo1 N/A 177 1 boolean C.bo2 N/A 178 1 byte C.b1 N/A 179 1 byte C.b2 N/A 180 4 (loss due to the next object alignment) Instance size: 184 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

    4.首位补位会被继承: 例如:

    public static class A { long a; } public static class B extends A { long b; int c; }

    这里A类在首位有补位,若不考虑补位的话B的大小正好是8的倍数,应该不需补位,但是由于补位也会继承,所以B需要在末尾补位:

    test.MainTest$A object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 (alignment/padding gap) 16 8 long A.a N/A Instance size: 24 bytes Space losses: 4 bytes internal + 0 bytes external = 4 bytes total test.MainTest$B object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 (alignment/padding gap) 16 8 long A.a N/A 24 8 long B.b N/A 32 4 int B.c N/A 36 4 (loss due to the next object alignment) Instance size: 40 bytes Space losses: 4 bytes internal + 4 bytes external = 8 bytes total

    5.不同的JVM环境下,对象头的大小不同: 对于类A:

    public static class A{ long l1, l2; }

    我们执行:

    Layouter l = new HotSpotLayouter(new X86_32_DataModel()); System.out.println("***** " + l); System.out.println(ClassLayout.parseClass(A.class, l).toPrintable()); l = new HotSpotLayouter(new X86_64_DataModel()); System.out.println("***** " + l); System.out.println(ClassLayout.parseClass(A.class, l).toPrintable()); l = new HotSpotLayouter(new X86_64_COOPS_DataModel()); System.out.println("***** " + l); System.out.println(ClassLayout.parseClass(A.class, l).toPrintable());

    输出为:

    ***** VM Layout Simulation (X32 model, 8-byte aligned, compact fields, field allocation style: 1) test.MainTest$A object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 8 (object header) N/A 8 8 long A.l1 N/A 16 8 long A.l2 N/A Instance size: 24 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total ***** VM Layout Simulation (X64 model, 8-byte aligned, compact fields, field allocation style: 1) test.MainTest$A object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 16 (object header) N/A 16 8 long A.l1 N/A 24 8 long A.l2 N/A Instance size: 32 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total ***** VM Layout Simulation (X64 model (compressed oops), 8-byte aligned, compact fields, field allocation style: 1) test.MainTest$A object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 (alignment/padding gap) 16 8 long A.l1 N/A 24 8 long A.l2 N/A Instance size: 32 bytes Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

    6. 对象头结构探究与验证: 我们用两个空域的类对象来查看对象实例头结构:

    public static class A{ } public static class B{ }

    执行:

    out.println(VM.current().details()); out.println(ClassLayout.parseInstance(new A()).toPrintable()); out.println(ClassLayout.parseInstance(new B()).toPrintable());

    结果:

    # Running 64-bit HotSpot VM. # Using compressed oop with 0-bit shift. # Using compressed klass with 3-bit shift. # Objects are 8 bytes aligned. # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] test.MainTest$A 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) 5d f0 00 20 (01011101 11110000 00000000 00100000) (536932445) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total test.MainTest$B 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) ed f1 00 20 (11101101 11110001 00000000 00100000) (536932845) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

    对于64bits的JVM,开启指针压缩的对象头占12bytes(指针压缩将8bytes的reference类型压缩成了4bytes,本来对象头包括MarkWord和一个指向对象类型的reference类型,32bitsJVM的MarkWord占32bits,64bitsJVM的MarkWord占64bits,即8bytes,加上压缩指针后的对象类型指针,就是12bytes) 7. 轻量锁状态下的对象头: 轻量锁(thin lock)就是没有被争夺过的锁,重量锁(fat lock)就是被同时几个线程所争夺过的锁。 一个轻量级锁的例子:

    public static void main(String[] args) throws Exception { out.println(VM.current().details()); final A a = new A(); ClassLayout layout = ClassLayout.parseInstance(a); out.println("**** Fresh object"); out.println(layout.toPrintable()); synchronized (a) { out.println("**** With the lock"); out.println(layout.toPrintable()); } out.println("**** After the lock"); out.println(layout.toPrintable()); } public static class A{ }

    结果:

    # Running 64-bit HotSpot VM. # Using compressed oop with 0-bit shift. # Using compressed klass with 3-bit shift. # Objects are 8 bytes aligned. # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] **** Fresh object test.MainTest$A 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) 5d f0 00 20 (01011101 11110000 00000000 00100000) (536932445) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total **** With the lock test.MainTest$A object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 20 ea e4 02 (00100000 11101010 11100100 00000010) (48556576) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 5d f0 00 20 (01011101 11110000 00000000 00100000) (536932445) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total **** After the lock test.MainTest$A 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) 5d f0 00 20 (01011101 11110000 00000000 00100000) (536932445) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

    首先我们回顾下普通的加锁过程,首先在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),结构如下:

    虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁记录目前的Mark Word的拷贝(称为Displaced Mark Word),同时,紧跟着一个owner指针指向对象头;

    这时,为了获取锁,该线程尝试CAS更新(compareAndSet(0000……,48556576))这个对象的Mark Word更新为指向Lock Record的指针:如果更新成功,则变成:

    如果更新失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧。如果指向,说明当前线程已经拥有了这个对象的锁(重入锁),那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占了。

    转载请注明原文地址: https://ju.6miu.com/read-40462.html

    最新回复(0)