JVM 知识点题目解答之一

http://www.iteye.com/topic/1119390

因最近一直在学习 JVM,看到 BlueDavy 的一篇文章 JVM知识点题目,于是便激起了我去解答的兴趣。 


字节码的加载 
1、写一段将目录中指定的.class文件加载到JVM的程序,并通过Class对象获取到完整类名等信息; 

      对于 ClassLoader 的加载机制、过程及双亲委派模型等这里就不详细介绍了,基本上属于老生常谈的东西了。不过不了解朋友的可以看一下该作者文章: 
深入JVM(4):关于ClassLoader的一些知识 

      先定义一个 DirectoryClassLoader继承 ClassLoader,通过加载指定的目录下的 class 文件,以下贴出该类的主要代码,完整的代码请从附件中下载。 

Java代码   收藏代码
  1. package it.denger.jvm.classloader;  
  2.   
  3. package it.denger.jvm.classloader;  
  4.   
  5. public class DirectoryClassLoader extends ClassLoader {  
  6.       
  7.     protected File directory;  
  8.     protected ClassLoader parent;  
  9.   
  10.     public DirectoryClassLoader(File directory, ClassLoader parent) {  
  11.         // 将父 ClassLoader 传入super,用于classloader加载时向上查找  
  12.         super(parent);     
  13.         .....  
  14.     }  
  15.           
  16.     public Class<?>[] findAndLoadAllClassInDirectory(){  
  17.         // 获取当前目录中所有 class 文件的相对路径               
  18.         String[] classPaths = getAllClassRelativePathInDirectory(directory);  
  19.         List<Class<?>> classes = new ArrayList<Class<?>>(classPaths.length);  
  20.         for (String classPath : classPaths){  
  21.             try {  
  22.                 // 将所有 class 文件相对路径转换为相应的包名  
  23.                 // 如 it/denger/Pear.class 则转换为 it.denger.Pear  
  24.                 String className = convertPathToClassName(classPath);  
  25.                 // 调用父类的 loadClass,该方法实现通过向上 一级级的ClassLoader  
  26.                 // 进行查找,当所有上级全部无法找到时则会调用本ClassLoader的  
  27.                 // findClass 方法进行查找,也就是所谓的 “双亲委派模型”  
  28.                 Class<?> classz = loadClass(className);  
  29.                 if  (classes != null){  
  30.                     classes.add(classz);  
  31.                 }  
  32.             } catch (Exception e) {  
  33.                 e.printStackTrace();  
  34.             }  
  35.         }  
  36.         return classes.toArray(new Class<?>[0]);  
  37.     }  
  38.   
  39.     // 重写父类 findClass,当所有 Parent Class Loader 无法找到类时,  
  40.     // 则会通过调用这里所该方法进行最后的查找  
  41.     protected java.lang.Class<?> findClass(String name) throws ClassNotFoundException {     
  42.         Class<?> classz = null;  
  43.         // 通过 className 获取到对应的 File 对象  
  44.         File classFile = new File(directory, convertClassNameToPath(name));  
  45.         if (classFile.exists()) {  
  46.             try {  
  47.                 // 重点在这里,从 class 的文件流中加载 Class 对象  
  48.                 classz = loadClassFromStream(name, new FileInputStream(classFile), (int)classFile.length());  
  49.             } catch (Exception e) {  
  50.                 throw new ClassNotFoundException(String.format("Find class %s error!", name), e);  
  51.             }  
  52.         }  
  53.         .....  
  54.         return classz;  
  55.     }  
  56.   
  57.     protected byte[] loadBytesFromStream(InputStream stream, int len) throws IOException{  
  58.         byte[] streamBytes = new byte[len];  
  59.         int read, off = 0;  
  60.         while(len > 0 && (read = stream.read(streamBytes, off, len)) != -1 ){  
  61.             off += read;  
  62.             len -= read;  
  63.         }  
  64.         return streamBytes;  
  65.     }  
  66.   
  67.     // 通过调用父类的 defineClass 将 字节 转为 Class 对象  
  68.     protected Class<?> loadClassFromBytes(String name, byte []classBytes){  
  69.         return defineClass(name, classBytes, 0, classBytes.length);  
  70.     }  
  71.       
  72.     protected Class<?> loadClassFromStream(String name, InputStream stream, int len){  
  73.         return loadClassFromBytes(name, loadBytesFromStream(stream, len));  
  74.     }  
  75. }  

     OK, 接下为写个 Case 测试一把: 
