为什么会存在sun.misc.Unsafe类,它在现实世界中如何使用?

276

我最近了解到sun.misc.Unsafe包,惊讶于它所能实现的功能。

当然,该类未被记录在文档中,但我想知道是否有使用它的充分理由。在哪些情况下可能需要使用它?在现实世界的场景中,它可能如何使用?

此外,如果您确实需要它,这是否不表示您的设计可能存在问题?

为什么Java甚至要包括这个类?


7
JDK 的开发人员目前正在审查此 API,以确定是否将其转换为 Java 9 中的公共 API。如果您使用它,值得花费 5 分钟填写调查问卷:https://www.surveymonkey.com/s/sun-misc-Unsafe。 - Andy Lynch
2
这篇帖子正在meta上讨论:http://meta.stackoverflow.com/questions/299139/what-to-do-with-close-open-wars - Jon Clements
16个回答

162

例子

  1. 虚拟机“内部化”。例如,在无锁哈希表中使用CAS(比较和交换)。 例如:sun.misc.Unsafe.compareAndSwapInt 它可以将真正的JNI调用转换为包含CAS特殊指令的本地代码。

    在此处阅读有关CAS的更多信息:http://en.wikipedia.org/wiki/Compare-and-swap

  2. 主机VM的sun.misc.Unsafe功能可用于分配未初始化的对象,然后将构造函数调用解释为任何其他方法调用。

  3. 可以跟踪来自本机地址的数据。使用java.lang.Unsafe类可以检索对象的内存地址,并通过不安全的get / put方法直接操作其字段!

  4. JVM的编译时优化。使用“magic”实现高性能VM,需要低级操作。例如:http://en.wikipedia.org/wiki/Jikes_RVM

  5. 分配内存,sun.misc.Unsafe.allocateMemory例如:-当调用ByteBuffer.allocateDirect时,DirectByteBuffer构造函数会在内部调用它。

  6. 跟踪调用堆栈并使用sun.misc.Unsafe实例化的值进行重播,对于仪器很有用

  7. sun.misc.Unsafe.arrayBaseOffset和arrayIndexScale可用于开发数组,这是一种将大型数组有效地拆分为较小对象以限制实时扫描、更新或移动大型对象成本的技术。

  8. http://robaustin.wikidot.com/how-to-write-to-direct-memory-locations-in-java

有关更多参考信息,请单击此处:http://bytescrolls.blogspot.com/2011/04/interesting-uses-of-sunmiscunsafe.html


1
如果使用Unsafe获取字段的地址,那么它总是可以被GC更改,那么这个操作不就变得无用了吗? - pdeva
获取您已分配的地址 - zudokod
你所说的“我”分配的是什么意思?这似乎在使用“new”运算符创建对象的地方使用,因此我的问题。 - pdeva
1
unsafe.allocateMemory和放置值 - zudokod
sun.misc.Unsafe.arrayBaseOffset和arrayIndexScale不能用于数组,除了消除边界检查之外。访问保留数组之外的数组可能会导致内存损坏和/或段错误。 - bestsss
1
关于第二点,我想知道如何像调用其他方法一样调用构造函数?因为除了在字节码中,我没有找到任何方法可以这样做。 - Miguel Gamboa

32

仅从在一些代码搜索引擎上运行搜索,我得到以下示例:

简单的类,用于获取对{@link Unsafe}对象的访问。需要{@link Unsafe}以允许对数组进行高效的CAS操作。请注意,在{@link java.util.concurrent.atomic}中的版本(例如{@link java.util.concurrent.atomic.AtomicLongArray})需要额外的内存排序保证,这些保证通常不需要在这些算法中,并且在大多数处理器上也很昂贵。

  • SoyLatte -适用于OSX javadoc摘录的Java 6。

/** 基于sun.misc.Unsafe的FieldAccessors的静态字段的基类。观察是,从反射代码的角度来看,只有九种类型的字段:八种基本类型和Object。使用Unsafe类而不是生成的字节码可节省内存和加载时间用于动态生成的FieldAccessors。*/

  • SpikeSource

/*需要将FinalFields发送到网络上的情况下,如何在接收端解组并重新创建对象?我们不想调用构造函数,因为它会为final字段建立值。我们必须像发送方那样完全重新创建最终场。sun.misc.Unsafe为我们完成此操作。*/

还有许多其他例子,请遵循上面的链接...


25

有趣的是,我以前甚至没听说过这个类(其实这可能是一件好事)。

我想到的一个事情是使用Unsafe#setMemory来清零曾经包含敏感信息的缓冲区(密码、密钥等)。你甚至可以对“不可变”的对象的字段进行清零操作(尽管我认为普通的反射也可以解决这个问题)。但我不是安全专家,所以请带着点怀疑的态度。


