首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》14.4 JVM的新方向

关灯直达底部

我们在第1章介绍了VMSpec(JVM规范)。这一文档确切指明了作为JVM标准实现的VM必须遵守的行为准则。当引入新行为时(比如Java 7的invokedynamic),所有实现都必须升级以支持新功能。

在这一节里,我们会谈到那些已经在讨论并有原型的各种修改最终实现的可能性。这项工作是在OpenJDK项目中开展的,该项目是Java参考实现的基础,也是Oracle JDK的起点。除了对规范的可能修改,我们也会涉及对OpenJDK/Oracle JDK代码的显著改动。

14.4.1 VM的合并

在Oracle收购了Sun公司之后,它就拥有了两款非常强的Java虚拟机:HotSpot VM(Sun带的)和JRockit(之前收购的BEA带的)。

Oracle很快就决定不再同时维护两个VM来浪费资源,要把它们合并起来。HotSpot VM被选作基础,JRockit特性会在将来发布Java时谨慎地引进。

名称有什么关系?

这个合并后的VM没有官方名称,尽管VM粉和Java社区大部分都支持HotRockit这个名称。它也确实挺吸引人,但还是要看Oracle的营销部门同不同意。

所以这对咱开发人员来说,这有什么关系呢?你现在用的VM(很可能是HotSpot VM)将来会增加很多新特性,包括(但不限于)下面这些:

  • 去掉PermGen,能防止一大类跟类加载有关的崩溃;

  • 加强JMX代理的支持,能让你对运行的VM有更多深入的了解;

  • 新的JIT编译方式,从JRockit中引入新的优化;

  • 任务控制,提供有助于对生产型应用进行调优和分析的先进工具。这些工具中有些可能是需要付费的外加JVM组件,不包含在免费下载的发布包中。

去掉PermGen

就像6.5.2节说的,类的元数据当前保存在VM中一个的特殊内存区里(PermGen)。它很快就会被填满,特别是对于那些在运行时会创建大量类的非Java语言和框架而言。PermGen区不回收,耗光之后还会导致VM崩溃。有关人员正在开展工作,要把元数据保存在自有内存区中,让噩梦一般的“java.lang.OutOfMemory-Error: PermGen space”消息永远地成为过去。

还有很多的小改进全都是为了让VM更小、更快、更灵活。假定HotSpot上大约已经投入了1000人年的工作量,跟投入工作量更多的JRockit结合起来形成的VM前景一定更加光明。

除了合并VM,还有大量的新特性正在制作中。其中之一就是可能会增加称为协同程序的并发特性。

14.4.2 协同程序

Java和JVM语言程序员了解最多的并发形式就是多线程。它依靠JVM的线程调度服务在处理器核心上启动和停止线程,但线程没办法控制这个调度。出于这一原因,多线程被称为“抢占式多任务”,因为调度器可以抢占正在运行的线程,迫使它放弃对CPU的控制。

协同程序的基本思想是允许执行单元部分参与控制对它们的调度。具体来说,协同程序会像普通线程那样运行,直到它遇到了一个“退位”指令。这会导致协同程序把自己挂起,并允许另一个协同程序继续在它的地盘运行。当原来的协同程序再次得到机会运行时,它会从退位之后的下一条语句继续向下执行,而不会从方法开始的地方。

因为这种多线程的方式靠正在运行的协同程序的相互协作,间或将运行机会退让给其他协同程序,这种多线程处理被称为“协作式多任务”。

关于协同程序如何工作的确切设计仍然处于热烈讨论的阶段,没有哪个是肯定要被采纳的。一个可能的模型是在一个单例共享线程(或类似于java.util.concurrent里的线程池)中创建和调度协同程序,如图14-3所示。

图14-3 一种可能的协同程序模型

正在执行协同程序的线程可能会被系统内的其他任何线程抢占,但JVM线程调度器不能强迫协同程序退位。也就是说,以相信执行池中所有其他协同程序为代价,协同程序就可以控制什么时候切换上下文。

这种控制意味着协同程序之间的同步可以做得更好。多线程代码必须构建复杂的锁策略来保护数据,但它很脆弱,因为上下文切换可能随时都会发生。这是我们在4.1节讨论的并发类型安全问题。相较而言,协同程序只要确保退位点数据的一致性,因为它知道其他任何时候自己都不会被抢占。

这个折中的额外担保是以相信其他线程为交换条件的,这是对某些线程编程问题的有益补充。一些非Java语言已经开始支持协同程序(或与之很贴近的概念”纤维“),特别是Ruby和较新版的JavaScript。在VM层面增加协同程序的支持(但不一定是对Java语言)会对可以使用协同程序的语言有很大帮助。

