r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
//如果当前Reference对象是Cleaner类型的就进行特殊处理
c = r instanceof Cleaner ? (Cleaner) r : null;
// unlink 'r' from 'pending' chain
pending = r.discovered;
r.discovered = null;
} else {
// The waiting on the lock may cause an OutOfMemoryError
// because it may try to allocate exception objects.
if (waitForNotify) {
lock.wait();
}
// retry if waited
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
Thread.yield();
// retry
return true;
} catch (InterruptedException x) {
// retry
return true;
}
// clean 不为空的时候,走清理的逻辑
if (c != null) {
c.clean();
return true;
}
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
tryHandlePending
这段代码的意思是:
如果一个对象经过JVM检测他已经没有强引用了,但是还有 弱引用 或者 软引用 或者 虚引用的情况下,那么就会把此对象放到一个名为pending的链表里,这个链表是通过Reference.discovered域连接在一起的。
ReferenceHandler
这个线程会一直从链表中取出被pending的对象,它可能是WeakReference,也可能是SoftReference,当然也可能是PhantomReference和Cleaner。如果是Cleaner,那就直接调用Cleaner的clean方法,然后就结束了。其他的情况下,要交给这个对象所关联的queue,以便于后续的处理。
关于堆外内存分配和回收的代码我们就先分析到这里。需要注意的是对外内存回收的时机也是不确定的,所以不要持续分配一些大对象到堆外,如果没有被回收掉,这是一件很可怕的事情。毕竟它无法被JVM检测到。
内存屏障
硬件层的内存屏障分为两种:Load Barrier
和 Store Barrier
即读屏障和写屏障。内存屏障有两个作用:阻止屏障两侧的指令重排序;强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。在Unsafe中提供了三个方法来操作内存屏障:
//读屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//写屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//全能屏障,禁止load、store操作重排序
public native void fullFence();
先简单了解两个指令:
Store:将处理器缓存的数据刷新到内存中。
Load:将内存存储的数据拷贝到处理器的缓存中。
JVM平台提供了一下几种内存屏障:
屏障类型
指令示例
说明
LoadLoad Barriers
Load1;LoadLoad;Load2
该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作
StoreStore Barriers
Store1;StoreStore;Store2
该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)该操作先于Store2及其后所有存储指令的操作
LoadStore Barriers
Load1;LoadStore;Store2
确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作
StoreLoad Barriers
Store1;StoreLoad;Load2
该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令
StoreLoad Barriers同时具备其他三个屏障的效果,因此也称之为全能屏障
(mfence),是目前大多数处理器所支持的;但是相对其他屏障,该屏障的开销相对昂贵。
loadFence
实现了LoadLoad Barriers,该操作禁止了指令的重排序。
storeFence
实现了 StoreStore Barriers,确保屏障前的写操作能够立刻刷入到主内存,并且确保屏障前的写操作一定先于屏障后的写操作。即保证了内存可见性和禁止指令重排序。
fullFence
实现了 StoreLoad Barriers,强制所有在mfence指令之前的store/load指令,都在该mfence指令执行之前被执行;所有在mfence指令之后的store/load指令,都在该mfence指令执行之后被执行。
在 JDK 中调用了 内存屏障这几个方法的实现类有 StampedLock
。关于StampedLock
的实现我们后面会专门抽出一篇去讲解。它并没有去实现AQS队列。而是采用了 其他方式实现。
系统相关
这部分包含两个获取系统相关信息的方法。
//返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。
public native int addressSize();
//内存页的大小,此值为2的幂次方。
public native int pageSize();
在 java.nio
下的Bits类中调用了pagesize()方法计算系统中页大小:
private static int pageSize = -1;
static int pageSize() {
if (pageSize == -1)
pageSize = unsafe().pageSize();
return pageSize;
}
线程调度
线程调度中提供的方法包括:线程的挂起,恢复 和 对象锁机制等,其中获取对象的监视器锁方法已经被标记为弃用。
// 终止挂起的线程,恢复正常.java.util.concurrent包中挂起操作都是在LockSupport类实现的,其底层正是使用这两个方法
public native void unpark(Object thread);
// 线程调用该方法,线程将一直阻塞直到超时,或者是中断条件出现。
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);
将一个线程进行挂起是通过 park 方法实现的,调用park()
后,线程将一直 阻塞 直到 超时 或者 中断 等条件出现。unpark
可以释放一个被挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在LockSupport
类中,LockSupport 类中有各种版本 pack 方法,但最终都调用了Unsafe.park()
方法。 我们来看一个例子:
package leetcode;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;
/**
* @author: rickiyang
* @date: 2019/8/10
* @description:
*/
public class TestUsafe {
private static Thread mainThread;
public Unsafe getUnsafe() throws Exception {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
return (Unsafe) theUnsafeField.get(null);
}
public void testPark() throws Exception {
Unsafe unsafe = getUnsafe();
mainThread = Thread.currentThread();
System.out.println(String.format("park %s", mainThread.getName()));
unsafe.park(false, TimeUnit.SECONDS.toNanos(3));
new Thread(() -> {
System.out.println(String.format("%s unpark %s", Thread.currentThread().getName(),
mainThread.getName()));
unsafe.unpark(mainThread);
}).start();
System.out.println("main thread is done");
}
public static void main(String[] args) throws Exception {
TestUsafe testUsafe = new TestUsafe();
testUsafe.testPark();
}
}
运行上面的例子,那你会发现在第29行 park
方法设置了超时时间为3秒后,会阻塞当前主线程,直到超时时间到达,下面的代码才会继续执行。
对象操作
Unsafe类中提供了多个方法来进行 对象实例化 和 获取对象的偏移地址 的操作:
// 传入一个Class对象并创建该实例对象,但不会调用构造方法
public native Object allocateInstance(Class<?> cls) throws InstantiationException;
// 获取字段f在实例对象中的偏移量
public native long objectFieldOffset(Field f);
// 返回值就是f.getDeclaringClass()
public native Object staticFieldBase(Field f);
// 静态属性的偏移量,用于在对应的Class对象中读写静态属性
public native long staticFieldOffset(Field f);
// 获得给定对象偏移量上的int值,所谓的偏移量可以简单理解为指针指向该变量;的内存地址,
// 通过偏移量便可得到该对象的变量,进行各种操作
public native int getInt(Object o, long offset);
// 设置给定对象上偏移量的int值
public native void putInt(Object o, long offset, int x);
// 获得给定对象偏移量上的引用类型的值
public native Object getObject(Object o, long offset);
// 设置给定对象偏移量上的引用类型的值
public native void putObject(Object o, long offset, Object x););
// 设置给定对象的int值,使用volatile语义,即设置后立马更新到内存对其他线程可见
public native void putIntVolatile(Object o, long offset, int x);
// 获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
public native int getIntVolatile(Object o, long offset);
// 与putIntVolatile一样,但要求被操作字段必须有volatile修饰
public native void putOrderedInt(Object o, long offset, int x);
allocateInstance
方法在这几个场景下很有用:跳过对象的实例化阶段(通过构造函数)、忽略构造函数的安全检查(反射newInstance()时)、你需要某类的实例但该类没有public的构造函数。
举个例子:
public class User {
private String name;
private int age;
private static String address = "beijing";
public User(){
name = "xiaoming";
}
public String getname(){
return name;
}
}
/**
* 实例化对象
* @throws Exception
*/
public void newInstance() throws Exception{
TestUsafe testUsafe = new TestUsafe();
Unsafe unsafe = testUsafe.getUnsafe();
User user = new User();
System.out.println(user.getname());
User user1 = User.class.newInstance();
System.out.println(user1.getname());
User o = (User)unsafe.allocateInstance(User.class);
System.out.println(o.getname());
}
打印的结果可以看到最后输出的是null,说明构造函数未被加载。可以进一步实验,将User类中的构造函数设置为 private,你会发现在前面两种实例化方式检查期就报错。但是第三种是可以用的。这是因为allocateInstance
只是给对象分配了内存,它并不会初始化对象中的属性。
下面是对象操作的使用示例:
public void testObject() throws Exception{
TestUsafe testUsafe = new TestUsafe();
Unsafe unsafe = testUsafe.getUnsafe();
//通过allocateInstance创建对象,为其分配内存地址,不会加载构造函数
User user = (User) unsafe.allocateInstance(User.class);
System.out.println(user);
// Class && Field
Class<? extends User> userClass = user.getClass();
Field name = userClass.getDeclaredField("name");
Field age = userClass.getDeclaredField("age");
Field location = userClass.getDeclaredField("address");
// 获取实例域name和age在对象内存中的偏移量并设置值
System.out.println(unsafe.objectFieldOffset(name));
unsafe.putObject(user, unsafe.objectFieldOffset(name), "xiaoming");
System.out.println(unsafe.objectFieldOffset(age));
unsafe.putInt(user, unsafe.objectFieldOffset(age), 18);
System.out.println(user);
// 获取定义location字段的类
Object staticFieldBase = unsafe.staticFieldBase(location);
System.out.println(staticFieldBase);
// 获取static变量address的偏移量
long staticFieldOffset = unsafe.staticFieldOffset(location);
// 获取static变量address的值
System.out.println(unsafe.getObject(staticFieldBase, staticFieldOffset));
// 设置static变量address的值
unsafe.putObject(staticFieldBase, staticFieldOffset, "tianjin");
System.out.println(user + " " + user.getAddress());
}
对象实例布局与内存大小
一个Java对象占用多大的内存空间呢?这个问题很值得读者朋友去查一下。 因为这个输出本篇的重点所以简单说一下。一个 Java 对象在内存中由对象头、示例数据和对齐填充构成。对象头存储了对象运行时的基本数据,如 hashCode、锁状态、GC 分代年龄、类型指针等等。实例数据是对象中的非静态字段值,可能是一个原始类型的值,也可能是一个指向其他对象的指针。对齐填充就是 padding,保证对象都采用 8 字节对齐。除此以外,在 64 位虚拟机中还可能会开启指针压缩,将 8 字节的指针压缩为 4 字节,这里就不再过多介绍了。
也就是说一个 Java 对象在内存中,首先是对象头,然后是各个类中字段的排列,这之间可能会有 padding 填充。这样我们大概就能理解字段偏移量的含义了,它实际就是每个字段在内存中所处的位置。
public class User {
private String name;
private int age;
}
TestUsafe testUsafe = new TestUsafe();
Unsafe unsafe = testUsafe.getUnsafe();
for (Field field : User.class.getDeclaredFields()) {
System.out.println(field.getName() + "-" + field.getType() + ": " + unsafe.objectFieldOffset(field));
}
结果:
name-class java.lang.String: 16
age-int: 12
从上面的运行结果中可以:
age:偏移值为12,即前面 12 个字节的对象头;
name:name从16字节开始,因为int 类型的age占了4个字节。
继续算下去整个对象占用的空间,对象头12,age 4,name 是指针类型,开启指针压缩占用4个字节,那么User对象整个占用20字节,因为上面说的padding填充,必须8字节对齐,那么实际上会补上4个字节的填充,即一共占用了24个字节。
按照这种计算方式,我们可以字节写一个计算size的工具类:
public static long sizeOf(Object o) throws Exception{
TestUsafe testUsafe = new TestUsafe();
Unsafe unsafe = testUsafe.getUnsafe();
HashSet fields = new HashSet();
Class c = o.getClass();
while (c != Object.class) {
for (Field f : c.getDeclaredFields()) {
if ((f.getModifiers() & Modifier.STATIC) == 0) {
fields.add(f);
}
}
//如果有继承父类的话,父类中的属性也是要计算的
c = c.getSuperclass();
}
//计算每个字段的偏移量,因为第一个字段的偏移量即在对象头的基础上偏移的
//所以只需要比较当前偏移量最大的字段即表示这是该对象最后一个字段的位置
long maxSize = 0;
for (Field f : fields) {
long offset = unsafe.objectFieldOffset(f);
if (offset > maxSize) {
maxSize = offset;
}
}
//上面计算的是对象最后一个字段的偏移量起始位置,java中对象最大长度是8个字节(long)
//这里的计算方式是 将 当前偏移量 / 8 + 8字节 的padding
return ((maxSize/8) + 1) * 8;
}
上面的工具类计算的结果也是24。
class相关操作
//静态属性的偏移量,用于在对应的Class对象中读写静态属性
public native long staticFieldOffset(Field f);
//获取一个静态字段的对象指针
public native Object staticFieldBase(Field f);
//判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。 当且仅当ensureClassInitialized方法不生效时返回false
public native boolean shouldBeInitialized(Class<?> c);
//确保类被初始化
public native void ensureClassInitialized(Class<?> c);
//定义一个类,可用于动态创建类,此方法会跳过JVM的所有安全检查,默认情况下,ClassLoader(类加载器)和ProtectionDomain(保护域)实例来源于调用者
public native Class<?> defineClass(String name, byte[] b, int off, int len,
ClassLoader loader,
ProtectionDomain protectionDomain);
//定义一个匿名类,可用于动态创建类
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
数组 操作
数组操作主要有两个方法:
//返回数组中第一个元素的偏移地址
public native int arrayBaseOffset(Class<?> arrayClass);
//返回数组中一个元素占用的大小
public native int arrayIndexScale(Class<?> arrayClass);
CAS操作
相信所有的开发者对这个词都不陌生,在AQS类中使用了无锁的方式来进行并发控制,主要就是CAS的功劳。
CAS的全称是Compare And Swap 即比较交换,其算法核心思想如下
执行函数:CAS(V,E,N)
包含3个参数
V表示要更新的变量
E表示预期值
N表示新值
如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。通俗的理解就是CAS操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明没有别的线程修改该值,当前线程可以进行修改,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作。
Unsafe类中提供了三个方法来进行CAS操作:
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
另外,在 JDK1.8中新增了几个 CAS 的方法,他们的实现是基于上面三个方法做的一层封装:
//1.8新增,给定对象o,根据获取内存偏移量指向的字段,将其增加delta,
//这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
//获取内存中最新值
v = getIntVolatile(o, offset);
//通过CAS操作
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
//1.8新增,方法作用同上,只不过这里操作的long类型数据
public final long getAndAddLong(Object o, long offset, long delta) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, v + delta));
return v;
}
//1.8新增,给定对象o,根据获取内存偏移量对于字段,将其 设置为新值newValue,
//这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
public final int getAndSetInt(Object o, long offset, int newValue) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, newValue));
return v;
}
// 1.8新增,同上,操作的是long类型
public final long getAndSetLong(Object o, long offset, long newValue) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, newValue));
return v;
}
//1.8新增,同上,操作的是引用类型数据
public final Object getAndSetObject(Object o, long offset, Object newValue) {
Object v;
do {
v = getObjectVolatile(o, offset);
} while (!compareAndSwapObject(o, offset, v, newValue));
return v;
}
CAS在java.util.concurrent.atomic相关类、Java AQS、CurrentHashMap等实现上有非常广泛的应用。