Java代码   收藏代码
  1. public class DirectoryClassLoaderTest extends TestCase{  
  2.       
  3.     protected DirectoryClassLoader directoryClassLoader;  
  4.     protected String classDirectory;  
  5.       
  6.     protected void setUp() throws Exception {  
  7.         classDirectory = "/Users/denger/Workspaces/Java/backup/classes";  
  8.         directoryClassLoader = new DirectoryClassLoader(new File(classDirectory), this.getClass().getClassLoader());  
  9.     }  
  10.   
  11.     public void testShouldBeLoadedAllClassFileInDirectory(){  
  12.         Class<?>[] classes = directoryClassLoader.findAndLoadAllClassInDirectory();  
  13.         assertTrue(classes.length > 0);  
  14.         for(Class<?> classz : classes){  
  15.             assertNotNull(classz);  
  16.             System.out.println("Class: " + classz.getName() + " - ClassLoader: " + classz.getClassLoader());  
  17.         }  
  18.     }  
     输入结果: 
     Class: Apple - ClassLoader: it.denger.jvm.classloader.DirectoryClassLoader@3d434234 
     Class: Banana - ClassLoader: it.denger.jvm.classloader.DirectoryClassLoader@3d434234 
     Class: it.denger.Pear - ClassLoader: it.denger.jvm.classloader.DirectoryClassLoader@3d434234 
     Class: java.lang.String - ClassLoader: null 

     目录结构: 
 

     可以看出,以上目录下的"所有" class 文件均已经加载,并且所对应的 ClassLoader 都为 DirectoryClassLoader。唯独 java.lang.String 类的 ClassLoader 为 null,其原因是由于 ClassLoader 的双亲委派模型,因为 String 属于 jre 核心库,已由 BootStrap ClassLoader 加载,而 BootStrap 的加载过程是由 C 实现,所以这里自然就是空了。 

     其实这题还有另外一个更加简单的方法,就是通过 URLClassLoader 进行加载,示例代码如下: 
Java代码   收藏代码
  1. String classFileDirectory = "file:///Users/denger/Workspaces/Java/backup/classes/";  
  2. URLClassLoader classLoader = new URLClassLoader(new URL[]{new URL(classFileDirectory)});  
  3. System.out.println(classLoader.loadClass("it.denger.Pear"));  
     不过我想以上这种通过URLClassLoader的实现这并不是作者所要考察的。 


2、一段展示代码,里面包含一个全局静态整型变量,问如果用两个ClassLoader加载此对象,执行这个整型变量++操作后结果会是怎么样的?  
     继续使用上题的 ClassLoader 对象,修改 Apple.java为: 
Java代码   收藏代码
  1.  public class Apple{  
  2.     public static int count = 1;  
  3.     public static void addCount(){  
  4.         count++;  
  5.     }  
  6.     public static int getCount(){  
  7.         return count;  
  8.     }  
  9. }  
     实际上,当两个ClassLoader加载此Class,并分别通过实例调用 addCount 时,其 count 是2还是3取决于分别加载的这两个 Apple Class 是否相等。 
     从 JVM加载 Class 的过程来说,对于同一包路径同名的 Class 是不会在同一ClassLoader进行重复加载的,当然,如果如这题所说,使用两个 ClassLoader 实例进行加载同一 Class ,这时候会产生两个 Class实例,原因是由于 ClassLoader 实例之间的相互隔离的。 
     以下测试用例是使用不同 ClassLoader 实例加载 Apple Class 之后并分别调用 addCount 一次: 
Java代码   收藏代码
  1. public void testRepeatedLoadClass() {  
  2.         DirectoryClassLoader steveDirectoryClassLoader = new DirectoryClassLoader(  
  3.                 new File(classDirectory), this.getClass().getClassLoader());  
  4.           
  5.         DirectoryClassLoader myDirectoryClassLoader = new DirectoryClassLoader(  
  6.                 new File(classDirectory), this.getClass().getClassLoader());  
  7.   
  8.         Class<?> steveApple = steveDirectoryClassLoader.loadClass("Apple");  
  9.         Class<?> myApple = myDirectoryClassLoader.loadClass("Apple");  
  10.   
  11.         // 产生不同的 Apple Class 实例,原因是以上两个 ClassLoader 实例是相互隔离的,他们都并知道对方加载了哪些 Class  
  12.         assertTrue(steveApple != myApple);    
  13.   
  14.         // 分别调用 addCount  
  15.         steveApple.getMethod("addCount").invoke(null);   
  16.         myApple.getMethod("addCount").invoke(null);  
  17.           
  18.         // 其 count 都为2,都只是 ++ 了自己的 count  
  19.         assertTrue(Integer.parseInt(steveApple.getMethod("getCount").invoke(null).toString()) == 2);  
  20.         assertTrue(Integer.parseInt(steveApple.getMethod("getCount").invoke(null).toString()) == Integer.parseInt(myApple.getMethod("getCount").invoke(null).toString()));  
  21.     }  




方法的执行  
1、A a=new A();a.execute();和 IA a=new A();a.execute();执行有什么不同;  
     
      简单的说,如果想知道方法的执行上有什么不同,那么先看一下它们的bytecode(字节码)有什么不同吧 
      Java 代码如下: 
Java代码   收藏代码
  1. package it.denger.jvm.code;  
  2.   
  3. public class MethodExecute {  
  4.   
  5.     public static void main(String[] args) {  
  6.         A a = new A();  
  7.         a.execute();  
  8.           
  9.         IA ia = new A();  
  10.         ia.execute();  
  11.     }  
  12. }  
  13.   
  14. interface IA{  
  15.     public void execute();  
  16. }  
  17.   
  18. class A implements IA{  
  19.     public void execute() {  
  20.         System.out.println("A execute call....");  
  21.     }  
  22. }  
      通过  javap -c it.denger.jvm.code.MethodExecute  命令查看代码的字节码信息,其中 main 方法的字节码如下: 
Java代码   收藏代码
  1. public static void main(java.lang.String[]);  
  2.   Code:  
  3.    0:   new #2//class it/denger/jvm/code/A  
  4.    3:   dup  
  5.    4:   invokespecial   #3//Method it/denger/jvm/code/A."<init>":()V  
  6.    7:   astore_1  
  7.    8:   aload_1  
  8.    9:   invokevirtual   #4//Method it/denger/jvm/code/A.execute:()V  
  9.    12:  new #2//class it/denger/jvm/code/A  
  10.    15:  dup  
  11.    16:  invokespecial   #3//Method it/denger/jvm/code/A."<init>":()V  
  12.    19:  astore_2  
  13.    20:  aload_2  
  14.    21:  invokeinterface #5,  1//InterfaceMethod it/denger/jvm/code/IA.execute:()V  
  15.    26:  return  
  16. }  
      对 exeucte 方法的调用主要在第 9 行和第 21行,不难看出,他们的调用所使用的指令不一样,分别为 invokevirtual(调用对象方法) 和 invokeinterface(调用接口中方法)。 
      然而以上两个指令在到底在执行时有何不同,这得从 VM 的 Spec 说起了,你可以从以下几个链接中了解个大概: 
       Java字节码invokeinterface.... invokevirtual的含义和区别  
       invokeinterface ,   invokevirtual  
       What is the point of invokeinterface?  
      最后当然少不了 VM Spec 了, Sun VM Spec  


