>  기사  >  Java  >  Java의 클래스 로더 클래스 로더에 대한 심층 분석

Java의 클래스 로더 클래스 로더에 대한 심층 분석

高洛峰
高洛峰원래의
2017-01-13 09:33:351596검색

클래스 로딩 과정
클래스 로더의 주요 역할은 클래스 파일을 JVM에 로딩하는 것입니다. 아래 그림과 같이 프로세스는 세 단계로 나뉩니다.

1. 로드: 로드할 클래스 파일을 찾고 해당 바이트 스트림을 JVM에 로드합니다.
2. 로드할 클래스 파일 클래스는 속성, 메서드, 참조 클래스와 같은 정보를 보유하기 위해 가장 기본적인 메모리 구조를 할당합니다. 이 단계에서는 클래스를 여전히 사용할 수 없습니다.
(1) 확인: 형식 및 보안과 같은 로드된 바이트 스트림을 확인합니다.
(2) 메모리 할당: 이 클래스의 속성을 나타낼 메모리 공간을 준비합니다.
(3) 분석: 상위 클래스, 구현된 인터페이스 등 이 클래스에서 참조하는 다른 클래스를 로드합니다.
3. 초기화: 클래스 변수에 값을 할당합니다.

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

클래스 로더 레벨
아래 그림의 점선 위에는 JDK에서 제공하는 중요한 클래스 로더가 몇 가지 있습니다.

( 1) 부트스트랩 클래스 로더: 메인 함수가 포함된 클래스를 시작할 때 JAVA_HOME/lib 디렉터리 또는 -Xbootclasspath에 지정된 디렉터리에 jar 패키지를 로드합니다.
(2) 확장 클래스 로더: JAVA_HOME을 로드합니다. /lib/ext 디렉토리 또는 지정된 디렉토리의 -Djava.ext.dirs jar 패키지.
(3) 시스템 클래스 로더: classpath 또는 -Djava.class.path로 지정된 디렉터리에 클래스나 jar 패키지를 로드합니다.

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

알아야 할 사항은 다음과 같습니다.

1. Bootstrap Class Loader를 제외한 다른 클래스 로더는 java.lang.ClassLoader 클래스의 하위 클래스입니다. 🎜>2.Bootstrap 클래스 로더는 Java에서 구현되지 않습니다. 개인화된 클래스 로더를 사용하지 않으면 java.lang.String.class.getClassLoader()가 null이 되고 확장 클래스 로더의 상위 로더가 로드됩니다.
3. 클래스 로더를 얻는 여러 가지 방법:
(1) 부트스트랩 클래스 로더 얻기: 부트스트랩 클래스 로더를 얻으려면 결과가 null이어야 합니다. 다음 방법으로 확인할 수 있습니다. rt.jar 패키지에 있는 클래스 객체의 getClassLoader 메서드(예: java.lang.String.class.getClassLoader())를 사용하여 확장 클래스 로더를 가져오거나 얻은 다음
( 2) 확장 클래스 로더 얻기: JAVA_HOME/lib/ext 디렉토리에 있는 jar 패키지에 있는 클래스 객체의 getClassLoader 메서드를 사용하거나 시스템 클래스 로더를 먼저 얻은 후 가져옵니다. getParent 메소드를 통해
(3) 시스템 클래스 로더 획득: 메인 함수가 포함된 클래스 객체의 getClassLoader 메소드를 호출하거나 Thread.currentThread().getContextClassLoader()를 호출하거나 메인 클래스 내에서 ClassLoader.getSystemClassLoader()를 호출합니다. function;
(4) 사용자 정의 클래스 로더 얻기: 클래스 객체 getClassLoader 메서드를 호출하거나 Thread.currentThread().getContextClassLoader()를 호출합니다.



클래스 로더의 동작 원리

1. 프록시 원칙
2. 가시성 원칙
3. 고유성 원칙
4. 프록시 원칙
프록시 원칙은 다음을 의미합니다. 클래스 로더는 아래 그림과 같이 상위 로더 에이전트에도 로드를 요청합니다.

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

프록시 모드를 사용하는 이유는 무엇인가요? 우선, 이는 클래스의 반복 로딩을 줄일 수 있습니다. (다른 이유가 있나요?)


오해받기 쉬움:

일반적으로 클래스 로더의 프록시 순서는 Parent First라고 생각됩니다.

