java安全沙箱(一)之ClassLoader双亲委派机制

本文深入探讨Java类加载机制,包括双亲委派模型的工作原理及其安全性保障。介绍了类加载器的不同层级,如启动类加载器、扩展类加载器和应用类加载器,并通过源码分析展示了它们的初始化过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

java是一种类型安全的语言,它有四类称为安全沙箱机制的安全机制来保证语言的安全性,这四类安全沙箱分别是:

本篇博客主要介绍“类加载体系”的基本原理;如需了解其它几类安全机制可以通过上面的博客链接进入查看。

简介

“类加载体系”及ClassLoader双亲委派机制。java程序中的 .java文件编译完会生成 .class文件,而 .class文件就是通过被称为类加载器的ClassLoader加载的,而ClassLoder在加载过程中会使用“双亲委派机制”来加载 .class文件,先上图:

151224_tHEL_1990220.png

看着图从上往下介绍:

  1. BootStrapClassLoader:启动类加载器,该ClassLoader是jvm在启动时创建的,用于加载 $JAVA_HOME/jre/lib下面的类库(或者通过参数-Xbootclasspath指定)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不能直接通过引用进行操作。

  2. ExtClassLoader:扩展类加载器,ClassLoader是在sun.misc.Launcher作为一个内部类ExtClassLoader定义的(即 sun.misc.Launcher$ExtClassLoader),ExtClassLoader会加载 $JAVA_HOME/jre/lib/ext下的类库(或者通过参数-Djava.ext.dirs指定)。

  3. AppClassLoader:应用程序类加载器,ClassLoader同样是在sun.misc.Launcher里作为一个内部类AppClassLoader定义的(即 sun.misc.Launcher$AppClassLoader),AppClassLoader会加载java环境变量CLASSPATH所指定的路径下的类库,而CLASSPATH所指定的路径可以通过System.getProperty("java.class.path")获取;当然,该变量也可以覆盖,可以使用参数-cp,例如:java -cp 路径 (可以指定要执行的class目录)。

  4. CustomClassLoader:自定义类加载器,ClassLoader是指我们自定义的ClassLoader,比如tomcat的StandardClassLoader属于这一类;当然,大部分情况下使用AppClassLoader就足够了

ClassLoader初始化源码

下面贴下jdk关于类加载的源码,上述四种类加载器中CustomClassLoader是用户自定义的,BootStrapClassLoader是jvm创建的,就不展示了;这里展示下AppClassLoader和ExtClassLoader的启动过程,前面介绍过,AppClassLoader和ExtClassLoader都是在sun.misc.Launcher里定义的,而我的sun.misc.Launcher没有源码,大家将就看看反编译的代码吧。如果想看sun.*包下的类源码,大家可以下载openjdk来查看。

 public Launcher(){
        ExtClassLoader extclassloader;
        try{
            extclassloader = ExtClassLoader.getExtClassLoader();
        }
        catch(IOException ioexception) {
            throw new InternalError("Could not create extension class loader");
        }
        try{
            loader = AppClassLoader.getAppClassLoader(extclassloader);
        }
        catch(IOException ioexception1){
            throw new InternalError("Could not create application class loader");
        }
        Thread.currentThread().setContextClassLoader(loader);
        String s = System.getProperty("java.security.manager");
        if(s != null){
            SecurityManager securitymanager = null;
            if("".equals(s) || "default".equals(s))
                securitymanager = new SecurityManager();
            else
                try{
                    securitymanager = (SecurityManager)loader.loadClass(s).newInstance();
                }
                catch(IllegalAccessException illegalaccessexception) { }
                catch(InstantiationException instantiationexception) { }
                catch(ClassNotFoundException classnotfoundexception) { }
                catch(ClassCastException classcastexception) { }
            if(securitymanager != null)
                System.setSecurityManager(securitymanager);
            else
                throw new InternalError((new StringBuilder()).append("Could not create SecurityManager: ").append(s).toString());
        }
    }

可以看到在Launcher构造函数的执行过程如下:

  1. 通过ExtClassLoader.getExtClassLoader()创建了ExtClassLoader;

  2. 通过AppClassLoader.getAppClassLoader(ExtClassLoader)创建了AppClassLoader,并将ExtClassLoader设为AppClassLoader的parent ClassLoader

  3. 通过Thread.currentThread().setContextClassLoader(loader)把AppClassLoader设为线程的上下文 ClassLoader;

  4. 根据jvm参数-Djava.security.manager创建安全管理器安全管理器的相关内容会在后续博客安全管理器及Java API中介绍),此时jvm会设置系统属性"java.security.manager"为空字符串""。

