关于Class对象、类加载机制、虚拟机运行时内存布局的全面解析和推测

简介:

本文是对Java的类加载机制,Class对象,反射原理等相关概念的理解、验证和Java虚拟机中内存布局的一些推测。本文重点讲述了如何理解Class对象以及Class对象的作用。

欢迎探讨,如有错误敬请指正

如需转载,请注明出处 http://www.cnblogs.com/nullzx/


1. 类加载机制

当我们编写好一个“.java”文件,通过javac编译器编译后会形成一个“.class”文件。当我们运行该文件时,Java虚拟机就通过类加载器(类加载器本质就是一段程序)把“.class”文件加载到内存,在方法区形成该类各方法的代码段和描述该类细节信息的常量池,同时在堆区形成一个表示该类的Class对象(一个java.lang.Class类的实例)。Class对象中存储了指向了该类所有属性和方法的详细信息的指针(同时,还存储了指向该类的父类的Class对象的指针)。我们能够通过Class对象直接创建该类的实例,并调用该类的所有方法,这就是我们所说的反射。

类加载器不仅仅可以加载本地文件系统中的“.class”文件,还可以通过各种形式进行加载,比如通过网络上的获取的数据流作为 “.class”。

类加载器本质上实现一个解析的工作,把表示该类的字节数据变成方法区中的字节码和并在堆区产生表示该类的Class对象。

 

1.1 类加载器(ClassLoader)的层次结构

Java默认提供的三个ClassLoader(JAVA_HOME表示JDK的安装目录)

BootStrapClassLoader:称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JAVA_HOME\jre\lib目录下JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等。该加载器不是ClassLoader的子类,由C/C++语言实现其功能。

ExtensionClassLoader:称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME\jre\lib\ext目下的所有jar。它是ClassLoader的子类,由Java语言实现。

AppClassLoader:称为应用程序类加载器,负责加载当前应用程序目录下的所有jar和class文件以及环境变量CLASSPATH指定的jar(即JAVA_HOME/lib/dt.jar和JAVA_HOME/lib/tools.jar)和第三方jar。AppClassLoader是ClassLoader的子类,由Java语言实现。

注意JDK中有两个lib目录,一个是JAVA_HOME/lib,另一个是JAVA_HOME/jre/lib。

在java中,还存在两个概念,分别是系统类加载器和线程上下文类加载器,其实都是指是AppClassLoader加载器。

 

1.2 类加载器双亲委派模型

ClassLoader使用的是双亲委托来搜索类。每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系)。

AppClassLoader的父加载器是ExtensionClassLoader,而Extension ClassLoader的父加载器是BootstrapClassLoader,而Bootstrap ClassLoader是虚拟机内置的类加载器,本身没有父加载器。

image

(图片来自于http://blog.csdn.net/u011080472/article/details/51332866

当一个ClassLoader对象需要加载某个类时,在它试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,父类加载器继续向上委托,直到BootstrapClassLoader类加载器为止。即,首先由最顶层的类加载器BootstrapClassLoader在指定目录试图加载目标类,如果没加载到,则把任务回退给ExtensionClassLoader,让它在指定目录进行加载,如果它也没加载到,则继续回退给AppClassLoader 进行加载,以此类推。如果所有的加载器都没有找到该类,则抛出ClassNotFoundException异常。否则将这个找到的“*.class”文件进行解析,最后返回表示该类的Class对象。

java代码中我们只能使用ExtensionClassLoader和AppClassLoader的实例,这两种类加载器分别有且只有一个实例。我们无法通过任何方法创建这两个类的额外的实例,可以理解为设计模式中的单例模式。

 

1.3 为什么要使用双亲委托这种模型?

1)这样可以避免重复加载,当父亲已经加载了该类的时候,子类就没有必要,也不应该再加载一次。

2)核心类通过Java自带的加载器加载,可以确保这些类的字节码没有被篡改,保证代码的安全性。

