深入理解JVM,类加载机制ClassLoader流程

编译原理请查看之前内容,.java文件编译过程和执行过程分析以及计算机简单认识

需要了解更多Java原创资料,请加qq:1811112688,或者加老薛的微信,lukun0402。

本内容全部原创,纯手敲,对你有帮助可以点赞、关注哦!!!转载请注明出处:https://www.jianshu.com/p/deb040582060

1 类加载器

1:编译、加载过程

image

2:java虚拟机何时会结束什么周期?

 i:正常执行完毕程序
ii:执行了System.exit();
iii:程序执行期间发生了异常或者错误而终止
iv:由于底层操作系统出错,导致虚拟机结束进程

3:ClassLoader将数据加载到内存中经过的步骤:

image
i、加载:加载类的二进制数据
ii、链接
  .验证 确保加载的类的正确性。 
  .准备 类中的静态变量分配内存,并且其初始化为默认值。
  .解析 把类中的符号引用变为直接引用。
iii、初始化为类中的类中的静态变量赋值(正确的初始值)

3-1:验证:

类文件的结构检查:

确保类文件遵从Java类文件的固定格式(防止伪造字节码文件)

语义检查:

确保类本身符合Java语言的语法定义,这一步在编译器也会操作。比如在java规范中方法的重写和void无关的,但是在虚拟机规范中,重写的定义和Java规范中的重写定义不同。在虚拟机规范中方法返回值+方法签名构成了一个方法的重写定义。这一部分有可能会被恶意用户利用。

字节码验证:

确保字节码可以被Java虚拟机安全的执行。一条指令包含操作码+一个或多个操作数。例如:invokespecial #1

二进制兼容性验证

确保引用的类之间协调一致。比如某个类A中引用了其他类B的的f();方法,name在执行A类的时候会检查B类中是否存在f();方法,如果不存在?就会抛出NoSuchMethodException。

PS:这里不存在的原因是由于:我们都知道如果出现这种情况下,编译期间就会报错,但是如果别人恶意篡改了呢?又或者A类和B类的编译时通过不同的版本编译的,那么就会出现不兼容情况。

3-2:准备:

准备比阶段Java虚拟机会为了的静态变量分配内存,并设置默认的初始值。例如对于Person类,在准备阶段。会给Person类中的int类型的静态变量num分配4个字节的内存空间,并且赋值默认值0。

class Person{
    static int num = 10;//在准备阶段时,num在内存中的值为0
}

3-3:解析:

在解析阶段,Java虚拟机会把类的二进制数据中的符号引用换为直接引用。例如:

class A{
  B b = new B();
  void method(){
    b.fun();//这段代码在A类的二进制数据中其实就是符号引用
  }
}
class B{
    void fun(){ 
  }
}

PS:其实符号引用为了方便程序开发。程序员能够看懂在什么情况下,调用了那个方法而已,但是在计算机执行的过程中,真正能够让计算机读懂的是二进制数据。在二进制数据中,Java虚拟机会将符号引用替换为一个指针,这个指针指向了方法该方法的栈帧地址,由fun方法的全名和相关描述符组成。</pre>

3-4:初始化:

在初始化阶段Java虚拟机会为类的静态变量赋予初始值。静态变量初始化分为两种:

在静态变量声明处进行初始化:

public class A{
   public static int num1 = 10;//准备阶段num值为0,初始化阶段值为    10
   public static double num2;//准备阶段num值为0.0,初始化阶段值为0.0
}

在静态代码块中进行初始化:

public class A{
  public static double num2;//准备阶段num值为0.0
  static{
    num2 = 12;//初始化阶段值为12.0
  }
}

3-5:初始化的面试题

public class Test02 {//启动器类 会被加载
  public static void main(String[] args) {
      Singleton s1 = Singleton.getInstance();//调用静态方法 类被加载
      System.out.println(s1.count1);
      System.out.println(s1.count2);
 }
}
class Singleton{
  / /1:准备阶段各个变量的值分别为null,0,0
  private static Singleton singleton = new Singleton();//2:初始化阶段 开始赋值
  public static int count1;//5:count1变为1
  public static int count2 = 0;//6:count2重新赋值0
  private Singleton(){
    count1++;//3:初始化阶段变成1
    count2++;//4:初始化阶段变成1
  }
  public static Singleton getInstance(){
    return singleton;
  }
}

