JVM模型

  1. 云栖社区>
  2. 博客列表>
  3. 正文

JVM模型

一心飞扬 2018-01-25 10:03:09 浏览20645 评论0

摘要: JVM类加载器 系统默认类加载器类型: 1、Bootstrap ClassLoader:此类加载器采用C++编写,一般开发中是看不到的; 2、Extension ClassLoader:用来进行扩展类的加载,一般对应的是jre/lib/ext目录下的类,是ClassLoader类的子类; 3、AppClassLoader:加载classpath指定的类,是最常使用的一种类加载器,是ClassLoader类的子类 ,是用户自定义的类加载器的默认父加载器。

JVM类加载器

系统默认类加载器类型:
1、Bootstrap ClassLoader:此类加载器采用C++编写,一般开发中是看不到的;
2、Extension ClassLoader:用来进行扩展类的加载,一般对应的是jre/lib/ext目录下的类,是ClassLoader类的子类;
3、AppClassLoader:加载classpath指定的类,是最常使用的一种类加载器,是ClassLoader类的子类 ,是用户自定义的类加载器的默认父加载器。

类加载流程

1、类加载机制:把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的实现过程。其中包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段。通过 -XX:+TraceClassLoading 可查看类加载的信息。
2、当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。
3、加载阶段:通过一个类的全限定名来获取此类的二进制字节流并加载到内存中;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;在java堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口;Class对象用来封装类在方法区内的数据结构(反射机制就是基于堆中Class句柄对类进行各种操作)。

加载class文件的方式:

 1、从本地系统中直接加载  
 2、通过网络下载.class文件  
 3、从zip、jar等归档文件中加载.class文件  
 4、从专有数据库中提取.class文件  
 5、将Java源文件动态编译为.class

4、验证阶段:验证是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;包括文件格式验证、元数据验证、字节码验证、符号引用验证;如果验证到输入的字节流不符合Class文件的存储格式,就抛出一个java.lang.VerifyError异常或其子类异常。JVM在编译阶段已经对源码文件进行相关验证,编译成合法的.class文件,这里还继续验证主要是防止恶意用户使用其它工具手工生成.class文件侵入虚拟机造成危害的情况。当然,如果从提高性能方面考虑,避免每次启动程序时反复验证,可以通过 -Xverify:none来关闭验证,可缩短虚拟机加载的时间,前提是你要能足够保证.class文件足够安全。

5、准备阶段:准备阶段是正式为类静态变量分配内存并设置类静态变量初始值(各数据类型的零值)的阶段,这些内存将在方法区中进行分配。类的成员变量只有在生成对象的时候才会分配内存。static final类型的,在准备阶段就会被赋上正确的值。

6、解析阶段:解析阶段是在虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

符号引用(Symbolic References):以一组符号来描述所引用的目标(类、接口、方法、字段等)。只要能无歧义地定位到目标即可,并且与JVM的实际内部布局无关,而引用的目标也不一定已经加载到内存中。符号引用的形式已经由JVM规范规定了。
直接引用(Direct References): 直接引用可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。如果有了直接引用则目标必定已经在内存中存在了。

解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info 四种常量类型:
1、类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。
2、字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束。
3、类方法解析:对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。
4、接口方法解析:与类方法解析步骤类似,知识接口不会有父类,因此,只递归向上搜索父接口就行了。

7、初始化阶段:初始化阶段是执行类构造器()方法的过程。为类的静态变量(static变量)和static{}语句赋予正确的初始值。子类的调用前保证父类的被调用。
类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源。

类的初始化步骤:

1、假如这个类还没有被加载和连接,那就先进行加载和连接;  
2、假如类存在直接的父类,并且这个父类还没有被初始化,那就先初始化直接父类;  
3、假如类中存在初始化语句,那就依次执行这些初始化语句;  

继承类的类初始化顺序关系案例代码:

