Java分布式应用学习笔记02再谈JVM

1.  前言-为何要再谈JVM

很多人认为,分布式Java应用看上去好像和Java虚拟机没什么太多关注的东西,别说分布式系统了,就是一个单机JavaSE系统也不用将JVM学得透透的,有个JVM执行不就行了吗。据笔者的经验回答则是,在大型分布式系统,尤其是云计算服务平台,SAAS也好,PAAS也罢,要求编写的应用必须要高效,你就当你运行的是一个配置很小的机器上,要求资源比较苛刻。所以了解,甚至再学习JVM相关知识还是十分必要的。之前笔者写过一篇关于JVM加载类的文章http://suhuanzheng7784877.iteye.com/blog/964784。后来发现这些还是不够的,还是需要在总结一下JVM运行时的其他环节。对于我们开发分布式的Java系统是有一种潜移默化的作用,主要用于自己回顾用。

2.  JVM结构

JVM主要负责什么工作呢,首先它负责将我们编写的java文件编译成为class文件;第二它负责加载已有的class文件;第三它还负责class的执行;第四JVM还负责内存的分配与回收,这也是为什么大家在写代码的过程中不用手工写代码分配资源和强制回收资源了,有个强势的管家——JVM为您做了;JVM还要和底层操作系统接口进行交互完成多线程资源的同步、并发机制,所以为什么我们实现一个Runnable接口或者集成Thread就能完成多线程操作了,表面上看您除了编写一些特殊的类什么都没做,实际上是JVM为您默默做了那么多。

Sun,哦,不,笔者老是改不了口,OracleJava规范JVM如下:

Class文件被类加载器加载,JVM为其开辟内存空间,内存空间又分为四个大部分:方法区、堆区、栈区、本地方法栈区。JVM启动优先级比较小的线程来运行垃圾回收器,时刻监视并回收开辟的内存区域。

 

3.  JVM到底如何编译Java代码

运行class文件需要将java文件编译成为class文件,JavaSDK如何编译的呢?javacjava源代码编译class过程如下:

 

1:分析输入到符号表:将java文件内容的字符串转变为关键字序列,之后生成一个类似于树状结构的内容,叫做抽象语法树。所谓输入到符号表的意思,就是将类中出现的符号输入类自身的符号表中等。所谓符号一般是指:父类、接口,因为要根据这些符号生成无参构造函数。

2:处理注解阶段:在JDK1.5之前,JVM没有这个阶段,但是JDK1.5之后咱们在Java类文件中写的注解需要JVM来进行处理、加强,至于注解保留到何时才生效,具体请参考http://suhuanzheng7784877.iteye.com/blog/1054838

之后根据抽象语法树中等等信息联系到一起,进行有效性判断,一些列的语义判断,校验,优化某些代码,之后就是我们看到的class文件。class中除了字节码,还包括了JVM执行class的相关信息:

结构信息:class的文件格式版本号以及各部分的数量与大小。

元数据:元数据代表类的继承关系、接口、声明信息、属性、方法声明信息、常量池信息。

方法信息:包括字节码、异常处理表、局部变量区大小

我们利用反编译工具反编译一下以前自己写过的class源码,是不是发现确实JDK编译此文件的时候和我们源代码有些许出入,那是它为我们做了优化。

 

4.  JVM到底如何执行Java代码

JVM执行class文件有3种方式,第一是解释执行、第二是编译执行、第三就是反射执行。可能大家对于第三个反射执行更为熟悉,因为这也是对大家相对真真正正编代码体验过的,而前两者都是由JVM给咱做的,所以听上去就十分陌生。

1):解析执行

JVM中解析执行专门执行那些执行频率不高的代码,解析执行内部原理十分类似于咱们学过的汇编语言,JVM用一些自己自定义的指令(汇编语言也有自己一些简单的指令),来完成既有代码的执行过程,说白了,到了最底层,面向对象还是要过程化的。