public class Test02 {//启动器类 会被加载
   public static void main(String[] args) {
     Singleton s1 = Singleton.getInstance();//调用静态方法 类被加载
     System.out.println(s1.count1);
     System.out.println(s1.count2);
   }
  }
class Singleton{
 //1:准备阶段各个变量的值分别为0,0,null,
 public static int count1;//2:初始化阶段 开始赋值 0
 public static int count2 = 0;//3:初始化阶段 开始赋值 0
 private static Singleton singleton = new Singleton();//4:初始化阶段 开始赋值
 private Singleton(){
   count1++;//5:初始化阶段变成1
   count2++;//6:初始化阶段变成1
 }
  public static Singleton getInstance(){
   return singleton;
 }
}

4:类的初始化步骤:

  • 类如果没有被加载和连接,先加载和连接

  • 类存在直接父类,且这个类没有被初始化,那就先初始化直接父类

  • 类中若存在初始化语句,依次执行初始化语句

5:如何确定一个类被加载了?

Java程序对于类的使用分为两个部分:

i:主动使用
 A:创建一类的实例 new Person();
 B*:访问某个类、接口中的静态变量,或者对于静态变量做读写;
 C:调用类中的静态方法;
 D:反射 (Class.forName("xxx.xxx.xxx"));
 E*:初始化一个类的子类的,当前类加载。
 F:Java虚拟机标明的启动器类  (Test.class(main)|Person.class|Student.class,此时Test就是启动器类).
ii:被动使用
 a:引用常量不会导致该类发生初始化[常量它是在编译器确定的]
 b: 通过数组定义类的引用,不会触发该类的初始化
 c:通过子类引用父类的静态变量,不会导致子类初始化。

剩余的都是被动使用。。。

PS:注意,java程序首次通过主动使用时,系统会初始化该类或者接口。

5-1: 创建实例

public class Tes01{
 public static void main(String[] args){
   new A();//此时会导致A类会被加载
 }
}
class A{
}

5-2: 访问某个类的静态变量

public class Tes01{
   public static void main(String[] args){
   System.out.println(A.num);//此时会导致A类会被加载
 }
}
class A{
 static{
   System.out.println("我被初始化了。。");
 }
 static int num = 10;
}
5-2-1:特例:
public class Tes01{
   public static void main(String[] args){
     System.out.println(A.num);//此时会导致A类初始化
   }
}
class A{
   static{
     System.out.println("我被初始化了。。");
   }
 static final int num = new Random().nextInt(50);//编译器无法确定,必须要初始化当前A类才能确定值,所以访问时A会被初始化
}

 public class Tes01{
   public static void main(String[] args){
     System.out.println(A.num);//此时会不会导致A类初始化
   }
}
class A{
   static{
     System.out.println("我被初始化了。。");
   }
   static final int num = 10/2;//编译器可以确定值,所以访问时A不会被初始化
}

public class Tes01{
   public static void main(String[] args){
     System.out.println(S.num);//此时会不会导致S类初始化,只会初始化F类
 }
}
 class F{
 static int num = 10;
 static{
   System.out.println("F 我被初始化了。。");
 }
}
class S extends F{
  static{
     System.out.println("S 我被初始化了。。");
   }
}

PS:

1:如果final修饰的静态常量可以在编译期间确定值,那么不会导致当前类初始化,如果访问的final修饰的静态常量不可以在编译期间确定值,那么会导致当前类初始化。

2:只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才会认为是对于当前类和接口的主动使用。

5-3:调用类的静态方法

public class Tes01{
  public static void main(String[] args){
   new A().fun();//此时会导致A类会被加载
  }
}
class A{
 static void fun(){}
}

5-4:反射

public class Tes01{
   public static void main(String[] args){
     Class.forName("com.mage.jvm.A");//此时会导致A类会被加载
   }
}
class A{
}

5-5:初始化子类

public class Tes01{
   public static void main(String[] args){
     new B();//此时会导致A类会被加载,包括B类的间接父类也会被加载
   }
}
class A{
}
class B extends A{
}

