学习 jdk16 的 jdk.incubator.foreign 包中 api,这个包还是在 incubator 下,自身不打算写啥货色,然而遇到了一个很有意思的用法,这里记录一下。
这个有相似 jni,jna 的性能,能够调用 C 函数。这个是 jdk16 的新形式,调用 C 函数时,能够传递根底类型,也能够传递地址。
当传递指针时,须要先创立 MemorySegment,而后调用 address() 办法获取地址,这个地址应该是理论内存地址。
就比方传递 int 数组,创立 MemorySegment,调配空间,填充数据
try (MemorySegment segment = MemorySegment.allocateNative(10 * 4)) {for (int i = 0 ; i < 10 ; i++) {MemoryAccess.setIntAtIndex(segment, i);
}
}
所以我一开始传递 byte[] 的话,就须要先创立 MemorySegment,而后将数据先拷贝到其中。我就在想,要是能获取到 byte[] 的理论内存地址,把这个地址传递过来是不是就能够缩小一次拷贝操作吗。
百度了一下,还真有获取 java 对象地址的办法,导入这个包,调用 VM.current().addressOf,就能获取理论物理地址了。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.15</version>
</dependency>
import org.openjdk.jol.vm.VM;
byte[] b1 = new byte[24];
long address = VM.current().addressOf(b1);
当初获取到一个地址了,下边就是测试把这个地址传递给 C 函数,C 函数从其中获取的内容是否合乎预期。
测试代码:
C 定义的函数
int getIntFromPoint(void* p){return ((int*)p)[0];
}
int getByteFromPoint(void* p) {return ((unsigned char*)p)[0];
}
java 代码:
public static void main(String[] args) {System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
Path paths = Path.of("C:\\Users\\h6706\\source\\repos\\screenShot\\x64\\Release\\screenShot.dll");
MethodHandle getByteFromPoint = CLinker.getInstance().downcallHandle(LibraryLookup.ofPath(paths).lookup("getByteFromPoint").get(),
MethodType.methodType(int.class, MemoryAddress.class),
FunctionDescriptor.of(CLinker.C_INT, CLinker.C_POINTER)
);
MethodHandle getIntFromPoint = CLinker.getInstance().downcallHandle(LibraryLookup.ofPath(paths).lookup("getIntFromPoint").get(),
MethodType.methodType(int.class, MemoryAddress.class),
FunctionDescriptor.of(CLinker.C_INT, CLinker.C_POINTER)
);
byte[] b1 = new byte[24];
long address = VM.current().addressOf(b1);
try {Field theInternalUnsafe1 = Unsafe.class.getDeclaredField("theInternalUnsafe");
theInternalUnsafe1.setAccessible(true);
jdk.internal.misc.Unsafe theInternalUnsafe = (jdk.internal.misc.Unsafe) theInternalUnsafe1.get(null);
long ARRAY_BASE_OFFSET = theInternalUnsafe.arrayBaseOffset(byte[].class);
//b[4] 设置为 3,这里尝试一下 java 间接操作内存
theInternalUnsafe.putByte(null, VM.current().addressOf(b1) + ARRAY_BASE_OFFSET + 4, (byte) 3);
try {b1[0] = 8;
b1[3] = 1;
// byte[] 也是对象,他也有对象头,8 字节根本信息,4 字节指针,4 字节数组长度,所有 ARRAY_BASE_OFFSET = 16
int result = (int) getByteFromPoint.invoke(new MemoryAddressImpl(null, address + ARRAY_BASE_OFFSET));
int result2 = (int) getIntFromPoint.invoke(new MemoryAddressImpl(null, address + ARRAY_BASE_OFFSET));
System.out.println(result);
System.out.println(result2);
} catch (Throwable throwable) {throwable.printStackTrace();
}
System.out.println(Arrays.toString(b1));
} catch (Exception e) {e.printStackTrace();
}
}
/**
* 这个获取地址的思路是, 调用 getInt,* 获取 array 中偏移 16 字节地位的值,也就是从 16 地位开始取 4 字节,其实也就是获取到了 Object[0] 的值,必定是一个地址
* <p>
* 但这个地址在开启指针压缩的 jvm 中是还须要再放大 8 倍才是理论地址
*/
public static long getAddress(jdk.internal.misc.Unsafe theInternalUnsafe, Object target) {Object[] array = new Object[1];
array[0] = target;
// 这里其实有两个状况,如果 jvm 开启压缩制作,地址只占有 4 字节,也能够调用 getInt
long anInt = theInternalUnsafe.getLong(array, 16);
/**
* 如果开启指针压缩,则上一步获取的地址并不是理论地址,* 指针压缩是,内存被 jvm 依照 8 字节(不是 8 比特)分块,anInt 就代表是第几块内存
*
* 所以左移 3 次,放大八倍就能够取得到理论内存地址,当然这里也可能不是 3,只是目前在我的电脑上查看是 3
*
* 至于为啥还要加个 0,这个我也不分明, 但如同和调试无关
*
* */
anInt = 0 + (anInt << 3);
return anInt;
}
其中 getAddress 办法是依据 VM.current().addressOf 源码写的,测试没问题。