Java中的类加载器_动力节点Java学院整理

从java的动态性到类加载机制

Java是一种动态语言。那么怎样理解这个“动态”呢?或者说一门语言具备了什么特性,才能称之为动态语言呢?对于java,我是这样理解的。

JVM(java虚拟机)执行的不是本地机器码指令,而是执行一种称之为字节码的指令(存在于class文件中)。这就要求虚拟机在真正执行字节码之前,先把相关的class文件加载到内存中。虚拟机不是一次性加载所有需要的class文件,因为它在执行的时候根本不会知道以后会用到哪些class文件。它是每用到一个类,就会在运行时“动态地”加载和这个类相关的class文件。这就是java被称之为动态性语言的根本原因。除了动态加载类之外,还会动态的初始化类,对类进行动态链接。动态初始化和动态链接放在其他文章中进行介绍。本文中只关心类的加载。

在JVM中负责对类进行加载的正是本文要介绍的类加载器(ClassLoader),所以,类加载器是JVM不可或缺的重要组件。

java中的类加载器及类加载器工作原理

java中(指的是javase)有三种类加载器。每个类加载器在创建的时候已经指定他们对应的目录, 也就是说每个类加载器去哪里加载类是确定的,我认为在ClassLoader类中应该会有getTargetPath()之类的方法, 得到他们对应的路径,找了找jdk的文档,发现是没有的。以下是这三种类加载器和他们对应的路径:

* AppClassLoader  --   加载classpath指定的路径中的类 
 * ExtClassLoader   --   加载jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下的类 
 * BootStrap           --   加载JRE/lib/rt.jar中的类

那么类加载器是如何工作的呢?可以参看jdk中ClassLoader类的源码。这个类的实现使用了模板方法模式,首先是loadClass方法来加载类,loadClass方法又调用了findClass方法,该方法读取并返回类文件的数据,findClass方法返回后,loadClass方法继续调用defineClass方法,将返回的数据加工成虚拟机运行时可识别的类型信息。所以,我们如果开发自己的类加载器,只需要继承jdk中的ClassLoader类,并覆盖findClass方法就可以了,剩下的而工作,父类会完成。其他java平台有的根据自己的需求,实现了自己特定的类加载器,例如javaee平台中的tomcat服务器,Android平台中的dalvik虚拟机也定义了自己的类加载器。

虚拟机加载类有两种方式,一种方式就是上面提到的ClassLoader.loadClass()方法,另一种是使用反射API,Class.forName()方法,其实Class.forName()方法内部也是使用的ClassLoader。Class类中forName方法的实现如下:

public static Class<?> forName(String name, boolean initialize,
  ClassLoader loader)
  throws ClassNotFoundException
 {
 if (loader == null) {
  SecurityManager sm = System.getSecurityManager();
  if (sm != null) {
 ClassLoader ccl = ClassLoader.getCallerClassLoader();
 if (ccl != null) {
  sm.checkPermission(
 SecurityConstants.GET_CLASSLOADER_PERMISSION);
 }
  }
 }
 return forName0(name, initialize, loader);
 }

 /** Called after security checks have been made. */
 private static native Class forName0(String name, boolean initialize,
   ClassLoader loader)
 throws ClassNotFoundException;

类加载器的三个特性 

类加载器有三个特性,分别为委派,可见性和单一性,其他文章上对这三个特性的介绍如下:

* 委托机制是指将加载一个类的请求交给父类加载器,如果这个父类加载器不能够找到或者加载这个类,那么再加载它。
  * 可见性的原理是子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类加载器加载的类。
  * 单一性原理是指仅加载一个类一次,这是由委托机制确保子类加载器不会再次加载父类加载器加载过的类。

其中,委派机制是基础,在其他资料中也把这种机制叫做类加载器的双亲委派模型,其实说的是同一个意思。可加性和单一性是依赖于委派机制的。

以下代码测试类加载器的委派机制:

 ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();
 System.out.println(appClassLoader); //sun.misc.Launcher$AppClassLoader@19821f

 ClassLoader extClassLoader = appClassLoader.getParent();
 System.out.println(extClassLoader); //sun.misc.Launcher$ExtClassLoader@addbf1
 //AppClassLoader的父加载器是ExtClassLoader

 System.out.println(extClassLoader.getParent()); //null
 //ExtClassLoader的父加载器是null, 也就是BootStrap,这是由c语言实现的

