世界末日noip
Java作为一种语言,JVM作为一个平台刚刚庆祝了其20岁生日。 Java起源于机顶盒,手机和Java卡以及各种服务器系统,因此它正成为物联网的通用语言。 很明显,Java无处不在!
不太明显的是,Java还沉浸在各种低延迟应用程序中,例如游戏服务器和高频交易应用程序。 只有由于类和包的Java可见性规则的严重不足,才使这成为可能,该类提供了对有争议的小类sun.misc.Unsafe的访问 。 此类曾经是,现在仍然是分隔符; 一些人热爱它,另一些人则热衷于讨厌它-最重要的部分是,它帮助JVM和Java生态系统发展到今天。 Unsafe类在Java的一些严格的严格安全标准上基本上为了速度而妥协。
就像多情的讨论JCrete ,或者我们的“ 怎么办sun.misc.Unsafe ”使命纸和博客如这一个在DripStat,在Java世界中,如果sun.misc.Unsafe可能发生的事情产生意识(沿和一些较小的专用API)在没有足够的API替换的情况下就消失了。 Oracle的最终建议( JEP260 )现在通过提供良好的迁移路径解决了该问题。 但是问题仍然存在-一旦不安全的尘埃落定,这个Java世界将会如何?
组织
瞥一眼sun.misc.Unsafe功能部件集令人不安地意识到,它被用作所有功能部件的一站式转储场。
尝试对这些功能进行分类会产生以下五组用例:
- 对变量和数组内容的原子访问,自定义内存隔离
- 序列化支持
- 自定义内存管理/高效的内存布局
- 与本机代码或其他JVM的互操作性
- 高级锁定支持
为了寻求所有这些功能的替代,我们至少可以宣布最后一个功能的成功。 Java已经有相当长一段时间为此功能提供了一个功能强大(坦率地说非常好)的官方API,即java.util.concurrent.LockSupport 。
原子访问
原子访问是sun.misc.Unsafe经常使用的功能之一,具有基本的“放置”和“获取”(带有或不带有可变语义)以及比较和交换(CAS)操作。
public long update() {
for(;;) {
long version = this.version;
long newVersion = version + 1;
if (UNSAFE.compareAndSwapLong(this, VERSION_OFFSET, version, newVersion)) {
return newVersion;
}
}
}
但是,等等,Java是否不通过某些官方API提供对此的支持? 绝对地,通过Atomic类,是的,它与基于sun.misc.Unsafe的API一样丑陋,实际上由于其他原因而变得更糟,让我们看看原因。
AtomicX类实际上是真实的对象。 例如,假设我们要在存储系统中维护一条记录,并且想要跟踪某些统计信息或元数据(例如版本计数器):
public class Record {
private final AtomicLong version = new AtomicLong(0);
public long update() {
return version.incrementAndGet();
}
}
尽管代码可读性强,但它在每个数据记录中用两个不同的对象而不是一个对象污染了我们的堆,即原子实例以及我们的实际记录本身。 问题不仅是多余的垃圾生成,还包括额外的内存占用以及原子实例的其他取消引用。
但是,我们可以做得更好-还有另一个API, java.util.concurrent.atomic.AtomicXFieldUpdater类 。
AtomixXFieldUpdater是普通Atomic类的内存优化版本,为API简化而交换了内存占用量。 使用此组件,单个实例可以支持一个类的多个实例(在我们的示例中为Records),并且可以更新易失性字段。
public class Record {
private static final AtomicLongFieldUpdater<Record> VERSION =
AtomicLongFieldUpdater.newUpdater(Record.class, "version");
private volatile long version = 0;
public long update() {
return VERSION.incrementAndGet(this);
}
}
这种方法的优点是可以在对象创建方面产生更有效的代码。 而且,更新程序是一个静态的final字段,对于任何数量的记录,只需要一个更新程序,最重要的是,该更新程序在今天可用。 另外,它是受支持的公共API,几乎应该始终是您的首选策略。 另一方面,查看更新程序的创建和使用,它仍然很丑陋,可读性不强,并且坦率地违反直觉。
但是我们可以做得更好吗? 是的, Variable Handles (或亲切的称呼为“ VarHandles”)在设计板上,并提供了更具吸引力的API。
VarHandles是对数据行为的抽象。 它们不仅在字段上而且在数组或缓冲区内的元素上都提供类似于volatile的访问。
乍看之下的例子乍一看似乎很奇怪,所以让我们看看发生了什么。
public class Record {
private static final VarHandle VERSION;
static {
try {
VERSION = MethodHandles.lookup().findFieldVarHandle
(Record.class, "version", long.class);
} catch (Exception e) {
throw new Error(e);
}
}
private volatile long version = 0;
public long update() {
return (long) VERSION.addAndGet(this, 1);
}
}
VarHandles是使用MethodHandles API创建的,该API是JVM内部链接行为的直接入口点。 我们使用MethodHandles-Lookup,传入包含的类,字段名称和字段类型,或者我们“取消反映” java.lang.reflect.Field实例。
那么,您可能会问,为什么这比AtomicXFieldUpdater API更好? 如前所述,VarHandles是所有类型的变量,数组甚至ByteBuffer的通用抽象。 也就是说,您对所有这些不同类型只有一个抽象。 从理论上讲,这听起来非常不错,但是在当前的原型中仍然有些欠缺。 由于编译器尚无法自动找出返回值,因此必须对返回值进行显式转换。 另外,由于实施的年轻原型状态,结果还有更多的奇怪之处。 我希望随着更多人参与VarHandles,以及随着Valhalla项目中提出的一些相关语言增强功能的出现,这些问题将在未来消失。
序列化
如今,另一个重要的用例是序列化。 无论是设计分布式系统,还是要将序列化的元素存储到数据库中,或者想要简化使用,Java对象都需要以某种方式快速进行序列化和反序列化。 座右铭是“越快越好”。 因此,许多序列化框架都使用Unsafe :: allocateInstance ,该实例化对象同时阻止构造函数的调用,这在反序列化中很有用。 由于通过反序列化过程重新创建了先前的对象状态,因此节省了很多时间,并且仍然是安全的。
public String deserializeString() throws Exception {
char[] chars = readCharsFromStream();
String allocated = (String) UNSAFE.allocateInstance(String.class);
UNSAFE.putObjectVolatile(allocated, VALUE_OFFSET, chars);
return allocated;
}
请注意,即使sun.misc.Unsafe仍然可用,该代码片段在Java 9中仍可能会中断,因为我们正在努力优化String的内存占用量。 这将删除Java 9中的char []值,并将其替换为byte []。 有关更多详细信息,请参阅有关提高String的内存效率的JEP草案 。
返回主题:还没有Unsafe :: allocateInstance的替代建议,但是jdk9-dev邮件列表正在讨论某些解决方案。 一种想法是将私有类sun.reflect.ReflectionFactory :: newConstructorForSerialization移至受支持的位置,以防止以不安全的方式实例化核心类。 另一个有趣的建议是冻结数组 ,这也可能在将来对序列化框架有所帮助。
可能看起来像下面的代码片段,由于没有提案,这完全是我的主意,但它基于当前可用的sun.reflect.ReflectionFactory API。
public String deserializeString() throws Exception {
char[] chars = readCharsFromStream().freeze();
ReflectionFactory reflectionFactory =
ReflectionFactory.getReflectionFactory();
Constructor<String> constructor = reflectionFactory
.newConstructorForSerialization(String.class, char[].class);
return constructor.newInstance(chars);
}
这将调用一个特殊的反序列化构造函数,该构造函数接受冻结的char []。 默认的String构造函数创建一个传递的char []的副本,以禁止外部突变。这种特殊的反序列化构造函数可以防止复制给定的char [],因为它是冻结数组。 稍后更多有关冻结阵列的信息。 同样,请记住,这只是我的人工翻译,在实际草稿中可能看起来有所不同。
内存管理
sun.misc.Unsafe最重要的用法可能是读写。 如第一节所述,不仅要写入堆,而且尤其要写入普通Java堆以外的区域。 在这种习惯用法中,获取本机内存(通过地址/指针表示),并手动计算偏移量。 例如:
public long memory() {
long address = UNSAFE.allocateMemory(8);
UNSAFE.putLong(address, Long.MAX_VALUE);
return UNSAFE.getLong(address);
}
有些人可能会说直接使用ByteBuffers可以实现相同的目的:
public long memory() {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8);
byteBuffer.putLong(0, Long.MAX_VALUE);
return byteBuffer.getLong(0);
}
从表面上看,这种方法似乎更有吸引力。 不幸的是,因为只能使用int( ByteBuffer :: allocateDirect(int) )创建DirectByteBuffer,所以ByteBuffer的数据限制为大约2 GB。 另外,ByteBuffer API上的所有索引也只有32位。 是比尔·盖茨曾经问过“谁将需要超过32位?”吗?
改型为使用long型的API将破坏兼容性,因此可以使用VarHandles。
public long memory() {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8);
VarHandle bufferView =
MethodHandles.byteBufferViewVarHandle(long[].class, true);
bufferView.set(byteBuffer, 0, Long.MAX_VALUE);
return bufferView.get(byteBuffer, 0);
}
在这种情况下,VarHandle API真的更好吗? 目前,我们受到同样的限制。 我们只能创建约2 GB的ByteBuffer ,并且针对ByteBuffer的视图的内部VarHandle实现也基于ints,但这可能是“可修复的”。 因此,目前尚无真正解决此问题的方法。 不过,这里的好处是,该API还是与第一个示例相同的VarHandle API。
一些其他选项正在讨论中。 Oracle工程师和JEP 193:可变句柄的项目所有者Paul Sandoz在Twitter上谈到了“ 内存区域”的概念。 尽管该概念仍然模糊不清,但该方法看起来很有希望。 干净的API 可能类似于以下代码片段。
public long memory() {
MemoryRegion region = MemoryRegion
.allocateNative("myname", MemoryRegion.UNALIGNED, Long.MAX_VALUE);
VarHandle regionView =
MethodHandles.memoryRegionViewVarHandle(long[].class, true);
regionView.set(region, 0, Long.MAX_VALUE);
return regionView.get(region, 0);
}
这只是一个主意,希望本地代码OpenJDK项目Project Panama会在不久的将来提出这些抽象的建议。 实际上,Project Panama是最合适的选择,因为这些内存区域还需要与希望将内存地址(指针)传递到其调用中的本机库一起使用。
互通性
最后一个主题是互操作性。 这不限于在不同的JVM之间进行有效的数据传输(也许通过共享内存,这也可以是一种内存区域,并且可以避免套接字通信缓慢)。 它还涵盖了使用本机代码进行的通信和信息交换。
巴拿马计划(Project Panama)扬帆起航,以一种更加类似于Java的高效方式取代了JNI。 遵循JRuby的人们可能会认识Charles Nutter在JNR,Java Native Runtime,尤其是JNR-FFI实现方面所做的努力。 FFI表示外来功能接口,是使用其他语言(如Ruby,Python等)的人的典型术语。
FFI基本上构建了一个直接从当前语言直接调用C(并取决于实现C ++)的抽象层,而无需像Java中那样创建粘合代码。
例如,假设我们要通过Java获取pid。 当前需要以下所有C代码:
extern c {
JNIEXPORT int JNICALL
Java_ProcessIdentifier_getProcessId(JNIEnv *, jobject);
}
JNIEXPORT int JNICALL
Java_ProcessIdentifier_getProcessId(JNIEnv *env, jobject thisObj) {
return getpid();
}
public class ProcessIdentifier {
static {
System.loadLibrary("processidentifier");
}
public native void talk();
}
使用JNR,我们可以将其简化为一个纯Java接口,该接口将由JNR实现绑定到本机调用。
interface LibC {
void getpid();
}
public int call() {
LibC c = LibraryLoader.create(LibC.class).load("c");
return c.getpid();
}
JNR在内部旋转绑定代码并将其注入JVM。 由于Charles Nutter是JNR的主要开发人员之一,并且还在巴拿马项目上工作,所以我们可能会期待一些类似的事情。
通过查看OpenJDK邮件列表 ,感觉我们很快就会看到绑定到本机代码的MethodHandle的另一种形式。 可能的绑定可能类似于以下片段:
public void call() {
MethodHandle handle = MethodHandles
.findNative(null, "getpid", MethodType.methodType(int.class));
return (int) handle.invokeExact();
}
如果您以前没有看过MethodHandles,这可能看起来很奇怪,但是与JNI版本相比,它显然更简洁和更具表现力。 很棒的是,就像反射型Method实例一样,MethodHandle可以(通常应该被缓存),一次又一次地调用。 您还可以将本地调用直接内联到jitted Java代码中。
但是,我仍然稍微偏爱JNR接口版本,因为从设计角度来看它更干净。 另一方面,我很确定我们将获得直接接口绑定,这是基于MethodHandle API的一种很好的语言抽象-如果不是来自规范,则来自某个仁慈的开源提交者。
还有什么?
Valhalla项目和Panama项目周围还有其他事情。 其中一些与sun.misc.Unsafe没有直接关系,但仍然值得一提。
值类型
这些讨论中最热门的话题可能是ValueTypes 。 这些是轻量级包装,其行为类似于Java原语。 顾名思义,JVM能够将它们视为简单值,并且可以执行普通对象无法实现的特殊优化。 您可以将它们视为用户可定义的原始类型。
value class Point {
final int x;
final int y;
}
// Create a Point instance
Point point = makeValue(1, 2);
这仍然是API草案,我们不太可能获得新的“ value”关键字,因为它可能破坏可能已经使用该关键字作为标识符的用户代码。
好的,但是ValueTypes到底有什么好呢? 如前所述,JVM可以将这些类型视为原始值,例如,可以提供将布局展平为数组的选项:
int[] values = new int[2];
int x = values[0];
int y = values[1];
它们也可能在CPU寄存器中传递,并且很可能不需要在堆上分配它们。 实际上,这将节省大量的指针取消引用,并将为CPU提供更好的选项来预取数据并进行逻辑分支预测。
今天,已经使用了类似的技术来分析巨大阵列中的数据。 Cliff Click的h2o架构正是这样做的,它可以在统一的原始数据上提供极快的map-reduce操作。
另外,ValueTypes可以具有构造函数,方法和泛型。 您可以想到这一点,正如Oracle Java语言架构师Brian Goetz雄辩地宣称的那样,“代码就像类,行为像int”。
另一个相关的功能是预期的“特殊专业名称”,或更广泛地说是“类型专业”。 这个想法很简单; 扩展泛型系统以不仅支持对象和ValueType,而且还支持基元。 使用这种方法,无处不在的String类将成为使用ValueTypes进行重写的候选对象。
专业泛型
为了使其实用(并使其向后兼容),将需要对泛型系统进行改装,并且一些新的特殊通配符将带来成功。
class Box<any T> {
void set(T element) { … };
T get() { ... };
}
public void generics() {
Box<int> intBox = new Box<>();
intBox.set(1);
int intValue = intBox.get();
Box<String> stringBox = new Box<>();
stringBox.set("hello");
String stringValue = stringBox.get();
Box<RandomClass> box = new Box<>();
box.set(new RandomClass());
RandomClass value = box.get();
}
在此示例中,设计的Box界面具有新的通配符 与已知的相反 。 它是JVM内部类型指定符接受任何类型的描述,无论是对象,包装器,值类型还是基元。
Brian Goetz本人在今年的JVM语言峰会(JVMLS)中提供了有关类型特殊化的精彩演讲。
数组2.0
Arrays 2.0的提议已经存在了很长一段时间,这在JVMLS 2012的 John Rose的演讲中可以看出 。 最显着的特征之一就是当前数组的32位索引限制的消失。 当前,Java中的数组不能大于Integer.MAX_VALUE 。 新阵列应接受64位索引。
另一个不错的功能是“冻结”数组的能力(正如我们在上面的序列化示例中所看到的),它使您可以创建可以传递的不可变数组,而不会对其内容进行任何更改。
而且,既然伟大的事物成对出现,我们可以期望Arrays 2.0支持特殊的泛型!
动态类
另一个有趣的提议是所谓的ClassDynamic提议。 该提议可能是迄今为止我们所提到的任何提议中最早的一个,因此目前没有很多信息。 但是,让我们尝试预期它的外观。
动态类带来与专用泛型相同的泛化概念,但范围更广。 它为典型的编码模式提供了一种模板机制。 想象一下从Collections :: synchronizedMap返回的collection作为一种模式,其中每个方法调用都只是原始调用的同步版本:
R methodName(ARGS) {
synchronized (this) {
underlying.methodName(ARGS);
}
}
使用提供给专家的动态类以及模式模板将大大简化重复模式的实现。 如前所述,在撰写本文时,没有太多可用的信息,但是我希望在不久的将来看到更多的信息,这很可能是Valhalla项目的一部分。
结论
总体而言,我对JVM和Java作为一种语言的开发方向和加快的速度感到满意。 许多有趣且必要的解决方案正在进行中,Java正在融合为现代状态,而JVM提供了新的效率和改进。
从我的角度来看,绝对建议人们投资我们称为JVM的天才技术,我希望所有JVM语言都将从新集成的功能中受益。
总的来说,我强烈建议您从2015年开始进行JVMLS演讲,以获取更多有关这些主题的更多信息,并且建议您阅读Brian Goetz关于Valhalla项目的演讲摘要。
世界末日noip