类的初始化时机:以下5种情形是对类的主动使用,其它使用Java类的方式都被看着是被动使用,不会导致类的初始化。
主动引用:
JVM规范规定以下5种情况,则必须执行初始化(加载、链接自然会在之前进入执行状态)

  1. 遇到new, getstatic, putstatic或invokestatic这4条字节码指令时,若类没有进行过初始化,则需要先触发初始化。对应的Java代码为通过关键字new一个实例,读或写一个类变量,调用类方法。
  2. 使用java.lang.reflect包中的方法操作类时,若类没有进行过初始化,则需要先触发初始化。
  3. 当初始化一个类时,若其父类还没初始化则会先初始化父类。
  4. 当虚拟机启动时,虚拟机会初始化入口函数所在的类。
  5. JDK1.7增加动态语言的支持。如果一个java.lang.invoke.MethodHandle实例最后的解析结果是 REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,而这个句柄所在的类没有进行初始化,则需要先触 发初始化。

除了上述5种情况外,其他引用类的方式是不会触发初始化的,并称为被动引用。下列示例则为被动引用

1. 通过子类访问父类静态字段不会导致子类初始化,仅仅会导致父类初始化。
2. Java代码中创建数组对象,不会导致数组的组件类(如SuperClass[]的组件类为SuperClass)初始化。因为创建数组类的字节码指令是newarray。
3. 类A访问类B的编译时静态常量不会导致类B的初始化。因为在编译阶段会将类使用到的常量直接存储到自身常量池的引用中,因此实际上运行时类A访问的是自身的常量与类B无关系。

a.对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;
b.而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
c.如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。
d.假设上面的类变量value被定义为:
public static final int value = 3;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为 3;我们可以理解为 static final 常量在编译期就将其结果放入了调用它的类的常量池中。

注意:如果静态变量是编译时常量,对它的访问时不会导致类初始化的。如下:x是编译时常量(static final修饰,同时值在编译时就可以确定),访问B.x是不会导致B类进行初始化
变量y不是常量,对b的访问会导致B进行类初始化,而z虽然属于常量(static final修饰),但是编译时是无法确定值内容的,需要运行时才确定,不属于编译时常量,因此对z进行访问会导致B进行类初始化。
class B{

  public static final int x = 6/3;
  public static int y = 6/3;
  public static final int z = new Random().nextInt();
  static{
        System.out.println("B类进行了初始化");
  }

}
public class Demo3 {

  public static void main(String[] args) {
        System.out.println(B.x);
  }

}

获取Class对象使用“类.class”和Class.forName("类的全限名称")都可以获取到,但是他们还是存在区别的,由于类.class”不是主动使用情况,类会被加载到内存中,但是不会进行类的初始化步骤,但是Class.forName("类的全限名称")由于是主动使用,就会进行类的初始化工作。
代码案例:

Class.forName和ClassLoader.loadClass的比较:https://yq.aliyun.com/articles/48608?spm=5176.100240.searchblog.26.dFHDoW

连接阶段:类被加载后,就进入连接阶段,连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。

初始化
初始化阶段是执行类构造器()方法的过程。类构造器()方法是由编译器自动收藏类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步

当一个类被主动使用时,Java虚拟才会对其初始化,如下六种情况为主动使用:

 1. 当创建某个类的新实例时(如通过new或者反射,克隆,反序列化等)
 2. 当调用某个类的静态方法时
 3. 当使用某个类或接口的静态字段时
 4. 当调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时
 5. 当初始化某个子类时
 6. 当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类)

Java编译器会收集所有的类变量初始化语句和类型的静态初始化器,将这些放到一个特殊的方法中:clinit。
实际上,static块的执行发生在“初始化”的阶段。初始化阶段,jvm主要完成对静态变量的初始化,静态块执行等工作。静态变量的赋值操作在此时执行。

静态属性初始化顺序位置差异案例:

双亲委托机制

在父亲委托机制中,各个加载器按照父子关系形成了树形结构,除了根类加载器以外,其余的类加载器都有且只有一个父加载器。