由打印结果可知,加载我们自己编写的类的加载器是AppClassLoader,AppClassLoader的父加载器是ExtClassLoader,在而ExtClassLoader的父加载器返回结果为null,这说明他的附加载器是BootStrap,这个加载器是和虚拟机紧密联系在一起的,在虚拟机启动时,就会加载jdk中的类。它是由C实现的,没有对应的java对象,所以返回null。但是在逻辑上,BootStrap仍是ExtClassLoader的父加载器。也就是说每当ExtClassLoader加载一个类时,总会委托给BootStrap加载。

系统类加载器和线程上下文类加载器 

在java中,还存在两个概念,分别是系统类加载器和线程上下文类加载器。

其实系统类加载器就是AppClassLoader应用程序类加载器,它两个值得是同一个加载器,以下代码可以验证:

 ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();
 System.out.println(appClassLoader); //sun.misc.Launcher$AppClassLoader@19821f

 ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader();
 System.out.println(sysClassLoader); //sun.misc.Launcher$AppClassLoader@19821f
 //由上面的验证可知, 应用程序类加载器和系统类加载器是相同的, 因为地址是一样的

这两个类加载器对应的输出,不仅类名相同,连对象的哈希值都是一样的,这充分说明系统类加载器和应用程序类加载器不仅是同一个类,更是同一个类的同一个对象。

每个线程都会有一个上下文类加载器,由于在线程执行时加载用到的类,默认情况下是父线程的上下文类加载器, 也就是AppClassLoader。

 new Thread(new Runnable() {

 @Override
 public void run() {
 ClassLoader threadcontextClassLosder = Thread.currentThread().getContextClassLoader();
 System.out.println(threadcontextClassLosder); //sun.misc.Launcher$AppClassLoader@19821f
 }
 }).start();

这个子线程在执行时打印的信息为sun.misc.Launcher$AppClassLoader@19821f,可以看到和主线程中的AppClassLoader是同一个对象(哈希值相同)。

也可以为线程设置特定的类加载器,这样的话,线程在执行时就会使用这个特定的类加载器来加载使用到的类。如下代码:

 Thread th = new Thread(new Runnable() {

 @Override
 public void run() {
 ClassLoader threadcontextClassLosder = Thread.currentThread().getContextClassLoader();
 System.out.println(threadcontextClassLosder); //jg.zhang.java.testclassloader.ClassLoaderTest$3@1b67f74
 }
 });

 th.setContextClassLoader(new ClassLoader() {});

 th.start();

在线程运行之前,为它设置了一个匿名内部类的类加载器对象,线程运行时,输出的信息为:jg.zhang.java.testclassloader.ClassLoaderTest$3@1b67f74,也就是我们设置的那个类加载器对象。

类加载器的可见性

下面验证类加载器的可见性,也就是 子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类加载器加载的类。

以下代码使用父加载器ExtClassLoader加载子加载器AppClassLoader路径下的类,由输出可知,是不可能实现的。

 try {
 Class.forName("jg.zhang.java.testConcurrent.Person", true,
  ClassLoaderTest.class.getClassLoader().getParent());
 System.out.println("1 -- 类被加载");
 } catch (ClassNotFoundException e) {
 //e.printStackTrace();
 System.out.println("1 -- 未找到类");
 }

输出为 :1 -- 未找到类 。说明抛出了ClassNotFoundException异常。原因是让ExtClassLoader加载 jg.zhang.java.testConcurrent.Person这个类因为这个类不在jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下,所以抛出ClassNotFoundException。所以父加载器不能加载应该被子加载器加载的类。也就是说这个类在父加载器中不可见。这种机制依赖于委派机制。

下面代码使用子加载器AppClassLoader 加载父加载器BootStrap中的类,这是可以实现的。

 try {
 Class.forName("java.lang.String", true,
  ClassLoaderTest.class.getClassLoader());
 System.out.println("2 -- 类被加载");
 } catch (ClassNotFoundException e) {
 //e.printStackTrace();
 System.out.println("2 -- 未找到类");
 }

输出为:2 -- 类被加载。说明成功加载了String类。是因为在指定由AppClassLoader加载String类时,由AppClassLoader一直委派到BootStrap加载。虽然是由子加载器的父加载器加载的,但是也可以说,父加载器加载的类对于子加载器来说是可见的。这同样依赖于委派机制。其实在虚拟机启动初期,java.lang.String已经被BootStrap预加载了,这时再次加载,虚拟机发现已经加载,不会再重复加载。这同时也证明了类加载器的单一性。

测试代码

到此为止,类加载器的知识就全部讲完了。以下是整个测试代码:

package jg.zhang.java.testclassloader;

