前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >虚拟机类加载机制

虚拟机类加载机制

作者头像
sanmutongzi
发布2020-03-04 15:34:45
2400
发布2020-03-04 15:34:45
举报
文章被收录于专栏:stream processstream process

转载请注明原帖地址:http://www.cnblogs.com/dongxiao-yang/p/5369195.html

java代码编译后产生的文件是各种Class字节码文件,这些文件都需要被jvm虚拟机加载到内存中才可以运行。从类被加载到jvm内存开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)7个阶段。如下图所示:

一 类加载的时机

类加载过程的第一个阶段:加载(Loading),什么时候开始,jvm规范并没有强制约束。但是对于阶段初始化(Initialization),虚拟机严格规定了有且只有5种情况必须对类进行初始化,显然在初始化前加载(Loading)阶段一定会先执行,所以这几种情况一定会调用类加载。

1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令最常见的Java代码场景是:使用new关键字实例化对象时、读取或者设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)时、以及调用一个类的静态方法的时候。

2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要触发父类的初始化。

4.当虚拟机启动时,用户需要指定一个执行的主类(包含main()方法的类),虚拟机会先初始化这个类。

5.当使用JDK1.7的动态语言支持时,如果一个java.lang.invokeMethodHandle实例的最后解析结果是REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。

二 类加载器

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的模块被称为"类加载器"。

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话说的更通俗一些:比较两个类是否“相等”,只有在这两个类由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个class文件,同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不同。

测试代码

代码语言:javascript
复制
 1 package com.youku.data.mr.too.jvm;
 2 
 3 import java.io.IOException;
 4 import java.io.InputStream;
 5 
 6 public class ClassLoaderTestLL {
 7     
 8     public static void main(String[] args)  {
 9         
10         ClassLoader mycl = new ClassLoader() {
11             @Override
12             public Class<?> loadClass(String name)
13                     throws ClassNotFoundException {
14                 // TODO Auto-generated method stub
15                 //return super.loadClass(name);
16                 String filename = name.substring(name.lastIndexOf(".")+1)+".class";
17                 //System.out.println(filename);
18                 InputStream is =getClass().getResourceAsStream(filename);
19                 if (is == null) {
20                     System.out.println("parent :"+filename);
21                     return super.loadClass(name);
22                 }
23                 byte[] b = null;
24                 System.out.println("son :"+filename);
25                 try {
26                     b = new byte[is.available()];
27                     is.read(b);
28                 } catch (IOException e) {
29                     // TODO Auto-generated catch block
30                     e.printStackTrace();
31                 }
32         
33                 return defineClass(name, b, 0, b.length);
34             }
35         };
36         
37         Object obi;
38         try {
39             obi = mycl.loadClass("com.youku.data.mr.too.jvm.ClassLoaderTestLL").newInstance();
40             System.out.println(obi.getClass());
41             
42             System.out.println(obi instanceof com.youku.data.mr.too.jvm.ClassLoaderTestLL);
43             
44         } catch (InstantiationException e) {
45             // TODO Auto-generated catch block
46             e.printStackTrace();
47         } catch (IllegalAccessException e) {
48             // TODO Auto-generated catch block
49             e.printStackTrace();
50         } catch (ClassNotFoundException e) {
51             // TODO Auto-generated catch block
52             e.printStackTrace();
53         }
54         
55 
56     }
57 
58 }

这段代码主要是参考了深入jvm虚拟机里面的代码,不同的是在一些地方增添了输出日志,结果如下。

son :ClassLoaderTestLL.class

parent :Object.class

parent :Throwable.class

parent :InstantiationException.class

parent :IllegalAccessException.class

parent :ClassNotFoundException.class

parent :ClassLoader.class

son :ClassLoaderTestLL$1.class

class com.youku.data.mr.too.jvm.ClassLoaderTestLL

false

不出意料的

System.out.println(obi.getClass());

System.out.println(obi instanceof com.youku.data.mr.too.jvm.ClassLoaderTestLL);

这两句输出的结果是类不相等。

加了

System.out.println("parent :"+filename);

System.out.println("son :"+filename);

这两段输出很清晰的显示了哪些类调用的新loadclass里的inputstream过程。

三 双亲委派模型

从java虚拟机的角度讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用c++实现,是虚拟机自身的一部分;另一种就是所有其它的类加载器,这些加载器都是有java语言实现,独立于虚拟机外部,并且全部都继承自抽象类java.lang.ClassLoader。也可以细化为3种类加载器。

  • 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所制定的路径中,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机中。启动类加载器无法被java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null替代即可。
  • 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):这个类加载器有sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader方法的返回值,所以一般也称为系统类加载器。它负责加载用户类路径(classpath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序没有自定义过加载器,一般情况下这个就是默认的加载器。
代码语言:javascript
复制

上图加载器之间的这种层次关系称为双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不会以继承的关系来实现,而都使用组合关系老复用父加载器的代码。

这个可以从ClassLoader这个类的源代码里看出来

代码语言:javascript
复制
 1  */
 2 public abstract class ClassLoader {
 3 
 4     private static native void registerNatives();
 5     static {
 6         registerNatives();
 7     }
 8 
 9     // The parent class loader for delegation
10     // Note: VM hardcoded the offset of this field, thus all new fields
11     // must be added *after* it.
12     private final ClassLoader parent;

其中的parent是以一个类内部对象的形式出现的

双亲委派模型的工作过程是:如果一个类加载器收到类类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器自己无法完成这个加载请求(它的搜索范围中没有找到所需要的类)时,子加载器才会尝试自己去加载。

为了维持双亲委派模型的原则,目前ClassLoader的loadclass方法代码为

代码语言:javascript
复制
 1   protected Class<?> loadClass(String name, boolean resolve)
 2         throws ClassNotFoundException
 3     {
 4         synchronized (getClassLoadingLock(name)) {
 5             // First, check if the class has already been loaded
 6             Class c = findLoadedClass(name);
 7             if (c == null) {
 8                 long t0 = System.nanoTime();
 9                 try {
10                     if (parent != null) {
11                         c = parent.loadClass(name, false);
12                     } else {
13                         c = findBootstrapClassOrNull(name);
14                     }
15                 } catch (ClassNotFoundException e) {
16                     // ClassNotFoundException thrown if class not found
17                     // from the non-null parent class loader
18                 }
19 
20                 if (c == null) {
21                     // If still not found, then invoke findClass in order
22                     // to find the class.
23                     long t1 = System.nanoTime();
24                     c = findClass(name);
25 
26                     // this is the defining class loader; record the stats
27                     sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
28                     sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
29                     sun.misc.PerfCounter.getFindClasses().increment();
30                 }
31             }
32             if (resolve) {
33                 resolveClass(c);
34             }
35             return c;
36         }
37     }

所以jdk1.2以后已经不提倡用户自定义classloader的时候自己重写loadClass()方法,应当把自己的类加载器逻辑写到findClass()方法中去,在loadClass()如果父类加载失败,则会调用自己的findClass()来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。

参考资料:深入理解java虚拟机 第二版

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016-04-08 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档