public class Tes01{
   public static void main(String[] args){
     new A();//先执行a的初始化
     new B();//此时会导致A类会被加载,由于已经初始化过了,所以不会初始化。但是在一个类加载器的情况下,如果有其他加载器,还是会加载的。
   }
}
class A{
   static{System.out.println("A");}
}
class B extends A{
 static{System.out.println("B");}
}

5-6:表明的启动器类

public class Tes01{
   public static void main(String[] args){
     System.out.println("hello");//执行时会导致当前Test01加载
   }
}

5-7:一些特殊的不会导致加载的情况

5-7-1:初始化父类并不包含接口
public class Test01{
   public static void main(String[] args){
     new Son();//会导致Father、Son加载,但是不会导致Sun的父接口Run加载;
   }
}
class Father{
   static{System.out.println("父类加载了");}
}
class Son extends Father{
   static{System.out.println("子类加载了");}
}
interface Run{
   Thread rThread = new Thread(){
   {
     System.out.println("run 执行了");
   }
  };
}
5-7-2:某些情境下会导致父接口初始化

程序首次使用特定接口的静态变量时,会导致该接口初始化。

public class Test01 {
   public static void main(String[] args) {
     System.out.println(SRun.sThread);//不会导致FRun加载
   }
}
interface FRun{
   int fNum  = new Random().nextInt(3);
   Thread fThread = new Thread(){
   //每个实例创建时都会执行一次
   {
     System.out.println("FRun invoked"+fNum);
   }
   };
}
interface SRun extends  FRun{
   int sNum  = new Random().nextInt(3);
   Thread sThread = new Thread(){
   //每个实例创建时都会执行一次
   {
     System.out.println("SRun invoked"+sNum);
   }
   };
}
5-7-3:初始化一个接口时,并不会初始化它的父接口
public class Test01 {
   public static void main(String[] args) {
     System.out.println(SRun.sThread);//不会导致FRun加载
   }
}
interface FRun{
   int fNum  = new Random().nextInt(3);
   Thread fThread = new Thread(){
   //每个实例创建时都会执行一次
   {
     System.out.println("FRun invoked"+fNum);
   }
   };
}
interface SRun extends  FRun{
   int sNum  = new Random().nextInt(3);
   Thread sThread = new Thread(){
   //每个实例创建时都会执行一次
   {
     System.out.println("SRun invoked"+sNum);
     System.out.println(fThread);//调用父类时,会导致父接口加载
   }
   };
}
5-7-4:被动使用不到值初始化
A: 通过数组定义类的引用,不会触发该类的初始化。
public class Test01 {
   public static void main(String[] args) {
     Person[] ps = new Person[10];
   }
}
class Person{
   static{
     System.out.println("被初始化了。。。。");
   }
}
B:通过子类引用父类的静态变量,不会导致子类初始化。
public class Test01 {
   public static void main(String[] args) {
     System.out.println(S.num);
   }
}
class P{
   static int num = 10;
   static{
      System.out.println("父类被加载了。。。");
   }
}
class S extends P{
   static{
     System.out.println("子类类被加载了。。。。");
   }
}
C:调用ClassLoader类的loadClass方法加载一个类,并不是对于类的主动使用。
package com.mage.test;
public class Test03{
   public static void main(String[] args) throws Exception {
     F.class.getClassLoader().loadClass("com.mage.test.F");
     ClassLoader.getSystemClassLoader().loadClass("com.mage.test.F");
   }
}
class F{
  static{
    System.out.println("F 我被初始化了。。");
 }
}

5:加载.class文件的方式:

-1:本地系统当中直接加载 *
-2:通过网络下载.class文件
-3:从jar、zip等归档文件中加载.class文件 *
-4:数据库(.class文件)
-5:动态编译.class</pre>

当一个类加载到内存中之后,会在堆内存当中产生一个对应的Class的对象,所有当前的类的 实例以及当前该类都共享这一份Class实例。

6:类加载器

java虚拟机自带的加载器:
 根类加载器(bootstrap)
 扩展类加载器(PlatformClassLoader、低版本 Ext)
 系统类加载器(AppClassLoader) 
