扩展ByteBuffer类

18

有没有办法创建一个继承ByteBuffer类的类?

ByteBuffer中的一些抽象方法是包私有的,如果我创建java.nio包,则会抛出安全异常。

出于性能原因,我想这样做-例如,getInt大约有10个方法调用,以及相当多的if。即使所有检查都被保留,只有方法调用被内联化且大小端检查被删除,我创建的测试表明它可以快约4倍。


1
我的NIO缓冲区性能优化顶级技巧:使用-server,使您的方法尽可能小(这样它们可以内联)而且有时最好切换到byte[]。 - Tom Hawtin - tackline
类似问题 在thatsjava.com上。 - finnw
相关链接:https://dev59.com/0UbRa4cB1Zd3GeqPy0rJ 和 https://dev59.com/UVPTa4cB1Zd3GeqPil56 - finnw
@finnw,我不会创建一个子类,而是创建一个包装器,可以根据需要暴露底层的ByteBuffer,即仅用于NIO操作。这意味着您每次读/写只会承担一次ByteBuffer开销,这是一个很小的代价。对于每个其他访问,请使用包装器。 - Peter Lawrey
@finnw,我在我的回答中添加了如何绕过验证器声明类的信息,你可以尝试一下,但这确实是一个笨拙的解决方案,如果你选择这样做的话。 - bestsss
显示剩余12条评论
7个回答

15
无法继承ByteBuffer,感谢上帝。原因是没有受保护的构造函数。为什么要感谢上帝呢?因为只有两个真正的子类确保了JVM可以大量优化涉及ByteBuffer的任何代码。如果需要真正地扩展该类,则需要编辑字节码,并将受保护的属性添加到c-tor和DirectByteBuffer(和DirectByteBufferR)中。扩展HeapBuffer毫无意义,因为您可以访问底层数组。使用-Xbootclasspath/p并在需要的包中扩展自己的类(不在java.nio之内)。这是完成它的方法。另一种方法是使用sun.misc.Unsafe并在address()后直接访问内存来执行所需操作。我想出于性能原因,例如getInt有大约10个方法调用以及相当多的if语句。即使所有检查都留下来,而只有方法调用被插入,big / small endian检查被删除,我进行的测试表明,它可能快4倍。现在好处是使用gdb并检查真正生成的机器代码,您会惊讶于将删除多少个检查。我无法想象为什么一个人想要扩展这些类。它们存在是为了允许良好的性能,而不仅仅是OO多态执行。如何声明任何类并绕过Java验证器。关于Unsafe:Unsafe有两种绕过验证器的方法,如果您有一个扩展ByteBuffer的类,可以调用其中任何一个。你需要一些被黑客攻击的版本(但这很容易)ByteBuffer w /公共访问和受保护的c-tor只是为了编译器。以下是这些方法。您可以在自己的风险上使用它们。在声明该类之后,甚至可以使用new关键字(前提是有适当的c-tor)。
public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);    
public native Class defineClass(String name, byte[] b, int off, int len);

你说:“我无法想象为什么一个人会想要扩展类”,这是因为有时候你需要将 ByteBuffer 传递给一个固定的 API(只能接受 ByteBuffer),但你需要让 ByteBuffer 表现出自定义的行为方式(例如通过实现 Closeable 并在调用 close() 时释放一些底层资源,或者可能需要使用自定义数据存储来支持 ByteBuffer)。我之前遇到过多次这种需求。 - Luke Hutchison

10

通过使用反射,您可以忽略保护级别,但这会严重破坏性能目标。

您不能创建 java.nio 包中的类 - 这样做(并以任何方式分发结果)将违反 Sun 的 Java 许可证,理论上可能会导致法律纠纷。

我认为,如果不使用本地方法,想实现您想要的功能可能是不可能的 - 但我也怀疑您是否屈服于过早优化的诱惑。假设您的测试是正确的(微基准测试经常不是):您真的确定访问 ByteBuffer 将成为实际应用程序性能瓶颈的5%吗?当您的应用程序只花费5%的时间获取数据并处理95%的时间时,无论 ByteBuffer.get() 可以快4倍都没多大意义。

为了实现(可能仅仅是理论上的)性能而绕过所有检查并不是一个好主意。性能调整的基本规则是“首先使其正确工作,然后再使其更快”。

