`

java类加载机制概述1

    博客分类:
  • JAVA
阅读更多

从事java研发必然少不了对java类加载机制的涉及,本文结合例子讲述java classloader工作机制。

一 jvm 类加载机制

1)jvm位置:java是运行在java虚拟机上的程式,java虚拟机物理层面上来讲,就是我们安装在电脑上的jre目录/lib/jvm.dll(版本不同,可能存在于jre目录/lib/client/jvm.dll,jre目录/lib/server/jvm.dll),这是java字节码运行的基础,它不是由java语言编写,所以我们阅读jdk源码时遇到native函数,基本上就是调用jvm相关的代码。

2)jdk和jre关系:从oracle官网上下载java环境,可以选择jdk或者jre进行安装,他们的关系可以理解为子集的概念,jdk是jre运行环境再加上一些java开发的工具集,查看jdk目录结构如下(例子为jdk1.6.37版本)

D:.
├─bin
│  └─server
├─include
│  └─win32
├─jre
│  ├─bin
│  │  ├─dtplugin
│  │  ├─plugin2
│  │  └─server
│  └─lib
│      ├─amd64
│      ├─applet
│      ├─audio
│      ├─cmm
│      ├─deploy
│      ├─ext
│      ├─fonts
│      ├─im
│      ├─images
│      │  └─cursors
│      ├─management
│      ├─security
│      ├─servicetag
│      └─zi
│          ├─Africa
│          ├─America
│          │  ├─Argentina
│          │  ├─Indiana
│          │  ├─Kentucky
│          │  └─North_Dakota
│          ├─Antarctica
│          ├─Asia
│          ├─Atlantic
│          ├─Australia
│          ├─Etc
│          ├─Europe
│          ├─Indian
│          ├─Pacific
│          └─SystemV
└─lib
    └─visualvm
        ├─etc
        ├─platform
        │  ├─config
        │  │  ├─ModuleAutoDeps
        │  │  └─Modules
        │  ├─core
        │  │  └─locale
        │  ├─docs
        │  ├─lib
        │  │  └─locale
        │  ├─modules
        │  │  ├─ext
        │  │  │  └─locale
        │  │  └─locale
        │  └─update_tracking
        ├─profiler
        │  ├─config
        │  │  └─Modules
        │  ├─lib
        │  │  ├─deployed
        │  │  │  ├─jdk15
        │  │  │  │  └─windows-amd64
        │  │  │  └─jdk16
        │  │  │      └─windows-amd64
        │  │  └─locale
        │  ├─modules
        │  │  └─locale
        │  └─update_tracking
        └─visualvm
            ├─config
            │  └─Modules
            ├─core
            │  └─locale
            ├─modules
            │  └─locale
            └─update_tracking

 

 

java官方文档描述jre和jdk关系如图:(链接http://docs.oracle.com/javase/7/docs/)



 

 

在安装jdk时可以选择是否同时安装jre,如果选择安装,那么系统中就存在两份jre,具体程序运行时会执行哪个jre,windows系统默认搜索规则是:

1. 当前目录下有沒有 JRE子目录

2. 父目录下 JRE 子目录

3.查 詢 Window Registry(HKEY_LOCAL_MACHINE\Software\JavaSoft\Java Runtime Environment\)

注意:安装环境会建议建立JAVA_HOME环境变量并将其加入path中,这样可以避免因为默认搜索规则出的结果造成混淆。如果没有加,可以搜索下自己系统中有几个java.exe,本人系统中有三个,分别在c:/windows/system32/java.ext;

d:/program files/java/jdk_1_6_37/bin/java.exe;

d:/program files/java/jre/bin/java.exe

 

因为没有将JAVA_HOME路径加入到path,path路径是c:/windows/system32;......所以在命令行下执行java Main系统默认执行的是c:/windows/system32/java.exe(除非命令行在其他两个java.exe所在目录),这一点可以通过分别修改三个路径下java.exe文件到新名字java1.exe来验证到底执行的是哪个目录

3)java类加载机制:jdk带有三个系统类加载器:bootstrap加载器;扩展加载器;系统加载器,他们的关系如下表

类加载器

被加载加载器 parent 父类 类型 默认加载目录/文件 备注

bootstrap加载器

       

 sun.boot.class.path系统属性所指路径,指向jre下/lib,如rt.jar

 

虚拟机出于安全等因素考虑,不会加载< Java_Runtime_Home >/lib存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的

扩展加载器

 

bootstrap加载器

 

bootstrap加载器(因为此加载器由非java语言编写,在jvm中标识为null,所以一个加载器的parent为null表示它是由bootstrap加载器加载)

 java.lang.ClassLoader->java.security.SecurClassLoader->java.net.URLClassLoader sun.misc.Launcher$ExtClassLoader  java.ext.dirs属性所指路径,指向java.exe所在jre下/lib/ext子目录,可以将自己的class文件放入这个目录,交由扩展加载器加载,可以通过–Djava.ext.dirs=xxx 改变  jvm中只存在一份,一旦建立,再通过System.setProperty()修改系统属性不会起作用

系统加载器

 

bootstrap加载器

 

扩展加载器

 java.lang.ClassLoader->java.security.SecurClassLoader->java.net.URLClassLoader sun.misc.Launcher$AppClassLoader  默认为.目录

再取java.class.path属性所指路径,可以通过java -cp xxx 来改变

最后取环境变量CLASSPATH下的class文件和jar文件

  jvm中只存在一份,一旦建立,再通过System.setProperty()修改系统属性不会起作用

 

在 Java 之中,每个类都是由某个类型加载器(ClassLoader 的实体)来载入,因此,Class 类型的实体中,都会有记录载入它的ClassLoader 的实体(注意:如果值是null,不代表它不是由类加载器载入,而是代表这个类別是由(bootstrap loader,也有人称root loader)所载入,只不过这个类型加载器不由java书写,所以逻辑上没有实体 )

 

二 自定义类加载器

加载类到内存中分两种方式:1)预加载 ;2)显示加载。预加载是虚拟机在启动的时候将rt.jar中的类一次加载到内存,因为这些类都是基础类,会被频繁使用到,预加载可以减少运行时IO开销,显示加载可以:1)使用new()操作符 2)java.lang.Class 裡的forName() 3)java.lang.ClassLoader 裡的loadClass()

要查看类加载详情,可以使用java -verbose:class xxx来输出。看下面一段代码:

 

public class Main
{
public static void main(String args[])
{
A a1 = new A() ;
a1.print() ;
B b1 = new B() ;
b1.print() ;
}
}

public class A //与Main在同一个路径下
{
public void print()
{
System.out.println("Using Class A") ;
}
}

public class B //与Main在同一个路径下
{
public void print()
{
System.out.println("Using Class B") ;
}
}
 到Main所在目录执行javac *.java,查看生成了三个calss文件,再执行java -verbose:class Main > load.log ,查看load.log内容如下:

 

 

[Opened D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar]
[Loaded java.lang.Object from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar]
[Loaded java.io.Serializable from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar]
[Loaded java.lang.Comparable from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar]
...
...
[Loaded java.security.Principal from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar]
[Loaded Main from file:/D:/deep_java/]
[Loaded A from file:/D:/deep_java/]
Using Class A
[Loaded B from file:/D:/deep_java/]
Using Class B
[Loaded java.lang.Shutdown from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from D:\Program Files\Java\jdk1.6.0_37\jre\lib\rt.jar]
 由此可见class类加载顺序。

 

也可以使用以下方式加载类:

 

import java.net.* ;
public class Test
{
public static void main(String args[]) throws Exception
{
		Class c = Class.forName(args[0]) ; //第一种载入class对象方法
		Object o = c.newInstance() ;
		//Class c = Class.forName(args[0],true,off.getClass().getClassLoader()) ;//true参数表示载入同时进行初始化,这个参数在SPI接口和实现类加载中非常有用
		Test off = new Test() ;
		System.out.println("类型准备载入") ;
		//ClassLoader loader = off.getClass().getClassLoader() ;//第二种载入class对象方法,使用了对象引用Class的classloader
		//Class c = loader.loadClass(args[0]) ;
		System.out.println("类型准备实例化") ;
		Object o = c.newInstance() ;
		Object o2 = c.newInstance() ;
		}
}
 

 

了解了默认类加载机制后,可以手工打造一个加载器,ExtClassLoader和AppClassLoader都是继承URLClassLoader,自己的加载器也可以继承自这个类:

 

import java.net.* ;
public class Test
{
public static void main(String args[]) throws Exception
{
		URL u = new URL("file:/D:/deep_java/test/lib/") ;
		URLClassLoader ucl = new URLClassLoader(new URL[]{ u }) ;
		Class c = ucl.loadClass(args[0]) ;
		Assembly asm = (Assembly) c.newInstance() ;
		asm.start() ;
		URL u1 = new URL("file:/D:/deep_java/test/lib/") ;
		URLClassLoader ucl1 = new URLClassLoader(new URL[]{ u1 }) ;
		Class c1 = ucl1.loadClass(args[0]) ;
		Assembly asm1 = (Assembly) c1.newInstance() ;
		asm1.start() ;
		System.out.println(Test.class.getClassLoader()) ;
		System.out.println(u.getClass().getClassLoader()) ;
		System.out.println(ucl.getClass().getClassLoader()) ;
		System.out.println(c.getClassLoader()) ;
		System.out.println(asm.getClass().getClassLoader()) ;
		System.out.println(u1.getClass().getClassLoader()) ;
		System.out.println(ucl1.getClass().getClassLoader()) ;
		System.out.println(c1.getClassLoader()) ;
		System.out.println(asm1.getClass().getClassLoader()) ;
		System.out.println(Assembly.class.getClassLoader()) ;
		
}
 deep_java/test/ 目录结构如下:

 

 

├─Test.class
├─Assembly.class
├─lib
│  ├─ClassA.class
│  ├─ClassB.class
│  ├─ClassC.class
Assembly 是一个接口,ClassA ClassB ClassC都实现了这个接口,Test主程序在运行时将参数名作为Class名动态加载。命令行输入java -verbose:class Test ClassA 执行结果如下:
sun.misc.Launcher$AppClassLoader@37b90b39
null
null
java.net.URLClassLoader@55f33675
java.net.URLClassLoader@55f33675
null
null
java.net.URLClassLoader@525483cd
java.net.URLClassLoader@525483cd
sun.misc.Launcher$AppClassLoader@37b90b39
注意:两个对象的类加载器是不同的,基础类的加载器是bootstrap加载器,所以打印出来是null
 现在如果把lib目录下的class都移到test目录下,即目录为:

 

 

├─Test.class
├─Assembly.class
├─ClassA.class
├─ClassB.class
├─ClassC.class
 代码中url的路径URL u1 = new URL("file:/D:/deep_java/test/lib/") ;修改为

 

URL u1 = new URL("file:/D:/deep_java/test/") ;看看执行结果是啥:

 

sun.misc.Launcher$AppClassLoader@53004901
null
null
sun.misc.Launcher$AppClassLoader@53004901
sun.misc.Launcher$AppClassLoader@53004901
null
null
sun.misc.Launcher$AppClassLoader@53004901
sun.misc.Launcher$AppClassLoader@53004901
sun.misc.Launcher$AppClassLoader@53004901
 为啥ClassA类的加载器都变成了系统类加载器AppClassLoader呢,这是因为jvm的双亲委托机制在起作用。注意下加载Test主程序的加载器是sun.misc.Launcher$AppClassLoader@53004901,自己创建的类加载器URLClassLoader默认将sun.misc.Launcher$AppClassLoader@53004901作为自己的parent,当loadClass方法被调用时,默认机制会首先请求parent去findClass(),如果找不到再自己加载,因为sun.misc.Launcher$AppClassLoader@53004901默认加载目录是当前目录,刚好能够加载到ClassA,所以每次ClassA都能被这个加载器加载。

 

 

大家可能会注意到ClassLoader的两个方法,loadClass 和 findClass,看下它的源码:

 

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

protected synchronized Class<?> loadClass(String name, boolean resolve)
	throws ClassNotFoundException
    {
	// First, check if the class has already been loaded
	Class c = findLoadedClass(name);
	if (c == null) {
	    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.
	        c = findClass(name);
	    }
	}
	if (resolve) {
	    resolveClass(c);
	}
	return c;
    }
loadClass(String name)的逻辑是:先检查已经加载的类,如果未加载,先请求parent加载器加载,如果未找到再调用自己的findClass(name)方法。双亲委托机制在loadClass中体现出来。因此自己写类加载器,最好是覆盖findClass(name)方法,而不是loadClass方法,保留默认的双亲委托机制,以免程序在应用程式下可以用,迁移到web下出现问题。至于为啥java要使用双亲委托机制,主要是考虑安全问题,如果一个java核心类被用户自己的class覆盖了,程序运行时可能会出现不可预知的错误,系统将会变得脆弱。双亲委托机制可以保证只要不篡改jre目录下的jar文件,虚拟机加载的基础类就不会被应用程序私有类影响。

 

看看自己如何重写findClass方法

 

import mylib.Target;

public class Test{
	public static void main(String args[]) throws Exception{
		MyClassLoader mcl = new MyClassLoader("myClassLoaderA");
		System.out.println("myClassLoaderA->parent="+mcl.getParent()) ;
		Class target = mcl.loadClass("Target");
		System.out.println("Target classloader ===============" + target.getClassLoader());
		Object o = (Object) target.newInstance();
		MyClassLoader mclB = new MyClassLoader("myClassLoaderB");
		System.out.println("myClassLoaderB->parent="+mclB.getParent()) ;
		Class targetb = mcl.loadClass("Target");//注释1
		Target o1 = (Target) target.newInstance();//注释2
		System.out.println("Target classloader ===============" + o1.getClass().getClassLoader());
	}
}

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class MyClassLoader extends ClassLoader {
	private String name;
	public MyClassLoader(String name) {
		super(); // 通过这个构造方法生成的类加载器,它的父加载器是系统类加载器
		this.name = name;
	}

	public MyClassLoader(String name, ClassLoader loader) {
		super(loader); // 通过这个这个构造方法生成的类加载器,该加载器的父加载器是loader,如果为空,则父加载器为根加载器
		// 子类继承父类,如果不显式写出调用父类的哪个构造方法,那么就默认调用父类的无参构造函数
		this.name = name;
	}
	public String toString() {
		return this.name;
	}
	// 要重写findclass这个方法,loadclass会调用它
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		// TODO Auto-generated method stub
		byte[] data = null;
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(
					"D:\\deep_java\\classLoader\\mylib\\" + name
							+ ".class");
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		ByteArrayOutputStream abos = new ByteArrayOutputStream();
		int ch = 0;
		try {
			while (-1 != (ch = fis.read())) {
				abos.write(ch); // 把字节一个一个写到输出流中
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		data = abos.toByteArray(); // 把输出流中的字节弄成一个字节数组
		return this.defineClass("mylib." + name, data, 0,
				data.length, null);
	}
}	
目录结构为:
 
├─Test.class
├─MyClassLoader.class
├─mylib
├-├─Target.class
 注释1:不同类加载器加载的类不能相互赋值,即使两个类字节码完全相同,如果两个classloader类从同一个class文件加载,执行时会报链接错误Exception in thread "main" java.lang.LinkageError: loader (instance of MyClassL//oader): attempted duplicate class definition for name: "mylib/Target"
注释2:不同类加载器加载的类之间不能赋值或者隐式转换,会报运行时类型转换错误 java.lang.ClassCastException: mylib.Target cannot be cast to mylib.Target at Test.main(Test.java:19)

二 特殊类加载器

jvm默认的类加载机制和双亲委托机制能够解决类加载的安全问题,但是有些场景下无法满足

场景一:SPI规范定义的接口在java基础包中,预加载时是被bootstrap加载器载入,但是实现类是各个厂商的jar包,会被其他加载器加载 

场景二:某些框架必须查看用户创建的而非本身创建的类和资源,所以两种特殊类加载器应运而生:

 

1 线程上下文加载

默认使用系统类加载器,如果不做任何修改,任何线程的默认加载器是系统类加载器,但是线程加载器是可以自己制定的,制定的就可以不遵循双亲委派。因为很多情况下双亲委派解决不了,所以需要定制的,这也算sun给自己开的一个后门

2 伙伴加载器

Eclipse 新闻组中用来解释伙伴类加载的流行示例是 HibernateHibernate 框架必须查看用户创建而非 Hibernate 本身一部分的类和资源
 

 

 

 

  • 大小: 38.7 KB
分享到:
评论

相关推荐

    java类加载机制.xmind

    该文件是JVM中关于类加载机制的知识整理的思维导图,包括类加载机制概述、类加载的生命周期、加载时机、加载过程、类加载、类的初始化和实例化等几个大方面进行了讲解,其中类加载中还对JVM三种预定义类加载器进行了...

    Java基础加强之类加载器

    学习概述:本模块深入讲解了Java类加载方面的知识,Java类加载器和类加载机制以及类加载原理  学习目标:掌握类加载机制和原理,能够独立开发自己的类加载器。  1.类的加载  什么是类加载? 类加载是指将类的...

    Java反射机制——类的加载方法,创建对象,获取方法以及结构

    一、java反射机制概述 Reflection (反射)被视为动态语言的关键,为什么这么说呢,是因为它在运行时就确定下来了。反射机制允许程序在执行期间借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的...

    JAVA_API1.6文档(中文)

    java.security.interfaces 提供的接口用于生成 RSA Laboratory Technical Note PKCS#1 中定义的 RSA(Rivest、Shamir 和 Adleman AsymmetricCipher 算法)密钥,以及 NIST 的 FIPS-186 中定义的 DSA(数字签名算法)...

    JAVA反射机制详解视频

    (类的加载概述和加载时机) (类加载器的概述和分类) (获取class文件对象的三种方式) (通过反射获取无参构造方法并使用) (通过反射获取带参构造方法并使用) (通过反射获取私有构造方法并使用) (通过反射获取成员变量并...

    J2SE笔记讲解个人修订(1.1).docx

    14 JAVA类加载器CLASSLOADER 15 JAVA简单工厂模式 16 JAVA中的注解 17 JAVA 图形界面 18 JAVA多线程 19 JAVA 反射机制 20 JAVA克隆CLONE(复制) 21 JAVA 网络编程 22 JAVA 其他未归类 23 JNI概述

    Java 1.6 API 中文 New

    java.security.interfaces 提供的接口用于生成 RSA Laboratory Technical Note PKCS#1 中定义的 RSA(Rivest、Shamir 和 Adleman AsymmetricCipher 算法)密钥,以及 NIST 的 FIPS-186 中定义的 DSA(数字签名算法)...

    JavaAPI1.6中文chm文档 part1

    java.security.interfaces 提供的接口用于生成 RSA Laboratory Technical Note PKCS#1 中定义的 RSA(Rivest、Shamir 和 Adleman AsymmetricCipher 算法)密钥,以及 NIST 的 FIPS-186 中定义的 DSA(数字签名算法)...

    java api最新7.0

    java.security.interfaces 提供的接口用于生成 RSA Laboratory Technical Note PKCS#1 中定义的 RSA(Rivest、Shamir 和 Adleman AsymmetricCipher 算法)密钥,以及 NIST 的 FIPS-186 中定义的 DSA(数字签名算法)...

    疯狂JAVA讲义

    第1章 Java概述 1 1.1 Java语言的发展简史 2 1.2 Java的竞争对手及各自优势 4 1.2.1 C#简介和优势 4 1.2.2 Ruby简介和优势 4 1.2.3 Python的简介和优势 5 1.3 Java程序运行机制 5 1.3.1 高级语言的运行机制 6...

    Java疯狂讲义课后习题答案

    因此,我们可以使用该工具来快速...疯狂Java讲义笔记汇总 目录 一、基础类型 二、流程控制与数组 三、面向对象 四、基础类库 五、集合 六、泛型 七、异常 八、数据库 九、注释 十、输入输出 十一、网络 十二、类加载机制

    JavaAPI中文chm文档 part2

    java.security.interfaces 提供的接口用于生成 RSA Laboratory Technical Note PKCS#1 中定义的 RSA(Rivest、Shamir 和 Adleman AsymmetricCipher 算法)密钥,以及 NIST 的 FIPS-186 中定义的 DSA(数字签名算法)...

    java jdk-api-1.6 中文 chmd

    java.security.interfaces 提供的接口用于生成 RSA Laboratory Technical Note PKCS#1 中定义的 RSA(Rivest、Shamir 和 Adleman AsymmetricCipher 算法)密钥,以及 NIST 的 FIPS-186 中定义的 DSA(数字签名算法)...

    深入理解_Java_虚拟机 JVM_高级特性与最佳实践

    / 170 第7章 虚拟机类加载机制 / 171 7.1 概述 / 171 7.2 类加载的时机 / 172 7.3 类加载的过程 / 176 7.3.1 加载 / 176 7.3.2 验证 / 178 7.3.3 准备 / 181 7.3.4 解析 / 182 7.3.5 初始化 / 186 7.4 类...

    JAVA基础课程讲义

    静态初始化块(经常用来初始化类,加载类信息时执行!) 67 package 68 JDK中的主要包 68 import 68 eclipse的使用 69 继承(extend, inheritance) 70 为什么需要继承?继承的作用? 70 继承介绍 70 如何实现继承? ...

    Java虚拟机

    第三部分分析了虚拟机的执行子系统,包括类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎。第四部分讲解了程序的编译与代码的优化,阐述了泛型、自动装箱拆箱、条件编译等语法糖的原理;讲解了虚拟机的热点探测...

    [Java参考文档]

    javax.management.loading 提供实现高级动态加载的类。 javax.management.modelmbean 提供了 ModelMBean 类的定义。 javax.management.monitor 提供 monitor 类的定义。 javax.management.openmbean 提供开放数据...

    Java服务器程序设计

    全书从java服务器的体系结构、开发工具和管理工具、编程技术、安全机制等四个方面全面介绍java服务器的升友技术。通过阅读本书,读者不仅能够知道用java服务器体系结构开发servlet与用传统cgi编写程序的好处,而且还...

Global site tag (gtag.js) - Google Analytics