[讨论] 请问组内有没有对JamVM源码了解的大虾?

pavelnedlove 2012-10-14

源码版本: jamvm-1.5.1

 

原本想着jamvm麻雀虽小,于是小弟断断续续看了该源码几次,主要集中在加载、解析、GC等重要过程上,发现看的时候还是比较吃力,网上可供参考的资料又较少,特此来看看,请了解的同学不吝赐教。

RednaxelaFX 2012-10-17
楼主可以把具体不理解的地方发出来。不然别人也很难帮上忙

JamVM用作入门阅读还不错。作为一个小JVM它至少在很多关键的地方上都用了比较正常的设计而没偷懒(例如说JamVM用直接指针来实现引用,而没偷懒用多重间接(句柄))。

题外话:要实现一个Java平台,除了JVM这个核心之外,还有很多周边的东西要实现,其中Java标准库是大头。现在的主流有三种选项:OpenJDK、Apache Harmony、GNU Classpath。其中Apache Harmony已死(要说哪里还在大规模使用那就是Android了吧…但Android用的版本也是fork出来的),而GNU Classpath已经跟不上时代发展,只有OpenJDK的class library是比较靠谱的选择了。
而JamVM还只能跟GNU Classpath搭配使用。诶…
pavelnedlove 2012-10-22
RednaxelaFX 写道
楼主可以把具体不理解的地方发出来。不然别人也很难帮上忙

JamVM用作入门阅读还不错。作为一个小JVM它至少在很多关键的地方上都用了比较正常的设计而没偷懒(例如说JamVM用直接指针来实现引用,而没偷懒用多重间接(句柄))。

题外话:要实现一个Java平台,除了JVM这个核心之外,还有很多周边的东西要实现,其中Java标准库是大头。现在的主流有三种选项:OpenJDK、Apache Harmony、GNU Classpath。其中Apache Harmony已死(要说哪里还在大规模使用那就是Android了吧…但Android用的版本也是fork出来的),而GNU Classpath已经跟不上时代发展,只有OpenJDK的class library是比较靠谱的选择了。
而JamVM还只能跟GNU Classpath搭配使用。诶…



首先感谢撒迦的回复,之前的问题确实太泛了,下面先提几个最早遇到的问题。


1. 在jamvm-1.5.1的主函数jam.c中,一开始有这么一段代码

 

#ifdef THREADED
#ifdef DIRECT
#ifdef INLINING
    printf("inline-");
#else /* INLINING */
    printf("direct-");
#endif /* INLINING */
#endif /* DIRECT */
    printf("threaded interpreter");
#ifdef USE_CACHE
    printf(" with stack-caching\n");
#else /* USE_CACHE*/
    printf("\n");
#endif /* USE_CACHE */
#else /* THREADED */
    printf("switch-based interpreter\n");
#endif /*THREADED */
 

之前不明白这些条件编译的意思,后来看到组内有过关于 threaded code 的讨论,才觉得可能是这么回事。后来根据撒伽的推荐看了《虚拟机:系统与进程的通用平台一文》的相关章节才有所了解。现在,我的问题是: 代码中提及的 stack-caching 是什么东西,有什么作用?

 

2.还是在jam.c中,295行的main函数中,有如下代码

 

