JVM 知识点题目解答之一

因最近一直在学习 JVM,看到 [url=http://blog.bluedavy.com/?page_id=2]BlueDavy[/url] 的一篇文章 [url=http://www.blogjava.net/BlueDavy/archive/2009/03/27/262419.html]JVM知识点题目[/url],于是便激起了我去解答的兴趣。


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

对于 ClassLoader 的加载机制、过程及双亲委派模型等这里就不详细介绍了,基本上属于老生长谈的东西了。不过不了解朋友的可以看一下该作者文章:
[url=http://www.cnblogs.com/ChrisWang/archive/2009/11/17/Inside-JVM-4-ClassLoader-Knowledge-Sharing.html]深入JVM(4):关于ClassLoader的一些知识[/url]

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

package it.denger.jvm.classloader;

public class DirectoryClassLoader extends ClassLoader {

protected File directory;
protected ClassLoader parent;

public DirectoryClassLoader(File directory, ClassLoader parent) {
// 将父 ClassLoader 传入super,用于classloader加载时向上查找
super(parent);
.....
}

public Class<?>[] findAndLoadAllClassInDirectory(){
// 获取当前目录中所有 class 文件的相对路径
String[] classPaths = getAllClassRelativePathInDirectory(directory);
List<Class<?>> classes = new ArrayList<Class<?>>(classPaths.length);
for (String classPath : classPaths){
try {
// 将所有 class 文件相对路径转换为相应的包名
// 如 it/denger/Pear.class 则转换为 it.denger.Pear
String className = convertPathToClassName(classPath);
// 调用父类的 loadClass,该方法实现通过向上 一级级的ClassLoader
// 进行查找,当所有上级全部无法找到时则会调用本ClassLoader的
// findClass 方法进行查找,也就是所谓的 “双亲委派模型”
Class<?> classz = loadClass(className);
if (classes != null){
classes.add(classz);
}
} catch (Exception e) {
e.printStackTrace();
}
}
return classes.toArray(new Class<?>[0]);
}

// 重写父类 findClass,当所有 Parent Class Loader 无法找到类时,
// 则会通过调用这里所该方法进行最后的查找
protected java.lang.Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> classz = null;
// 通过 className 获取到对应的 File 对象
File classFile = new File(directory, convertClassNameToPath(name));
if (classFile.exists()) {
try {
// 重点在这里,从 class 的文件流中加载 Class 对象
classz = loadClassFromStream(name, new FileInputStream(classFile), (int)classFile.length());
} catch (Exception e) {
throw new ClassNotFoundException(String.format("Find class %s error!", name), e);
}
}
.....
return classz;
}

protected byte[] loadBytesFromStream(InputStream stream, int len) throws IOException{
byte[] streamBytes = new byte[len];
int read, off = 0;
while(len > 0 && (read = stream.read(streamBytes, off, len)) != -1 ){
off += read;
len -= read;
}
return streamBytes;
}

// 通过调用父类的 defineClass 将 字节 转为 Class 对象
protected Class<?> loadClassFromBytes(String name, byte []classBytes){
return defineClass(name, classBytes, 0, classBytes.length);
}

protected Class<?> loadClassFromStream(String name, InputStream stream, int len){
return loadClassFromBytes(name, loadBytesFromStream(stream, len));
}
}

OK, 接下为写个 Case 测试一把:
public class DirectoryClassLoaderTest extends TestCase{

protected DirectoryClassLoader directoryClassLoader;
protected String classDirectory;

protected void setUp() throws Exception {
classDirectory = "/Users/denger/Workspaces/Java/backup/classes";
directoryClassLoader = new DirectoryClassLoader(new File(classDirectory), this.getClass().getClassLoader());
}

public void testShouldBeLoadedAllClassFileInDirectory(){
Class<?>[] classes = directoryClassLoader.findAndLoadAllClassInDirectory();
assertTrue(classes.length > 0);
for(Class<?> classz : classes){
assertNotNull(classz);
System.out.println("Class: " + classz.getName() + " - ClassLoader: " + classz.getClassLoader());
}
}
输入结果:
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

目录结构:
[img]http://dl.iteye.com/upload/attachment/613397/6f5ab522-6790-339b-b33d-5f753dc63324.png[/img]

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

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


[b]2、一段展示代码,里面包含一个全局静态整型变量,问如果用两个ClassLoader加载此对象,执行这个整型变量++操作后结果会是怎么样的?[/b]
继续使用上题的 ClassLoader 对象,修改 Apple.java为:
 public class Apple{
public static int count = 1;
public static void addCount(){
count++;
}
public static int getCount(){
return count;
}
}
实际上,当两个ClassLoader加载此Class,并分别通过实例调用 addCount 时,其 count 是2还是3取决于分别加载的这两个 Apple Class 是否相等。
从 JVM加载 Class 的过程来说,对于同一包路径同名的 Class 是不会在同一ClassLoader进行重复加载的,当然,如果如这题所说,使用两个 ClassLoader 实例进行加载同一 Class ,这时候会产生两个 Class实例,原因是由于 ClassLoader 实例之间的相互隔离的。
以下测试用例是使用不同 ClassLoader 实例加载 Apple Class 之后并分别调用 addCount 一次:

public void testRepeatedLoadClass() {
DirectoryClassLoader steveDirectoryClassLoader = new DirectoryClassLoader(
new File(classDirectory), this.getClass().getClassLoader());

DirectoryClassLoader myDirectoryClassLoader = new DirectoryClassLoader(
new File(classDirectory), this.getClass().getClassLoader());

Class<?> steveApple = steveDirectoryClassLoader.loadClass("Apple");
Class<?> myApple = myDirectoryClassLoader.loadClass("Apple");

// 产生不同的 Apple Class 实例,原因是以上两个 ClassLoader 实例是相互隔离的,他们都并知道对方加载了哪些 Class
assertTrue(steveApple != myApple);

// 分别调用 addCount
steveApple.getMethod("addCount").invoke(null);
myApple.getMethod("addCount").invoke(null);

// 其 count 都为2,都只是 ++ 了自己的 count
assertTrue(Integer.parseInt(steveApple.getMethod("getCount").invoke(null).toString()) == 2);
assertTrue(Integer.parseInt(steveApple.getMethod("getCount").invoke(null).toString()) == Integer.parseInt(myApple.getMethod("getCount").invoke(null).toString()));
}

~


[size=large]方法的执行[/size]
[b]1、A a=new A();a.execute();和 IA a=new A();a.execute();执行有什么不同;[/b]

简单的说,如果想知道方法的执行上有什么不同,那么先看一下它们的bytecode(字节码)有什么不同吧
Java 代码如下:
package it.denger.jvm.code;

public class MethodExecute {

public static void main(String[] args) {
A a = new A();
a.execute();

IA ia = new A();
ia.execute();
}
}

interface IA{
public void execute();
}

class A implements IA{
public void execute() {
System.out.println("A execute call....");
}
}
通过 [b]javap -c it.denger.jvm.code.MethodExecute[/b] 命令查看代码的字节码信息,其中 main 方法的字节码如下:
public static void main(java.lang.String[]);
Code:
0: new #2; //class it/denger/jvm/code/A
3: dup
4: invokespecial #3; //Method it/denger/jvm/code/A."<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4; //Method it/denger/jvm/code/A.execute:()V
12: new #2; //class it/denger/jvm/code/A
15: dup
16: invokespecial #3; //Method it/denger/jvm/code/A."<init>":()V
19: astore_2
20: aload_2
21: invokeinterface #5, 1; //InterfaceMethod it/denger/jvm/code/IA.execute:()V
26: return
}
对 exeucte 方法的调用主要在第 9 行和第 21行,不难看出,他们的调用所使用的指令不一样,分别为 invokevirtual(调用对象方法) 和 invokeinterface(调用接口中方法)。
然而以上两个指令在到底在执行时有何不同,这得从 VM 的 Spec 说起了,你可以从以下几个链接中了解个大概:
[url=http://liuxuan.info/blog/2011/04/13/explain-java-bytecode-invokeinterface-invokespecial-invokestatic-invokevirtual/]Java字节码invokeinterface.... invokevirtual的含义和区别[/url]
[url=https://www.vmth.ucdavis.edu/incoming/Jasmin/ref--32.html]invokeinterface[/url], [url=https://www.vmth.ucdavis.edu/incoming/Jasmin/ref--35.html]invokevirtual[/url]
[url=http://stackoverflow.com/questions/1504633/what-is-the-point-of-invokeinterface]What is the point of invokeinterface?[/url]
最后当然少不了 VM Spec 了,[url=http://java.sun.com/docs/books/jvms/second_edition/html/Instructions2.doc6.html]Sun VM Spec[/url]


[b]2、反射的性能低的原因是?[/b]
关于这题,在 Sun 官方的 [url=http://docs.oracle.com/javase/tutorial/reflect/index.html]The Reflection API [/url]明确说明了:
[quote]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.[/quote]
大致意思是说因为反射涉及到动态解析类型,以致于某些 jvm 不能够对其进行执行时的优化, 因此使用反射的性能低于非反射的性能..blabla...
上面所说的动态解析,意味着对于对象创建的过程、方法调用的过程是动态,通过动态加载字节码在 JVM 中进行执行,并且当使用 Class.forName 或 getMethod 时会执行权限校验检查以及lookup的过程,以至于这些调用操作必定将存在时间成本。
另外说的JVM不能进行优化,大致是说的是在对 Class 进行编译时候的优化(如在语义分析过程中会自动拆箱装箱处理),因为编译过程中是无法知道反射代码的真正所需要做的事情, 另外也可能无法发挥 JIT 的最大优化潜力。

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


[b]3、编写一段程序,动态的创建一个接口的实现,并加载到JVM中执行;(可以允许用BCEL等工具)[/b]
既然这里提到可以使用 [url=http://commons.apache.org/bcel/]BCEL[/url] 来实现接口的动态实现,那么就直接使用 BCEL 吧,顺便再复习一下 Builder 模式来进行代码的实现,将 Class 的创建与其具体的组成过程进行解耦。
首先定义一个 User 接口:
package it.denger.jvm.code.bcel;

public interface IUser {

void jump();

void run();
}
然后针对该接口动态类生成定义一个 Builder 接口:
package it.denger.jvm.code.bcel;

import org.apache.bcel.generic.ClassGen;

public interface UserClassGenBuilder {

static final String USER_CLASS_NAME = "it.denger.jvm.code.bcel.IUser";

UserClassGenBuilder init(String className);

UserClassGenBuilder implementJumpMethod();

UserClassGenBuilder implementRunMethod();

ClassGen build();
}
对于 User 接口来说,具体实现可能有 Student、Staff等,以下以 Student 为例,定义 StudentClassGenBuilder,动态生成 Student User 及实现 jump 和 run 方法:
public class StudentClassGenBuilder implements UserClassGenBuilder{

protected String className;

protected ClassGen classGen;

public StudentClassGenBuilder init(String className) {
this.className = className;
classGen = new ClassGen(className, "java.lang.Object",
"<generated>", ACC_PUBLIC | ACC_SUPER,
new String[] { USER_CLASS_NAME });
classGen.addEmptyConstructor(ACC_PUBLIC);

return this;
}

public UserClassGenBuilder implementJumpMethod() {
InstructionFactory instructionFactory = new InstructionFactory(classGen);
InstructionList instructionList = new InstructionList();
ConstantPoolGen constantPool = classGen.getConstantPool();
MethodGen methodGen = new MethodGen(ACC_PUBLIC,
Type.VOID, new Type[0], new String[0],
"jump", className, instructionList, constantPool);

instructionList.append(instructionFactory.createPrintln("I'm jump...."));
instructionList.append(InstructionFactory.createReturn(Type.VOID));
methodGen.setMaxStack();
classGen.addMethod(methodGen.getMethod());
return this;
}

public UserClassGenBuilder implementRunMethod() {
InstructionFactory instructionFactory = new InstructionFactory(classGen);
InstructionList instructionList = new InstructionList();
ConstantPoolGen constantPool = classGen.getConstantPool();
MethodGen methodGen = new MethodGen(ACC_PUBLIC,
Type.VOID, new Type[0], new String[0],
"run", className, instructionList, constantPool);

instructionList.append(instructionFactory.createPrintln("I'm run...."));
instructionList.append(InstructionFactory.createReturn(Type.VOID));
methodGen.setMaxStack();
classGen.addMethod(methodGen.getMethod());
return this;
}

public ClassGen build() {
return classGen;
}
可以看出以上已经完成了两个方法的实现,并返回 ClassGen 对象(用于生成具体 Class 的对象)。但该题最终是需要将动态的实现的 Class 加载至 JVM中,并调用动态实现的方法,于是以下的 UserClassGenLoader 便产生了:
public class UserClassGenLoader {

protected UserClassGenBuilder builder;

public UserClassGenLoader(UserClassGenBuilder builder){
this.builder = builder;
}

public Class<?> loadClass(String className) throws ClassNotFoundException, IOException{
ClassGen classGen = this.builder.init(className)
.implementJumpMethod().implementRunMethod().build();

return loadClassForClassGen(classGen);
}

protected Class<?> loadClassForClassGen(ClassGen classGen) throws ClassNotFoundException, IOException{
ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
classGen.getJavaClass().dump(arrayOutputStream);
byte[] classBytes = arrayOutputStream.toByteArray();
Map<String, byte[]> classByteMap = new HashMap<String, byte[]>();

classByteMap.put(classGen.getClassName(), classBytes);
ByteClassLoader byteClassLoader = new ByteClassLoader(this.getClass()
.getClassLoader(), classByteMap);

return byteClassLoader.loadClass(classGen.getClassName());
}
}
至此已经完成从动态接口实现至将其加载至 JVM 中,并获取最终的 Class 对象,最后写一个 TestCase 测试一把:
public void testShouldBeLoadStudentNewInstance() {
try {
Class<?> studentClass = new UserClassGenLoader(new StudentClassGenBuilder())
.loadClass("it.denger.jvm.code.bcel.Student");

IUser studentUser = (IUser) studentClass.newInstance();
assertNotNull(studentUser);
assertEquals(studentUser.getClass().getName(), "it.denger.jvm.code.bcel.Student");

studentUser.jump();
studentUser.run();
} catch (Exception e) {
fail(e.getMessage());
}
}
}

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


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

[url=http://www.iteye.com/topic/1119390]进入论坛讨论[/url]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值