2、反射的性能低的原因是?  
      关于这题,在 Sun 官方的  The Reflection API  明确说明了: 
引用
Because reflection involves types that are dynamically resolved, certain Java virtual machine optimizations can not be performed. Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.

      大致意思是说因为反射涉及到动态解析类型,以致于某些 jvm 不能够对其进行执行时的优化, 因此使用反射的性能低于非反射的性能..blabla... 
      上面所说的动态解析,意味着对于对象创建的过程、方法调用的过程是动态,通过动态加载字节码在 JVM 中进行执行,并且当使用 Class.forName 或 getMethod 时会执行权限校验检查以及lookup的过程,以至于这些调用操作必定将存在时间成本。 
      另外说的JVM不能进行优化,大致是说的是在对 Class 进行编译时候的优化(如在语义分析过程中会自动拆箱装箱处理),因为编译过程中是无法知道反射代码的真正所需要做的事情, 另外也可能无法发挥 JIT 的最大优化潜力。 

      值一提的是该问题是作者在09年发出,到目前为止,JDK反射提升越来越好了,从1.6版本后基本上与非反射方法调用相差无几了。当然,最好能在反射的代码进行缓存 Class 或 Method 对象,避免重复调用 getMethod 和 Class.forName,从而减少访问检查及lookup 的过程。 
    