再贴下ExtClassLoader源码:

 public static ExtClassLoader getExtClassLoader()
            throws IOException{
            File afile[] = getExtDirs();
            return (ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction(afile) {
                public Object run() throws IOException{
                    int i = dirs.length;
                    for(int j = 0; j < i; j++)
                        MetaIndex.registerDirectory(dirs[j]);
                    return new ExtClassLoader(dirs);
                }

                final File val$dirs[];

                {
                    dirs = afile;
                    super();
                }
            });
            PrivilegedActionException privilegedactionexception;
            privilegedactionexception;
            throw (IOException)privilegedactionexception.getException();
        }
        
private static File[] getExtDirs()
        {
            String s = System.getProperty("java.ext.dirs");
            File afile[];
            if(s != null)
            {
                StringTokenizer stringtokenizer = new StringTokenizer(s, File.pathSeparator);
                int i = stringtokenizer.countTokens();
                afile = new File[i];
                for(int j = 0; j < i; j++)
                    afile[j] = new File(stringtokenizer.nextToken());

            } else
            {
                afile = new File[0];
            }
            return afile;
        }

反编译的源码,大家将就看下;这里大家关注下getExtDirs()这个方法,它会获取属性"java.ext.dirs"所对应的值,然后通过系统分隔符分割,然后加载分割后的字符串对应的目录作为ClassLoader的类加载库。

下面看看AppClassLoader源码:

public static ClassLoader getAppClassLoader(ClassLoader classloader) throws IOException{
            String s = System.getProperty("java.class.path");
            File afile[] = s != null ? Launcher.getClassPath(s) : new File[0];
            return (AppClassLoader)AccessController.doPrivileged(new PrivilegedAction(s, afile, classloader) {
                public Object run() {
                    URL aurl[] = s != null ? Launcher.pathToURLs(path) : new URL[0];
                    return new AppClassLoader(aurl, extcl);
                }

                final String val$s;
                final File val$path[];
                final ClassLoader val$extcl;

                {
                    s = s1;
                    path = afile;
                    extcl = classloader;
                    super();
                }
            });
        }

首先获取"java.class.path"对应的属性,并转换为URL[]并设置为ClassLoader的类加载库,注意这里的方法入参classloader就是ExtClassLoader,在创AppClassLoader会传入ExtClassLoader作为parent ClassLoader。

上面就是ClassLoader的启动和初始化过程,后面会把loader作为应用程序的默认ClassLoader使用,看下面的测试用例:

public static void main(String... args) {
		ClassLoader loader = Test.class.getClassLoader();
		System.err.println(loader);
		while (loader != null) {
			loader = loader.getParent();
			System.err.println(loader);
		}
	}

可以看到ClassLoader的层次结构,输出结果为:

190137_AhAa_1990220.png

ClassLoader双亲委派机制源码

前面谈到了ClassLoader的几类加载器,而ClassLoader使用双亲委派机制来加载class文件的。

ClassLoader的双亲委派机制是这样的(这里先忽略掉自定义类加载器CustomClassLoader):

  1. 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

  2. ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

  3. 如果BootStrapClassLoader加载失败(例如$JAVA_HOME/jre/lib里查找到该class),会使用ExtClassLoader来尝试加载;

  4. ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

下面贴下ClassLoader的loadClass(String name, boolean resolve)源码:

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;
	}

代码很明朗:首先找缓存(findLoadedClass),没有的话就判断有没有parent,有的话就用parent来递归的loadClass,然而ExtClassLoader并没有设置parent,则会通过findBootstrapClassOrNull来加载class,而findBootstrapClassOrNull则会通过JNI方法”private native Class findBootstrapClass(String name)“来使用BootStrapClassLoader来加载class。

然后如果parent未找到class,则会调用findClass来加载class,findClass是一个protected的空方法,可以覆盖它以便自定义class加载过程。

另外,虽然ClassLoader加载类是使用loadClass方法,但是鼓励用 ClassLoader 的子类重写 findClass(String),而不是重写loadClass,这样就不会覆盖了类加载默认的双亲委派机制

双亲委派机制为什么安全

前面谈到双亲委派机制是为了安全而设计的,但是为什么就安全了呢?举个例子,ClassLoader加载的class文件来源很多,比如编译器编译生成的class、或者网络下载的字节码。而一些来源的class文件是不可靠的,比如我可以自定义一个java.lang.Integer类来覆盖jdk中默认的Integer类,例如下面这样:

package java.lang;

/**
 * hack
 */
public class Integer {
	public Integer(int value) {
		System.exit(0);
	}
}

初始化这个Integer的构造器是会退出JVM,破坏应用程序的正常进行,如果使用双亲委派机制的话该Integer类永远不会被调用,以为委托BootStrapClassLoader加载后会加载JDK中的Integer类而不会加载自定义的这个,可以看下下面这测试个用例:

public static void main(String... args) {
		Integer i = new Integer(1);
		System.err.println(i);
	}

执行时JVM并未在new Integer(1)时退出,说明未使用自定义的Integer,于是就保证了安全性。


转载于:https://my.oschina.net/xionghui/blog/499725