当第一个类加载器要加载类的时候,它先不直接加载类,而是交(委托)给它的父类加载器去加载,而父类加载器又交(委托)给它的父类加载器去加载,就这样一直往上的走,当到了BootStrap这个类加载器时,它没有父加载器,然后它就到自己所管辖的范围去找,找到了就加载出来,没找到它就交给它的儿子加载器去加载,这时候它的儿子加载器才会去管辖的范围找,如果又没找到,那就又给儿子找,就这样一直往下,当最后回到第一个类加载器的时候,如果还是没找到的话就报异常。

搜索类是否加载是从下往上一层层搜索,最后搜索到Bootstrap Classloader,依然没有搜索到,则从上往下依次加载,直到当前加载器。

双亲委托机制好处:
1、它的好处是可以集中管理,有这样一个情况,我有一个类要加载,这时候MyClassLoader1找到了这个类并加载了此类,而MyClassLoader2也找到了这个类也加载了此类,这时候内存中就存在了2个一样的字节码,这样就很浪费资源。使用双亲委托机制可以避免重复加载。
2、考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

线程上下文类加载器

ContextClassLoader用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题。

此前我对线程上下文类加载器(ThreadContextLoader)的理解仅仅局限于下面这段话:
Java提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的SPI有JDBC、JCE、JNDI、JAXP和JBI等。 这些SPI 的接口由Java核心库来提供,而这些SPI的实现代码则是作为Java应用所依赖的jar包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由引导类加载器来加载的;SPI的实现类是由系统类加载器来加载的。引导类加载器是无法找到SPI的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。

而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

真正理解线程上下文类加载器(多案例分析):http://blog.csdn.net/yangcheng33/article/details/52631940

假如有这么一个场景:JDBC提供了一套接口,具体的实现由各个数据库厂家实现,JDBC这套接口需要用到具体实现类,比如获取到具体实现类的Class信息,通过反射进行操作,但是JDBC这套接口是由启动类加载器加载的,而厂家提供的具体实现类由应用类加载器加载,由于双亲委托机制导致启动类加载器无法看到应用加载器这个子类加载器,线程上下文类加载器就是解决这个问题的,可以打破双亲委派模型,启动类加载器可以使用子类加载应用加载器加载类。其实实现很简单,就是Thread类中定义一个属性用子类加载器赋值,父类加载器就可以获取到该属性看到子类加载器了。
Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader();

JDK中还提供了另外一种方式可以实现这种效果:Reflection.getCallerClass()可以获取到调用该方法的类的Class,然后通过Class获取到调用类的类加载器。
例子:
public class B {

  public static Class<?> forName(String className) {
        return forName0(className, true, Reflection.getCallerClass());
  }
  private static Class<?> forName0(String name, boolean initialize, Class clz) {
        System.out.println("***>" + clz);
        System.out.println(Reflection.getCallerClass());
        return null;
  }

}
public class A {

  private void fun(){
        B.forName("a.b");
  }
  public static void main(String[] args) {
        new A().fun();
  }

}
输出结果:
*>class jvm.A
class jvm.B
分析:B中的forName是由A类调用的,所以使用Reflection.getCallerClass()获取到的是A的Class,B中的forName0方法 是被B中的forName方法调用,所以forName0中的Reflection.getCallerClass()获取到的是B的Class。

Class类中的forName大致就是上面这种实现,可以看下代码:
public static Class<?> forName(String className)

            throws ClassNotFoundException {
    return forName0(className, true,
                    ClassLoader.getClassLoader(Reflection.getCallerClass()));

}
所以,使用Class.forName()加载类时,内部又调用了forName0的时候使用的是当前类类加载器传入,这时候会使用双亲委派模型加载指定类。

还有DriverManager类中:
public static Connection getConnection(String url,

    String user, String password) throws SQLException {
    java.util.Properties info = new java.util.Properties();
    if (user != null) {
        info.put("user", user);
    }
    if (password != null) {
        info.put("password", password);
    }
    return (getConnection(url, info, Reflection.getCallerClass()));

}
我们发现会将当前类加载器一般是应用类加载器传入过去,在看看getConnection里面的代码:
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized (DriverManager.class) {

        // synchronize loading of the correct classloader.
        if (callerCL == null) {
            callerCL = Thread.currentThread().getContextClassLoader();
        }

}
我们发现优先使用的是Reflection.getCallerClass()。getClassLoader方式获取应用类加载器,这种方式获取不到再使用Thread.currentThread().getContextClassLoader()这种线程上下文方式获取。

