一线大佬深入讨论JDK中的Unsafe类,给出虚拟机具体实现

2022-10-31 11:19:02 浏览数 (1)

Unsafe类

99%的开发者不使用Unsafe类,也可能从未听说过它,但是有1%的开发者使用Unsafe类,这些1%的开发者通常写一些广泛使用的库,使得99%的开发者被传递性地使用Unsafe类(尽管Unsafe类的意图是仅为JDK内部提供服务)。

目前的用户的困境是:不使用Unsafe类会受到诸多限制,其他可选方案很可能是低效的,比如ByteBuffer API。但如果使用Unsafe类,就失去了Java这门安全语言对于安全的保证,用户也可能被Unsafe类的锋利所伤害,导致虚拟机崩溃,代码变得不可移植,或者JDK升级后代码行为发生改变等。基于这些原因,Java社区逐渐为Unsafe类中一些确实有用的方法提供了安全的替代方案。JEP 193引入的VarHandle,它可以替代JUC.atomic和Unsafe类的部分操作,并提供了标准的内存屏障方法。

JEP 370提供了外部内存访问API(JDK15二次孵化)可以安全且高效地访问堆外内存。

尽管如此,在笔者写作本文之时,Unsafe类仍然是很多三方库实现某些少见需求的首选,在它的替代品日臻完善期间,还是有必要单独讨论下Unsafe类。本节剩余内容将简单介绍Unsafe类中的一些重要方法。

堆外内存

Java堆又叫堆内内存,它交由垃圾回收器全权负责,垃圾回收器在其上分配内存、储存对象、释放内存。与之相对的概念是堆外内存(Off-heap),这部分内存不受垃圾回收器控制,由开发者自行负责。调用 java.nio.ByteBuffer.allocateDirect()可以分配堆外内存,allocateDiret()实际上是借助Unsafe.AllocateMemory实现分配堆外内存分配的,如代码清单4-20所示:

代码清单4-20 Unsafe.allocateMemory/freeMemory底层实现

代码语言:javascript复制
UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory0(...)) {
size_t sz = (size_t)size;
sz = align_up(sz, HeapWordSize);
void* x = os::malloc(sz, mtOther);
return addr_to_java(x);
} UNSAFE_END
UNSAFE_ENTRY(void, Unsafe_FreeMemory0(...)) {
void* p = addr_from_java(addr);
os::free(p);
} UNSAFE_END

在底层实现中,调用os::malloc/free完成。os::malloc/free又是调用glibc的malloc/free函数,所以堆外内存是指由malloc直接分配的一片内存,虚拟机的垃圾回收器不会回收这片内存中的对象,这片内存的管理和释放全权交给开发者。

内存屏障

虽然JSR 133删除模型规定了若干需要插入内存屏障的位置,虚拟机很好地完成了这些任务,但是Java层面是不支持内存屏障的,为了支持以后JUC可能出现的新API特性,也为了Java开发者能进行高级并发编程,JEP 171在Unsafe类中增加了内存屏障方法 loadFence/storeFence/fullFence方法,如代码清单4-21所示:

代码清单4-21 Unsafe.loadFence/storeFence/fullFence底层实现

代码语言:javascript复制
UNSAFE_LEAF(void, Unsafe_LoadFence(JNIEnv *env, jobject unsafe)) {
OrderAccess::acquire();
} UNSAFE_END
UNSAFE_LEAF(void, Unsafe_StoreFence(JNIEnv *env, jobject unsafe)) {
OrderAccess::release();
} UNSAFE_END
UNSAFE_LEAF(void, Unsafe_FullFence(JNIEnv *env, jobject unsafe)) {
OrderAccess::fence();
} UNSAFE_END

通过调用它们可以直接使用内存屏障指令,如x86的lfence、sfence和mfence。

阻塞和唤醒

java.util.concurrent(简称JUC)包含很多并发组件,这些并发组件可以阻塞线程的执行,唤醒线程。这些行为的背后都依赖 java.util.concurrent.locks.LockSupport的park/unpark方法,而LockSupport.park/unpark又最终调用Unsafe.park/unpark实现其功能,如代码清单4-22所示:

代码清单4-22 Unsafe.park/unpark底层实现

代码语言:javascript复制
UNSAFE_ENTRY(void, Unsafe_Park(...)) {
...
thread->parker()->park(isAbsolute != 0, time);
} UNSAFE_END
UNSAFE_ENTRY(void, Unsafe_Unpark(...)) {
Parker* p = NULL;
...
if (p != NULL) {
HOTSPOT_THREAD_UNPARK((uintptr_t) p);
p->unpark();
}
} UNSAFE_END

JUC背后与虚拟机交互的接口就是Unsafe.park/unpark,可以说Unsafe.park/unpark是整个JUC的基石。

对象数据修改

Java提供了java.io.Serializable,配合 ObjectOutputStream/ObjectInputStream可以实现对象序列化和反序列化,但这是一个很慢的操作,还限制类必须提供无参的public构造方法。一些高性能第三方库会使用Unsafe类完成序列化和反序列化操作,它们调用Unsafe.getInt(Object o, long offset)等获取对象o所在偏移offset处的字段进行序列化。用Unsafe.allocateInstance调用对象的构造方法生成对象,再用Unsafe.putLong(Object o, long offset, long x)等将对象o所在偏移offset的字段设置为x值,以此来反序列化。这种方式被广泛用于一些第三方库,如著名开源分布式NoSQL数据库系统Cassandra。

本章小结

4.1节讨论了JVM中五花八门的线程以及它们的作用。4.2节从源码角度分析线程API的实现,同时扩展分析线程API实现时涉及的其他重要模块如JavaCalls、os,并简单提及ParkEvent、Parker、OrderAccess组件。4.3节讨论了线程栈帧的实现。4.4节讨论虚拟机层的代码如何与Java层的代码交互,以此引出JNI和JavaCalls模块。4.5节讨论JDK中的Unsafe类,并给出它在虚拟机的具体实现。

本文给大家讲解的内容是一线大佬深入讨论JDK中的Unsafe类,给出虚拟机具体实现

  1. 下篇文章给大家讲解的是详细讨论解释器的内部构造和解释执行过程;
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!

本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

0 人点赞