写屏障
记忆集缩减了跨代引用GC Roots
扫描范围过大的问题,
记忆集通常使用卡表实现,卡表元素变脏,表示卡表元素对应的卡页中的对象存在跨代引用。
那么卡表元素何时变脏?卡变由谁维护?
卡表元素何时变脏?
有其他分代区域对象引用了本区域对象时,其对应的卡表元素就应该变脏,
变脏的时机原则上应该发生在引用类型字段赋值的那一刻。
如果是解释执行的字符串,虚拟机有充分的介入空间。但经过即时编译后的代码
已经是纯粹的机器指令流了,因此必须找到一个在机器码层面的手段把维护卡表的动作
放到每一个赋值操作之中。
写屏障(Write Barrier)
在HotSpot
虚拟机里是通过写屏障(Write Barrier
)技术维护卡表状态的。注意,这不是内存屏障。
写屏障可以看做是虚拟机层面对“引用类型字段赋值”这个动作的AOP
切面,在引用对象赋值时会产生
一个环形(Around
)通知,提供程序执行额外的动作,赋值前的部分的写屏障叫做写前屏障(Pre-Write Barrier
),
赋值后的则叫写后屏障(Post-Write Barrier
)。
void oop_field_store(oop* field, oop new_value) {
// 引用字段赋值操作
*field = new_value;
// 写后屏障,在这里更新卡表
post_write_barrier(field, new_value);
}
应用写屏障后,虚拟机会为所有赋值操作生成相应指令,无论更新的是不是老年代对新生代对象的引用,
每次只要对引用进行更新,都会触发更新卡表的动作。这个额外的开销与Minor GC
时扫描整个老年代的代价要低得多。
伪共享问题(False Sharing)
伪共享是处理并发底层细节时一种经常需要考虑的问题,现代中央处理器的缓存系统中是以缓存行(Cache Line
)为单位存储的,
当多线程修改相互独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或同步)而导致性能降低。
Java
验证伪共享
public class DemoOne {
public static class T {
volatile long x;
}
T[] ts = new T[2];
public void method() throws InterruptedException {
ts[0] = new T();
ts[1] = new T();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000000; i++)
ts[0].x = i;
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000000; i++)
ts[1].x = i;
});
long start = System.currentTimeMillis();
t1.start();t2.start();
t1.join();t2.join();
System.out.println("use time : " + (System.currentTimeMillis() - start));
}
public static void main(String[] args) throws InterruptedException {
new DemoOne().method();
}
}
public class DemoTwo {
public static class T {
long p1, p2, p3, p4, p5, p6;
volatile long x;
long p7, p8, p9, p10, p11, p12;
}
T[] ts = new T[2];
public void method() throws InterruptedException {
ts[0] = new T();
ts[1] = new T();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000000; i++)
ts[0].x = i;
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000000; i++)
ts[1].x = i;
});
long start = System.currentTimeMillis();
t1.start();t2.start();
t1.join();t2.join();
System.out.println("use time : " + (System.currentTimeMillis() - start));
}
public static void main(String[] args) throws InterruptedException {
new DemoTwo().method();
}
}
DemoOne
用时:3247 毫秒
DemoTwo
用时:1368 毫秒
解释
操作系统一次读取 64 个字节为一个缓存行(通常情况)。
使用了 volatile 关键字,x变量对于线程是可见的,x发生变化时会使缓存行失效。
T[] ts = new T[2];
对象大小很小时,两个对象会在同一个缓存行中。其中某个对象的x发生变化都会使缓存行失效。互相影响。
可以通过填充对象大小来避免(
Disruptor
的实现方式),可以通过使用
@Contended
避免。
卡变更新伪共享问题处理
不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏。
if (CARD_TABLE [this address >> 9] != 0)
CARD_TABLE [this address >> 9] = 0;
在JDK7
之后,HotSpot
虚拟机增加了一个新的参数-XX:+UseCondCardMark
,用来决定是否开启卡表更新的条件判断。
开启会增加一次额外判断的开销,不开启会有伪共享问题。