用户自定义加载器
 java.lang.ClassLoader的子类
 可以定制类的加载方法 

注意:类加载器并不需要等到一个类"首次主动使用"时在加载它。 可以预先加载。JVM规范中定义类,类加载器可以在预料到某个类可能 需要加载时预先加载。 hotspot、jrokit、j9都是对于JVM规范的一种实现。

ps:注意用户的自定义类加载的父加载器 不一定全是系统类加载器

6-1:JVM加载器详解:

根加载器 无父加载器,负责加载虚拟机核心类库。比如LANG包下的类。它的实现以来与底层操作系统,属于虚拟机实现的一部分,没有继承==JAVA.LANG.CLASSLOADER==类
扩展加载器 它的父类是根加载器,一般加载jre/lib/ext下的目录。如果将自定义创建好的jar文件放入该目录,则jar文件会自动交由扩展类加载器加载。扩展类加载器是java.lang.ClassLoader的子类。
系统类加载器 应用类加载器,父加载器是扩展类加载器。从配置的ClassPath或者系统指定的(一般为当前java命令执行的当前目录)中加载类。是用户自定义类加载器的父加载器。也是java.lang.ClassLoader的子类
自定义类加载器 用户自定义类加载器需要继承java.lang.ClassLoader。
public class Test01 {
  public static void main(String[] args) {
     String str = "";
     //NULL -> 根加载器 bootstrap[c、c++] 
     System.out.println(str.getClass().getClassLoader());

     //jdk.internal.loader.ClassLoaders$AppClassLoader@512ddf17
     //应用类加载器
     System.out.println(new Person().getClass().getClassLoader());

     //jdk.internal.loader.ClassLoaders$PlatformClassLoader@2752f6e2                     
  //EXT加载  扩展类加载器
     System.out.println(new Person().getClass().getClassLoader().getParent());

   //根加载器 bootstrap[c、c++]
   System.out.println(new Person().getClass().getClassLoader().getParent().getParent());
  }
}
class Person{}

6-2:类加载器关系图

image

6-3: 类加载器的双亲委派机制(父类委托机制)

类加载器将类加载到java虚拟机中。jdk1.2之后,类的加载机制采用父类委托机制,可以更好的保证安全性。除了Java虚拟机自带的根加载器以外,其他类加载都有且只有一个父加载器。当Java程序请求加载loader1加载A类时,loader1首先委派自己的父加载器加载A类,若父加载器可以加载,交由父加载器完成该加载任务,否则由loader1加载A类。

PS:各个加载器按照父子关系形成一个树形结构,除了根加载器外,其余加载器都有且包含一个父加载器。

6-3-1: 树形结构图:
image

ps:执行过程,loader2首先会在自己的命名空间中查找A类是否被加载,如果已经加载了,直接返回代表A类的Class对象的引用。如果A类没有被加载,loader2会请求loader1代为加载,loader1再请求系统类加载器加载。一直请求到根加载器加载,如果在请求过程中都不能加载,此时系统类加载器尝试加载,如果能够加载成功,则将A对应的Class对象返回给loader1,loader1再返回给loader2,如果系统类加载也不能加载成功,则交由loader1进行加载,loader1如果也失败,则loader2尝试加载。如果所有的父加载器以及loader2本身都没有加载成功,则抛出ClassNotFountException异常。

注意,在这个过程中,如果有一个类加载器成功加载了A类,name这个类加载器称之为==定义类加载器==,所有能够成功返回Class对象的引用的类加载器包括定义类加载器在内都称之为==初始类加载器==

例如:如果loader1加载A类成功,那么loader1称之为A类的定义类加载器,而loader1和loader2都称之为A类的初始类加载器。

6-3-2: 父类委托机制中的父类含义:

父类委托机制中的父子关系大多数场景下不见得是父子的继承关系,大多数场景下其实是组合的关系。并且在一些场景下我们还能看到父子加载器是同一个加载器的两个实例。比如:

自定义类加载器中同一个加载器的两个实例,出现父子关系。
public class Test04 {
   //这里只是为了表现一个加载器的两个不同实例,也可以出现父子关系
 //本次代码写的不作为具体的自定义类加载器
 public static void main(String[] args) {
   ClassLoader classLoader1 = new MyClassLoader();
   ClassLoader classLoader2 = new MyClassLoader(classLoader1);
   }
}
class MyClassLoader extends ClassLoader{
   public MyClassLoader(){
   }
   public MyClassLoader(ClassLoader cl){
   }
   @Override
   protected Class<?> findClass(String name) throws     ClassNotFoundException {
     return super.findClass(name);
   }
}
Java中ClassLoader构造器:

ClassLoader文档注释:

CLASSLOADER
protected ClassLoader()
Creates a new class loader using the ClassLoader returned by the method getSystemClassLoader() as the parent class loader.If there is a security manager, its checkCreateClassLoader method is invoked. This may result in a security exception.
Throws:SecurityException -If a security manager exists and its checkCreateClassLoader method doesn't allow creation of a new class loader.

ps:这里我们能看到通过ClassLoader构造器返回的是通过getSystemClassLoader()获取到的加载器作为当前加载器的父类。

ClassLoader源码解读:

public abstract class ClassLoader {
   protected ClassLoader() {
   //一:
     this(checkCreateClassLoader(), null, getSystemClassLoader());
   }
   //三:
   private ClassLoader(Void unused, String name, ClassLoader parent) 
   {
     this.name = name;
     //四:
     this.parent = parent;
     this.unnamedModule = new Module(this);
     if (ParallelLoaders.isRegistered(this.getClass())) {
       parallelLockMap = new ConcurrentHashMap<>();
       package2certs = new ConcurrentHashMap<>();
       assertionLock = new Object();
     } else {
       // no finer-grained lock; lock on the classloader instance
       parallelLockMap = null;
       package2certs = new Hashtable<>();
       assertionLock = this;
     }
       this.nameAndId = nameAndId(this);
   }
 //二:
 public static ClassLoader getSystemClassLoader() {
   switch (VM.initLevel()) {
   case 0:
   case 1:
   case 2:
   // the system class loader is the built-in app class loader during startup
     return getBuiltinAppClassLoader();
   case 3:
     String msg = "getSystemClassLoader cannot be called during the system class loader instantiation";
     throw new IllegalStateException(msg);
   default:
       // system fully initialized
     assert VM.isBooted() && scl != null;
     SecurityManager sm = System.getSecurityManager();
     if (sm != null) {
       checkClassLoaderPermission(scl, Reflection.getCallerClass());
     }
     return scl;
   }
 }
}

ps: 在源码中我们看到当要构建ClassLoader对象时[一],需要通过getSystemClassLoader方法获取到一个ClassLoader对象[二],将获取到的对象传入带参构造器的中[三],执行了this.parent = parent;[四]。

总结一句话,当我们创建类加载器对象时,传入的类加载器A是创建的类加载器B的父加载器。默认情况下创建的类加载器的父加载器是系统类加载器。return getBuiltinAppClassLoader();

6-3-3:为什么需要设计父类委托机制
6-3-3-1:why? 出于何种考虑设计的父类委托机制?

父类委托机制可以提高软件系统的安全性,由于用户自定义的类加载器是无法在父类委托机制下加载应该由父加载器加载的类。防止了不安全甚至是恶意代码的加载。比如lang包下的类是由根加载器加载,其他任何用户自定义加载器都无法加载包含了恶意代码的java.lang.Object类。

例子:假设自己设计一个类,然后是存在恶意代码的,如果没有父类委托机制,通过自己编写的加载器,直接加载到内存中,这就有问题了。如果存在父类委托机制,那么交由父加载器加载,父加载器按照jvm规则加载,如果存在恶意代码,就不会加载。

6-3-3-2: how? 如何保证安全?

需要提及两个概念,一个是命名空间,一个是运行时包。

命名空间:存在的目的就是为了防止同名。

每个类加载器都有自己的命名空间,命名空间=该类加载器+所有父加载器所加载的类组成。

JAVA虚拟机为每一个类加载器维护一个命名空间。在同一个命名空间下,不会出现名字完全相同的两个类,不同命名空间下,有可能会出现相同两个类。