1. 클래스를 로드할 때 클래스 로더는 먼저 클래스를 로드했는지 확인하고, 그렇지 않으면 상위 로더에게 프록시를 요청합니다.
2. . Bootstrap 클래스 로더로 이동합니다.
3. Bootstrap 클래스 로더가 클래스를 로드하려고 시도하고, 로드가 성공하면 ClassNotFoundException이 발생합니다.
4. Sub 클래스 로더는 예외를 포착하고 로드를 시도합니다. 성공하면 로드를 시작하는 하위 클래스 로더까지 ClassNotFoundException이 발생합니다.
이러한 이해는 부트스트랩 클래스 로더, 확장 클래스 로더 및 시스템 클래스 로더에 대해서는 정확하지만 일부 개인화된 로더는 그렇지 않습니다. 예를 들어 IBM Web Sphere Portal Server에서 구현되는 일부 클래스 로더는 상위 마지막, 하위입니다. 로더가 먼저 로드를 시도합니다. 로드에 실패하면 상위 로더가 호출됩니다. 그 이유는 특정 버전의 log4j가 모든 애플리케이션에서 사용될 것으로 예상되면 이를 WAS_HOME 라이브러리에 넣으면 WAS가 로드되기 때문입니다. 시작 시. 애플리케이션이 다른 버전의 log4j를 사용하려는 경우, parent First를 사용하면 이를 달성할 수 없습니다. log4j의 클래스가 이미 상위 로더에 로드되었기 때문입니다. 그러나 Parent Last를 사용하면 애플리케이션 로드를 담당하는 클래스 로더가 다른 버전의 log4j를 먼저 로드합니다.


가시성 원칙
아래 그림과 같이 클래스 로더에 대한 각 클래스의 가시성이 다릅니다.

지식을 확장하여 OSGi는 이 기능을 활용합니다. 각 번들은 별도의 클래스 로더에 의해 로드되므로 각 클래스 로더는 특정 클래스의 버전을 로드할 수 있으므로 전체 시스템에서 하나의 여러 버전을 사용할 수 있습니다. 수업.

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

<br/>

고유성 원칙
각 클래스는 로더에 최대 한 번 로드될 수 있습니다.

확장 지식 1: 정확히 말하면 싱글턴 패턴에서 참조하는 싱글톤은 클래스 로더 그룹에서 특정 클래스의 개체에 대한 복사본 하나만을 의미합니다.

확장 지식 2: 클래스는 여러 클래스 로더에 의해 로드될 수 있습니다. 각 클래스 객체는 자체 네임스페이스에 있습니다. 클래스 객체를 비교하거나 인스턴스에서 유형 변환을 수행할 때 해당 이름이 동시에 비교됩니다. Space, 예:

클래스 객체가 KlassA라고 가정하면 Klass 클래스는 ClassLoaderA에 의해 로드됩니다. 클래스 객체가 KlassB라고 가정하면 KlassA는 KlassB와 동일하지 않습니다. 동시에 ClassA의 인스턴스가 KlassB로 캐스팅되면 ClassCastException이 발생합니다.
개인화된 클래스 로더가 필요한 이유
개인화된 클래스 로더는 Java 언어에 많은 유연성을 추가합니다. 주요 용도는 다음과 같습니다.

클래스는 네트워크와 같은 여러 위치에서 로드될 수 있습니다. 데이터베이스를 생성하거나 소스 파일을 즉시 컴파일하여 클래스 파일을 얻을 수도 있습니다.
2. 개인화된 클래스 로더는 원칙적으로 런타임에 특정 버전의 클래스 파일을 로드할 수 있습니다.
3. 일부 클래스 언로드
4. 개인화 후 클래스 로더는 클래스를 로드하기 전에 클래스의 암호를 해독하고 압축을 풀 수 있습니다.


클래스의 암시적 및 명시적 로드
암시적 로드: 클래스가 참조, 상속 또는 인스턴스화될 때 로드에 실패하면 NoClassDefFoundError가 발생합니다.

명시적 로딩: 다음 방법을 사용하세요. 로딩에 실패하면 ClassNotFoundException이 발생합니다.

cl.loadClass(), cl은 클래스 로더의 인스턴스입니다.
Class.forName()은 현재 클래스의 클래스 로더를 사용하여 로드합니다.
클래스의 정적 블록 실행
다음 클래스가 있다고 가정합니다:

package cn.fengd; 
  
public class Dummy { 
 static { 
 System.out.println("Hi"); 
 } 
}

다른 테스트 클래스 만들기:

package cn.fengd; 
  
public class ClassLoaderTest { 
  
 public static void main(String[] args) throws InstantiationException, Exception { 
  
 try { 
  /* 
  * Different ways of loading. 
  */
  Class c = ClassLoaderTest.class.getClassLoader().loadClass("cn.fengd.Dummy"); 
 } catch (Exception e) { 
  // TODO Auto-generated catch block 
  e.printStackTrace(); 
 } 
 } 
  
}