int main(int argc, char *argv[]) {
    Class *array_class, *main_class;
    Object *system_loader, *array;
    MethodBlock *mb;
    InitArgs args;
    int class_arg;
    char *cpntr;
    int status;
    int i;

    setDefaultInitArgs(&args);
    class_arg = parseCommandLine(argc, argv, &args);

    args.main_stack_base = &array_class;
    initVM(&args);

   if((system_loader = getSystemClassLoader()) == NULL) {
        printf("Cannot create system class loader\n");
        printException();
        exitVM(1);
    }

    mainThreadSetContextClassLoader(system_loader);

 

之前翻过点资料,有看到说 SystemClassLoader 和 ContextClassLoader 是一回事,但看这段代码的时候一直觉得很奇怪,不知组内有没有人能做个总结?(菜鸟一只,轻拍~)

 

问题还有很多,先提这两个,望不吝赐教啊~

 

PS: 小弟一直对编译器及虚拟机方面感兴趣,目前由于实验室工作(刚进不久啊)主要与java运行时系统有关,因此         想多探讨这方面的内容。目前看过的书只有《深入java虚拟机(第二版)》和周志明老师翻译的《java虚拟机规范》,不知撒迦对入门读物有没有什么好的推荐,或者有什么指导没~ 

 

RednaxelaFX 2012-10-22
这边回答第一个问题。您的第二个问题留机会给这个群组的其他人回答吧,呵呵。
刚翻了下草稿箱发现2010年写过篇很长的《图解栈顶缓存》,不过当时没把“图解”的那些动画GIF画完所以那帖坑了…抱歉

Stack caching,或者叫top-of-stack caching,就是在一个栈的栈顶(或附近)元素频繁被访问的假设下,对栈顶(或附近)的元素进行缓存的一种优化方式。中文翻译的话通常叫“栈顶缓存”吧。

作为参考,可以阅读Anton Ertl发表的论文Stack caching for interpretersImplementation of Stack-Based Languages on Register Machines(这篇特别值得一读),和其它一些与“stack caching”相关的论文。

这里有组首藤一幸做的老演示稿,里有图解(slide 14-16):
首藤一幸 写道




HotSpot VM里的解释器也有使用top-of-stack caching,简称TOSCA (= Top-Of-Stack CAche)。在HotSpot VM里看到TOS的话就是top-of-stack的意思,多半都跟TOSCA有关系。

Stack caching有许多可能的变种。有两个指标值得关注:
1、缓存了栈顶附近的多少个元素?如果缓存了n个元素,那么就叫n-TOS caching;
2、缓存带有多少种“状态”?如果有n种状态那么就叫n-state TOS caching。
这俩概念不要弄混了。特别是n-state的概念,下面再展开讲。

=====================================================

先看n-TOS的方面。
从抽象数据结构来举例:可以想像把Java标准库自带的那个java.util.Stack包装一下,变成类似这样:
import java.util.EmptyStackException;
import java.util.Stack;

public class StackWith1TOSCA<E> {

  private enum TosState {
    NOT_CACHED,
    CACHED;
  }

  private Stack<E> theStack = new Stack<E>();
  private E        topOfStackElement; // the cache
  private TosState state = TosState.NOT_CACHED;
  
  public void push(E elem) {
    if (state == TosState.CACHED) {
      theStack.push(topOfStackElement);
    }
    topOfStackElement = elem;
    state = TosState.CACHED;
  }
  
  public E pop() {
    if (state == TosState.NOT_CACHED) throw new EmptyStackException();
    
    E result = topOfStackElement;
    
    if (theStack.isEmpty()) {
      state = TosState.NOT_CACHED;
      topOfStackElement = null;
    } else {
      topOfStackElement = theStack.pop();
    }
    
    return result;
  }
}


那么如果有这样的Java代码:
static void foo(Object o) {
  Object temp = o;
}

里面那句赋值对应的字节码是:
aload_0
astore_1


如果是在一个没有stack caching的抽像的栈上执行这两条字节码指令,那么VM需要做的操作会是:
stack.push(locals[0]);
locals[1] = stack.pop();

很明显这里实际上只需要访问栈顶的那个元素,为此而付出完整的压栈/出栈操作的代价太浪费了。
而如果应用上前面例子里的StackWith1TOSCA来实现表达式栈,那么要做的事情就大致变成(省略细节若干…):
topOfStackElement = locals[0];
locals[1] = topOfStackElement;

这样看起来会不会清爽些?

当然,真要用StackWith1TOSCA的话那些“细节若干”是“省略”不了的,所以上面的代码例子仅供理解概念用,不要以为这样在Java层面实现栈就真的会快多少…

真正能发挥出stack caching的性能优势的地方在更底层的应用场景上:一个直观实现的栈是整个都在内存里的,而如果选择把栈顶附近的若干个元素缓存在寄存器里的话,那这些元素的访问速度就会比直观实现有大幅提升。在虚拟机里的表达式栈(或者说操作数栈)正是此类应用场景。

如果没有stack caching,那概念上说上面的字节码例子有两次内存读,两次内存写;而用上1-TOS的stack caching的话,上面字节码例子就变成一次内存读,一次寄存器写,一次寄存器读,一次内存写。可见内存访问次数减少了。
(实际上还有很多别的细节但请容许我在本文先忽略掉…)

=====================================================

然后再看看n-state TOS caching是啥概念。

前面的StackWith1TOSCA例子里可以看到已经有“state”的概念出现:我们必须要知道当前在缓存里到底有没有值,不然就无从判断压栈/出栈时数据该何去何从了。这个例子用了两种状态,NOT_CACHED和CACHED;对于不关心栈里元素类型的stack caching来说,1-TOS用两种状态就够用了。

实际上“状态”可以记录许多东西,取决于到底要实现怎样的TOSCA。

一个例子:如果我们现在不用1-TOS,而用3-TOS caching的话,那很明显我们的“状态”不但要记录“有没有缓存栈顶元素”,还得记录“到底栈顶附近的三个元素到底放在哪个变量里了”。作为homework,大家可以试试把上面的StackWith1TOSCA改造成StackWith3TOSCA试试看。

另一个例子:如果我们的栈需要跟踪栈里的元素的类型,同时我们使用1-TOS caching的话,那就意味着要记录的“状态”里必须记住栈顶元素是什么类型的。HotSpot VM的解释器就是这样的例子,它虽然只用了1-TOS caching,但它的TosState却有9种有效值:
// TosState describes the top-of-stack state before and after the execution of
// a bytecode or method. The top-of-stack value may be cached in one or more CPU
// registers. The TosState corresponds to the 'machine represention' of this cached
// value. There's 4 states corresponding to the JAVA types int, long, float & double
// as well as a 5th state in case the top-of-stack value is actually on the top
// of stack (in memory) and thus not cached. The atos state corresponds to the itos
// state when it comes to machine representation but is used separately for (oop)
// type specific operations (e.g. verification code).

enum TosState {         // describes the tos cache contents
  btos = 0,             // byte, bool tos cached
  ctos = 1,             // char tos cached
  stos = 2,             // short tos cached
  itos = 3,             // int tos cached
  ltos = 4,             // long tos cached
  ftos = 5,             // float tos cached
  dtos = 6,             // double tos cached
  atos = 7,             // object cached
  vtos = 8,             // tos not cached
  number_of_states,
  ilgl                  // illegal state: should not occur
};

也就是说这个解释器的TOSCA可以描述为1-TOS, 9-state caching。

大家可以想像一个n > 1的n-TOS如果跟带类型的TOSCA结合起来状态数量的膨胀速度会有多快。
实际上多数虚拟机就算用了stack caching也只会用1-TOS,因为简单高效;大不了1-TOS外带类型。
也有复杂一些的例子,例如Sun JDK 1.1.x里的解释器在x86上的实现,它用的是2-TOS, 3-state caching:
state 1: 栈顶未缓存;
state 2: 栈顶元素缓存在ebx寄存器;
state 3: 栈顶元素缓存在ecx寄存器,栈顶下面一个元素缓存在ebx寄存器。
大家可以想像一下在这些状态间切换要做些什么事情。

=====================================================

有几种办法来实现stack caching。

一种比较常见的、可移植性较好的是:把解释器主循环写在一个函数里,然后在这函数里声明一个或多个局部变量来存着栈顶(或附近)元素的值。当需要访问栈顶(或附近)元素的时候,根据当前栈顶缓存状态来决定是从这些局部变量还是从真的“栈”取值。这种做法在C或C++写的解释器里相当常见,作者会寄希望于C或C++编译器能比较聪明的把那些缓存栈顶元素用的局部变量分配到实际寄存器上,这样就达到前面说的减少内存访问、提升执行速度的效果。
但这样做很大程度上依赖编译器的优化,无法保证编译器给生成出来的代码跟预期的一样;如果编译器没把缓存TOS用的局部变量分配到实际寄存器上,那就没啥意义了。

JamVM的stack caching就是用上述方法实现的。它的栈顶缓存比较有趣,可以缓存最多两个int(或与int等宽数据)或者一个long(或与long等宽数据)。
它的DISPATCH(level, ins_len)宏中level参数是用来跟踪“状态”用的。

于是另一种更可靠的、但移植性较差的方式就是直接用更底层的方式来实现虚拟机的表达式栈。
有的虚拟机选择用汇编来实现表达式栈的访问逻辑,这好理解。而HotSpot VM比较有趣,它是自己用C++实现了一个汇编器,然后在自己内部调用自己的汇编器来生成解释器的代码。这样的好处是减少了对外部汇编器的依赖(也就减少了在不同工具链间移植时可能遇到的不兼容性),但自己实现一个汇编器实在是蛋疼啊,开发和维护都有成本的

=====================================================

最后顺带一提:在虚拟机里使用stack caching不是只有解释器才可以用的,编译器也可以用。可以把n-TOS caching看作一种非常简单的寄存器分配算法,把它用在JIT编译器里。
shuJIT是一个可以插在早期Sun JDK里的JVM上的JIT编译器,使用了多元素多状态的stack caching。另一个叫TYA的JIT编译器也类似;
微软的SSCLI里带的简易JIT编译器也用类似的思路来实现寄存器分配。

在编译器里用多状态stack caching有个好处是:通常对TOS状态的跟踪都可以在编译时计算好,不用等到运行时每次要访问栈的时候都去检查一下(这是前面“省略的细节”的一部分)。这样stack caching的效果就更好了。
Global site tag (gtag.js) - Google Analytics