一个类在被类加载器加载到内存中之后,是可以被其他类加载器继续加载的。只不过这两个类加载器不能存在父子关系。如果一个类在加载到内存中之前,已经存在一个类加载器加载过了,那么在相同命名空间下就不会被加载,

运行时包:同一类加载器加载的属于相同包的类组成了运行时包。

运行时包 = 包名 + 定义类加载器(第一次加载一个类成功的加载器称之为当前类的定义类加载器,后续所有的子加载器称之为该类) 。

这样才能防止用户自定义类冒充核心类库,调用一些包可见的方法。

package com.mage.jvm01;
public class Test01 {
  public static void main(String[] args) {
     //测试调用String类下的lastIndexOf方法,该方法是默认的访问权限
     byte b = 12;
     String.lastIndexOf(new byte[12],b,2,"",1);//编译报错,由于方法不可访问到
   }
}

结论:不能直接访问默认访问权限的不同包下的类。

package java.lang;
public class System {
   public static void main(String[] args) {
     byte b = 12;
     //冒充为lang包下的类,访问默认修饰符的方法,编译通过,执行出错。
     String.lastIndexOf(new byte[12],b,2,"",1);
   }
}

ps:由于运行时包不光要看加载类的包名还要看加载该类的类加载器。由于lang下的类和我们自定义的lang包下的类不由同一类加载器加载,所以他们不再一个包下。

6-3-3-3: 面试题

能不能自己写个类叫java.lang.System

答案:通常不可以,但可以采取另类方法达到这个需求。 解释:为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。

但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。

6-4:自定义类加载器

6-4-1:自定义类加载器步骤

要创建用户自定义类加载器,需要继承java.lang.ClassLoader类,然后重写一些findClass(String str)方法。这个方法根据指定的类的名字,返回对应的Class对象的引用。

源码解析

第一步:

protected Class<?> findClass(String name) throws           ClassNotFoundException {
   throw new ClassNotFoundException(name);
 }

Finds the class with the specified binary name. This method 
should be overridden by class loader implementations that follow 
the delegation model for loading classes, and will be invoked by 
the loadClass method after checking the parent class loader for 
the requested class.</pre>

ps:这里findClass只会抛出一个异常,我们需要重写findClass方法。在官方文档中的描述,根据只指定的binary name(二进制名称)查找类。通过遵守父类委托机制的类加载器需要重写这个方法区加载一个类,在执行请求一个类时会先检查父加载器,然后将执行loadClass方法。

第二步:

public Class<?> loadClass(String name) throws     ClassNotFoundException {
   return loadClass(name, false);
}

Loads the class with the specified binary name. This 
method searches for classes in the same manner as 
the loadClass(String, boolean) method. It is invoked by the 
Java virtual machine to resolve class references. Invoking 
this method is equivalent to invoking loadClass(name, false).

ps:执行该方法和执行loadClass(String, boolean) 这个方法的搜索类的方式相同。该方法的执行时通过jvm区识别类的引用,和执行loadClass(String, boolean)相同。在代码中我们也发现其实底层就是调用loadClass(String, boolean)。

第三步:

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);
       }
     } 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
   PerfCounter.getParentDelegationTime().addTime(t1 - t0);                         PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
   PerfCounter.getFindClasses().increment();
   }
 }
 if (resolve) {
   resolveClass(c);
 }
 return c;
 }
}

ps:这里我们要关注几点,第一点就是当前方法首先会检查类是否被加载过,如果没有被加载,会执行parent != null的判定,这里的parent就是当前加载器的父加载器。如果存在父加载器,就会交由父加载器加载。如过是空则交由根加载器加载。

第四步:关于parent变量的解释:

protected ClassLoader() {
   this(checkCreateClassLoader(), null, getSystemClassLoader());
}

i:默认情况下创建ClassLoader时会调用其他构造器。且传入的parent为系统类加载器。

private ClassLoader(Void unused, String name, ClassLoader parent) {
 this.name = name;
 this.parent = parent;//这行代码很重要
 this.unnamedModule = new Module(this);
 if (ParallelLoaders.isRegistered(this.getClass())) {
   parallelLockMap = new ConcurrentHashMap<>();
   package2certs = new ConcurrentHashMap<>();
   assertionLock = new Object();
 } else {
   // no finer-grained lock; lock on the classloader instance
   parallelLockMap = null;
   package2certs = new Hashtable<>();
   assertionLock = this;
 }
 this.nameAndId = nameAndId(this);
 }