通过4条常用指令完成对类的解析和执行。Invokestatic负责调用static方法,invokevirtual负责调用实例对象方法,invokeinterface对应的是调用接口方法,invokespecial负责private方法的调用以及编译源码后(就是class)生成的构造方法。

 

如下一段代码

Java代码  收藏代码
  1. import java.io.UnsupportedEncodingException;  
  2.   
  3. public class UTest {  
  4.   
  5.     public static void main(String[] args) throws UnsupportedEncodingException {  
  6.         String c = java.net.URLDecoder  
  7.                 .decode("%E4%BA%A7%E5%93%81%E6%8F%8F%E8%BF%B0%E5%9F%9F%E8%B6%8A%E7%95%8C","utf-8");  
  8.         System.out.println(" " + c);  
  9.     }  
  10.   
  11. }  
 编译成class后,使用javap –c UTest看字节码内容

Java代码  收藏代码
  1. Compiled from "UTest.java"  
  2. public class UTest extends java.lang.Object{  
  3. public UTest();  
  4.   Code:  
  5.    0:   aload_0//装载局部变量第一个值到操作数栈  
  6.    1:   invokespecial   #8//Method java/lang/Object."<init>":()V//初始化构造器  
  7.    4:   return  
  8.   
  9. public static void main(java.lang.String[])   throws java.io.UnsupportedEncoding  
  10. Exception;  
  11.   Code:  
  12.    0: ldc     #19//String %E4%BA%A7%E5%93%81%E6%8F%8F%E8%BF%B0%E5%9F%9F%E8%B  
  13. 6%8A%E7%95%8C//将字符串值装载到常量池中  
  14.    2: ldc     #21//String utf-8//将字符串值装载到常量池中  
  15.    4: invokestatic    #23//Method java/net/URLDecoder.decode:(Ljava/lang/Str  
  16. ing;Ljava/lang/String;)Ljava/lang/String;  
  17. //调用静态方法java.net.URLDecoder.decode  
  18.    7:   astore_1//将操作数栈中栈顶的值弹出放入局部变量区  
  19.    8: getstatic       #29//Field java/lang/System.out:Ljava/io/PrintStream;  
  20.    11:  new     #35//class java/lang/StringBuilder  
  21.    14:  dup  
  22.    15:  ldc     #37//String  
  23.    17:invokespecial  #39//Method java/lang/StringBuilder."<init>":(Ljava/la  
  24. ng/String;)V  
  25.    20:  aload_1  
  26.    21:invokevirtual  #42//Method java/lang/StringBuilder.append:(Ljava/lang  
  27. /String;)Ljava/lang/StringBuilder;  
  28.    24:invokevirtual  #46//Method java/lang/StringBuilder.toString:()Ljava/l  
  29. ang/String;  
  30.    27:invokevirtual  #50;//Method java/io/PrintStream.println:(Ljava/lang/St  
  31. ring;)V  
  32.    30:  return  
  33. }  

 估计大家都看烦了,习惯了面向对象思维的大家一看这种汇编似的语言确实头疼,耐不下心。如果不是搞JVM开发的,其实了解一下就可以,毕竟了解一下JVM运行原理,对于我们写代码,无异于又过了一层思考。

解释执行比较慢,但是呢,比较省资源,省内存,因为采用栈这种数据结构进行指令的收编和采集,运用,空间利用率可见是比较节俭的。

2):编译执行:

为了提升性能,JDK还可以编译执行,也叫做即时编译器,通常是对执行频率比较高的代码进行及时编译执行。编译执行牺牲了部分空间资源,换来的是优化了字节码的编译实际代码块,就是说我们编写的代码在JVM编译器看来是可优化的,所以它采用优化编译,将原先的代码进行一些调整,进而更好地利用JVM资源。编译执行那个主要分为2类,一个是客户端类型,属轻量级;一类是服务端类型,属于重量级。

客户端执行的优化策略有如下3种措施

