您的位置:首页 > 编程语言 > Java开发

java虚拟机类加载机制与反射全解

2017-06-17 17:38 246 查看

java虚拟机类加载机制与反射全解

引子:

开门见山,先来个经典面试题:(如果你已经懂了,那么你可以离开了,如果你一脸懵逼,那么请好好看本文,理解透彻很有好处!)

class SingleTon {
private static SingleTon singleTon = new SingleTon();
public static int count1;
public static int count2 = 0;

private SingleTon() {
count1++;
count2++;
}

public static SingleTon getInstance() {
return singleTon;
}
}

public class 非常有意思的题 {
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance();
System.out.println("count1=" + singleTon.count1);
System.out.println("count2=" + singleTon.count2);
}
}


[align=left]你们自己做做看!到末尾我再给答案并详细解释。
[/align]

类加载的过程:

类的加载:

在加载阶段,虚拟机需要完成以下三件事:

1):通过一个类的全限定名来获取定义此类的二进制字节流。

2):将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3):在内存中生存一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

类的连接:

验证:

验证是来连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证,源数据验证,字节码验证,符合引用验证。

1:文件格式验证

第一阶段要验证字节流是否符合Class文件格式规范,并且能被当前版本的虚拟机处理,这一阶段可能包括下面这些的验证点:

是否以魔树0xCAFEBABE开头。

主次版本号是否在当前虚拟机处理范围内。

常量池的常量中是否有不被支持的常量类型(检查常量tag标识)。

指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。

CONSTANT_UTF8_INFO型的常量中是否有不符合utf8编码的数据。

class文件中各个部分及文件本身是否有被删除的或附加其他信息。

2:元数据验证

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,这个阶段可能包括的验证点如下:

这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)

这个类的父类是否继承了不允许被继承的类(被final修饰的类)

如果这个类不是抽象类,是否实现了其父类或者接口之中要求实现的方法

类中的字段,方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同)

3:字节码验证

第三阶段主要目的是通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。这个阶段将对类的方法提进行检验分析,保证不会危害虚拟机,例如:

保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作

保证跳转指令不会跳转到方法体以外的字节码指令上

保证方法体中的类型转换是有效的

4:符号引用验证

最后一个阶段的校验发生在虚拟机将符号引用转化成直接引用的时候,这个转化动作将在连接的第三阶段---解析阶段发生。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验以下内容:

符号引用中通过字符串描述的全限定名是否能找到对应的类

在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段

符号引用的类,字段,方法是否可以被当前类访问

准备:

该阶段是正式为类变量分配内存并设置变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配。这里进行内存分配仅包括类变量,实例变量将在对象实例化时在java堆中分配内存。

例子:public static int v=1;那么v在准备阶段后的值是0而不是1,因为这时候尚未执行任何java代码,而把v赋值为1是编译后,存放于类构造器<clinit>()方法之中,所以v被赋值为1将在初始化阶段执行。特殊情况:如果类字段的字段属性表中存在ConstantValue属性,那么准备阶段变量就会被初始化ConstantValue属性所指定的值。例如:public
static final int v=1;准备阶段v赋值为1.

解析:

将虚拟机常量池内的符号引用替换成直接引用的过程。

类的初始化:

类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与以外,其余动作全是有虚拟机主导控制。到了初始化阶段,才真正开始执行类中定义的java程序阶段(或者说是字节码),在准备阶段,变量已经赋过一次系统要求的值,而在初始化阶段,则根据程序员去初始化变量的值。初始化阶段是执行类构造器<clinit>()方法的过程。

1:初始化的时候会初始化类变量和执行静态块,静态块语句只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态块中可以赋值,不能访问。例子:

static {

i=1;//赋值可以

system.out,print(i);//访问编译过不了

}

static int i=0;

2:<clinit>()方法与类的构造函数不同,他不需要现实的调用父类构造器,虚拟机会保证父类的<clinit>()方法在子类的<clinit>()之前,因此在虚拟机第一个被执行<clinit>()方法的类肯定是Object。父类定义的静态语句块要优先于子类的变量赋值,例子:

static class Parent{
public static int a=1;
static{
a=2;
}
}

static class Sub extends Parent{
public static int b=a;
}