4
我以前从来没听说过这门课...我已经告诉你这么多次了!叹气 + :( - Tim Bender
7
没必要,因为Java使用复制式代际垃圾回收器,你的敏感信息很可能已经在“空闲”内存的其他位置等待被覆盖。 - Daniel Cassidy
39
我也没听说过它,但我喜欢它们的 park() 文档:"阻塞当前线程,并在发生平衡 unpark 时返回,或者已经发生了平衡 unpark,或者线程被中断,或者如果不是绝对时间并且时间不为零,则经过给定的时间纳秒,或者如果是绝对时间,则自 Epoch 以来经过了给定的截止日期(毫秒),_或出于无 '原因' 而虚假地返回_”。几乎和“内存在程序退出时释放,或在随机间隔中释放,以先到者为准”一样好。 - aroth
1
@Daniel,有趣,我没有考虑过那个。现在你可以看到为什么我不是安全专家了。 :) - Mike Daniels

24

通过使用Eclipse进行参考跟踪,对Java 1.6.12库进行简要分析后发现,似乎每个Unsafe实用功能都以有用的方式暴露出来。

CAS操作通过Atomic *类公开。 内存操作函数通过DirectByteBuffer公开。 同步指令(park,unpark)通过AbstractQueuedSynchronizer公开,后者又由Lock实现使用。


AtomicXXXUpdaters太慢了,当你真正需要它们时:CAS - 你实际上负担不起使用它们。如果你要做金属,你不会使用抽象级别和众多的检查。在循环中失败CAS是不好的,特别是当硬件决定错误地预测分支(由于高争用)但是有更多的比较/分支只会伤害。Park/Unpark通过LockSupport公开,而不是AQS(后者更像是锁实现而不是park/unpark)。 - bestsss

21

Unsafe.throwException - 允许在不声明异常的情况下抛出已检查异常。

这在处理反射或AOP时非常有用。

假设你为用户定义的接口构建了通用代理,并且用户可以通过在接口中声明异常来指定在特定情况下抛出的异常。那么这是我所知道的唯一方式,在接口的动态实现中引发已检查的异常。

import org.junit.Test;
/** need to allow forbidden references! */ import sun.misc.Unsafe;

/**
 * Demonstrate how to throw an undeclared checked exception.
 * This is a hack, because it uses the forbidden Class {@link sun.misc.Unsafe}.
 */
public class ExceptionTest {

    /**
     * A checked exception.
     */
    public static class MyException extends Exception {
        private static final long serialVersionUID = 5960664994726581924L;
    }

    /**
     * Throw the Exception.
     */
    @SuppressWarnings("restriction")
    public static void throwUndeclared() {
        getUnsafe().throwException(new MyException());
    }

    /**
     * Return an instance of {@link sun.misc.Unsafe}.
     * @return THE instance
     */
    @SuppressWarnings("restriction")
    private static Unsafe getUnsafe() {
        try {

            Field singleoneInstanceField = Unsafe.class.getDeclaredField("theUnsafe");
            singleoneInstanceField.setAccessible(true);
            return (Unsafe) singleoneInstanceField.get(null);

        } catch (IllegalArgumentException e) {
            throw createExceptionForObtainingUnsafe(e);
        } catch (SecurityException e) {
            throw createExceptionForObtainingUnsafe(e);
        } catch (NoSuchFieldException e) {
            throw createExceptionForObtainingUnsafe(e);
        } catch (IllegalAccessException e) {
            throw createExceptionForObtainingUnsafe(e);
        }
    }

    private static RuntimeException createExceptionForObtainingUnsafe(final Throwable cause) {
        return new RuntimeException("error while obtaining sun.misc.Unsafe", cause);
    }


    /**
     * scenario: test that an CheckedException {@link MyException} can be thrown
     * from an method that not declare it.
     */
    @Test(expected = MyException.class)
    public void testUnsingUnsaveToThrowCheckedException() {
        throwUndeclared();
    }
}

14
你可以使用Thread.stop(Throwable)完成相同的操作,无需使用不安全的方法。在同一线程中,你可以随意抛出任何异常(因为没有编译检查)。 - bestsss
你可以纯粹通过字节码来实现这一点(或者使用Lomboc来代替你完成)。 - Antimony
1
@bestsss,该方法已被存根并在Java 8的当前线程中抛出“UnsupportedOperationException”。然而,仍然可以使用抛出“ThreadDeath”的无参数版本。 - gparyani
@damryfbfnetsi,我已经有一段时间没有关注核心jdk的讨论了,也没有计划转移到Java 8。然而,这是一个相当令人困惑的想法,因为无论如何都很容易通过字节码生成来实现,除非现在验证器实际上检查方法是否声明了可抛出异常...但这可能会导致向后不兼容,因为有关抛出异常的元数据可以自由丢弃。 - bestsss