/**
 * 参考ImportNew上的一篇文章<<类加载器的工作原理>>,
 * 文章地址:http://www.importnew.com/6581.html
 *
 * Java类加载器基于三个机制:委托、可见性和单一性。
 * 委托机制是指将加载一个类的请求交给父类加载器,如果这个父类加载器不能够找到或者加载这个类,那么再加载它。
 * 可见性的原理是子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类加载器加载的类。
 * 单一性原理是指仅加载一个类一次,这是由委托机制确保子类加载器不会再次加载父类加载器加载过的类。
 *
 * 三种类加载器: 每个类加载器在创建的时候已经指定他们对应的目录, 也就是说每个类加载器去哪里加载类是确定的
 * 我认为在ClassLoader类中应该会有getTargetPath()之类的方法, 得到他们对应的路径,找了找jdk的文档,发现是没有的.
 * AppClassLoader -- 加载classpath指定的路径中的类
 * ExtClassLoader -- 加载jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下的类
 * BootStrap  -- 加载JRE/lib/rt.jar中的类
 *
 *
 *
 * @author zhangjg
 *
 */
public class ClassLoaderTest {

 public static void main(String[] args) {
 test1();
 test2();
 test3();
 }

 /**
 * 验证线程上下文类加载器
 */
 private static void test3() {
 /**
 * 1 每个线程都会有一个上下文类加载器,由于在线程执行时加载用到的类,默认情况下是父线程
 * 的上下文类加载器, 也就是AppClassLoader
 */
 new Thread(new Runnable() {

 @Override
 public void run() {
 ClassLoader threadcontextClassLosder = Thread.currentThread().getContextClassLoader();
 System.out.println(threadcontextClassLosder); //sun.misc.Launcher$AppClassLoader@19821f
 }
 }).start();

 /**
 * 2 也可以给创建的线程设定特定的上下文类加载器
 */
 Thread th = new Thread(new Runnable() {

 @Override
 public void run() {
 ClassLoader threadcontextClassLosder = Thread.currentThread().getContextClassLoader();
 System.out.println(threadcontextClassLosder); //jg.zhang.java.testclassloader.ClassLoaderTest$3@1b67f74
 }
 });

 th.setContextClassLoader(new ClassLoader() {});

 th.start();
 }

 /**
 * 测试可见性,可见性依赖于委托机制
 */
 private static void test2() {

 /**
 * 1 让ExtClassLoader加载 jg.zhang.java.testConcurrent.Person这个类
 * 因为这个类不在jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下
 * 所以抛出ClassNotFoundException
 *
 * 所以父加载器不能加载应该被子加载器加载的类,这个类在父加载器中不可见
 * 这种机制依赖于委派机制
 */

 try {
 Class.forName("jg.zhang.java.testConcurrent.Person", true,
  ClassLoaderTest.class.getClassLoader().getParent());
 System.out.println("1 -- 类被加载");
 } catch (ClassNotFoundException e) {
 //e.printStackTrace();
 System.out.println("1 -- 未找到类");
 }

 /**
 * 2 让AppClassLoader加载java.lang.String类
 * 没有抛出异常,说明类被正常加载了
 * 虽然是由AppClassLoader一直委派到BootStrap而加载的
 * 所以可以说,父加载器加载的类对于子加载器来说是可见的,这同样依赖于委派机制
 *
 * 其实在虚拟机启动初期,java.lang.String已经被BootStrap预加载了
 * 这时再次加载,虚拟机发现已经加载,不会再重复加载
 */
 try {
 Class.forName("java.lang.String", true,
  ClassLoaderTest.class.getClassLoader());
 System.out.println("2 -- 类被加载");
 } catch (ClassNotFoundException e) {
 //e.printStackTrace();
 System.out.println("2 -- 未找到类");
 }

 }