system.out.print(Sub.b);//输出2
3:接口中不能使用静态语句块,但仍有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法,但接口与类不同的是,执行接口的<clinit>()方法不需要执行父接口的<clinit>()方法,只有当父接口中定义的变量使用时,父接口才会初始化,另外,接口的实现类在初始化的时候也一样不会执行接口的<clinit>()方法。

4:虚拟机会保证一个类的<clinit>方法在多线程的环境中正确的加锁同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的<clinit>()方法,其它线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕,其它线程被唤醒后并不会执行<clinit>(),因为同一个类加载器下,一个类只会被初始化一次。例子:

public class DeadLoopClass {
static{//初始化调用
if(true){//如果不加上if(true) 拒绝编译
System.out.println(Thread.currentThread()+"init DeadLoopClass");
while(true){
}
}
}

static class Test{
public static void main(String[] args) {
Runnable r=new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread()+"start");
new DeadLoopClass();//会初始化
System.out.println(Thread.currentThread()+"run over");
}
};
Thread t1=new Thread(r);
t1.start();
Thread t2=new Thread(r);
t2.start();
}
}
}
输出结果:
Thread[Thread-1,5,main]start
Thread[Thread-0,5,main]start
Thread[Thread-1,5,main]init DeadLoopClass


类初始化的时机:

虚拟机规范严格规定了有且只有5种情况必须立即对类进行初始化:

1):new实例化对象和Class.forName(),读取或设置一个类的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外),调用静态方法。

对于final,如果该类变量的值在编译时就可以确定下来,那么这个类变量相当于宏
10c8b
变量,java编译器会在编译时直接把这个类变量出现的地方替换成它的值,因此即使程序使用该静态类变量,不会导致该类的初始化。例子:

public class TestFinal {
static {
System.out.println("初始化");
}
static final String compileConstant="hello world";

static class compileConstantTest{
public static void main(String[] args) {
System.out.println(TestFinal.compileConstant);
}
}
}
输出结果:hello world
可以看出,并没有初始化。


对于final,如果该类变量的值在编译时就可以不能确定下来,必须等到运行是才能确定。则会导致初始化。例子:

public class TestFinal {
static {
System.out.println("初始化");
}
static final String compileConstant=System.currentTimeMillis()+"";

static class compileConstantTest{
public static void main(String[] args) {
System.out.println(TestFinal.compileConstant);
}
}
}
输出结果:初始化
当前时间
可以看出已经初始化了


2):使用reflect反射调用时,如果类没有初始化过,则需要初始化。

3):当初始化一个类时,如果父类还没有初始化,则需要先初始化父类。

4):当虚拟机启动时,用户需要指定一个含有main方法的类,虚拟机会先初始化这个类。

5):当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandler实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先初始化。

这5种场景的行为称为对一个类的主动引用,除此之外,所有引用类的方式都不会触发初始化,称为被动引用。

被动引用例子1:对于静态字段,只有直接定义的类才会被初始化,因此通过子类来引用父类的静态字段,只会触发父类的初始化。

public class NotInit {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}

class SuperClass{
static{
System.out.println("superClass init");
}
public static int value=110;
}
class SubClass extends SuperClass{
static{
System.out.println("subClass init");
}
}
 输出结果:
superClass init
110
可以看出只有父类被初始化了
被动引用例子二:通过数组定义来引用类,不会初始化该类,但是会初始化一个名为【Lorg.fenixsoft.classloading.SuperClass】继承Object的子类,创建动作由字节码指令newarray触发。

把上个例子主函数的输出语句换成SuperClass[] sc=new SuperClass[5];什么都不输出。

类加载器:

          类加载器简介:

              类加载器负责加载所有的类,系统为所有被载入内存的类生成一个java.lang.Class实例,一旦这个类被载入jvm,同一个类就不会再次被载入,但是怎样才算是同一个类呢?在jvm中,一个类采用其全限定类名和其类加载器作为唯一标识。例如:在lry包下有一个叫做Renyou的类,被类加载器的实例cl加载,则该Renyou类对应的Class对象在jvm表示为(Renyou,lry,cl)。这意味着两个类加载器加载的同名类是不同的。为了进一步验证这个结论:

public class ClassLoaderInstance {
public static void main(String[] args) throws Exception {
ClassLoader loader=new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName=name.substring(name.lastIndexOf('.')+1)+".class";
InputStream is=getClass().getResourceAsStream(fileName);
if(is==null){
return super.loadClass(name);
}
byte[]b=new byte[is.available()];
is.read(b);
return defineClass(name, b, 0,b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};

Object obj=loader.loadClass("com.renyou.classLoader.ClassLoaderInstance").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof com.renyou.classLoader.ClassLoaderInstance);
}
}
输出结果:
class com.renyou.classLoader.ClassLoaderInstance
false

结果分析:可以看出obj是实例化出来的对象,但是obj与类做所属类型检查的时候却是false,不难看出此时虚拟机存在两个ClassLoaderInstance类,一个是由系统应用程序类加载器加载的另一个是我们自定义的类加载器加载的,虽然来自同一个class文件,但依然是两个独立的类,所以做对象所属类型检查输出false。这两个ClassLoaderInstance类对应的Class对象可以具体表示为(com.renyou.classLoader,ClassLoaderInstance,系统应用程序类加载器实例)(com.renyou.classLoader,ClassLoaderInstance,自定义类加载器实例)这样就很清晰明了了。

ps:在这先不介绍具体的类加载器,我们留到双亲委派模型的时候在详细介绍。

[align=left]
[/align]

          类加载器机制:

jvm类加载机制主要有如下三种:
全盘负责:当一个类负责载入某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器载入,除非显示使用另一个加载器载入。
父类委托:先让父加载器试图加载该Class,只有在父加载器无法加载该类时才尝试从自己的类路径加载该类。
缓存机制:缓存机制保证所有被加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区里寻找该Class,只有当缓存区不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转为Class对象,存入缓存区中。这就是为什么修改了Class后,必须重启jvm,程序所做的修改才会生效的原因。

类加载器加载Class大致要经过这8个步骤:
1:检测缓存区是否有此Class,有则进8,无则进二。
2:如无父加载器(即要么parent是根加载器,要么自己就是根加载器),则进4,否则进3。
3:请求父加载器去加载目标类,成功载入进8,否则进5。
4:请求根加载器去加载目标类,成功载入进8,否则进7。
5:从当前类加载器尝试寻找Class文件(从与此ClassLoader相关的类路径中寻找),如果找到进6,否则进7。
6:从文件中载入Class,成功载入进8。
7;抛ClassNotFoundException。
8:返回对应的java.lang.Class对象。

如果这部分看不懂,我相信你看完我的双亲委派模型肯定懂了!!!

          双亲委派模型:

        从java虚拟机角度来看,只存在两种不同的类加载器,一种是启动类加载器(Bootstrap(有一个前端框架也是这个名字) ClassLoader,还有很多别名,例如:根/引导/原始类加载器),这个类加载器由c++实现(只限于HotSpot虚拟机),是虚拟机自身的一部分;还有一种是由java实现的类加载器,独立于虚拟机外部,并且都继承于ClassLoader。类加载分的更细致一点分为这三种:
Bootstrap ClassLoader:它负责加载java核心类,具体点就是它负责将存放在<JAVA_HOME>\lib目录中的或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib下也不会被识别加载)类库加载到虚拟机内存。根类加载器无法被java程序直接引用,我们在编写自定义类加载器的时候,如果需要把加载请求委派给根类加载器,那直接使用null代替即可。看下面的例子:

例子1:验证如果你想把加载请求委派给根类加载器,那直接使用null代替即可。

/**
* Returns the class loader for the class.  Some implementations may use
* null to represent the bootstrap class loader. This method will return
* null in such implementations if this class was loaded by the bootstrap
* class loader.
*
* <p> If a security manager is present, and the caller's class loader is
* not null and the caller's class loader is not the same as or an ancestor of
* the class loader for the class whose class loader is requested, then
* this method calls the security manager's {@code checkPermission}
* method with a {@code RuntimePermission("getClassLoader")}
* permission to ensure it's ok to access the class loader for the class.
*
* <p>If this object
* represents a primitive type or void, null is returned.
*
* @return  the class loader that loaded the class or interface
*          represented by this object.
* @throws SecurityException
*    if a security manager exists and its
*    {@code checkPermission} method denies
*    access to the class loader for the class.
* @see java.lang.ClassLoader
* @see SecurityManager#checkPermission
* @see java.lang.RuntimePermission
*/
@CallerSensitive
public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if (cl == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
}
return cl;
}
看英文注释和源码可知如果你想把加载请求委派给根类加载器,那直接使用null代替即可。