上面两种方式都可以实现顶层ClassLoader获取到底层ClassLoader加载的类,也就是获取到Class实例,通过反射进行相关操作。这样反射就实现了不同命名空间下的两个类互相访问问题。

Reflection的getCallerClass的使用:http://blog.csdn.net/freeideas/article/details/43528571

双亲模式的破坏

双亲模式是默认的模式,但不是必须这么做 :

 1、Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent
 2、OSGi的ClassLoader形成网状结构,根据需要自由加载Class

当Java虚拟机要加载一个类时,到底调用哪个加载器去加载呢?

1、首先它会调用当前线程的类加载器去加载线程中的第一个类。

 Thread a = Thread.currentThread();
 System.out.println(a.getContextClassLoader().getClass().getName());
 运行上面的代码输出:
 sun.misc.Launcher$AppClassLoader

2、如果类A中引用了类B,Java虚拟机将使用加载类A的加载器来加载类B。

3、还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。

 Thread a = Thread.currentThread();
 System.out.println(a.getContextClassLoader().getClass().getName());
 a.setContextClassLoader(System.class.getClassLoader());--------------------------这里不写.getClass().getName())是因为null没有字节码,写了程序报空指针异常。
 System.out.println(a.getContextClassLoader());
 运行上面的代码输出:
 sun.misc.Launcher$AppClassLoader
 null

问:能不能自己写一个类叫java.lang.System?
答:一把情况下不能,因为委托机制的原因,当你写了这个类时,到加载的时候,流程会一直想上走,当到BootStrap时,它发现自己的管辖范围有,于是他就直接加载一个System给你了,而这个System是rt.jar中的类。如果自己写个类加载器就可以了,或者类名相同但是包名不相同也是可以的,但是不能类名和包名都相同。比如楼主可以写个类叫:com.lang.String。

运行时包

访问级别:
public--所有外部类都可以访问(公有)
protected--包内和子类可访问(保护)
不写(default)--包内可访问 (默认)
private--本类可以访问(私有)

不同类加载器的命名空间

反射可以实现不同命名空间下的两个类互相访问

JVM在搜索类的时候,又是如何判定两个class是相同的呢?

JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。比如网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译之后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.lang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。现在通过实例来验证上述所描述的是否正确:
public class NetworkClassLoader extends ClassLoader {

  private String rootUrl;
  public NetworkClassLoader(String rootUrl) {
        super(null);//设置父加载器不是AppClassloader,而是Bootstrap Classloader
        this.rootUrl = rootUrl;
  }
  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = null;// this.findLoadedClass(name); // 父类已加载
        // if (clazz == null) { //检查该类是否已被加载过
        byte[] classData = getClassData(name); // 根据类的二进制名称,获得该class文件的字节码数组
        if (classData == null) {
              throw new ClassNotFoundException();
        }
        clazz = defineClass(name, classData, 0, classData.length); // 将class的字节码数组转换成Class类的实例
        return clazz;
  }
  private byte[] getClassData(String name) {
      //实现指定路径下读取class文件的二进制流
  }
  private String classNameToPath(String name) {
        return rootUrl + "/" + name.replace(".", "/") + ".class";
  }

}

public class NetClassLoaderSimple {

  private NetClassLoaderSimple instance;
  public void setNetClassLoaderSimple(Object obj) {
        this.instance = (NetClassLoaderSimple)obj;
  }

}

public class NewworkClassLoaderTest {

