ホームページ  >  記事  >  Java  >  Java のクラス ローダー クラス ローダーの詳細な分析

Java のクラス ローダー クラス ローダーの詳細な分析

高洛峰
高洛峰オリジナル
2017-01-13 09:33:351635ブラウズ

クラスロードのプロセス
クラスローダーの主な仕事は、クラスファイルをJVMにロードすることです。以下の図に示すように、プロセスは 3 つのステップに分かれています:

1. ロードするクラス ファイルを見つけて、そのバイト ストリームを JVM にロードします。
2. 最も基本的なものをクラスに割り当てます。メモリ構造には、プロパティ、メソッド、参照クラスなどの情報が保持されます。この段階では、クラスはまだ使用できません。
(1) 検証: フォーマットやセキュリティなど、ロードされたバイト ストリームを検証します。
(2) メモリ割り当て: このクラスのメモリ領域を準備します。その属性、メソッド、および参照されるクラスを表します。
(3) 分析: 親クラス、実装されたインターフェイスなど、このクラスによって参照される他のクラスを読み込みます。
3. 初期化: クラス変数に値を代入します。

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

クラスローダーのレベル
下図の点線の上は、JDKが提供するいくつかの重要なクラスローダーです。詳細は次のとおりです:

(1) ブートストラップクラスローダー: メインを含むクラスを開始するとき。関数、JAVA_HOME/lib ディレクトリまたは -Xbootclasspath で指定されたディレクトリに Jar パッケージをロードします。
(2) 拡張クラスローダー: JAVA_HOME/lib/ext ディレクトリまたは -Djava.ext で指定されたディレクトリに Jar パッケージをロードします。先生たち。
(3) システム クラス ローダー: クラスパスまたは -Djava.class.path で指定されたディレクトリにクラスまたは jar パッケージをロードします。

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

知っておくべきことは次のとおりです:

1. ブートストラップ クラス ローダーを除き、他のクラス ローダーは java.lang.ClassLoader クラスのサブクラスです。パーソナライズされたクラス ローダーを使用しない場合、 java.lang.String.class.getClassLoader() は null になり、拡張クラス ローダーの親ローダーも null になります。
3. クラス ローダーを取得するいくつかの方法:
(1) ) ブートストラップ クラス ローダーの取得: ブートストラップ クラス ローダーを取得しようとすると、取得するものは null である必要があります。次の方法で確認できます。rt.jar パッケージ内のクラス オブジェクトの getClassLoader メソッド (java.lang.String.class.getClassLoader() など) を使用して拡張クラス ローダーを取得または取得し、 getParent メソッドを使用して取得します。
(2) 拡張クラス ローダーを取得する: JAVA_HOME/lib/ext ディレクトリの jar パッケージ内のクラス オブジェクトの getClassLoader メソッドを使用するか、最初にシステム クラス ローダーを取得してから、その getParent を通じて取得します。メソッド;
(3) システム クラス ローダーの取得: クラス オブジェクトの main 関数 getClassLoader メソッドを呼び出すか、main 関数で Thread.currentThread().getContextClassLoader() または ClassLoader.getSystemClassLoader() を呼び出します。ユーザー定義のクラス ローダー: クラス オブジェクトの getClassLoader メソッドを呼び出すか、Thread().getContextClassLoader(); クラス ローダーの動作原理
1. エージェンシーの原則
2.原則
3. 独自性原則

4. エージェンシー原則

エージェンシー原則が指すもの クラスローダーがクラスをロードするとき、その親ローダーはプロキシロードを要求し、親ローダーもその親ローダーにプロキシロードを要求します。以下の図に示すように。


プロキシ モードを使用する理由は何ですか?まず第一に、これによりクラスの繰り返しロードが軽減されます。 (他に理由はありますか?)





誤解されやすい:

一般に、クラスローダーの代理順序は親からであると考えられています。つまり:

1. クラスをロードするとき、クラスローダーはまずクラスがロードされているかどうかを確認し、ロードされていない場合は親ローダーにプロキシを依頼します。
2.ブートストラップ クラス ローダー
3. ブートストラップ クラス ローダーはクラスをロードしようとしますが、ロードが失敗した場合は ClassNotFoundException がスローされ、サブローダーがクラスをロードします。
4. サブクラスローダーは例外をキャッチした後にロードを試み、成功した場合は戻り、失敗した場合は ClassNotFoundException をスローします。
この理解は、ブートストラップ クラス ローダー、拡張クラス ローダー、システム クラス ローダーなどのローダーには正しいですが、一部のパーソナライズされたローダーは正しくありません。たとえば、IBM Web Sphere Portal Server によって実装される一部のクラス ローダーは、親ローダーです。ロードが失敗すると、親ローダーが呼び出されます。その理由は、特定のバージョンの log4j がすべてのアプリケーションで使用されることが予想される場合、WAS の起動時にそれを WAS_HOME ライブラリーに置くことです。それをロードしてください。アプリケーションが別のバージョンの log4j を使用したい場合、Parent First を使用するとこれを実現できません。これは、log4j 内のクラスがすでに親ローダーにロードされているためです。ただし、Parent Last を使用すると、アプリケーションのロードを担当するクラス ローダーが別のバージョンの log4j を最初にロードします。


可視性の原則
以下の図に示すように、クラスローダーに対する各クラスの可視性は異なります。

拡張知識として、OSGi はこの機能を利用しており、各バンドルは個別のクラス ローダーによってロードされるため、各クラス ローダーは特定のクラスのバージョンをロードできるため、システム全体で 1 つのクラスの複数のバージョンを使用できます。

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

<br/>

一意性の原則
各クラスはローダーに最大 1 回ロードできます。

拡張知識 1: 正確には、シングルトン パターンで参照されるシングルトンは、クラス ローダーのグループ内の特定のクラスのオブジェクトの 1 つのコピーのみを指します。

拡張知識 2: クラスは複数のクラス ローダーによってロードできます。各クラス オブジェクトは独自の名前空間にあり、クラス オブジェクトを比較するとき、またはインスタンスで型変換を実行するとき、次のようにそれぞれの名前空間が同時に比較されます。

Klass クラスは、クラス オブジェクトが KlassA であると仮定すると、ClassLoaderA によってロードされます。また、クラス オブジェクトが KlassB であると仮定すると、ClassLoaderB によってもロードされます。その場合、KlassA は KlassB と等しくありません。同時に、ClassA のインスタンスが KlassB にキャストされると、ClassCastException がスローされます。
パーソナライズされたクラス ローダーの理由
パーソナライズされたクラス ローダーは、Java 言語に多くの柔軟性を追加します。主な用途は次のとおりです:

1. クラスは、ネットワーク上、データベース内などの複数の場所からロードでき、さらには瞬時にコンパイルすることもできます。クラス ファイルを取得するためのソース ファイル
2. パーソナライズされたクラス ローダーは、原則として実行時にクラス ファイルをロードできます。クラスローダーは、クラスをロードする前にクラスを復号化および圧縮解除できます。

クラスの暗黙的ロードと明示的ロード

暗黙的ロード: クラスが参照、継承、またはインスタンス化されるとき、ロードが失敗すると、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(); 
 } 
 } 
  
}

実行後の効果は何ですか?

Hiは出力されません。 loadClass の使用後に Class オブジェクトが初期化されていないことがわかります。

Load ステートメントの後に c.newInstance(); を追加すると、Hi 出力があり、クラス オブジェクトはクラスがインスタンス化されたときにのみ初期化されます。

ローディングステートメントを変更した場合、 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 までご連絡ください。