3.类加载详解

1.类加载的时机

1. 遇到 new 、 get static 、 put static 和 invoke static 这四条字节码指令时,如果对应的类没有初始化,则要对对应的类先进行初始化。
    这四个指令对应到我们java代码中的场景分别是:
       new关键字实例化对象的时候;
       读取或设置一个类的静态字段(读取被final修饰,已在编译器把结果放入常量池的静态字段除外) ;
       调用类的静态方法时。
2. 使用 java.lang.reflect 包方法时对类进行反射调用的时候。
3. 初始化一个类的时候发现其父类还没初始化,要先初始化其父类。
4. 当虚拟机开始启动时,用户需要指定一个主类,虚拟机会先执行这个主类的初始化。
 

2.类加载的过程

主要分为三大阶段:加载阶段、链接阶段、初始化阶段

其中链接阶段又分为:验证、准备、解析

类的卸载,当一个类对应的对象都已经回收的时候,会触发卸载。

加载
“加载”是“类加载”(Class Loading)过程的第一步。这个加载过程主要就是靠类加载器实现的,主要目标就是将不同来源的class文件,都加载到JVM内存(方法区)中。
到了方法区,需要将加载的信息,封装到java.lang.Class对象中。
验证
保证二进制字节流中的信息符合虚拟机规范,并没有安全问题。
那么可以使用 -Xverify:none 参数关闭,以缩短类加载时间。
准备
仅仅为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即零值
这里不包含用final修饰的static,因为final在编译的时候就会分配了(编译器的优化)
同时这里也[不会为实例变量分配初始化]
数据类型 默认值
int 0
long 0L
short (short)0
char 'u0000'
byte (byte) 0
boolean false
float 0.0f
double 0.0d
reference null
解析
解析是虚拟机将常量池的符号引用替换为直接引用的过程
解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的
初始化
 初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码(初始化成为代码设定的默认值)
其实初始化过程就是调用类初始化方法的过程,完成对static修饰的类变量的手动赋值还有主动调用静态代码块。

初始化过程的注意点

方法是编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的.
静态代码块只能访问到出现在静态代码块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问.
虚拟机会保证在多线程环境中一个类的方法被正确地加锁,同步.当多条线程同时去初始化一个类时,只会有一个线程去执行该类的方法,其它线程都被阻塞等待,直到活动线程执行方法完毕.其他线程虽会被阻塞,只要有一个方法执行完,其它线程唤醒后不会再进入方法.同一个类加载器下,一个类型只会初始化一次.
 
类加载器
不同类加载器对象,如果对同一个类进行加载,会形成不同的Class对象。

 启动类加载器(Bootstrap ClassLoader)

负责加载 JAVA_HOMElib 目录中的,或通过-Xbootclasspath参数指定路径中的,

    且被虚拟机认可(按文件名识别,如rt.jar)的类。
    由C++实现,不是ClassLoader子类
 扩展类加载器(Extension ClassLoader)
     负责加载 JAVA_HOMElibext 目录中的,
     或通过java.ext.dirs系统变量指定路径中的类库。
应用程序类加载器(Application ClassLoader)
      负责加载用户路径(classpath)上的类库
JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

自定义类加载器
自定义类加载器步骤
1.继承ClassLoader
2.重写fifindClass()方法
3.调用defifineClass()方法
自定义类加载器,代码如下

 public class MyClassLoader extends ClassLoader{

   private String classpath;
   public MyClassLoader(String classpath) {
       this.classpath = classpath;
  }
   @Override
   protected Class<?> findClass(String namethrows ClassNotFoundException{
       try {
           byte [] classDate=getData(name);         
           if(classDate==null){}
else{
             //defineClass方法将字节码转化为类
return defineClass(name,classDate,0,classDate.length);
          }         
      } catch (IOException e) {         
           e.printStackTrace();
      }

  return super.findClass(name);

}

  //返回类的字节码
   private byte[] getData(String classNamethrows IOException{
       InputStream in null;
       ByteArrayOutputStream out null;
       String path=classpath + File.separatorChar + className.replace('.',File.separatorChar)+".class";
       try {
          in=new FileInputStream(path);
          out=new ByteArrayOutputStream();
          byte[] buffer=new byte[2048];
          int len=0;
          while((len=in.read(buffer))!=-1){
              out.write(buffer,0,len);
          }
           return out.toByteArray();
      }
       catch (FileNotFoundException e) {
           e.printStackTrace();
} finally{
           in.close();
           out.close();
      }
       return null;
  }
}
测试代码如下(在Eclipse中):
import java.lang.reflect.Method;

 public class TestMyClassLoader {

   public static void main(String []argsthrows Exception{
       //自定义类加载器的加载路径
       MyClassLoader myClassLoader=new MyClassLoader("D:\lib");
       //包名+类名
       Class c=myClassLoader.loadClass("jvm.classloader.Test");
       if(c!=null){

           Object obj=c.newInstance();

           Method method=c.getMethod("say", null);
           method.invoke(obj, null);
           System.out.println(c.getClassLoader().toString());
      }
  }
}

双亲委派模型

JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。

当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,
只有当父类加载器无法完成加载任务时,才会尝试执行加载任务
采用双亲委派的一个好处是:
比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,
这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象.
什么要使用双亲委托这种模型呢?
因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。
破坏双亲委派模型
为什么需要破坏双亲委派?
JDK1.2之后才提出双亲委派模型,但是有些JDK的API(类)早在JDK1.1就写好了。
双亲委派模型存在的问题:
JDK1.1写好的一些API没有按照双亲委派模型的特点去设计,所以在【不修改JDK1.1代码】的情况下,
也【必须要按照双亲委派这种模型去执行类加载】的话,会导致类加载时的加载不到类的情况。
举例:
Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供
比如mysql的就写了 MySQL Connector ,这些实现类都是以jar包的形式放到classpath目录下
那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类(classpath下),然后进行管理,但是DriverManager由启动类加载器加载,只能加载JAVA_HOME的lib下文件,
而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况。
当【启动类加载器】加载DriverManager的时候,却需要【应用程序类加载器】去完成Driver实现类的类加载。
正常的类加载是按照双亲委派模型去加载的,也就是当加载一个类的时候,是【子类委托父类去加载的过程】。
但是以上这个例子,需要由父类类加载器去委托子类类加载器去进行加载,才能完成DriverManager的加载过程,也就是破坏了正常的双亲委派模型。
原文地址:https://www.cnblogs.com/wangyang1991/p/13256504.html