실행 후 효과는 무엇입니까?

안녕하세요가 출력되지 않습니다. loadClass를 사용한 후 Class 객체가 초기화되지 않은 것을 볼 수 있습니다.

Load 문 뒤에 c.newInstance()를 추가하면 Hi 출력이 발생하며, 해당 클래스 객체는 다음과 같은 경우에만 초기화됩니다. 클래스가 인스턴스화됩니다.

Loading 문을 변경하면 Class c = Class.forName("cn.fengd.Dummy", false, ClassLoader.getSystemClassLoader());

Hi가 출력되지 않습니다. false 매개변수는 이 클래스의 객체를 초기화할 필요가 없음을 의미하기 때문에

Load 문 뒤에 c.newInstance()를 추가하면 Hi 출력이 발생하고 클래스 객체가 초기화됩니다. 클래스가 인스턴스화될 때만.

Class.forName("cn.fengd.Dummy") 또는 new Dummy()로 변경하면 어떻게 되나요?

은 모두 Hi를 출력합니다.

자주 묻는 질문 분석:

1.由不同的类加载器加载的指定类型还是相同的类型吗?
在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间.我们可以用两个自定义类加载器去加载某自定义类型(注意,不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果。这个大家可以写两个自定义的类加载器去加载相同的自定义类型,然后做个判断;同时,可以测试加载java.*类型,然后再对比测试一下测试结果。

2.在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?
Class.forName(String name)默认会使用调用类的类加载器来进行类加载。我们直接来分析一下对应的jdk的代码:

//java.lang.Class.java
 
 publicstatic Class<?> forName(String className)throws ClassNotFoundException {
 
return forName0(className, true, ClassLoader.getCallerClassLoader());
 
}
 
//java.lang.ClassLoader.java
 
// Returns the invoker&#39;s class loader, or null if none.
 
static ClassLoader getCallerClassLoader() {
 
  // 获取调用类(caller)的类型
 
 Class caller = Reflection.getCallerClass(3);
 
  // This can be null if the VM is requesting it
 
 if (caller == null) {
 
  returnnull;
 
 }
 
 // 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader
 
 return caller.getClassLoader0();
 
}
 
//java.lang.Class.java
 
//虚拟机本地实现,获取当前类的类加载器
native ClassLoader getClassLoader0();

3.在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是?
在不指定父类加载器的情况下,默认采用系统类加载器。可能有人觉得不明白,现在我们来看一下JDK对应的代码实现。众所周知,我们编写自定义的类加载器直接或者间接继承自java.lang.ClassLoader抽象类,对应的无参默认构造函数实现如下:

//摘自java.lang.ClassLoader.java
protected ClassLoader() {
 
  SecurityManager security = System.getSecurityManager();
 
  if (security != null) {
 
  security.checkCreateClassLoader();
 
  }
 
  this.parent = getSystemClassLoader();
 
  initialized = true;
 
}

我们再来看一下对应的getSystemClassLoader()方法的实现:

privatestaticsynchronizedvoid initSystemClassLoader() {
 
  //...
 
  sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
 
  scl = l.getClassLoader();
 
  //...
 
}

我们可以写简单的测试代码来测试一下:

System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());

本机对应输出如下:

sun.misc.Launcher$AppClassLoader@197d257

所以,我们现在可以相信当自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为系统类加载器。同时,我们可以得出如下结论:

即时用户自定义类加载器不指定父类加载器,那么,同样可以加载如下三个地方的类:

(1)6e5475ece90bdb5cedfc1a16b6632d24/lib下的类

(2)9da487fed862e2e75b98522999bd48b8/lib/ext下或者由系统变量java.ext.dir指定位置中的类

(3)当前工程类路径下或者由系统变量java.class.path指定位置中的类

4.在编写自定义类加载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类,就肯定会加载失败吗?
JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可以得出如下结论:
即时用户自定义类加载器不指定父类加载器,那么,同样可以加载到6e5475ece90bdb5cedfc1a16b6632d24/lib下的类,但此时就不能够加载6e5475ece90bdb5cedfc1a16b6632d24/lib/ext目录下的类了。
    说明:问题3和问题4的推断结论是基于用户自定义的类加载器本身延续了java.lang.ClassLoader.loadClass(…)默认委派逻辑,如果用户对这一默认委派逻辑进行了改变,以上推断结论就不一定成立了,详见问题5。

5.编写自定义类加载器时,一般有哪些注意点?
(1)一般尽量不要覆写已有的loadClass(…)方法中的委派逻辑
一般在JDK 1.2之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能正常工作。在JVM规范和JDK文档中(1.2或者以后版本中),都没有建议用户覆写loadClass(…)方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写findClass(…)逻辑。举一个例子来验证该问题:

//用户自定义类加载器WrongClassLoader.Java(覆写loadClass逻辑)
publicclassWrongClassLoaderextends ClassLoader {
 
 public Class<?> loadClass(String name) throws ClassNotFoundException {
 
  returnthis.findClass(name);
 
 }
 
 protected Class<?> findClass(String name) throws ClassNotFoundException {
 
  //假设此处只是到工程以外的特定目录D:/library下去加载类
 
  具体实现代码省略
 
 }
 
}

    通过前面的分析我们已经知道,用户自定义类加载器(WrongClassLoader)的默

       认的类加载器是系统类加载器,但是现在问题4种的结论就不成立了。大家可以简

       单测试一下,现在6e5475ece90bdb5cedfc1a16b6632d24/lib、9da487fed862e2e75b98522999bd48b8/lib/ext和工

       程类路径上的类都加载不上了。

问题5测试代码一

publicclass WrongClassLoaderTest {
 
 publicstaticvoid main(String[] args) {
 
  try {
 
  WrongClassLoader loader = new WrongClassLoader();
 
  Class classLoaded = loader.loadClass("beans.Account");
 
  System.out.println(classLoaded.getName());
 
  System.out.println(classLoaded.getClassLoader());
 
  } catch (Exception e) {
 
  e.printStackTrace();
 
  }
 
 }
 
}

(说明:D:"classes"beans"Account.class物理存在的)

输出结果:

java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系统找不到指定的路径。)
 
 at java.io.FileInputStream.open(Native Method)
 
 at java.io.FileInputStream.<init>(FileInputStream.java:106)
 
 at WrongClassLoader.findClass(WrongClassLoader.java:40)
 
 at WrongClassLoader.loadClass(WrongClassLoader.java:29)
 
 at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)
 
 at java.lang.ClassLoader.defineClass1(Native Method)
 
 at java.lang.ClassLoader.defineClass(ClassLoader.java:620)
 
 at java.lang.ClassLoader.defineClass(ClassLoader.java:400)
 
 at WrongClassLoader.findClass(WrongClassLoader.java:43)
 
 at WrongClassLoader.loadClass(WrongClassLoader.java:29)
 
 at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)
 
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
 
 at java.lang.ClassLoader.defineClass1(Native Method)
 
 at java.lang.ClassLoader.defineClass(ClassLoader.java:620)
 
 at java.lang.ClassLoader.defineClass(ClassLoader.java:400)
 
 at WrongClassLoader.findClass(WrongClassLoader.java:43)
 
 at WrongClassLoader.loadClass(WrongClassLoader.java:29)
 
 at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)

   

这说明,连要加载的类型的超类型java.lang.Object都加载不到了。这里列举的由于覆写loadClass(…)引起的逻辑错误明显是比较简单的,实际引起的逻辑错误可能复杂的多。
问题5测试二

//用户自定义类加载器WrongClassLoader.Java(不覆写loadClass逻辑)
publicclassWrongClassLoaderextends ClassLoader {
 
 protected Class<?> findClass(String name) throws ClassNotFoundException {
 
  //假设此处只是到工程以外的特定目录D:/library下去加载类
 
  具体实现代码省略
 
 }
 
}

将自定义类加载器代码WrongClassLoader.Java做以上修改后,再运行测试代码,输出结果如下:

beans.Account
 
WrongClassLoader@1c78e57

这说明,beans.Account加载成功,且是由自定义类加载器WrongClassLoader加载。

这其中的原因分析,我想这里就不必解释了,大家应该可以分析的出来了。

(2)正确设置父类加载器
通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都可以随便举出例子了。
(3)保证findClass(String )方法的逻辑正确性
事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码内容。

6.如何在运行时判断系统类加载器能加载哪些路径下的类?
一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到;

二是可以直接通过获取系统属性java.class.path 来查看当前类路径上的条目信息 , System.getProperty("java.class.path")

7.如何在运行时判断标准扩展类加载器能加载哪些路径下的类?
方法之一:

try {
  URL[] extURLs = ((URLClassLoader)ClassLoader.getSystemClassLoader().getParent()).getURLs();
 
  for (int i = 0; i < extURLs.length; i++) {
 
   System.out.println(extURLs[i]);
 
  }
 
 } catch (Exception e) {//…}

       本机对应输出如下:

file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/dnsns.jar
 
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/localedata.jar
 
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunjce_provider.jar
 
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunpkcs11.jar

更多深入解析Java中的Class Loader类加载器相关文章请关注PHP中文网!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.