`

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类加载机制 深入研究Java类加载机制

    #### 一、Java类加载机制概述 Java类加载机制是Java程序运行的第一步,它对于理解Java虚拟机(JVM)的行为至关重要。类加载过程涉及到类的加载、链接(验证、准备、解析)、初始化等阶段,并且这一过程是由类加载器...

    java类加载机制.xmind

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

    java类加载器

    在Java中,类加载器(Class Loader)是一项核心机制,用于将字节码(.class文件)加载到JVM中,使其成为运行时的对象。类加载器不仅实现了类的加载功能,还确保了Java程序在多线程环境中的安全性和隔离性。类加载器...

    Java类加载内幕详细讲解

    在Java世界中,类加载机制是一项极为重要的技术。它不仅关乎程序的启动与运行,更是Java动态性与灵活性的基础。本文旨在深入探讨Java类加载的过程、原理及其在实际开发中的应用。 #### 二、类与数据的概念 在Java...

    反射机制和类加载机制学习总结

    #### 六、类加载机制概述 类加载机制是指Java虚拟机如何将Java类从文件系统加载到内存中,并准备好让Java程序使用的整个过程。这个过程包括了类的加载、连接和初始化三个阶段。 1. **加载**:找到并加载类的二进制...

    Java类加载内幕

    Java 的类加载机制是其核心特性之一,虽然不是经常被提及的热门话题,但对于每一位 Java 开发者来说,掌握这一机制的工作原理至关重要。本文旨在深入剖析 Java 类加载的过程,帮助读者理解类加载的基本概念及其在 ...

    ClassLoader类加载机制和原理详解

    1. 类加载机制概述 Java的类加载机制遵循“双亲委派模型”(Delegation Model)。当一个类被加载时,它首先会尝试由当前线程的Context ClassLoader进行加载,如果该类加载器无法加载,则向上委托给父类加载器,直至...

    Java类动态加载机制在铁路互联网售票中的设计与实现.pdf

    ### Java类动态加载机制在铁路互联网售票中的设计与实现 #### 概述 随着铁路互联网售票系统的广泛应用,其面临着越来越高的并发访问压力。为解决这一问题,系统采用分布式内存数据库集群来存储和处理如余票查询、...

    JVM实战-JVM类加载机制案例分析

    本次实验的主要目的是深入理解Java虚拟机(JVM)中的类加载机制。通过实践操作,掌握类的加载、连接与初始化过程,了解不同类型的类加载器及其工作原理,学会创建自定义的类加载器,并对类的卸载有所认识。实验将结合...

    类的加载机制笔记

    #### 二、类的加载机制概述 类的加载过程主要包括以下几个阶段: 1. **加载(Loading)** 2. **验证(Verification)** 3. **准备(Preparation)** 4. **解析(Resolution)** 5. **初始化(Initialization)** 6...

    使用Java类加载和类路径实现一个简单的在线购物网站.txt

    本文将详细介绍如何利用Java的类加载机制和类路径管理来构建一个简易的在线购物网站。该示例通过实现一个`ShoppingCart`类来管理用户的购物车信息,并提供添加商品、移除商品以及计算总价等功能。此外,我们还将探讨...

    Java反射动态加载实例类

    ### Java反射机制与动态加载实例类 在Java中,反射是一种强大的工具,允许程序在运行时检查和修改其结构和行为。通过反射,我们可以动态地加载类、创建对象、访问和修改字段、调用方法等。本文将深入探讨Java反射...

    java 类加载 。。。反射

    Java 类加载与反射是Java开发中的...总的来说,Java的类加载和反射机制是其动态性和灵活性的关键,理解和掌握这些概念对提升Java编程能力至关重要。在开发过程中,合理利用这些特性可以提高代码的可扩展性和维护性。

    JAVA基础知识概述

    - **安全模型**:通过沙箱安全机制、类加载机制等方式保护运行中的程序不受恶意代码攻击。 #### 二、Java的关键概念与特性 Java具备一系列重要的特性和概念,使得它成为一种非常强大且易于使用的编程语言。 #####...

    Java语言概述- Java语言

    1. **面向对象**:Java是一个完全面向对象的编程语言,支持类、接口和继承等面向对象特性,使得代码结构清晰,易于维护和扩展。 2. **简单性**:Java的设计目标之一是简化C++的复杂性,去除了一些容易导致错误的...

    Java3d加载3D模型的技术

    1. Java3D概述 Java3D是一种基于Java的三维图形编程接口,由Sun Microsystems开发。它提供了一种平台独立的三维图形编程接口,允许开发者创建交互式的三维应用程序。 2. 加载3D模型文件 Java3D提供了多种方式来...

    Java反射机制概述.zip

    1. **类加载器(Class Loader)**:Java程序中的类并非一开始就全部加载到内存,而是根据需要由类加载器动态加载。这为反射提供了基础。 2. **类对象(Class Object)**:每个类都有一个对应的Class对象,它包含了...

    java面试题--反射机制

    动态加载类是Java反射机制的重要应用场景之一。通过`Class.forName()`方法或者`ClassLoader`的`loadClass()`方法,可以根据类名字符串在运行时加载类。 #### 七、操作成员 - **创建对象**:使用`Class`对象的`new...

Global site tag (gtag.js) - Google Analytics