ii:这会将系统类加载作为当前类加载器的父类。

编写自定义类加载器
/**
 * 编写自定义类加载器:
 *  1:继承ClassLoader
 *  2:重写findClass
 *  3:通过自定义的loadClassData,将字节码文件读取到字节数组中
 *  4:通过defineClass方法将字节数组中的数据转换为Class对象
 *  5:测试
 */
public class MyClassLoader extends ClassLoader{
   //定义加载的目录:
   private String path = "";
   //定义加载文件的后缀
   private final static  String fileType = ".class";
   //定义类加载的名称
   private String name ;
  public MyClassLoader(String name){
    super();
    this.name = name;
   }
   public MyClassLoader(String name,ClassLoader parent){
     super(parent);
     this.name = name;
   }
   //重写findClass
   @Override
   protected Class<?> findClass(String name) throws         ClassNotFoundException {
     byte[ ] data = loadClassData(name);
     final Class<?> aClass = defineClass(name, data, 0, data.length);
     return aClass;
   }
   //定义loadClass方法 通过全限定名称 获取字节数组
   private byte[] loadClassData(String name){
     //声明返回数据
     byte[ ] data = null;
     try(InputStream is = new FileInputStream(new File(path+name+fileType));
     ByteArrayOutputStream baos = new ByteArrayOutputStream() ){
       int len = 0;
       while((len = is.read())!= -1){
       baos.write(len);
     }
     data = baos.toByteArray();
   }catch (IOException e){
     e.printStackTrace();
   }
   return data;
  }
 public void setPath(String path) {
   this.path = path;
 }
 @Override
 public String toString() {
   return "MyClassLoader{" +
   "path='" + path + '\'' +
   ", name='" + name + '\'' +
   '}';
 }
 public static void main(String[] args) {
   MyClassLoader loader1 = new MyClassLoader("loader01");
   loader1.setPath("/Users/iongst/app/client/");
   MyClassLoader loader2 = new MyClassLoader("loader02",loader1);
   loader2.setPath("/Users/iongst/app/serverlib/");
   MyClassLoader loader3 = new MyClassLoader("loader03",null);
   loader3.setPath("/Users/iongst/app/otherlib/");
   loader(loader1);
   loader(loader2);
   loader(loader3);
 }
 private static void loader(ClassLoader classLoader) {
   try {
     Class clz = classLoader.loadClass("A");
     Constructor c = clz.getConstructor(null);
     c.newInstance(null);
   } catch (ClassNotFoundException e) {
     e.printStackTrace();
   } catch (NoSuchMethodException e) {
     e.printStackTrace();
   }catch (Exception e){
     e.printStackTrace();
  }
 }
}
自定义类加载器关系
image
一个类被加载多次
A 被加载:    jdk.internal.loader.ClassLoaders$AppClassLoader@799f7e29
ßB 被加载:jdk.internal.loader.ClassLoaders$AppClassLoader@799f7e29
A 加载:MyClassLoader{path='/Users/iongst/app/otherlib/',name='loader03'}
ßB 加载:
MyClassLoader{path='/Users/iongst/app/otherlib/', name='loader03'}

PS:A类和B类被重复加载多次。由于选择的类加载器不同,导致系统类加载器和loader3在各自的命名空间中都存在A、和B类

image
面试问题:

在A类中的B类会交由那个类加载器加载呢?

答案分析:同样会交由加载A类的加载器加载B类,同时在加载B类时也会遵守父类委托机制。测试以上代码,主需要将将在A类的某个目录下的B类文件删除,查看B类时由那个类加载即可。

当前 A 被加载:MyClassLoader{path='/Users/iongst/app/client/', name='loader01'}
ßB 被加载:jdk.internal.loader.ClassLoaders$AppClassLoader@799f7e29</pre>
6-4-2 命名空间不同导致的可见性关系