JVM在判定两个Class对象是否相同时,不仅要满足两个类名相同,而且要满足由同一个类加载器加载。只有两者同时满足的情况下,JVM才认为这两个Class对象是相同的。

 

1.4 自定义类加载器

除了Java默认提供的三个类加载器之外,用户还可以根据需要定义自已的类加载器,自定义的类加载器都必须继承自java.lang.ClassLoader类。

 

既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?

1)因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的class文件或jar时就需要定义自己的ClassLoader。

2)对于那些已经加密过的Class文件,自定义ClassLoader可以在解析Class文件前,进行解密操作。这样相互配合的方式保证了代码的安全性。

 

1.5 自定义类加载器的步骤

主要分为两步

1)继承java.lang.ClassLoader

2)重写父类的findClass方法

下面是API文档中给出的自定义加载器的实现模型

     class NetworkClassLoader extends ClassLoader {
         String host;
         int port;

         public Class findClass(String name) {
             byte[] b = loadClassData(name);
             return defineClass(name, b, 0, b.length);
         }

         private byte[] loadClassData(String name) {
             // load the class data from the connection
              . . .
         }
     }

 

下面的代码是一个类加载器的具体实现。MyClassLoader类加载器主要加载任意指定目录下的“*.class”文件,而这个指定的目录又不在环境变量ClassPath所表示的目录中。

 

package demo;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class MyClassLoader extends ClassLoader {
	private String path;

	@Override
	public Class<?> findClass(String name){
		byte[] data = null;
		try {
			data = loadClassData(path);
		} catch (IOException e) {
			e.printStackTrace();
		}
		return defineClass(name, data, 0, data.length);
	}
	
	private byte[] loadClassData(String path) throws IOException{
		File f = new File(path);
		FileInputStream fis = new FileInputStream(f);
		byte[] data = new byte[(int) f.length()];
		fis.read(data);
		fis.close();
		return data;
	}
	
	/*
	 * 定义了带两个参数的loadClass方法,为了多传递一个path参数
	 * 内部一定要调用父类的loadClass方法,因为该方法内实现了双亲委派模型
	*/
	public Class<?> loadClass(String path, String name) throws ClassNotFoundException{
		this.path = path;
		return super.loadClass(name);
	}
	
	public static void main(String[] args) throws ClassNotFoundException{
		MyClassLoader mcl = new MyClassLoader();
		/*打印当前类加载器的父加载器*/
		System.out.println(mcl.getParent());
		System.out.println("==========");
		
		Class<?> cls1 = mcl.loadClass("D:/用户目录/我的文档/Eclipse/Person.class"
				,"javalearning.Person");
		
		System.out.println(cls1.getClassLoader());
		System.out.println("==========");
		
		Class<?> cls2 = mcl.loadClass(null, "java.lang.Thread");
		System.out.println(cls2.getClassLoader());
		System.out.println("==========");
		
	}
}

 

通过代码实现可以看出,自定义类加载器的核心精髓是调用ClassLoader类中的defineClass方法。

 

下面是运行结果

sun.misc.Launcher$AppClassLoader@4e0e2f2a
==========
demo.MyClassLoader@2a139a55
==========
null
==========

 

从运行结果看出,MyClassLoader的父加载器是AppClassLoader(这是在ClassLoader的构造函数中缺省的实现方式)。Person.Class由MyClassLoader加载(父类加载器都没有加载成功),而当MyClassLoader加载String.class时,委托到BootstrapClassLoader加载,发现BootstrapClassLoader已加载完毕,结果null表示String类的加载器是BootstrapClassLoader。

 

Person类

package javalearning;
public class Person{
	public int age;
	public String name;
	
	public Person(){
		name = "zx";
		age = 18;
	}
	
	@Override
	public String toString(){
		return name +" "+ age;
	}
}

 

2. 谈谈java.lang.Class和java.lang.Object之间的悖论

通过java的语法学习,我们知道以下三点

1)java.lang.Class类继承java.lang.Object类