  public static void main(String[] args) {
        try {
              //测试加载网络中的class文件
              //String rootUrl = "http://localhost:8080/httpweb/classes";
              String rootUrl = "file:///D:/src";
              String className = "classloader.NetClassLoaderSimple";
              NetworkClassLoader ncl1 = new NetworkClassLoader(rootUrl);
              NetworkClassLoader ncl2 = new NetworkClassLoader(rootUrl);
              Class<?> clazz1 = ncl1.loadClass(className);
              Class<?> clazz2 = ncl2.loadClass(className);
              System.out.println(clazz1 == clazz2);//对比发现不同classloader加载的相同class文件的Class实例也不相等
              Object obj1 = clazz1.newInstance();
              Object obj2 = clazz2.newInstance();
              clazz1.getMethod("setNetClassLoaderSimple", Object.class).invoke(obj1, obj2);
        } catch (Exception e) {
              e.printStackTrace();
        }
  }

}

结论:从结果中可以看出,虽然是同一份class字节码文件,但是由于被两个不同的ClassLoader实例所加载,所以JVM认为它们就是两个不同的类。
详细代码:

类的卸载

编写自己的类加载器

定义自已的类加载器分为两步:

 1、继承java.lang.ClassLoader
 2、重写父类的findClass方法

读者可能在这里有疑问,父类有那么多方法,为什么偏偏只重写findClass方法?
因为JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写loadClass搜索类的算法。下图是API中ClassLoader的loadClass方法:

2、findClass、defineClass和loadClass(这里与之相关的设计模式是-----模板方法模式)

    如果,覆盖loadClass方法,程序将不会用委托机制在创建代码,所以一般都覆盖findClass方法。

protected Class<?> loadClass(String name, boolean resolve)

    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) { //加上锁,同步处理,因为可能是多线程在加载类

         //检查,是否该类已经加载过了,如果加载过了,就不加载了
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {//如果自定义的类加载器的parent不为null,就调用parent的loadClass进行加载类
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);//如果自定义的类加载器的parent为null,就调用findBootstrapClass方法查找类,就是Bootstrap类加载器
                }
            } catch (ClassNotFoundException e) {
            }
            if (c == null) {
                long t1 = System.nanoTime();
                c = findClass(name);//如果parent加载类失败,就调用自己的findClass方法进行类加载
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;

}

ClassLoader执行的流程:

 1、调用ClasLoader的loadClass()方法
 1、loadClass()首先调用findLoadedClass()方法查找当前类加载器是由已经加载过该类,已经加载过则直接返回已经加载过的类的Class实例
 2、如果本身类加载器没有找到当类之前的加载信息,则调用父加载器的loadClass方法,即将请求委派给父类加载器,父类加载器也是先查找自己是否曾经加载过, 有则返回,没有继则继续委派给父类加载器,这样使用递归的方式一层一层委派最终到了Bootstrap ClassLoader
 3、如果到了顶层启动类加载器也没有从历史加载记录中查找到,表示该类之前没有加载过,需要进行加载,启动类加载器调用findClass(findClass方法实际上是定义了当前类加载器加载类的逻辑)从自己的加载路径中将类加载,加载成功则返回class实例,没有加载成功则返回null,这些启动类加载器方法全部执行完成,返回到下一层类加载器中,下一层类加载器判断返回结果,有则返回,null则表示委派给上层类加载器的任务没有完成,需要自己加载该类,则调用自己类加载器的加载逻辑findClass方法,成功则返回class实例,失败则返回null,这样递归一层层的返回
 4、如果这样一层层往下递归返回倒当前类加载器,当前类加载器判断返回结果,有则返回class实例,null则调用自己的加载逻辑findClass()方法进行加载,所以我们一般写自定义类加载器一般都是复写findClass即可,不会破坏JDK中默认的双亲委派机制模型,如果自己类加载逻辑findClass()也无法加载成功,则抛出异常:

通过以上步骤发现通过递归调用一层层的调用,实现了从下往上的查找和从上往下的加载这个双亲委托加载机制。

我们熟悉了ClassLoader的执行流程,如果想破坏这种双亲委派模式也就很简单,比如Tomcat的WebAppClassLoader类加载器就是先使用自己加载器加载类,加载不到再委派给父加载器来完成,我们实现这种场景其实很简单,重写loadClass方法,先调用findLoadedClass查找当前加载器加载过的缓存中是否存在,存在则返回,不存在则调用findClass执行本类加载器自己的加载逻辑进行加载类,而不是调用parent.loadClass方法,这样就实现了优先当前类加载器加载,当当前类加载器加载不到后,再调用parent.loadClass委派给父加载器进行加载。

