Java.JVM.白盒测试总结

发表于:2017-9-14 15:26

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:IT诸葛亮    来源:51Testing软件测试网采编

  每个使用Java的开发者都知道Java字节码是在JRE中运行(JRE:Java运行时环境)。JVM则是JRE中的核心组成部分,承担分析和执行Java字节码的工作,而Java程序员通常并不需要深入了解JVM运行情况就可以开发出大型应用和类库。尽管如此,如果你对JVM有足够了解,就会对Java有更好的掌握,并且能解决一些看起来简单但又尚未解决的问题。
  所以,在本篇文章中,我将会介绍JVM工作原理,内部结构,Java字节码的执行及指令的执行顺序,并会介绍一些常见的JVM错误及其解决方案。最后会简单介绍下JavaSE7带来的新特性。
  虚拟机
  JRE由JavaAPI和JVM组成,JVM通过类加载器(ClassLoader)加类Java应用,并通过JavaAPI进行执行。
  虚拟机(VM:VirtualMachine)是通过软件模拟物理机器执行程序的执行器。最初Java语言被设计为基于虚拟机器在而非物理机器,重而实现WORA(一次编写,到处运行)的目的,尽管这个目标几乎被世人所遗忘。所以,JVM可以在所有的硬件环境上执行Java字节码而无须调整Java的执行模式。
  JVM的基本特性:
  基于栈(Stack-based)的虚拟机:不同于Intelx86和ARM等比较流行的计算机处理器都是基于寄存器(register)架构,JVM是基于栈执行的。
  符号引用(Symbolicreference):除基本类型外的所有Java类型(类和接口)都是通过符号引用取得关联的,而非显式的基于内存地址的引用。
  垃圾回收机制:类的实例通过用户代码进行显式创建,但却通过垃圾回收机制自动销毁。
  通过明确清晰基本类型确保平台无关性:像C/C++等传统编程语言对于int类型数据在同平台上会有不同的字节长度。JVM却通过明确的定义基本类型的字节长度来维持代码的平台兼容性,从而做到平台无关。
  网络字节序(Networkbyteorder):Javaclass文件的二进制表示使用的是基于网络的字节序(networkbyteorder)。为了在使用小端(littleendian)的Intelx86平台和在使用了大端(bigendian)的RISC系列平台之间保持平台无关,必须要定义一个固定的字节序。JVM选择了网络传输协议中使用的网络字节序,即基于大端(bigendian)的字节序。
  Sun公司开发了Java语言,但任何人都可以在遵循JVM规范的前提下开发和提供JVM实现。所以目前业界有多种不同的JVM实现,包括OracleHostpotJVM和IBMJVM。Google公司使用的DalvikVM也是一种JVM实现,尽管其并未完全遵循JVM规范。与基于栈机制的Java虚拟机不同的是DalvikVM是基于寄存器的,Java字节码也被转换为DalvikVM使用的寄存器指令集。
  Java字节码
  JVM使用Java字节码—一种运行于Java(用户语言)和机器语言的中间语言,以达到WORA的目的。Java字节码是部署Java程序的最小单元。
  在介绍Java字节码之前,我们先来看一下什么是字节码。下面涉及的案例是曾在一个真实的开发场景中遇到过的情境。
  现象
  一个曾运行完好的程序在更新了类库后却不能再次运行,并抛出了如下异常:
