RednaxelaFX对《HotSpot实战》的笔记(32)

RednaxelaFX
RednaxelaFX (Script Ahead, Code Behind)

在读 HotSpot实战

HotSpot实战
  • 书名: HotSpot实战
  • 作者: 陈涛
  • 页数: 347
  • 出版社: 人民邮电出版社
  • 出版年: 2014-3
  • 第250页 7.3 即时编译器 7.3.1 概述
    通过-Xcomp选项则可以让虚拟机以编译模式运行。

    很多人会误解-Xcomp的实际作用,而书中也只有这么一句而已。 我之前一篇笔记里有举例子说明HotSpot就算在-Xcomp模式下仍然可能(而且非常可能)用解释器来执行方法的:http://rednaxelafx.iteye.com/blog/1038324

    HotSpot虚拟机中常见的即时编译器包括客户端编译器和服务器端编译器。它们也分别被称为C1编译器和C2编译器。C1编译器能够做一些快速的优化;而C2编译器所做的优化会耗费更多的时间,但能够产生更高效的代码。从JDK6开始HotSpot加入了多级编译器,解释器可以和C1、C2编译器一起协同运行,在JDK7 -server模式下默认启用多级编译器。 清单7-16定义了虚拟机中的编译等级,具体如下所示。 ■ 第0级:CompLevel_none,采用解释器解释执行,不采集性能监控数据,可以升级到第1级。 ■ 第1级:CompLevel_simple,采用C1编译器,会把热点代码迅速的编译成本地代码,如果需要可以采集性能数据。 ■ 第2级:CompLevel_limited_profile,采用C2编译器,进行更耗时的优化,甚至可能根据第1级采集的性能数据采取基金的优化措施。 ■ 第3级:CompLevel_full_profile,采用C1编译器,采集性能数据进行优化措施(level+MDO) ■ 第4级:CompLevel_full_optimization,采用C2编译器,进行完全的优化。

    书中第250页这段文字不幸几乎每句都有错⋯orz 后面第251页也有一句:

    编译策略甚至也支持对第0级展开分析,若C1编译器在以十分缓慢的速度生成第3级代码时,而C2队列又很小,则完全可以选择对解释器展开性能分析。

    结合这句,书里对第0层的说明基本正确。在多层编译模式下,HotSpot的解释器确实只是选择性对方法做性能分析。书中提到了其中一个条件,而还有一些别的条件会触发解释器做性能分析,例如第3层C1编译失败但第4层C2仍然有机会编译该方法的话。 第1层是用C1编译,但是不做任何profiling所以不采集任何性能数据。一个Java方法一旦进入这一层执行基本上就一直停留在此了;只能通过逆优化回到第0层的解释器里,而无法向更高层迁移。用这层编译得到的代码跟在Client VM里的C1编译的基本一致;不过在JDK8的C1里新实现的消除数据边界检查和循环不变量外提优化只在Client VM的C1才做,而在Server VM的多层编译模式下不做,也就是说这个第1层比Client VM里的C1会稍微少做一点点优化。 第2层不是用C2而是用C1编译。它不使用任何性能数据来引导优化。它只是比第1层多了方法入口和循环计数器的代码而已,并且可以升级到更高层。 第3层是用C1编译,但它也不使用任何性能数据来引导优化。其实HotSpot VM的C1是故意比较保守不使用性能数据来优化代码的,以便代码更稳定。这一层编译在第2层的基础上增加了采集更细致的性能数据的代码,包括空指针检查、条件分支、虚方法调用、类型检查等。最近还新加了参数类型和返回值类型profiling。同时这一层编译会禁用部分优化,例如消除条件表达式和合并基本块这俩都不做。采集性能数据的额外开销和禁用部分优化带来的性能下降使得第3层会比第2层显著的慢,不过这还是比解释器采集性能数据要快多了。 第4层是用C2编译做完全的优化没错。 另外就是多层编译模式在JDK8的HotSpot Server VM才默认开启,在JDK7是没有默认开启的喔。原本是计划要在JDK7就默认开启,但Oracle内部做性能测试的时候发现在一些重要的测试中开启多层编译反而变慢了,所以推迟了。而JDK8用了个治标不治本的方式解决了那几个测试中性能下降的问题,所以默认就打开了。大家能猜出性能下降的原因不?>_< 我之前也有写过两篇笔记提到了HotSpot的多层编译系统: http://hllvm.group.iteye.com/group/topic/39806#post-260654 http://rednaxelafx.iteye.com/blog/1022095 引用我的笔记里的: 在多层编译模式下,HotSpot Server VM(此时应该叫做HotSpot Tiered VM了)会同时用上解释器、C1和C2: Tier 0:CompLevel_none,解释器 Tier 1:CompLevel_simple ,C1的正常编译(没有任何profiling)。这跟在Client VM里C1的工作模式几乎一样。进入该层的方法无法继续升级到更高层,除非先逆优化到解释器里。 Tier 2:CompLevel_limited_profile ,C1带基本profiling的编译(有方法调用和循环计数器,跟解释器用的计数器的位置和作用一样)。可以升级到更高层。 Tier 3:CompLevel_full_profile ,C1带所有profiling(包括空指针检查、条件分支、虚方法调用、类型检查等。最近还新加了参数类型和返回值类型profiling),同时禁用少量优化。可以升级到更高层。 Tier 4:CompLevel_full_optimization,C2编译。

    2014-04-27 08:43:00 1人喜欢 回应
  • 第68页 2.3.6 init_globals函数:初始化全局模块
    此外,若在Windows 64位平台上开启SHE机制(即通过VM选项UseVectoredExceptions关闭Vectored Exceptions机制,默认关闭),则需要向OS模块注册SHE。

    这里和后面第237页的

    (3)向系统注册Code Cache区。仅在Windows 64位平台上生效,默认开启SHE机制(即VM选项UseVectoredExceptions默认关闭Vectored Exceptions机制),此时向OS模块注册SHE。

    不知为啥书里都把Structured Exception Handling(SEH)给缩写成SHE了⋯手误吧。 话说回来,UseVectoredExceptions参数从JDK8开始deprecate,已经没有任何效果;到JDK9就会不再接受该参数。这个变更是 https://bugs.openjdk.java.net/browse/JDK-7188234 原本这个参数有效的时候也并不是在“Windows 64位平台上”可用,而是在“Windows Itanium(IA-64)”上才使用。即便在64位x86上HotSpot也不用它的。因为Oracle不再支持在Itanium上运行HotSpot VM,所以这个参数也就没用了。 关于“向量化异常处理”: http://msdn.microsoft.com/en-us/library/windows/desktop/ms681420.aspx

    2014-04-27 09:31:15 回应
  • 第82页 3.1.5 实战:用HSDB调试HotSpot

    这小节内容看起来好眼熟⋯我的博文:http://rednaxelafx.iteye.com/blog/1847971 不过HSDB就是那么个简单的工具,要介绍它的话大概就是会截这么些图吧。眼熟倒也不奇怪。 可以看出书中的文字和图片都是作者自己整理的(至少不是从我那篇直接复制过去的),这点我很高兴 >_<

    2014-04-27 09:50:27 回应
  • 第14页 1.2.2 HotSpot源代码结构

    嗯,跟我之前发过的笔记类似:http://hllvm.group.iteye.com/group/topic/26998#post-193368 不过不知道为啥书里这段的目录名大小写都不大对,所有目录名都写成大写开头了,而实际HotSpot代码里目录名都是全小写的。当然Windows对路径的大小写不敏感⋯ 第15页的:

    Adlc:平台描述文件

    adlc是Architecture Description Language Compiler,是平台描述文件的编译器。平台描述文件是各个平台相关目录(hotspot/src/cpu/<arch>/vm 或 hotspot/src/os_cpu/<os_arch>/vm)里的.ad后缀的文件。adlc负责把.ad文件“编译”为C++源码。生成出来的C++源码是C2的指令选择和寄存器分配的平台相关部分。

    Ci:动态编译器

    ci不是动态编译器啦,是compiler interface,具体来说是动态编译器访问VM运行时系统的一个抽象层。

    2014-04-27 09:56:07 回应
  • 第167页 5.1.4 栈上分配和逸出分析
    在栈中分配的基本思路是这样的:分析局部变量的作用域仅限于方法内部,则JVM直接在栈帧内分配对象空间,避免在堆中分配。 注意 不产生垃圾,自然就不会产生收集垃圾。 这个分析过程称为逸出分析,而栈帧内分配对象的方式称为栈上分配。这样做的目的是,减少新生代的收集次数,间接提高JVM性能。虚拟机是允许对逸出分析开关进行配置的,从Sun Java 6u23以后,HotSpot默认开启逸出分析。

    嗯但是Oracle/Sun的HotSpot VM从来没在产品里实现过栈上分配,而只实现过它的一种特殊形式——标量替换(scalar replacement)。这俩是不一样的喔。栈上分配还是要分配完整的对象结构,只不过是在栈帧里而不在GC堆里分配;标量替换则不分配完整的对象,直接把对象的字段打散看作方法的局部变量,也就是说标量替换后就没有对象头了,也不需要把该对象的字段打包为一个整体。 其效果可以参考我之前的两篇笔记: http://rednaxelafx.iteye.com/blog/659108 http://rednaxelafx.iteye.com/blog/774673 另外我们在研发JVM的时候也发现其实在栈上分配Java对象带来的minor GC频率的降低并不是最大的受益点;TLAB的快速分配已经非常快了,而当young gen大小配置合理时minor GC的开销并不大。如果能做到标量替换就能更好的使用寄存器,以及允许JIT编译器做更有效的优化,这才是最重要的地方⋯ 话说Sun JDK6确实是从6u23开始默认开启逃逸分析的。 https://bugs.openjdk.java.net/browse/JDK-6873799 虽然代码变更似乎更早就放进去了,但在6u23之前的实际发布的产品里还是给禁用掉了么…

    2014-04-27 10:33:39 回应
  • 第255页 7.3.3 编译器的基本结构
    练习6 在Compilation中,使用了一些以“ci”为前最命名的类型,如ciMethod等。阅读这部分源代码,试比较ciMethod与methodOop的区别与联系?了解ciObject类层次体系与oop-klass类层次体系之间的关系?并体会设计ci模块的意义?

    前最笔误了…前缀。 不知道作者对ci的理解是怎样的呢?

    2014-04-28 04:48:08 回应
  • 第273页 8.2.2 数据传送指令
    在本例中,iload_0指令的作用是将第1个int类型局部变量推送至栈顶,在执行这段Codlet时,从rdi(或edi)寄存器中取出局部变量首地址,即第1个局部变量地址,如清单8-3所示。局部变量地址获得后,访问该地址的内存,就得到了第1个局部变量值。 清单8-3 mov (%edi),%eax 最后,读取该值至rax(或edi)寄存器返回。

    这段想表达的意思很明白,但细节写得有点怪。为啥最后一句会有“(或edi)”…?可能想说的是“(或eax)”吧。

    2014-04-28 06:36:46 回应
  • 第222页 6.2.6 优化:栈顶缓存
    表6-4 | 状态码 | TosState | 描述 | | ... | 3 | itos | 栈顶缓存int类型数据 | | ... | 8 | vtos | 栈顶缓存tos类型数据 |

    其实vtos的v是void,该TosState表示不缓存任何数据在寄存器里,而不是“缓存tos类型数据” >_< 操作数栈是空的时候,TosState肯定是vtos——没任何数据所以也没得好缓存的。而操作数栈不是空的时候也还是有可能进入vtos状态,例如说刚调用完一个返回类型为void的方法时。 关于栈顶缓存可以参考我以前写的一篇笔记:http://hllvm.group.iteye.com/group/topic/34814#post-231982 我以前写的JVM的演示稿也有提到,在第174-179页: http://www.valleytalk.org/2011/07/28/java-%E7%A8%8B%E5%BA%8F%E7%9A%84%E7%BC%96%E8%AF%91%EF%BC%8C%E5%8A%A0%E8%BD%BD-%E5%92%8C-%E6%89%A7%E8%A1%8C/

    清单6-11 void TemplateTable::iop2(Operation op) { transition(itos, itos); switch (op) { case add : __ pop_i(rdx); __ addl (rax, rdx); break; ...... // case sub/mul/_and/_or/_xor/shl/shr/ushr } } transition()是判断传给模板的tos_in和tos_out是否符合要求,tos_in和tos_out是指令执行前后的TosState。对于iadd指令来说,必须tosin和tosout都为itos才能通过,否则程序将异常终止。这符合HotSpot内部对逻辑的健壮性考虑,HotSpot内部规定了一套TosState规则(参考6.2.4小节),对于每个给定的字节码,其执行前后的TosState应当是明确的。不管是在字节码生成本地代码阶段,还是在调用字节码阶段,都应遵循这一套规则,这样才能保证栈顶缓存被正确的使用,避免状态不一致导致难以预料的系统错误。

    这段描述很容易让读者误解。transition()函数主要有两个作用: 1、文档:以代码为文档,标示出该模板期望的输入和输出栈顶状态; 2、正确性校验:在执行该函数来从模板生成具体代码的时候检查模板所声明的输入/输出类型与该函数所实现的是否一致。这就是书中所讲的。 但实际上这个transition()函数并不是说这个模板生成的代码(native code)就只能处理其指定的输入/输出状态。正好相反,它并不影响生成的native code。真正影响native code的是Template的_tos_in和_tos_out字段。 TemplateTable类有个静态成员变量,是所有字节码的Template实例的数组:

    // The TemplateTable defines all Templates and provides accessor functions
    // to get the template for a given bytecode.
    
    class TemplateTable: AllStatic {
      // ...
     private:
      static Template        _template_table     [Bytecodes::number_of_codes];
      // ...
    };

    然后在TemplateTable::initialize()函数里会对这个数组里的每个Template实例初始化其状态,例如:

    // Java spec bytecodes                ubcp|disp|clvm|iswd  in    out   generator             argument
    def(Bytecodes::_iadd                , ____|____|____|____, itos, itos, iop2                , add          );

    这个对TemplateTable::def()的调用就将iadd字节码对应的Template实例的_tos_in与_tos_out分别初始化为itos了。 TemplateTable::transition()函数在这个例子做的其实是检查TemplateTable::iop2()所实现的模板与iadd对应的模板实例里_tos_in与_tos_out两字段是否都匹配。但仅此而已。 真正在生成native code时,TemplateInterpreterGenerator::set_entry_points()调用到TemplateInterpreterGenerator::set_short_entry_points()、set_wide_entry_point(),里面就会设置好能匹配的TosState的入口。 回到iadd字节码的例子,它预期的进入状态是itos,跟它匹配的TosState可以是vtos和itos,

    void TemplateInterpreterGenerator::set_short_entry_points(Template* t, address& bep, address& cep, address& sep, address& aep, address& iep, address& lep, address& fep, address& dep, address& vep) {
      switch (t->tos_in()) {
        // ...
        case itos: vep = __ pc(); __ pop(itos); iep = __ pc(); generate_and_dispatch(t); break;
        // ...
      }
    }

    这里可以看到,实际生成的代码逻辑是,如果上个字节码执行后的TosState是vtos,那么到这个iadd的时候会从vep进入;如果上个字节码执行后是itos,那么从iep进入;其它TosState跟iadd不匹配,JVM要报错,它们的入口就会跳到报错的处理函数。这里vep跟iep之间有一个pop指令,将vtos调整到itos,这样就跟iep的地方所期待itos匹配上了。 换个方式说,对iadd的处理函数来说, (下面是Intel语法伪代码)

    aep:  // atos entry point
    lep:  // ltos entry point
    fep:  // ftos entry point
    dep:  // dtos entry point
      goto illegal_bytecode;
    
    vep:  // vtos entry point
      pop  eax;                   // 进入状态是vtos,先把左操作数取到eax
    iep:  // itos entry point,左操作数已经在eax
      pop  edi;                    // 取右操作数到edi
      add  eax, edi             // 对左右操作数求和存到eax
      movz ebx, byte ptr: [esi+1]  // 取下一条字节码
      inc  esi                     // 字节码指针自增1
      jmp  dispatchTable[itos][ebx] // 跳转到下一条字节码的处理函数

    这样讲解一下,书里后面清单6-12所示汇编代码应该更好理解一点吧。 (书中引用的汇编是hsdis插件输出的,用的是AT&T语法)

    清单6-12 ---------------------------------------------------------------------- iadd 96 iadd [0x027df080, 0x027df090] 16 bytes [Disassembling for mach='i386'] 0x027df080: pop %eax 0x027df081: pop %edi 0x027df082: add %edi,%eax 0x027df084: movzbl 0x1(%esi),%ebx 0x027df088: inc %esi 0x027df089: jmp *0x8515310(,%ebx,4) 其中,前3条指令是取两个操作数和执行add运算并将结果值写入eax寄存器,最后3条指令则表示执行完毕字节码idd返回到上层调用环境继续执行。

    实际上如果上一条字节码执行后TosState是itos的话,就会直接从书中例子的0x027df081: pop %edi进入,因为左操作数已经在栈顶缓存eax里了;而最后3条指令也不是“返回到上层调用环境”,而是直接跳转到下一条字节码的处理函数去,这是token-threaded解释器的特征。另外idd是iadd的笔误。 像这样个模板在进入的地方有适配代码的例子,我前面提到的JVM演示稿的第178-179页正好有个例子,有兴趣的读者可以参考一下。

    但我们看到HotSpot利用栈顶缓存技术,⋯⋯将原本应该是内存写、读操作分别变成了寄存器写、读操作,节省开销相当可观,在HotSpot整个范围之内,对于其他字节码和方法也都是按照这种方式进行的,从总体来看,JVM整体允许性能得到了提升。

    嗯在HotSpot的整个解释器范围内确实是贯彻使用了栈顶缓存,效果相当不错。我以前写过个例子对比HotSpot与Dalvik的解释器,可以看到:在运用了栈顶缓存后,字节码是基于栈还是基于寄存器的对于真正运算来说执行效率并不会差多少,但是由于前者要表达同样逻辑比后者需要更多的字节码指令,在指令分派这块开销还是会比后者大一些: https://gist.github.com/rednaxelafx/759495 书后面像是第274页也有提到栈顶缓存。

    2014-04-28 13:35:28 2回应
  • 第277页 8.2.3 实战:数组的越界检查
    读到这里,如果你觉得上述这一切设想只是异想天开的话,那么这个事实将让你大吃一惊:在当前网络攻击中,50%以上来自于缓存区溢出。

    嗯是啊⋯联想到最近的heartbleed事件,又是缓存溢出惹的祸。

    2014-04-28 09:55:33 回应
  • 第298页 8.7.2 invoke系列指令
    其实,在编译callInvokevirtual()方法时,在开始编译任何一条Java代码之前,编译器会立即插入一条aload_0指令,编号为0的局部变量存储的是对象自身引用“this”,aload_0指令将“this”推送至操作数栈中,作为add()方法的局部变量。

    虽然我猜大部分熟悉Java字节码的人都知道作者这里想说的是什么,但这个表述方式实在不太好⋯ 并不是“在编译任何一条Java代码之前”,那个aload_0就是add()调用所包含的压参动作嘛。 假如有这样的例子:

    public static int foo(char c, String str) {
      return str.indexOf(c);
    }

    编译它得到的字节码会是类似:

    aload_1
    iload_0
    invokevirtual java.lang.String.indexOf(C)I
    ireturn

    这里传递“this”给indexOf()方法的字节码就不是aload_0而是aload_1了。其实没啥,谁是“接收”调用的对象,就把谁先压到操作数栈上,然后再压入显式参数。 之前在一篇笔记里码过相关的文字,不想重复码了:http://rednaxelafx.iteye.com/blog/652719

    2014-04-28 13:58:55 回应
1人推荐
<前页 1 2 3 4 后页>