findClass实现的逻辑一般是:

 1、从指定的目录下获取到.class文件,读入到内存中,生成byte[]字节数组
 2、调用Class<?> defineClass(String name, byte[] b, int off, int len),name为类的全限名称,byte[]字节数组就是.class文件的字节数组,off为字节数组开始偏移量,一般为0表示从头开始,len为读取的字节数组长度。
 3、返回defineClass()的结果

自定义的类加载器本身就被限制为无法加载java.*的类哦

当然也可以重新loadClass方法,覆盖父类中的loadClass方法使用的双亲委托机制的实现方式:
public class MyClassLoader extends ClassLoader {

  //重写了loadClass方法,覆盖了父类中loadClass方法使用的双亲委托机制实现
  @Override
  public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
              Class<?> c = findLoadedClass(name);//首先查找是否已经加载,已经加载则直接从缓存中返回Class实例
        if (c == null) {
              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);
              c = defineClass(name, b, 0, b.length);
        }
        return c;
        }
        catch (Exception e) {
              throw new ClassNotFoundException(name);
        }
  }

}

ClassLoader myLoader = new MyClassLoader();
Class<?> clazz = myLoader.loadClass("classloader.ClassLoaderTest");
这里虽然myLoader类加载器的父加载器是AppClassloader,但是在加载的时候是首先是自己加载,自己无法加载的再委派给父加载器加载。
详细代码:

自定义类加载器默认的父加载器是AppClassloader,这在ClassLoader类中有指定:
protected ClassLoader() {

    this(checkCreateClassLoader(), getSystemClassLoader());

}

热替换

定义:当一个class被替换后,系统无需重启,替换的类立即生效
例子:
public class Worker {

public void doit(){
    System.out.println("I am version 5");
}

}
HelloMain中不停调用Worker中doit()方法,因此有输出:
I am version 5
HelloMain的运行过程中,替换为:I am version 6 ,替换后,HelloMain的输出变为
I am version 6
项目完整代码:

URLClassLoader

URLClassLoader是ClassLoader的子类,它用于从指向 JAR 文件和目录的 URL 的搜索路径加载类和资源。也就是说,通过URLClassLoader就可以加载指定jar中的class到内存中。
AppClassLoader和ExtClassLoader类加载器都继承了URLClassLoader

代码案例:
/**

  • String classPath = "loader.HelloImpl";// Jar中的所需要加载的类的类名
    String jarPath = "file:///D:/tmp/test.jar";// jar所在的文件的URL

*/
public static void loadJar1(String classPath, String jarPath) {

        ClassLoader cl;
        try {
              // 从Jar文件得到一个Class加载器
              cl = new URLClassLoader(new URL[] { new URL(jarPath) });
              // 从加载器中加载Class
              Class<?> clazz = cl.loadClass(classPath);
              //通过反射进行操作
        } catch (Exception e) {
              e.printStackTrace();
        }

}
案例参见博客:https://yq.aliyun.com/articles/43523?spm=5176.100240.searchblog.68.WmYpsu

个人总结