3、编写一段程序,动态的创建一个接口的实现,并加载到JVM中执行;(可以允许用BCEL等工具)  
      既然这里提到可以使用  BCEL  来实现接口的动态实现,那么就直接使用 BCEL 吧,顺便再复习一下 Builder 模式来进行代码的实现,将 Class 的创建与其具体的组成过程进行解耦。 
      首先定义一个 User 接口: 
Java代码   收藏代码
  1. package it.denger.jvm.code.bcel;  
  2.   
  3. public interface IUser {  
  4.   
  5.     void jump();  
  6.   
  7.     void run();  
  8. }  
      然后针对该接口动态类生成定义一个 Builder 接口: 
Java代码   收藏代码
  1. package it.denger.jvm.code.bcel;  
  2.   
  3. import org.apache.bcel.generic.ClassGen;  
  4.   
  5. public interface UserClassGenBuilder {  
  6.       
  7.     static final String USER_CLASS_NAME = "it.denger.jvm.code.bcel.IUser";  
  8.   
  9.     UserClassGenBuilder init(String className);  
  10.       
  11.     UserClassGenBuilder implementJumpMethod();  
  12.   
  13.     UserClassGenBuilder implementRunMethod();  
  14.   
  15.     ClassGen build();  
  16. }  
      对于 User 接口来说,具体实现可能有 Student、Staff等,以下以 Student 为例,定义 StudentClassGenBuilder,动态生成 Student User 及实现 jump 和 run 方法: 