编辑:如果如评论中所述,该应用程序实际上在 ByteBuffer 方法中花费了20-40%的时间,而且测试是正确的,这意味着速度提升的潜力为15-30% - 这是显着的,但在我看来并不值得开始使用 JNI 或玩弄 API源代码。我会首先尝试用完所有其他选择:

  • 您是否正在使用-server VM?
  • 能否修改应用程序以减少对 ByteBuffer 的调用,而不是尝试加快其中的调用?
  • 使用分析器查看调用来自哪里 - 也许有些完全不必要
  • 也许可以修改算法,或者您可以使用某种类型的缓存

我认为反射在这里不会有帮助(我本来也想这么说)... 我认为他想做的是实际上改变方法以避免检查。但也许我理解错了... 但无论如何,反射都会抵消任何速度提升。 - TofuBeer
我正在使用-server虚拟机。有没有一种更积极地内联方法调用的方式?(因为如果所有三到四级深度的方法都在那里内联,将会有显着的改进——我不认为虚拟机目前正在这样做,但不知道为什么)。我也认为这不值得去搞JNI。 - Sarmun
@Michael,这是一篇旧帖子,我很抱歉打扰您,但 ByteBuffer 中由分析器测量的时间可能是不正确的。在 Java 中为紧密循环进行分析可能会非常误导,并且取决于热点放置安全点的位置,当使用采样时。当使用钩子修改代码时,情况甚至更糟,因为它会严重破坏内联。 - bestsss
@bestsss,我曾认为分析器会夸大ByteBuffer的成本,但一旦我开始直接使用Unsafe进行比较,它确实产生了可衡量的差异。 - Peter Lawrey
@Peter,这是正确的做法,完整测试ByteBuffer与Unsafe。仍然有点惊讶差异可以超过10%。 - bestsss
显示剩余8条评论

2
ByteBuffer是抽象的,所以你可以扩展它...但我认为你想要做的是扩展实际被实例化的类,而这可能是不可能的。也可能是被实例化的特定类重写了那个方法,使其比ByteBuffer中的方法更有效率。
我还想说,你可能在一般情况下都是错误的,可能并不需要所有那些东西 - 也许对于你测试的内容来说不需要,但很可能代码存在的原因是其他平台(或其他情况)需要。
如果你确信自己是正确的,可以打开一个bug并看看他们有什么建议。
如果你想添加到nio包中,可以尝试在调用Java时设置引导类路径。这应该允许你将你的类放在rt.jar之前。输入java -X以查看如何操作,你需要使用-Xbootclasspath/p开关。

3
ByteBuffer有包私有的抽象_set和_get方法,因此您无法覆盖它。并且所有构造函数都是包私有的,所以您不能调用它们。 - Sarmun
你可以通过反射调用它们(获取方法并在其上调用setAccessible(true)),但这样会很慢。你应该能够通过bootclasspath添加一个类,但正如指出的那样,你不能将其发布。 - TofuBeer
3
“你可以扩展它”这句话并没有告诉我“如果你将你的子类放在引导类路径上,你就可以扩展它”。只要你干预引导类,你就可以随心所欲地做任何事情。对我来说,这很接近于负一分。 - Tom Hawtin - tackline
如果他想进行适当的测试,那就是他必须要做的。请注意,我说过要向Sun报告此问题。在这种情况下,这是一个有效的做法。 - TofuBeer

1
+50赏金,寻找绕过访问限制的方法(仅使用反射无法完成。也许可以使用sun.misc.Unsafe等方式?)
答案是:在Java中没有办法绕过所有的访问限制。
- sun.misc.Unsafe在安全管理器的授权下工作,因此它无法帮助。 - 正如Sarnum所说:
ByteBuffer具有包私有的抽象_set和_get方法,因此您无法覆盖它。而且所有构造函数都是包私有的,因此您无法调用它们。
- 反射允许您绕过很多东西,但前提是安全管理器允许。有许多情况下,您无法控制安全管理器,它是强制性的。如果您的代码依赖于与安全管理器的交互,那么它将不具备“可移植性”或在所有情况下可执行,可以这么说。 - 问题的底线是,试图覆盖字节缓冲区不会解决问题。

除了自己实现所需的方法之外,没有其他选择。在可能的情况下,将方法设为final将有助于编译器进行优化(减少运行时多态性和内联代码生成的需要)。


@finnw 没有任何方法可以通过sun.misc.Unsafe或其他任何方式来规避访问限制。 - Jérôme Verstrynge
@JVersty,Unsafe的命名很恰当,但除非你拥有SecurityManager/访问控制来显式地防止它,否则总有办法绕过访问控制。即使如此,如果你可以启动JVM,你可以做任何你想做的事情。 - Peter Lawrey
@Peter Lawrey,我们的意思是一样的。Unsafe并不是绕过访问权限的关键,安全管理器才是。 - Jérôme Verstrynge
Peter所说的完全正确,如果您启动虚拟机,您可以随心所欲地进行任何操作。如果代码在一个严格的安全框中运行,那么肯定有原因,而且安全检查的开销很可能会超过Unsafe引入的任何改进。 - bestsss