11

Unsafe

一组用于执行低级别,不安全操作的方法。尽管该类和所有方法都是公共的,但由于只有受信任的代码才能获得该类的实例,因此对该类的使用受到限制。

其一种用途是在java.util.concurrent.atomic类中:


7

为了实现高效的内存复制(至少对于短块而言比System.arraycopy()更快),Java LZFSnappy编解码器使用'getLong'和'putLong',这比逐字节复制要快得多;在复制16/32/64字节块等内容时尤其高效。


1
哎呀,arraycopy 在 x86-64 上使用 SSE 循环,比 getLong/putLong 更好(而且你还需要计算地址)。 - bestsss
你是否实际进行了测量?对于较短的代码块,在 x86-64 上使用 getLong/ putLong 的组合时,我看到性能表现更好:理想情况下,为了简便和全部一致性,我更喜欢使用 System.arraycopy();但是,实际测试显示对于我测试的案例来说不太适用。 - StaxMan
使用unsafe,我无法从压缩实现中获得任何有意义的性能。对于几个字节长的大数组副本,get/putLong确实有效,但当编译器必须检查长度时,这些操作可能会起作用。一些实现会在System.arrayCopy之后添加内存屏障(虽然可以禁用/启用),因此这可能是真正的罪魁祸首。 - bestsss
好的。新版JDK可能已经改变了这一点;当我观察到更快的操作时(使用JDK 1.6),我也感到惊讶。或者我忘记了某些特定用法上的区别。即使这些优化确实起作用,它们也很棘手(可能不稳定),因此测量效果至关重要。 - StaxMan

6
我最近在重新实现JVM时发现,令人惊讶的是,有很多类都是用Unsafe实现的。这个类主要为Java库实现者设计,包含一些基本不安全但构建快速原语所必需的功能。例如,它有获取和写入原始字段偏移量的方法、使用硬件级别同步、分配和释放内存等方法。它不适合普通的Java程序员使用;它没有文档说明,是特定于实现的,并且本质上是不安全的(因此被命名为Unsafe)。此外,我认为SecurityManager会在几乎所有情况下禁止访问它。
简而言之,它主要存在是为了让库实现者可以访问底层机器,而不必像AtomicInteger这样的某些类中声明每个方法。您不应该在常规Java编程中使用或担心它,因为整个目的是使其余的库足够快,以至于您不需要那种访问方式。

实际上,只有在反射被禁用的情况下,SecurityManager才禁止访问它。 - amara
@sparkleshy- 你能详细说明一下吗? - templatetypedef
虽然从getUnsafe获取实例确实有相当严格的要求,但是使用Unsafe.class.getDeclaredField("theUnsafe"),并设置.setAccessible(true),然后使用.get(null)也可以获取它。 - amara
@sparkleshy- 我很惊讶那个能运行 - 安全管理器应该会标记它。 - templatetypedef

6

离堆集合可能有助于分配大量内存并在使用后立即进行解除分配,而不会受到GC干扰。我编写了一个,用于基于sun.misc.Unsafe的离堆数组/列表的处理。


6
使用它可以高效地访问和分配大量的内存,例如在您自己的像素引擎中!(例如Minecraft风格游戏。)但是,根据我的经验,JVM通常无法消除您真正需要消除边界检查的情况。例如,如果您正在迭代一个大数组,但实际的内存访问被隐藏在循环中的非虚拟*方法调用下面,则JVM可能仍会对每个数组访问执行边界检查,而不是仅在循环前执行一次。因此,为了实现潜在的大型性能提升,您可以通过使用sun.misc.Unsafe的方法,在循环中消除JVM边界检查,直接访问内存,并确保在正确的位置进行任何边界检查。(您会在某个级别上进行边界检查,对吧?)
对于我自己开发的像素引擎,在区块生成和序列化期间(即我一次性读/写整个数组的地方),这导致了显着的性能提升。结果可能有所不同,但如果缺乏边界消除是您的问题,那么这将解决它。

这种方法存在一些潜在的主要问题:具体来说,当您向客户端提供可以无需边界检查访问内存的能力时,他们可能会滥用它。(不要忘记黑客也可以成为您接口的客户端...特别是在Java编写的像素引擎的情况下。)因此,您应该设计您的接口,以使内存访问无法被滥用,或者您应该极其小心地验证用户数据,在它与您的危险接口混合之前,它必须是正确的。考虑到黑客可能使用未经检查的内存访问执行的灾难性事情,最好采用两种方法。


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