Exceptioninthread"main"java.lang.NoSuchMethodError:com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)Vatcom.nhn.service.UserService.add(UserService.java:14)atcom.nhn.service.UserService.main(UserService.java:19)
  程序代码如下,并在更新类库之前未曾对这段代码做过变更:
  //UserService.java…publicvoidadd(StringuserName){admin.addUser(userName);}
  类库中更新过的代码前后对比如下:
  //UserAdmin.java-Updatedlibrarysourcecode…publicUseraddUser(StringuserName){Useruser=newUser(userName);UserprevUser=userMap.put(userName,user);returnprevUser;}//UserAdmin.java-Originallibrarysourcecode…publicvoidaddUser(StringuserName){Useruser=newUser(userName);userMap.put(userName,user);}
  简单来说就是addUser()方法在更新之前返回void而在更新之后返回了User类型实例。而程序代码因为不关心addUser的返回值,所以在使用的过程中并未做过改变。
  初看起来,com.mhn.user.UserAdmin.addUser()依然存在,但为什么会出现NoSuchMethodError?
  问题分析
  主要原因是程序代码在更新类库时并未重新编译代码,也就是说,虽然程序代码看起来依然是在调用addUser方法而不关心其返回值,而对编译的类文件来说,他是要明确知道调用方法的返回值类型的。
  可以通过下面的异常信息说明这一点:
  java.lang.NoSuchMethodError:com.nhn.user.UserAdmin.addUser(Ljava/langString;)V
  NoSuchMethodError是因为"com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V"方法找不到引起的。看一下"Ljava/lang/String;"和后面的"V"。在Java字节码表示中,"L<classname>;"表示类的实例。所以上面的addUser方法需要一个java/lang/String对象作为参数。就这个案例中,类库中的addUser()方法的参数未发生变化,所以参数是正常的。再看一下异常信息中最后面的"V",它表示方法的返回值类型。在Java字节码表示中,"V"意味着该方法没有返回值。所以上面的异常信息就是说需要一个java.lang.String参数且没有任何返回值的com.nhn.user.UserAdmin.addUser方法找不到。
  因为程序代码是使用之前版本的类库进编译的,class文件中定义的是应该调用返回"V"类型的方法。然而,在改变类库后,返回"V"类型的方法已不存在,取而代之的是返回类型为"Lcom/nhn/user/User;"的方法。所以便发生了上面看到的NoSuchMethodError。
  注释
  因为开发者未针对新类库重新编译程序代码,所以发生了错误。尽管如此,类库提供者却也要为此负责。因为之前没有返回值的addUser()方法既然是public方法,但后面却改成了会返回user实现,这意味着方法签名发生了明显的变化。这意味了该类库不能对之前的版本进行兼容,所以类库提供者必须事前对此进行通知。
  我们重新回到Java字节码,Java字节码是JVM的基本元素,JVM本身就是一个用于执行Java字节码的执行器。Java编译器并不会把像C/C++那样把高级语言转为机器语言(CPU执行指令),而是把开发者能理解的Java语言转为JVM理解的Java字节码。因为Java字节码是平台无关的,所以它可以在安装了JVM(准确的说,是JRE环境)的任何硬件环境执行,即使它们的CPU和操作系统各不相同(所以在WindowsPC机上开发和编译的class文件在不做任何调整的情况下就可以在Linux机器上执行)。编译后文件的大小与源文件大小基本一致,所以比较容易通过网络传输和执行Java字节码。
  Javaclass文件本身是基于二进制的文件,所以我们很难直观的理解其中的指令。为了管理这些class文件,JVM提供了javap命令来对二进制文件进行反编译。执行javap得到的是直观的java指令序列。在上面的案例中,通过对程序代码执行javap-c就可得到应用中的UserService.add()方法的指令序列,如下:
  publicvoidadd(java.lang.String);Code:0:aload_01:getfield#15;//Fieldadmin:Lcom/nhn/user/UserAdmin;4:aload_15:invokevirtual#23;//Methodcom/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)V8:return
  在上面的Java指令中,addUser()方法是在第五行被调用,即"5:invokevirtual#23"。这句的意思是索引位置为23的方法会被调用,方法的索引位置是由javap程序标注的。invokevirtual是Java字节码中最常用到的一个操作码,用于调用一个方法。另外,在Java字节码中有4个表示调用方法的操作码:invokeinterface_,invokespecial,invokestatic,_invokevirtual。他们每个的含义如下:
  invokeinterface:调用接口方法
  invokespecial:调用初始化方法、私有方法、或父类中定义的方法
  invokestatic:调用静态方法
  invokevirtual:调用实例方法
  Java字节码的指令集包含操作码(OpCode)和操作数(Operand)。像invokevirtual这样的操作码需要一个2字节长度的操作数。
  对上面案例中的程序代码,如果在更新类库后重新编译程序代码,然后我们再反编译字节码将看到如下结果:
  publicvoidadd(java.lang.String);Code:0:aload_01:getfield#15;//Fieldadmin:Lcom/nhn/user/UserAdmin;4:aload_15:invokevirtual#23;//Methodcom/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;8:pop9:return
  如上我们看到#23对应的方法变成了具有返回值类型"Lcom/nhn/user/User;"的方法。
  在上面的反编译结果中,代码前面的数字是具有什么含义?
  它是一个一字节数字,也许正因此JVM执行的代码被称为“字节码”。像aload_0,getfield和invokevirtual都被表示为一个单字节数字。(aload_0=0x2a,getfiled=0xb4,invokevirtual=0xb6)。因此Java字节码表示的最大指令码为256。
  像aload_0和aload_1这样的操作码不需要任何操作数,因此aload_0的下一个字节就是下一个指令的操作码。而像getfield和invokevirtual这样的操作码却需要一个2字节的操作数,因此第一个字节里的第二个指令getfield指令的一下指令是在第4个字节,其中跳过了2个字节。通过16进制编辑器查看字节码如下:
  2ab4000f2bb6001757b1
  在Java字节码中,类实例表示为"L;",而void表示为"V",类似的其他类型也有各自的表示。下表列出了Java字节码中类型表示。
  表1:Java字节码里的类型表示
  Java字节码
  类型
  描述
  B
  byte
  单字节
  C
  char
  Unicode字符
  D
  double
  双精度浮点数
  F
  float
  单精度浮点数
  I
  int
  整型
  J
  long
  长整型
  L<classname>
  引用
  classname类型的实例
  S
  short
  短整型
  Z
  boolean
  布尔类型
  [
  引用
  一维数组
  表2:Java代码的字节码示例
  java代码
  Java字节码表示
  doubled[][][]
  [[[D
  Objectmymethod(inti,doubled,Threadt)
  mymethod(I,D,Ljava/lang/Thread;)Ljava/lang/Object;
  在《Java虚拟机技术规范第二版》的4.3描述符(Descriptors)章节中有关于此的详细描述,在第6章"Java虚拟机指令集"中介绍了更多不同的指令。
  类文件格式
  在解释类文件格式之前,先看一个在JavaWeb应用中经常发生的问题。
  现象
  在Tomcat环境里编写和运行JSP时,JSP文件未被执行,并伴随着如下错误:
Servlet.service()forservletjspthrewexceptionorg.apache.jasper.JasperException:UnabletocompileclassforJSPGeneratedservleterror:Thecodeofmethod_jspService(HttpServletRequest,HttpServletResponse)isexceedingthe65535byteslimit"

21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号