2)按照语法规则,创建一个java.lang.Class对象必须先创建它的父类(java.lang.Object)的一个对象(准确的说是开辟一片内存区域作为Class对象,并它其中的一部分区域作为Object对象)

3)按照语法规则,创建一个类的对象,必须先存在表示该类的java.lang.Class对象

但是这三点又是矛盾的。这两个对象的创建没有办法顺序实现。所以不是先创建好一个,再创建另一个,而是通过自举实现的。也就是说是通过自举程序将两个对象创建好,然后才进入java的运行环境。而自举程序本身不是由Java语言实现的,而是由C和C++实现的。

所有的java.lang.Class对象的创建不是通过构造函数创建的,而是通过加载器生成的。每个类都有对应的用于反射该类的Class对象,每个类有且只对应一个Class对象。

每一个类都从Object类中继承了一个getClass的实例方法,返回表示该类的Class对象。

 

在java的堆区中,有一个特殊的Class对象,即Class.class。Class.class对象有两层含义。

第一,可以把它看成一个普通的对象一个属于Class类的实例

第二,它又表示是Class类本身用于反射的对象所以该对象的getClass方法返回它本身)或者说表示Class类本身的Class对象。

我们不能通过Class.class的newInstance方法产生Class类的实例,如果这么做,会抛出异常。另一个方面,假设能够产生这样的对象,我们怎么知道这个对象应该对应哪一个类呢?

 

3. 谈谈java.lang.Class和类加载器之间的悖论

类加载器也是一个类,也有对应的Class对象,但是Class对象又必须通过加载器的实例产生,显然这两点又是矛盾的。

三个默认的类加载器中ExtensionClassLoader和AppClassLoader是由java代码实现的,而BootstrapClassLoader是由C/C++实现的。也就是说BootstrapClassLoader没有,不需要有,也不可能有对应Class对象。ExtensionClassLoader类的实例和它对应ExtensionClassLoader.class对象都是由BootstrapClassLoader一并加载创建完成。创建完成后,再由ExtensionClassLoader对象加载AppClassLoader.class。

 

4 java.lang.Class对象和对象的内存布局

4.1 Class对象中到底存了什么?

从已有资料来看,Class对象在不同的虚拟机在实现上存储的内容都不一致,但是理论上来讲, Class对象内部一定存储了方法区中该类的所有方法签名,属性签名,和每个方法对应的字节码的地址。

 

4.2 实例和实例方法之间的关系?

obj.setName(“zhang san”)

在实际执行过程中等价于

setName(obj, “zhang san”)

也就是对象时作为参数传递到实例方法里面的,对象本身不含指向该类方法的指针(Class对象并不含有Class类的方法的指针,但含有表示该类的所有方法的指针,可能有点绕,自己要理解一下)。方法的具体实现都位于方法区中相应的代码段中。当虚拟机调用该方法时,只要将虚拟机执行引擎的PC(程序计数器)指向该方法的地址,然后将实例存入该方法的栈帧中即可。通过实例直接调用方法时,实际上没有,也没有必要通过Class对象。

下面的示例表示了,锁住Person.Class对象不能阻止其它线程的代码创建Person类的实例,并调用实例方法。

package javalearning;

public class ClassLockTest {
	
	public static class T1 extends Thread{
		private Class<?> cls;
		private boolean done;
		public T1(Class<?> cls){
			this.cls = cls;
		}
		
		@Override
		public  void run() {
			synchronized(cls){
				while(!done){
					
				}
			}
		}
		
		public void done(){
			done = true;
		}
	}
	
	public static void main(String[] args) throws InterruptedException{
		/*我们先让线程t1锁住Person.class对象,然后在主线程中创建该对象的实例,并调用toString方法*/
		Class<?> cls = Person.class;
		T1 t1 = new T1(cls);
		t1.start();
		
		while(!t1.isAlive()){
			System.out.println("t1 is not alive");
			Thread.sleep(500);
		}
		
		Person p = new Person();
		System.out.println(p);
		t1.done();
		System.out.println("over");
	}
}

 

运行结果