Java代码   收藏代码
  1. public class StudentClassGenBuilder implements UserClassGenBuilder{  
  2.   
  3.     protected String className;  
  4.   
  5.     protected ClassGen classGen;  
  6.   
  7.     public StudentClassGenBuilder init(String className) {  
  8.         this.className = className;  
  9.         classGen = new ClassGen(className, "java.lang.Object",  
  10.                 "<generated>", ACC_PUBLIC | ACC_SUPER,  
  11.                 new String[] { USER_CLASS_NAME });  
  12.         classGen.addEmptyConstructor(ACC_PUBLIC);  
  13.   
  14.         return this;  
  15.     }  
  16.   
  17.     public UserClassGenBuilder implementJumpMethod() {  
  18.         InstructionFactory instructionFactory = new InstructionFactory(classGen);  
  19.         InstructionList instructionList = new InstructionList();  
  20.         ConstantPoolGen constantPool = classGen.getConstantPool();  
  21.         MethodGen methodGen = new MethodGen(ACC_PUBLIC,  
  22.                 Type.VOID, new Type[0], new String[0],  
  23.               "jump", className, instructionList, constantPool);  
  24.   
  25.         instructionList.append(instructionFactory.createPrintln("I'm jump...."));     
  26.         instructionList.append(InstructionFactory.createReturn(Type.VOID));  
  27.         methodGen.setMaxStack();  
  28.         classGen.addMethod(methodGen.getMethod());  
  29.         return this;  
  30.     }  
  31.   
  32.     public UserClassGenBuilder implementRunMethod() {  
  33.         InstructionFactory instructionFactory = new InstructionFactory(classGen);  
  34.         InstructionList instructionList = new InstructionList();  
  35.         ConstantPoolGen constantPool = classGen.getConstantPool();  
  36.         MethodGen methodGen = new MethodGen(ACC_PUBLIC,  
  37.                 Type.VOID, new Type[0], new String[0],  
  38.               "run", className, instructionList, constantPool);  
  39.   
  40.         instructionList.append(instructionFactory.createPrintln("I'm run...."));      
  41.         instructionList.append(InstructionFactory.createReturn(Type.VOID));  
  42.         methodGen.setMaxStack();  
  43.         classGen.addMethod(methodGen.getMethod());  
  44.         return this;  
  45.     }  
  46.   
  47.     public ClassGen build() {  
  48.         return classGen;  
  49.     }  
      可以看出以上已经完成了两个方法的实现,并返回 ClassGen 对象(用于生成具体 Class 的对象)。但该题最终是需要将动态的实现的 Class 加载至 JVM中,并调用动态实现的方法,于是以下的 UserClassGenLoader 便产生了: 
Java代码   收藏代码
  1. public class UserClassGenLoader {  
  2.       
  3.     protected UserClassGenBuilder builder;  
  4.   
  5.     public UserClassGenLoader(UserClassGenBuilder builder){  
  6.         this.builder = builder;  
  7.     }  
  8.   
  9.     public Class<?> loadClass(String className) throws ClassNotFoundException, IOException{  
  10.         ClassGen classGen = this.builder.init(className)  
  11.                 .implementJumpMethod().implementRunMethod().build();  
  12.   
  13.         return loadClassForClassGen(classGen);  
  14.     }  
  15.   
  16.     protected Class<?> loadClassForClassGen(ClassGen classGen) throws ClassNotFoundException, IOException{  
  17.         ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();  
  18.         classGen.getJavaClass().dump(arrayOutputStream);  
  19.         byte[] classBytes = arrayOutputStream.toByteArray();  
  20.         Map<String, byte[]> classByteMap = new HashMap<String, byte[]>();  
  21.       
  22.         classByteMap.put(classGen.getClassName(), classBytes);  
  23.         ByteClassLoader byteClassLoader = new ByteClassLoader(this.getClass()  
  24.                 .getClassLoader(), classByteMap);  
  25.   
  26.         return byteClassLoader.loadClass(classGen.getClassName());  
  27.     }  
  28. }  
      至此已经完成从动态接口实现至将其加载至 JVM 中,并获取最终的 Class 对象,最后写一个 TestCase 测试一把: 
Java代码   收藏代码
  1. public void testShouldBeLoadStudentNewInstance() {  
  2.         try {  
  3.             Class<?> studentClass = new UserClassGenLoader(new StudentClassGenBuilder())  
  4.                     .loadClass("it.denger.jvm.code.bcel.Student");  
  5.   
  6.             IUser studentUser = (IUser) studentClass.newInstance();  
  7.             assertNotNull(studentUser);  
  8.             assertEquals(studentUser.getClass().getName(), "it.denger.jvm.code.bcel.Student");  
  9.   
  10.             studentUser.jump();  
  11.             studentUser.run();  
  12.         } catch (Exception e) {  
  13.             fail(e.getMessage());  
  14.         }  
  15.     }  
  16. }  

      运行之后显示绿条,并在控制台输出了: 
      I'm jump.... 
      I'm run.... 
      OK, 至此已经完成本题的所有要求。虽然这里只是简单的实现两个无参、无返回值并且只是 println 输出,但是 BCEL 能做的远远不止这些,理论上来说只要你能手写出的代码基本上都能通过其 API 来动态生成字节码。P.S: 上面代码没写什么注释不过其代码看上去应该比较好懂,有什么可以提出. 

    

如果对于以上我个人的分析和理解与你有什么偏差的话,希望能提出一起讨论,另外关于后面两大部分题,现在还在 writing 中,将在近期发出。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值