1、通过类加载器根据一个类的二进制名称(Binary Name)获取定义此类的二进制字节流,在读取类的二进制字节流时链接阶段的验证操作的文件格式验证已经开始
(加载、链接、初始化三个阶段是交叉混合进行的,并不是加载完成后才执行链接,也不是链接完成后才执行初始化的),
只有通过了文件格式验证后才能存储到方法区,若验证失败则抛出 java.lang.VerifyError 或其子异常类。
2、将字节流所代表的静态存储结构(Class文件结构)转化为方法区的运行时数据结构
3、在堆内存中生成一个代表类或接口的Class 实例,是对该类在方法区的运行时数据结构的封装,作为操作该类或接口元数据的入口(Reflection就是利用Class实例的)。
4、验证主要包括文件格式严重、版本号兼容性严重、Java语义分析保证符合Java规范(如是不是继承了final类、访问权限等等),首先对于被反复使用和验证过的类,验证过程是非必要的。可以通过 -Xverify:none 来关闭验证,可缩短虚拟机加载的时间。
5、连接准备阶段就是为静态属性赋默认值0
6、连接解析阶段实质就是将常量池内的符号引用替换为直接引用。
符号引用(Symbolic References):以一组符号来描述所引用的目标(类、接口、方法、字段等)。只要能无歧义地定位到目标即可,并且与JVM的实际内部布局无关,而引用的目标也不一定已经加载到内存中。符号引用的形式已经由JVM规范规定了。
直接引用(Direct References): 直接引用可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。如果有了直接引用则目标必定已经在内存中存在了。
7、类和接口均有初始化过程,实质上就是执行字节码中的<clinit>构造函数。类中静态字段和静态代码块均被代码重排到<clinit>函数中进行赋值等操作。并且父类必须已经初始化后再初始化子类。
接口的静态字段也被代码重排到<clinit>函数中进行赋值操作。但不要初始化该接口前必须其父接口完成了初始化,而是在真正使用到父接口(静态常量字段)时才触发初始化。
JVM会自动处理多线程环境下<clinit>函数的同步互斥执行。因此若在<clinit>执行耗时的操作则会阻塞其他线程的执行。

主动引用
JVM规范规定以下5种情况,则必须执行初始化(加载、链接自然会在之前进入执行状态)

  1. 遇到new, getstatic, putstatic或invokestatic这4条字节码指令时,若类没有进行过初始化,则需要先触发初始化。对应的Java代码为通过关键字new一个实例,读或写一个类变量,调用类方法。
  2. 使用java.lang.reflect包中的方法操作类时,若类没有进行过初始化,则需要先触发初始化。
  3. 当初始化一个类时,若其父类还没初始化则会先初始化父类。
  4. 当虚拟机启动时,虚拟机会初始化入口函数所在的类。
  5. JDK1.7增加动态语言的支持。如果一个java.lang.invoke.MethodHandle实例最后的解析结果是 REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,而这个句柄所在的类没有进行初始化,则需要先触 发初始化。

除了上述5种情况外,其他引用类的方式是不会触发初始化的,并称为被动引用。下列示例则为被动引用

  1. 通过子类访问父类静态字段不会导致子类初始化,仅仅会导致父类初始化。
  2. Java代码中创建数组对象,不会导致数组的组件类(如SuperClass[]的组件类为SuperClass)初始化。因为创建数组类的字节码指令是newarray。
  3. 类A访问类B的编译时静态常量不会导致类B的初始化。因为在编译阶段会将类使用到的常量直接存储到自身常量池的引用中,因此实际上运行时类A访问的是自身的常量与类B无关系。

继承中的构造方法

1、子类的构造过程中必须调用其基类的构造方法。
2、子类可以在自己的构造方法中使用super(argument_list)调用基类的构造方法。
  2.1、使用this(argument_list)调用本类的另外构造方法。
   2.2、如果调用super,必须写在子类构造方法的第一行。
3、如果子类的构造方法中没有显示的调用基类的构造方法,则系统默认调用基类的无参数构造方法。
4、如果子类构造方法中既没有显示调用基类构造方法,而基类又没有无参数的构造方法,则编译出错。

用云栖社区APP,舒服~

【云栖快讯】诚邀你用自己的技术能力来用心回答每一个问题,通过回答传承技术知识、经验、心得,问答专家期待你加入!  详情请点击

网友评论

作者关闭了评论
一心飞扬
文章2篇 | 关注1
关注
为您提供简单高效、处理能力可弹性伸缩的计算服务,帮助您快速构建更稳定、安全的应用,提升运维效... 查看详情
RDS是一种稳定可靠、可弹性伸缩的在线数据库服务。支持MySQL、SQL Server、Po... 查看详情
阿里云总监课正式启航

阿里云总监课正式启航