当前位置: 首页 > 科技观察

关于Unsafe和ByteBuffer的那些事儿

时间:2023-03-13 05:08:31 科技观察

本文转载自微信公众号《桐人的技术分享》,作者kiritomoe。转载本文请联系桐人的技术分享公众号。前言记得刚学Java的时候,刚学完语法基础,就接触到了反射这个Java提供的特性。虽然看起来这是一个很基础的知识点,但当时无疑是激动人心的,感觉瞬间就out了。“Java初学者”团队。随着工作经验的积累,我也逐??渐学到了很多让自己兴奋的类似知识点。Unsafe的使用无疑是其中之一。sun.misc.Unsafe是JDK原生提供的一个工具类,里面包含了Java语言中很多很酷的操作,比如内存分配和回收,CAS操作,类实例化,内存屏障等等。就像它的名字一样,因为它可以直接操作内存,执行低级系统调用,它提供的操作也比较危险。Unsafe对于扩展Java语言的表达能力,促进原本在较低层(C层)实现的核心库功能在较高层(Java层)代码中的实现起到了很大的作用。从JDK9开始,Java模块化设计的局限性使得非标准库模块无法访问sun.misc.Unsafe。但是在JDK8中,我们还是可以直接操作Unsafe的,如果不学习,后面可能就没有机会了。使用UnsafeUnsafe并不是设计为一般开发者调用的,所以我们不能通过new或者工厂方法来实例化Unsafe对象,通常通过反射获取Unsafe实例:=Unsafe.class.getDeclaredField("theUnsafe");field.setAccessible(true);return(Unsafe)field.get(null);}catch(Exceptione){thrownewRuntimeException(e);}}拿到之后就可以使用了这个全局单例对象可以为所欲为。功能概览图片来自网络,直接借用。上图包含了Unsafe的很多功能,还是比较全面的。如果全部介绍,文章篇幅就太长了,形式难免是流水帐。打算结合自己的一些项目经验和一些比赛经验,从实际的角度讲讲Unsafe的一些使用技巧。内存分配和访问借助Unsafe,Java实际上可以像C++一样直接操作内存。先来看一个ByteBuffer的例子,我们会开辟一个16字节的内存空间,依次写入和读取4个int类型的数据。publicstaticvoidtestByteBuffer(){ByteBufferdirectBuffer=ByteBuffer.allocateDirect(16);directBuffer.putInt(1);directBuffer.putInt(2);directBuffer.putInt(3);directBuffer.putInt(4);directBuffer.flip();System.出}熟悉nio操作的同学对上面的例子应该不会陌生,这是一种非常基础和标准的内存使用方式。Unsafe如何达到相同的效果?publicstaticvoidtestUnsafe0(){Unsafeunsafe=Util.unsafe;longaddress=unsafe.allocateMemory(16);unsafe.putInt(address,1);unsafe.putInt(address+4,2);unsafe.putInt(address+8,3);unsafe.putInt(地址+12,4);System.out.println(unsafe.getInt(地址));System.out.println(unsafe.getInt(地址+4));System.out.println(unsafe.getInt(address+8));System.out.println(unsafe.getInt(address+12));}两种代码的输出结果是一致的:下面的1234是针对Unsafe使用的API介绍的一个一:publicnativelongallocateMemory(longvar1);该native方法分配堆外内存,返回的long类型值为内存首地址,可作为其他UnsafeAPI的入参。如果你看过DirectByteBuffer的源码,你会发现它内部其实是被Unsafe封装的。说到DirectByteBuffer,这里额外提一下,ByteBuffer.allocateDirect分配的堆外内存会受到-XX:MaxDirectMemorySize的限制,而Unsafe分配的堆外内存不会受到限制。当然,它不会被-Xmx限制。如果你正在参加一些比赛,受到启发,你可以把“我明白了”放在公共屏幕上。看到另外两个APIputInt和getInt,你应该意识到肯定还有其他字节操作的API,比如putByte/putShort/putLong,当然put和get也是成??对出现的。该系列API也有一些需要注意的地方。建议成对使用,否则可能会因为字节序问题导致解析失败。你可以看到下面的例子:.unsafe.putInt(directBufferAddress,1);System.out.println("Unsafe.getInt()=="+Util.unsafe.getInt(directBufferAddress));directBuffer.position(0);directBuffer.limit(4);System.out.println("ByteBuffer.getInt()=="+directBuffer.getInt());directBuffer.position(0);directBuffer.limit(4);System.out.println("ByteBuffer.getInt()reverseBytes=="+Integer.reverseBytes(directBuffer.getInt()));}输出如下:Unsafe.putInt(1)Unsafe.getInt()==1ByteBuffer.getInt()==16777216ByteBuffer.getInt()reverseBytes==1可以发现当我们对putInt使用Unsafe,然后对getInt使用ByteBuffer时,结果会不符合预期,需要改变结果的字节顺序才能恢复。这其实是因为ByteBuffer内部判断了当前操作系统的字节顺序。对于int等多字节数据类型,我的测试机采用big-endian存储方式,而Unsafe默认采用small-short顺序存储。如果您有疑问,建议同时使用写入和读取API以避免字节序问题。不知道字节序的同学可以参考我的另一篇文章:《“字节序”是个什么鬼》。内存复制内存复制在实际应用场景中仍然是一个非常普遍的需求。比如我在上一篇文章中刚刚介绍过,当堆内的内存写入磁盘时,需要先将其复制到堆外的内存中。比如我们在做内存聚合的时候,需要对一些数据进行缓冲,这也涉及到内存拷贝。当然也可以使用ByteBuffer或者set/get来操作,但是肯定没有native方法效率高。Unsafe提供了内存拷贝的native方法,可以实现堆内到堆内、堆外到堆外、堆外和堆内之间的拷贝。简而言之,您可以在任何地方复制。publicnativevoidcopyMemory(Objectsrc,longoffset,Objectdst,longdstOffset,longsize);对于堆内存,我们可以直接将对象数组首地址传入src,并指定offset为对应数组类型的偏移量,可以通过arrayBaseOffset方法获取堆内内存存储对象的偏移量publicnativeintarrayBaseOffset(Classvar1);比如获取byte[]的固定偏移量,可以这样做:unsafe.arrayBaseOffset(byte[].class)对于堆外内存,会更直观,dst设置为null,设置dstOffset到Unsafe获得的内存地址。将堆上内存复制到堆外内存的示例代码:publicstaticvoidunsafeCopyMemory(){ByteBufferheapBuffer=ByteBuffer.allocate(4);ByteBufferdirectBuffer=ByteBuffer.allocateDirect(4);heapBuffer.putInt(1234);longaddress=((DirectBuffer)directBuffer).address();Util.unsafe.copyMemory(heapBuffer.array(),16,null,address,4);directBuffer.position(0);directBuffer.limit(4);System.out.println(directBuffer.getInt());}在实际应用中,大多数ByteBuffer相关的源码在内存拷贝时都会使用copyMemory方法。对象的非常规实例化在JDK9模块化之前,如果不想将某些类开放给其他用户使用,或者避免被随意实例化(单例模式),通常有两种常见的做法Case1:privateconstructorpublicclassPrivateConstructorFoo{privatePrivateConstructorFoo(){System.out.println("constructormethodisinvoked");}publicvoidhello(){System.out.println("helloworld");}}如果要实例化对象,首先想到的是通过反射创建publicstaticvoidreflectConstruction(){PrivateConstructorFooprivateConstructorFoo=PrivateConstructorFoo.class.newInstance();privateConstructorFoo.hello();}不出所料,我们得到一个异常java.lang.IllegalAccessException:Classio.openmessaging.Maincannotaccessamemberofclassmoe.cnkirito.PrivateConstructorFoowithmodiers稍微调整一下,调用构造函数创建一个实例publicstaticvoidreflectConstruction2(){Constructorconstructor=PrivateConstructorFoo.class.getDeclaredConstructor();constructor.setAccessible(true);PrivateConstructorFooprivateConstructorFoo=constructor.newInstance();privateConstructors()Foo;输出如下:constructormethodisinvokedhelloworld当然Unsafe也提供了了salocateInstance方法publicNativeBoctallocateInstance(class<?>var1)throwsInStantiationException;也也可以可以实例化,而且而且而且而且更为更为更为更为更为更为publicStaticVoidAllocateInstance()}同样有效!输出结果如下:helloworld注意这里有一个细节,allocateInstance没有触发构造方法案例2:包级实例packagemoe.cnkirito;classPackageFoo{publicvoidhello(){System.out.println("helloworld");}注意,这里我定义了一个包级可访问的对象PackageFoo,只有moe.cnkirito包下的类可以访问它。我们还尝试使用反射包com.bellamm;publicstaticvoidreflectConstruction(){ClassaClass=Class.forName("moe.cnkirito.PackageFoo");aClass.newInstance();}得到了预期的错误:java。lang.IllegalAccessException:Classio.openmessaging.Maincannotaccessamemberofclassmoe.cnkirito.PackageFoowithmodifiers""再次尝试不安全?packagecom.bellamm;publicstaticvoidallocateInstance()throwsException{ClassfooClass=Class.forName("moe.cnkirito.PackageFoo");Objectfoo=Util.unsafe.allocateInstance(fooClass);MethodhelloMethod=fooClass.getDeclaredMethod("hello");helloMethod.setAccessible(true);helloMethod.invoke(foo);}因为com.bellamm包,我们甚至无法定义PackageFoo类,只能在运行时通过反射获取moe.cnkirito.PackageFoo的方法机制,配合Unsafe实例化,最终实现调用,成功输出helloworld。我们花了很多时间进行实验来说明这两个限制情况和Unsafe的解决方案。我们还需要实际的应用场景来证明Unsafe#allocateInstance的价值。我简单列举两种场景:当序列化框架无法使用反射创建对象时,你可以尝试使用Unsafe创建它们作为自下而上的逻辑。获取包级保护类,然后利用反射机制修改一些源码实现或者调用一些native方法。此方法应谨慎使用,不建议用于生产。示例代码:动态修改堆外内存限制,覆盖JVM启动参数:-XX:MaxDirectMemorySizeprivatevoidhackMaxDirectMemorySize(){try{FielddirectMemoryField=VM.class.getDeclaredField("directMemory");directMemoryField.setAccessible(true);directMemoryField.set(newVM(),8L*1024*1024*1024);Objectbits=Util.unsafe.allocateInstance(Class.forName("java.nio.Bits"));FieldmaxMemory=bits.getClass().getDeclaredField("maxMemory");最大内存。setAccessible(true);maxMemory.set(bits,8L*1024*1024*1024);}catch(Exceptione){thrownewRuntimeException(e);}System.out.println(VM.maxDirectMemory());}总结和介绍这些三个Unsafe的用法已经是我个人认为比较常用的几个Unsafe案例了。会用Unsafe的人基本都知道不能盲目使用;当然本文也介绍了一些可能不得不使用Unsafe的实际场景,但更多的还是出现在各种底层源码中。