 /**
 * 验证三种类加载器的父子关系
 */
 private static void test1() {
 ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();
 System.out.println(appClassLoader); //sun.misc.Launcher$AppClassLoader@19821f

 ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader();
 System.out.println(sysClassLoader); //sun.misc.Launcher$AppClassLoader@19821f
 //由上面的验证可知, 应用程序类加载器和系统类加载器是相同的, 因为地址是一样的

 ClassLoader extClassLoader = appClassLoader.getParent();
 System.out.println(extClassLoader); //sun.misc.Launcher$ExtClassLoader@addbf1
 //AppClassLoader的父加载器是ExtClassLoader

 System.out.println(extClassLoader.getParent()); //null
 //ExtClassLoader的父加载器是null, 也就是BootStrap,这是由c语言实现的

 }

}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 深入解析Java中的Class Loader类加载器

    类加载的过程 类加载器的主要工作就是把类文件加载到JVM中.如下图所示,其过程分为三步: 1.加载:定位要加载的类文件,并将其字节流装载到JVM中: 2.链接:给要加载的类分配最基本的内存结构保存其信息,比如属性,方法以及引用的类.在该阶段,该类还处于不可用状态: (1)验证:对加载的字节流进行验证,比如格式上的,安全方面的: (2)内存分配:为该类准备内存空间来表示其属性,方法以及引用的类: (3)解析:加载该类所引用的其它类,比如父类,实现的接口等. 3.初始化:对类变量进行赋值. 类加载器

  • java 详解类加载器的双亲委派及打破双亲委派

    java 详解类加载器的双亲委派及打破双亲委派 一般的场景中使用Java默认的类加载器即可,但有时为了达到某种目的又不得不实现自己的类加载器,例如为了达到类库的互相隔离,例如为了达到热部署重加载功能.这时就需要自己定义类加载器,每个类加载器加载各自的类库资源,以此达到资源隔离效果.在对资源的加载上可以沿用双亲委派机制,也可以打破双亲委派机制. 一.沿用双亲委派机制自定义类加载器很简单,只需继承ClassLoader类并重写findClass方法即可.如下例子: ①先定义一个待加载的类Test,它

  • java基础学习笔记之类加载器

    类加载器 java类加载器就是在运行时在JVM中动态地加载所需的类,java类加载器基于三个机制:委托,可见,单一. 把classpath下的那些.class文件加载进内存,处理后成为字节码,这些工作是类加载器做的. 委托机制指的是将加载类的请求传递给父加载器,如果父加载器找不到或者不能加载这个类,那么再加载他. 可见性机制指的是父加载器加载的类都能被子加载器看见,但是子加载器加载的类父加载器是看不见的. 单一性机制指的是一个类只能被同一种加载器加载一次. 默认类加载器 系统默认三个类加载器:

  • classloader类加载器_基于java类的加载方式详解

    基础概念 Classloader 类加载器,用来加载 Java 类到 Java 虚拟机中.与普通程序不同的是.Java程序(class文件)并不是本地的可执行程序.当运行Java程序时,首先运行JVM(Java虚拟机),然后再把Java class加载到JVM里头运行,负责加载Java class的这部分就叫做Class Loader. JVM本身包含了一个ClassLoader称为Bootstrap ClassLoader,和JVM一样,BootstrapClassLoader是用本地代码实现

  • JAVA提高第七篇 类加载器解析

    今天我们学习类加载器,关于类加载器其实和JVM有很大关系,在这里这篇文章只是简单的介绍下类加载器,后面学习到JVM的时候还会详细讲到类加载器,本文分为下面几个小节讲解: 一.认识类加载器 1.什么是类加载器? 所谓的类加载器可以从其作用来理解,其功能就是将classpath目录下.class文件,加载到内存中来进行一些处理,处理完的结果就是一些字节码.那是谁把这些class类加载到内存中来的呢?就是类加载器. 2.JVM中默认的类加载器有哪些? java虚拟机中可以安装多个类加载器,系统默认三个

  • java类加载器和类反射使用示例

    一.一个命令对应一个进程. 当我们启动一个Java程序,即启动一个main方法时,都将启动一个Java虚拟机进程,不管这个进程有多么复杂.而不同的JVM进程之间是不会相互影响的.这也就是为什么说,Java程序只有一个入口--main方法,让虚拟机调用.而两个mian方法,对应的是2个JVM进程,启动的是两个不同的类加载器,操作的实际上是不同的类.故而不会互相影响. 二.类加载. 当我们使用一个类,如果这个类还未加载到内存中,系统会通过加载.连接.初始化对类进行初始化. 1.类加载:指的是将类的c

  • java 类加载与自定义类加载器详解

    类加载 所有类加载器,都是ClassLoader的子类. 类加载器永远以.class运行的目录为准. 读取classpath根目录下的文件有以下几种方式: 1 在Java项目中可以通过以下方式获取classspath下的文件: public void abc(){ //每一种读取方法,使用某个类获取Appclassloader ClassLoader cl = ReadFile.class.getClassLoader(); URL url = cl.getResource("a.txt&quo

  • Java中的类加载器_动力节点Java学院整理

    从java的动态性到类加载机制 Java是一种动态语言.那么怎样理解这个"动态"呢?或者说一门语言具备了什么特性,才能称之为动态语言呢?对于java,我是这样理解的. JVM(java虚拟机)执行的不是本地机器码指令,而是执行一种称之为字节码的指令(存在于class文件中).这就要求虚拟机在真正执行字节码之前,先把相关的class文件加载到内存中.虚拟机不是一次性加载所有需要的class文件,因为它在执行的时候根本不会知道以后会用到哪些class文件.它是每用到一个类,就会在运行时&q

  • 深入理解Java中的final关键字_动力节点Java学院整理

    Java中的final关键字非常重要,它可以应用于类.方法以及变量.这篇文章中我将带你看看什么是final关键字?将变量,方法和类声明为final代表了什么?使用final的好处是什么?最后也有一些使用final关键字的实例.final经常和static一起使用来声明常量,你也会看到final是如何改善应用性能的. final关键字的含义? final在Java中是一个保留的关键字,可以声明成员变量.方法.类以及本地变量.一旦你将引用声明作final,你将不能改变这个引用了,编译器会检查代码,如

  • Java中的super关键字_动力节点Java学院整理

    一.super关键字 在JAVA类中使用super来引用父类的成分,用this来引用当前对象,如果一个类从另外一个类继承,我们new这个子类的实例对象的时候,这个子类对象里面会有一个父类对象.怎么去引用里面的父类对象呢?使用super来引用,this指的是当前对象的引用,super是当前对象里面的父对象的引用. super关键字测试 package com.bjpowernode.test; /** * 父类 * * */ class FatherClass { public int value

  • Java 中的 BufferedReader 介绍_动力节点Java学院整理

    BufferedReader 介绍 BufferedReader 是缓冲字符输入流.它继承于Reader. BufferedReader 的作用是为其他字符输入流添加一些缓冲功能. BufferedReader 函数列表 BufferedReader(Reader in) BufferedReader(Reader in, int size) void close() void mark(int markLimit) boolean markSupported() int read() int

  • Java 中的Printstream介绍_动力节点Java学院整理

    PrintStream 介绍 PrintStream 是打印输出流,它继承于FilterOutputStream. PrintStream 是用来装饰其它输出流.它能为其他输出流添加了功能,使它们能够方便地打印各种数据值表示形式. 与其他输出流不同,PrintStream 永远不会抛出 IOException:它产生的IOException会被自身的函数所捕获并设置错误标记, 用户可以通过 checkError() 返回错误标记,从而查看PrintStream内部是否产生了IOException

  • Java中IO流简介_动力节点Java学院整理

    Java io简介 Java io系统的设计初衷,就是为了实现"文件.控制台.网络设备"这些io设置的通信.例如,对于一个文件,我们可以打开文件,然后进行读取和写入.在java 1.0中,java提供的类都是以字节(byte)为单位,例如,FileInputStream和FileOutputStream.而到了java 1.1,为了与国际化进行接轨,在java io中添加了许多以字符(Unicode)为单位进行操作的类. 在java io的称呼中,我们经常会提到"输入流&qu

  • Java 中的 DataInputStream 介绍_动力节点Java学院整理

    DataInputStream 介绍 DataInputStream 是数据输入流.它继承于FilterInputStream. DataInputStream 是用来装饰其它输入流,它"允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型".应用程序可以使用DataOutputStream(数据输出流)写入由DataInputStream(数据输入流)读取的数据. DataInputStream 函数列表 DataInputStream(InputStream in

  • Java中的PrintWriter 介绍_动力节点Java学院整理

    PrintWriter 介绍 PrintWriter 是字符类型的打印输出流,它继承于Writer. PrintStream 用于向文本输出流打印对象的格式化表示形式.它实现在 PrintStream 中的所有 print 方法.它不包含用于写入原始字节的方法,对于这些字节,程序应该使用未编码的字节流进行写入. PrintWriter 函数列表 PrintWriter(OutputStream out) PrintWriter(OutputStream out, boolean autoFlus

  • Java中的 FilterInputStream简介_动力节点Java学院整理

    FilterInputStream 介绍 FilterInputStream 的作用是用来"封装其它的输入流,并为它们提供额外的功能".它的常用的子类有BufferedInputStream和DataInputStream. BufferedInputStream的作用就是为"输入流提供缓冲功能,以及mark()和reset()功能". DataInputStream 是用来装饰其它输入流,它"允许应用程序以与机器无关方式从底层输入流中读取基本 Java

  • Java 中的CharArrayReader 介绍_动力节点Java学院整理

    CharArrayReader 介绍 CharArrayReader 是字符数组输入流.它和ByteArrayInputStream类似,只不过ByteArrayInputStream是字节数组输入流,而CharArray是字符数组输入流.CharArrayReader 是用于读取字符数组,它继承于Reader.操作的数据是以字符为单位! CharArrayReader 函数列表  CharArrayReader(char[] buf) CharArrayReader(char[] buf, i

随机推荐