方法内联:一个方法的执行离不开其他方法的支持,若调用的其他方法十分简单。那么调用的时候会将被调用那个方法里面所有的内容粘到主调方法中,这样做的好处是节省参数变量,中间变量的资源和返回值的资源申请位置。

    private void test(){

       test2("1");

    }

   

    private String test2(String a){

       //test2的方法体

       return null;

    }

在JVM编译中会变成如下方式

    private void test(){

       //test2的方法体

    }

去虚化处理:装载类的时候会进行类层次的分析,如果发现接口的方法是有一个实现类,那么JVM会说,别故弄玄虚了,就那么一个实现类,咱将实现类的方法实现内容全粘过来才是实在人,做人要厚道。

接口如下

public interface IA {

    public void t();

}

实现类如下

public class A implements IA {

    public void t(){

       //做你想做的事情

    }

}

下面是一个客户端调用类

class Demo{

    public void execute(IA ia){

       ia.t();

    }

}

那么JVM去虚化会将此代码替换成

class Demo{

    public void execute(IA ia){

       //做你想做的事情

    }

}

冗余消除:冗余消除是在编译期间发现代码可以进行折叠或者消除。

比如如下代码

public class A implements IA {

    final static boolean isDebug = false;

    public void t(){

       if(isDebug){

           System.out.println("容我三思");

       }

       System.out.println("全线出击");

    }

}

反编译后的class内容如下

import java.io.PrintStream;

public class A

  implements IA

{

  static final boolean isDebug = false;

  public void t()

  {

    System.out.println("全线出击");

  }

}

编译期间可以确定代码不会执行到“容我三思”的步骤,您只能“全线出击”。

服务器模式编译则比客户端模式编译更加费资源,因为编译器模式的很多优化策略是从整体类运行出发的,主要优化点是标量替换、栈上分配、同步消除三项。这三项优化措施都是基于分析变量是否逃逸的分析结果。这些我们知道有这么个事情,了解一下即可,因为咱们还没到开发JVM的程度。

3):反射执行:

先回顾一下反射的代码,再来看看JVM如何处理反射执行的过程的

       Class cls = Class.forName("java.lang.String");

       Object obj = cls.newInstance();

       Method method = cls.getMethod("valueOf", Object.class);

       String str = (String) method.invoke(obj, 1);

       System.out.println(str);

反射调用实际就是动态生成字节码,并加载到JVM中进行执行。流程如下:先让调用者所在的ClassLoader来加载创建Class对象,Class对象生出来了,之后校验Class是否为public权限的,如果不是,那么对不起,反射机制在伟大也不能违反原则,越雷池一步,不是public的类,您还是少碰为妙!之后JVM调用对象的newInstance方法,,该方法先去查找是否有缓存的ConstructorAccessor对象,如果没有则生成一个。生成ConstructorAccessor的过程需要MethodAccessorGenerator中的generate方法根据Class格式规范生成字节码,在生成字节码后,将其加载到ClassLoader中,并实例化完成对象的创建过程。之后就是寻遍方法数组,对照有没有要使用的”valueOf”方法,如果没有则抛出异常。如果有,则验证权限是否能暴露出来供反射调用,所以也是为什么反射调用执行稍微比直接对象调用方法有点慢的原因了。它需要验证使用权限,还要负责创建Method对象。传说是现在的JDK版本(Sun JDK1.6.26)在直接调用和反射调用的性能差别不大。这个笔者并没试过,改天有时间再做测试。

5.  总结

JVM内容很多,这次仅仅介绍了JVM编译、执行java代码的过程,类加载过程因为之前做过总结(http://suhuanzheng7784877.iteye.com/blog/964784)。了解了JVM编译,加载、执行的过程,以后我们写代码脑子里就多一层过滤,这是一种潜移默化的。JVM还有很多其他相关内容,比如JVM内存回收机制,也在笔者的blog:http://suhuanzheng7784877.iteye.com/blog/1000646做过相应的总结。在此不再赘述了。


原文地址:https://www.cnblogs.com/daichangya/p/12960010.html