<think>嗯,用户想了解Java中的双亲委派机制,可能是为了面试准备。我需要先回忆下双亲委派的基本概念和工作原理。根据引用[2],双亲委派机制主要是为了保证类加载的安全性,防止核心类被篡改。比如java.lang.String这样的类,如果各个类加载器自己加载,可能会导致多个不同版本的String类被加载,造成混乱。 接下来,用户可能需要知道类加载器的层次结构。引用[4]提到,JDK8及之前有启动类加载器、扩展类加载器、应用程序类加载器和自定义加载器。启动类加载器是C++实现的,负责核心类库,如jre/lib目录下的类。扩展类加载器处理jre/lib/ext,应用程序类加载器负责classpath下的类。 然后,双亲委派的工作流程是怎样的?根据引用[3],当加载个类时,首先由当前线程的类加载器尝试加载,如果找不到,会委派给父类加载器,直到启动类加载器。如果父类加载不了,子类才会自己加载。这样可以确保基础类优先被父加载器加载,避免重复和安全问题。 用户可能还会问到如何打破双亲委派机制。引用[1]和[5]提到,比如重写loadClass方法,Tomcat因为需要隔离不同应用的类,所以打破了双亲委派。还有像SPI机制使用线程上下文类加载器,如JDBC加载驱动时,启动类加载器无法加载第三方驱动,就需要用线程上下文类加载器来加载。 面试中常见的问题可能包括双亲委派的优点、如何打破、实际应用场景等。需要结合引用中的例子,比如Tomcat的热部署和类隔离,以及SPI机制的例子,说明打破双亲委派的必要性和方法。 另外,要注意双亲委派的缺点,比如上层类加载器无法访问下层类加载器加载的类,这在需要反向委派时会有限制。这时候就需要打破机制,比如使用线程上下文类加载器。 总结回答结构,应该先解释双亲委派是什么,然后层次结构,工作流程,优点,如何打破,实际案例,以及面试常见问题。这样用户能全面理解,应对面试中的各种相关问题。</think>### Java类加载双亲委派机制详解 #### 双亲委派机制定义 双亲委派机制Java类加载的核心规则,要求**子类加载器在加载类时必须先委派给父类加载器处理**,只有父类加载器无法完成加载时,子类加载器才会尝试自己加载[^2]。 #### 二、类加载器层次结构(JDK8及之前) 1. **启动类加载器(Bootstrap Class Loader)** - C++实现,加载`jre/lib`目录下的核心类(如`java.lang.*`) 2. **扩展类加载器(Extension Class Loader)** - Java实现,加载`jre/lib/ext`目录下的扩展类 3. **应用程序类加载器(Application Class Loader)** - 加载`classpath`下的应用程序类 4. **自定义类加载器** - 继承`ClassLoader`并重写`findClass()`方法[^4] #### 三、工作流程 ```mermaid graph TD A[子类加载器收到加载请求] --> B{是否已加载?} B --> |已加载| C[返回已加载的Class对象] B --> |未加载| D[委派给父类加载器] D --> E{父类是否加载成功?} E --> |成功| F[返回结果] E --> |失败| G[子类自行加载] ``` #### 四、核心优势 1. **安全性保障** 防止核心API(如`java.lang.String`)被篡改[^2] 2. **避免重复加载** 通过层级控制确保类唯性 3. **沙箱隔离** 不同层级的类加载器形成天然隔离 #### 五、打破双亲委派的场景 | 场景 | 实现方式 | 典型示例 | |---------------------|-----------------------------------|---------------------------| | SPI机制 | 线程上下文类加载器 | JDBC驱动加载[^1] | | 模块热部署 | 自定义类加载器 | Tomcat多应用隔离[^5] | | 动态代码支持 | 重写`loadClass()`方法 | OSGi框架 | #### 六、面试高频问题 1. **为什么需要双亲委派机制?** - 避免核心类被篡改(如自定义`java.lang.Object`会抛出安全异常) - 保证类加载的唯性[^2] 2. **如何实现自定义类加载器?** - 继承`ClassLoader`类 - 重写`findClass()`方法(注意不要破坏`loadClass()`的委派逻辑) 3. **Tomcat为什么打破双亲委派?** - 需求:同时部署多个Web应用,每个应用可能有相同类库的不同版本 - 实现:每个Web应用使用独立的`WebappClassLoader`[^5] 4. **SPI如何解决父加载器无法加载子资源的问题?** - 使用`Thread.currentThread().getContextClassLoader()`获取线程上下文类加载器 - 典型案例:JDBC驱动加载时通过`ServiceLoader`加载厂商实现[^1] #### 七、代码示例(自定义类加载器) ```java public class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = loadClassData(name); // 自定义加载逻辑 return defineClass(name, classData, 0, classData.length); } private byte[] loadClassData(String className) { // 从特定位置读取.class文件字节流 } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值