例子2:验证它负责加载java核心类

public class BootstrapTest {
public static void main(String[] args) {
URL[]urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url.toExternalForm());
}
}
}
输出:
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/classes


例子3:探究为什么想把加载请求委派给根类加载器,那直接使用null代替即可。
先看个小例子吧:

System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
输出:
sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742
null


推断出系统类加载器的父加载器是扩展类加载器,而当获取扩展类加载器的父加载器时得到了null,这时我有一个大胆地猜想,扩展类加载器强制的设置了父加载器为null,更加大胆地猜测就是如果父加载器为null,则会调用本地方法进行启动类加载尝试。看一下源码验证:

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);//null  启动启动类加载器
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
注释处可以看出猜测是正确的。

Extension ClassLoader:扩展类加载器,他负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext或者由java.ext.dirs系统属性指定的目录)中的jar包的类。开发者可以直接使用这个类加载器,通过这种方式,就可以为java扩展核心类以外的新功能,只要把自己开发的类打包成jar文件,然后放入%JAVA_HOME%/jre/lib/ext
路径下即可。
System ClassLoader:系统类加载器,他负责在jvm启动时加载来自java命令的-classpath选项,java.class,path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader.getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。
为什么如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器?可能很多人不明白

protected ClassLoader() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.parent = getSystemClassLoader();
initialized = true;
}
getSystemClassLoader源码:

private static synchronized void initSystemClassLoader() {
//...
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
scl = l.getClassLoader();
//...
}
我们简单测试一下:

System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());
结果:

sun.misc.Launcher$AppClassLoader@73d16e93
通过对比可以确定如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。

双亲委派:

我们的应用程序都是这3种类加载器互相配合使用进行加载的,如果有必要,还可以加入自己定义的类加载器,这些类加载器之间关系一般如图所示:



图示展示的类加载器的层次关系称为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父加载器。这里的类加载器之间的父子关系不会以继承的关系实现,而是以组合关系来复用父加载器的代码。
双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,他首先会委派给父加载器,因此所有的加载请求最终都应该传送到启动类加载器,只有当父加载器反馈自己无法完成这个加载请求,子加载器才会尝试自己去加载。
双亲委派模型的好处:java类随着他的类加载器一起具备了一种带有优先级的层次关系。例如Object类,他存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器,因此Object类在各种类加载器环境都是同一个类。相反,如果没有使用双亲委派,由各个类加载器自行加载的话,如果用户自己编写一个java.lang.Object的类,并放在程序的classpath里,那系统中会出现多个不同的Object类,java体系中的最基础的行为也就无法保证,应用程序也会一片混乱。

双亲委派的实现:请看我上面的Bootstrap ClassLoader里面的loadClass();

          破坏双亲委派模型:

              双亲委派模型并不是一个强制的约束模型,而是java设计者推荐给开发者的类加载器实现方式。到目前为止,双亲委派模型主要出现过三次较大规模的”被破坏“的情况,这里说的破坏并不带有贬义色彩,只要有足够的意义和理由,突破常规准则,何尝不是一种创新呢!
             双亲委派模型第一次被破坏:

              双亲委派模型第一次被破坏是在该模型出来之前-即JDK-1.2发布之前。由于双亲委派模型是在JDK-1.2之后才被引入的,而类加载器和抽象类ClassLoader早在jdk1.0就已经存在,面对用户已经实现的自定义类加载器代码,java设计者必须做出妥协。为了向前兼容,jdk1.2后ClassLoader添加了一个protected的方法findClass(),在此之前,用户继承ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的售后会调用private的方法loadClassInternal(),而这个方法唯一逻辑就是调用自己的loadClass()。而在jdk1.2之后一不提倡开发者去覆盖loadClass()方法,而是把自己的类加载逻辑放到findClass()中,在loadClass()(前面讲到了)方法的逻辑里如果父加载器加载失败,贼会调用自己的findClass()来完成加载,这样就保证了新写出来的类加载器是符合双亲委派模型了。
             双亲委派模型第二次被破坏:
            

              双亲委派模型第三次被破坏:

          创建自定义的类加载器:

反射查看类信息:

反射生成并操作对象:

反射生成jdk动态代理:

反射与泛型:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息