1

一个Java代理可以修改ByteBuffer的字节码并更改构造函数的访问修饰符。当然,您需要在JVM上安装代理,并且仍然需要编译子类以进行编译。如果您正在考虑这样的优化,则必须准备好接受挑战!

我从未尝试过这种低级别的操作。希望在代理程序钩入之前JVM不需要使用ByteBuffer。


我要补充一下,JMockit使用Java Agent方法,并且可以对私有构造函数进行仪器化。不确定它使用了什么黑魔法。 - Peter Davis
为什么要即时执行呢?你仍然需要一些启用字节码的版本来允许正常编译[当然,欺骗编译器也是一个选择,但是比较困难;或者你可以手动创建类文件]。然而,如果你有修改过的类版本并且可以安装任何工具,你可以轻松地改变引导路径。 - bestsss

1
最简单的获取Unsafe实例的方法是通过反射。但是,如果您无法使用反射,则可以创建另一个实例。您可以通过JNI来实现这一点。
我在字节码中尝试创建一个实例而不调用构造函数,允许您创建一个没有可访问构造函数的对象实例。然而,这并没有起作用,因为我得到了字节码的VerifyError。对象必须已经调用构造函数。
我所做的是使用一个ParseBuffer来包装一个直接的ByteBuffer。我使用反射来获取Unsafe引用和address。为了避免超出缓冲区的范围并杀死JVM,我分配比我需要的更多的页面,只要它们没有被触及,就不会为应用程序分配物理内存。这意味着我有更少的边界检查,并且只在关键点进行检查。
使用OpenJDK的调试版本,您可以看到Unsafe get/put方法变成单个机器代码指令。然而,这并不适用于所有JVM,并且可能在所有平台上都不能获得相同的改进。
使用这种方法,我认为您可以获得约40%的时间减少,但存在风险,这是普通Java代码所没有的,即您可以杀死JVM。我使用的用例是一个无对象创建的XML解析器和使用Unsafe处理数据的处理器,与使用普通的直接ByteBuffer相比。我在XML解析器中使用的技巧之一是使用getShort()和getInt()同时检查多个字节,而不是逐个字节地检查。
使用反射来获取Unsafe类是一次性的开销。一旦您拥有Unsafe实例,就没有开销了。

-1
我回答你想要的问题,而不是你问的那个问题。你真正的问题是:“怎样才能让它跑得更快?”答案是:“一次性处理整数数组,而不是单个处理。” 如果瓶颈确实是ByteBuffer.getInt()或者ByteBuffer.getInt(location),那么你不需要扩展类,你可以使用现有的IntBuffer类来大量获取数据以提高处理效率。
int totalLength = numberOfIntsInBuffer;
ByteBuffer myBuffer = whateverMyBufferIsCalled;
int[] block = new int[1024];
IntBuffer intBuff = myBuffer.asIntBuffer();
int partialLength = totalLength/1024;

//Handle big blocks of 1024 ints at a time
try{
  for (int i = 0; i < partialLength; i++) {
     intBuff.get(block);
     // Do processing on ints, w00t!
  }

  partialLength = totalLength % 1024; //modulo to get remainder
  if (partialLength > 0) {
    intBuff.get(block,0,partialLength);
    //Do final processing on ints
  }
} catch BufferUnderFlowException bufo {
   //well, dang!
}

这比一次获取一个int要快得多。迭代int[]数组,该数组具有设置和已知的良好边界,还可以通过消除边界检查和ByteBuffer可能引发的异常来使您的代码JIT更紧凑。

如果需要更高的性能,您可以调整代码或自己编写大小优化的byte[]到int[]转换代码。我曾经使用它来代替IntBuffer方法并进行部分循环展开以获得一些性能提升...但这绝不是建议的做法。


如果缓冲区仅包含int,则可以这样做。但在我的情况下,缓冲区可能包含不同类型的数据,而且没有大的连续块只包含单一类型,这种方法将无法帮助。 - Sarmun
然后从一个巨大的byte[]开始,自己编写与int/byte转换方法等效的代码。少一些安全检查、条件判断和异常处理,这样可能会比ByteBuffer方法更快。如果可能的话,直接操作原始类型通常更快,因为循环可以更紧密地优化。 - BobMcGee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接