JVM知识点题目解答

标签: jvm 知识 | 发表时间:2011-12-29 01:07 | 作者:
出处:http://www.iteye.com
因最近一直在学习 JVM,看到 BlueDavy 的一篇文章 JVM知识点题目,于是便激起了我去解答的兴趣。


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

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

      先定义一个 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

     目录结构:


     可以看出,以上目录下的"所有" 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的实现这并不是作者所要考察的。


2、一段展示代码,里面包含一个全局静态整型变量,问如果用两个ClassLoader加载此对象,执行这个整型变量++操作后结果会是怎么样的?
     继续使用上题的 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()));
	}

~


方法的执行
1、A a=new A();a.execute();和 IA a=new A();a.execute();执行有什么不同;
    
      简单的说,如果想知道方法的执行上有什么不同,那么先看一下它们的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....");
	}
}
      通过 javap -c it.denger.jvm.code.MethodExecute 命令查看代码的字节码信息,其中 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 说起了,你可以从以下几个链接中了解个大概:
      Java字节码invokeinterface.... invokevirtual的含义和区别
      invokeinterfaceinvokevirtual
      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 接口:
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 中,将在近期发出。




作者: denger 
声明: 本文系ITeye网站发布的原创文章,未经作者书面许可,严禁任何网站转载本文,否则必将追究法律责任!

已有 1 人发表回复,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [jvm 知识] 推荐:

JVM知识点题目解答

- - ITeye论坛最新讨论
因最近一直在学习 JVM,看到 BlueDavy 的一篇文章 JVM知识点题目,于是便激起了我去解答的兴趣. 1、写一段将目录中指定的.class文件加载到JVM的程序,并通过Class对象获取到完整类名等信息;.       对于 ClassLoader 的加载机制、过程及双亲委派模型等这里就不详细介绍了,基本上属于老生常谈的东西了.

JVM研究

- - 开源软件 - ITeye博客
每天接客户的电话都是战战兢兢的,生怕再出什么幺蛾子了. 我想Java做的久一点的都有这样的经历,那这些问题的最终根结是在哪呢. JVM全称是Java Virtual Machine,Java虚拟机,也就是在计算机上再虚拟一个计算机,这和我们使用 VMWare不一样,那个虚拟的东西你是可以看到的,这个JVM你是看不到的,它存在内存中.

jvm调优

- - 互联网 - ITeye博客
printf "%x\n" 21742  找到耗时最长的进程. jstack pid | grep 54ee  定位某个类的方法. jstack 10535|grep -A 10 2a1d (最后十行). jmap 查询pid 内存线程. 附:TOP命令中需要关注的值:. (1)load average:此值反映了任务队列的平均长度;如果此值超过了CPU数量,则表示当前CPU数量不足以处理任务,负载过高.

深入理解Java虚拟机(JVM高级特性与最佳实践java虚拟机)的一些知识总结

- - CSDN博客推荐文章
        要回答这个问题,先了解下GC的发展史、jvm运行时数据区的划分、jvm内存分配策略、jvm垃圾收集算法等知识.         先说下jvm运行时数据的划分,粗暴的分可以分为堆区(Heap)和栈区(Stack),但jvm的分法实际上比这复杂得多,大概分为下面几块:.         1、程序计数器(Program Conuter Register).

学习JVM的References

- LightingMan - 淘宝JAVA中间件团队博客
本blog中列举了我学习JVM的references,会不断的更新,为了避免版权问题,就不在blog上提供references的下载了,感兴趣的同学可自行下载或购买,:). |— [ Hotspot GC论文 ]. |— [ 其他JVM GC ]. |— Linux内核源代码情景分析. |— Linux 内核中断内幕.

深入理解JVM

- 小伟 - ITeye论坛最新讨论
1   Java技术与Java虚拟机. 说起Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成: Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API). 图1   Java四个方面的关系. 运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件).

jvm垃圾回收

- Cano - 淘宝共享数据平台 tbdata.org
在jvm中堆空间划分为三个代:年轻代(Young Generation)、年老代(Old Generation)和永久代(Permanent Generation). 年轻代和年老代是存储动态产生的对象. 永久带主要是存储的是java的类信息,包括解析得到的方法、属性、字段等等. 我们这里讨论的垃圾回收主要是针对年轻代和年老代.

JVM内存分配

- - 移动开发 - ITeye博客
计算机内存,它算是CPU与计算机打交道最频繁的区域,所有数据都是先经过硬盘至内存,然后由CPU再从内存中获取数据进行处理,又将数据保存到内存,通过分页或分片技术将内存中的数据再flush至硬盘. 那JVM的内存结构到底是如何呢. JVM做为一个运行在操作系统上,但又独立于os运行的平台,它的内存至少应该包括象寄存器、堆栈等区域.

Azul开源Zing Jvm

- - InfoQ cn
4月末,继Zing 5.2 之后,. Azul Systems宣布他们将无停顿(pauseless )的 Zing JVM提供给开源软件开发者和项目,以供开发和测试. Azul Systems 工程部副总裁和合作创始人Shyam Pillalamarri向InfoQ说明道:. 我们的部署很大一部分基于开源组件,所以我们认为:“假设我们不能将一些有价值的东西免费提供给开源项目贡献者,他们将一直受限于从Java虚拟机(JVM)视角所看到的内容”,他们将不会考虑额外的用例,或者选择其他能解决了所有内存或扩展性问题、类似Zing的系统.

JVM参数设置

- - 企业架构 - ITeye博客
-Xms768m -Xmx1280m  jvm堆的最小值和最大值设置,一般设成相同值,避免频繁分配堆空间. -XX:NewSize=128m -XX:MaxNewSize=128m  年轻代最小值和最大值设置(年轻代设定了,年老代也就定了),也可以用参数-XX:NewRatio=4,年老代和年轻代的大小比,这里128m有点小了,官方建议的是heap的3/8,差不多280m.