zx 18
over

 

4.3 Class对象有哪些功能?

1)反射(关于反射的使用会在后续博客中讲解)

2)多态的实现

我们通过以下代码来讲解Class对象在多态中的应用

package demo;

public class ClassObjectDemo1 {
	
	/*定义两个具有继承关系的类,两个类内部有同一个方法的不同实现*/
	public static class Person{
		public void speak(){
			System.out.println("i am a person");
		}
	}
	
	public static class Coder extends Person{
		public void speak(){
			System.out.println("i am a coder");
		}
	}
	
	/*定义了一个静态方法,静态方法会调用对应类型的speak方法*/
	public static void speakByType(Person p){
		p.speak();
	}
	
	public static void main(String[] args) {
		Person p0 = new Coder();
		speakByType(coder);
		
		Person p1 = new Person();
		speakByType(person);
	}
}

 

运行结果

i am a coder
i am a person

 

我们定义的静态方法speakByType,显然编译器在编译这个方法的时候不能确定到底调用哪一个speak方法,需要依据对象的具体类型才能确定符号引用。

现在我们通过字节码工具重点查看一下speakByType的字节码

    public static void speakByType(demo.ClassObjectDemo1$Person p) {
        /* L20 */
        0 aload_0;                /* p */
        1 invokevirtual 16;       /* void speak() */
        /* L21 */
        4 return;
    }

 

我们发现里面出现了一条字节码调用语句 invokevirtual方法。而invokevirtual指令在执行时,首先会找到当前对象的类的Class对象,然后通过该Class对象查找sepak方法,如果通过该Class对象查找到了签名一致的的sepak方法就会调用它。每个Class对象都会持有表示父类的Class对象的引用(通过Class的getSuperClass方法获取),Object.class除外,自己想想为啥?当在子类的Class对象中没有找到签名一致的speak方法时,就从其父类的Class对象中继续查找签名一致的speak方法。显然如果还没有找到,则会沿着有继承关系的Class的路径继续向下查找,如果直到Object.class对象中还未找到就会抛出异常。

 

3)instace of和向上转型

当我们判断某个对象是否属于某个类时,比如 a instance of A,显然只要判断

a.getClass() == A.class && a.getClass().classLoader() == A.classLoader()即可,如果不满足就沿着getSuperClass的路径继续向下找,如果直到Object.class还不满足条件就返回false。同理在运行时,我们还能依据Class对象判断向上转型是否正确。

 

4.4 Class.class对象存在的意义是什么?

我们通常不会通过Class.class对象来间接访问forName方法和其它相应方法,而是直接使用该类的方法。所以一种可能的情况就是利用Class对象进行类型判断,即判断一个对象是不是Class对象还是普通对象(判断obj.getClass() == Class.class是否成立)。另一种可能就是保持概念的完整性,每一个类都有一个Class对象与之对应。

 

4.5 假设B类继承了A类,那么B类的实例在内存中应该是什么样子的?

image

 

java语言的设计者考虑到对象向上转型等问题,每一个类的数据成员显然要按照继承关系的先后顺序排列,同时考虑执行效率,还存在数据对齐等问题。

 

4.6 Java在运行时的内存布局

用一个例子来看看javaVM运行时,类、对象、Class对象、ClassLoader的关系

package demo;

import java.lang.reflect.InvocationTargetException;


public class ClassObjectDemo0 {
	
	/*定义两个具有继承关系的类,两个类内部都为空*/
	public static class Person{
		
	}
	
	public static class Coder extends Person{
		
	}
	