在可能会实现的VM修改中,最后要讨论的是“元组”,这个VM特性提案对性能敏感的计算空间可能会产生很大的影响。

14.4.3 元组

在当今的JVM里,所有数据项不是原始类型就是引用类型(可能是引用对象或数组)。比较复杂的类型只能在类里定义,并传递对这些新类型实例对象的引用。这是一个简单而又相当优雅的模型,过去一直为Java服务得很好。

但要构建高性能系统,这个模型就会暴露几个缺陷。尤其是在游戏和金融软件这样的应用中,遇到这个简单模型局限性的情况十分常见。可以解决这个问题的办法之一就是采用元组。

元组(tuple)有时称为值对象,是能在原始类型和类之间架起桥梁的语言结构。像类一样,用元组可以定义包含原始类型、引用类型和其他元组的自定义复杂类型。像原始类型一样,在将它们传递给方法(或从方法中传递出来),保存在数组和其他对象中时,用的是整个值。如果你熟悉C(或.NET)环境,可以把它们看做结构(struct)的等价物。

我们来看一个例子:一个现有的Java API。

public class MyInputStream {  public void write(byte, int off, int len);}  

这让用户可以将特定数量的数据写到数组中的特定位置,很实用。但它设计得并不好。在理想的面向对象世界,偏移和长度应该被封装在数组内,并且无论是用户还是方法的实现者都应该不用再单独跟踪额外的信息。

实际上,在引入NIO时ByteBuffer就封装了这些信息。可惜这不是白来的,从ByteBuffer中创建新切片需要分配一个新对象,这会给垃圾收集子系统造成压力。尽管大多数垃圾收集器都非常擅长收集短命的对象,但在吞吐率非常高的延迟敏感环境中,这种分配操作会累加并最终导致应用出现令人无法接受的暂停。

如果我们能定义一个保存数组引用、偏移和长度的值对象(也就是元组)类型Slice会发生什么呢?在代码清单14-2中,我们会用新的tuple关键字来表示这个新概念。

代码清单14-2 作为元组的数组切片

public tuple Slice {  private int offset;  private int length;  private byte array;  public byte get(int i) {    return array[offset + i];  }}  

这个切片的构造结合了原始类型和引用类型的很多优势:

  • Slice值可以复制到方法中,也可以从方法中复制出来,就跟手工传递数组的引用和int值一样有效;
  • Slice元组在退出方法后会被清理掉(因为它们跟值类型一样);
  • 对偏移和长度的处理会干净地封装在元组中。

在日常编程中有很多类型会从元组的使用中受益,比如带有分子和分母的有理数、带有实部和虚部的复数,或者由ID和领域标识引用的用户主档(献给那些MMORPG迷们)。

在处理数组时元组也能对性能有所提升。现在的数组中要放同质的数据值集合——要么是原始类型,要么是引用类型。在使用数组时,元组允许我们对内存的布局做更多的控制。

来看一个例子:一个以原始类型long为键的简单散列表。

public class MyHashTable {  private Entry entries;}public class Entry {  private long key;  private Object value;}  

在当前的JVM化身中,entries数组中只能放Entry实例的引用。调用者每次查找表中的key,在用传入的值与相关Entry实例的key比较之前,必须把Entry实例解引用。

当用元组实现时,则可以在数组内展开Entry类型,因此能够省掉访问key产生的解引用开销。图14-4展示了当前情况,以及使用元组后得到的改善。

图14-4  JVM数组与元组

在考察元组的数组时,采用元组得到性能优势的关键之处也变得更加清晰。我们在第6章讨论过,大多数应用程序代码的性能都是由一级缓存的命中率决定的。在图14-4中,如果使用元组,扫描散列表的代码效率会更高。它不用再承担额外的缓存读取就能得到key值。这就是元组取得性能优势的本质——程序员在展开内存的数据时可以得到更优的空间局部性。1

1 空间局部性(spatial locality),如果程序访问某个存储器地址后,又在较短时间内访问临近的存储器地址,则程序具有良好的空间局部性。两次访问的地址越接近,空间局部性越好。——译者注

对可能出现在Java和JDK 8中的新特性,我们的讨论就到此为止了。其中有多少能变成现实也只能等到快要发布时才知道。如果你对特性的演进感兴趣,可以加入OpenJDK项目和Java Community Process,参加这些特性的开发活动。如果你对它们还不熟悉,请找到这些项目并看看如何加入。