同一个命名空间下的类时互相可见的。
子加载器的命名空间包含的所有父加载器的命名空间。因此子加载器记载的类可以看见父加载器加载的类。例如系统类加载器加载的类时可以看见根加载器加载的类的。
父加载器加载的类不能看见子加载器所加载的类。
如果两个加载器之间没有之间或间接的父子关系,那么他们各自加载的类互不可见。

测试用例:

在原来的代码的基础上,保证client存在A和B类,然后在sys文件夹中运行MyClassLoader.查看结果。

public static void main(String[] args) {
   MyClassLoader loader1 = new MyClassLoader("loader01");
   loader1.setPath("/Users/iongst/app/client/");
   MyClassLoader loader2 = new MyClassLoader("loader02",loader1);
   loader2.setPath("/Users/iongst/app/serverlib/");
   loader(loader1);
   try {
     Class clz = loader1.loadClass("A");
     Constructor c = clz.getConstructor(null);
     Object obj = c.newInstance(null);
     A a = (A)obj;
     System.out.println(a.num);
   } catch (Exception e) {
     e.printStackTrace();
   }
 }

结果:

A加载:MyClassLoader{path='/Users/iongst/app/client/', name='loader01'}
ßB 被加载:MyClassLoader{path='/Users/iongst/app/client/', name='loader01'}
A 被加载:MyClassLoader{path='/Users/iongst/app/client/', name='loader01'}
ßB 被加载:MyClassLoader{path='/Users/iongst/app/client/', name='loader01'}
Exception in thread "main" java.lang.NoClassDefFoundError: Aat     MyClassLoader.main(MyClassLoader.java:77)
Caused by: java.lang.ClassNotFoundException: A
 at     java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassL    oader.java:583)
   at         java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
 at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
 ... 1 more</pre>

原因描述:由于MyClassLoader的类加载器是Sys系统类加载器。而A和B的加载时通过loader01加载的。也就MyClassLoader看不见A类。导致java.lang.NoClassDefFoundError。

如果把client中的文件导入到sys中,就不会出现问题。

7:类的卸载

7-1:测试用例

public static void main(String[] args) {
   MyClassLoader loader1 = new MyClassLoader("loader01");
   loader1.setPath("/Users/iongst/app/client/");
   try {
     Class clz = loader1.loadClass("A");
     System.out.println("class hashcode"+clz.hashCode());
     Constructor c = clz.getConstructor(null);
     Object obj = c.newInstance(null);
     loader1 = null;
     clz = null;
     obj = null;
     loader1 = new MyClassLoader("loader01");
     loader1.setPath("/Users/iongst/app/client/");
     clz = loader1.loadClass("A");
     System.out.println("class hashcode"+clz.hashCode());
     c = clz.getConstructor(null);
     obj = c.newInstance(null);
 } catch (Exception e) {
   e.printStackTrace();
}
 }

7-2 结果分析

7-2-1:结论分析:

A类有loader1加载。在类加载器的内部实现中,用一个集合存放了加载类的引用。另一方面,一个Class对象总是会引用它的类加载器。调用getClassLoader()方法,即可获取对应的类加载器。所以class实例和loader1之间是一个双向的关联关系。Class<->ClassLoader

一个类的实例总是引用代表这个类的Class对象。在Object中定义了getClass()方法获取对于Class实例的引用。Java类中也存在静态class属性。引用代表当前这个类的Class对象。所以类的实例和Class对象是一个单向的引用关系。 Class <- instance.

7-2-2:画图分析
image
7-2-3: 结论

打印的两次的哈希值不同。因此clz变量两次引用了不同的Class对象。可见Java虚拟机的生命周期中,对A类先后加载两次。

7-3:卸载

当A类被加载、链接、和初始化后,它的声明周期就开始了。代表A的Class对象不再引用,及不可被触及,Class对象就会结束生命周期。A类的方法区内的数据也会被卸载。从而结束A类的生命周期。所以一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。

java虚拟机自带的类加载所加载的类,在虚拟机生命周期中,始终不会被卸载。因为java虚拟机本身会一直引用这些类加载器,而这些类加载器则会始终引用他们所加载的类的Class对象,因此这些Class对象始终是可触及的。

而用户自定义的类加载所加载的类时可以被卸载的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349