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是否指向当前线程的栈帧。如果指向,说明当前线程已经拥有了这个对象的锁(重入锁),那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占了。