	/*哈哈,main函数抛出的异常似乎有点多*/
	public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InterruptedException{
		
		/*通过对象找到该类的Class对象*/
		Coder coder = new Coder();
		System.out.println(coder.getClass());
		
		System.out.println("========================");
		
		/*通过Class对象可以看出继承关系*/
		System.out.println(Coder.class);
		System.out.println(Coder.class.getSuperclass());
		System.out.println(Coder.class.getSuperclass().getSuperclass());
		
		System.out.println("========================");
		
		/*查看每个Class对象的类加载器*/
		System.out.println(Coder.class.getClassLoader());
		System.out.println(Person.class.getClassLoader());
		System.out.println(Object.class.getClassLoader());
		
		System.out.println("========================");
		
		/*每个Class对象都是Class类的实例*/
		System.out.println(Coder.class.getClass());
		System.out.println(Person.class.getClass());
		System.out.println(Object.class.getClass());
		
		System.out.println("========================");
		
		/*Class.class的getClass方法会返回它本身*/
		System.out.println(Class.class.getClass());

		/*产看Class对象的的父类*/
		System.out.println(Class.class.getSuperclass());
	}
}

 

运行结果

/*通过对象找到该类的Class对象*/
class demo.ClassObjectDemo$Coder
========================
/*通过Class对象可以看出继承关系*/
class demo.ClassObjectDemo$Coder
class demo.ClassObjectDemo$Person
class java.lang.Object
========================
/*查看每个Class对象的类加载器*/
sun.misc.Launcher$AppClassLoader@4e0e2f2a
sun.misc.Launcher$AppClassLoader@4e0e2f2a
null /*说明Object类的加载器是BootstrapClassLoader*/
========================
/*每个Class对象都是Class类的实例*/
class java.lang.Class
class java.lang.Class
class java.lang.Class
========================
/*Class.class的getClass方法会返回它本身*/
class java.lang.Class
========================
/*查看Class.class对象的父类*/
class java.lang.Object

 

通过上面的结果,我们可以推测这些对象,方法,加载器等在堆和栈中布局的一种可能。

Class类的对象在内存的分布

在堆区中,我们一般的对象我们用浅黄色表示,Class对象用浅蓝色表示(原谅我的辨色能力,什么颜色请自行体会)。

在堆区中,绿色箭头表示getClass方法返回的对象。显然,非Class对象的getClass方法返回这个类对应的Class对象。Class类继承Object类,Object类定义了getClass方法,所有的Class对象也有getClass方法。如果把Class对象看成普通对象,那么它的getClass方法就会返回表示整个Class类的Class对象,即Class.class。而Class.class对象的getClass方法返回它本身。

堆区中,每个Class对象的黑色虚线都指向了方法区中表示该类的全部信息,所以我们能够通过Class对象进行反射操作。

堆区中的黑色实线表示Class对象的getSupperClass方法返回的对象,由于所有的类都继承于Object类,所以有的Class对象最终都指向于Object.clss,而Object.class没有父类。

我们想要实现反射,一般使用Class.forName方法进行类加载,forName方法本质上就是调用ClassLoadr实例的loadClass方法。推测,为了方便每个加载器查找某个类是否已加载器过,每个类加载器可能都有一张表,记录每个已加载的类和对应Class对象的地址。

 

4.7 数组与Class对象

不同数据类型,不同维度的数组都对应不同的Class对象

所有具有相同元素类型和维数的数组都共享该Class对象。

package javalearning;

public class ArrayTest {
	public static void main(String[] args){
		int[] a1 = new int[10];
		int[][] a2 = new int[5][3];
		Class<?> c1 = a1.getClass();
		Class<?> c2 = a2.getClass();
		System.out.println(c1 == c2);/*结果false*/
		System.out.println(c2.getComponentType() == c1);/*结果true*/
	}
}

 

由于数组没有构造函数,我们也就没有办法通过它的Class对象直接创建数组对象。为了实现这个功能,JDK中就提供了Array类(java.lang.reflect.Array)来弥补这个缺陷。有关Array类的功能和使用,请参考Java的API文档,注意区分java.util.Arrays类。

 

5 参考内容

[1]. classpath、path、JAVA_HOME的作用及JAVA环境变量配置

[2]. 深入分析Java ClassLoader原理

[3]. Java魔法堂:类加载器入了个门

posted @ 2017-10-09 13:02  nullzx  阅读(4203)  评论(1编辑  收藏  举报