JVM仮想マシン---(14)クラスローディング機構

免責事項:この記事はブロガーオリジナル記事です、続くBY-SAのCC 4.0を著作権契約、複製、元のソースのリンクと、この文を添付してください。
このリンク: https://blog.csdn.net/qq1021979964/article/details/98214894

        Java型を形成し、メモリ内にクラスファイルからロードされたクラスを記述し、データを確認し、初期化を解析し変換する仮想マシンのデータは、仮想マシンのクラスローディング機構である仮想マシンとして使用することができます。

まず、クラスのロード時間

        クラスを含むこれまでのメモリ、そのライフサイクル全体、アンロードするために、仮想マシンのメモリにロードされ始め:(初期化)を初期化し、負荷(ロード)、検証(検証)、準備(準備)、分析(Resolution)を、使用7段(使用)及びアンローディング(アンロード)。前記検証、製造は、分析接続部3(リンク)と呼ばれます。

 

唯一の5例は(ロード、検証、準備は当然、この前に開始する必要がある一方で)すぐにクラスを初期化する必要があります。

1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

5.当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

        「唯一無二」、アクティブな参照のために呼ばれるクラスの振る舞いのためにこれらの5つのシナリオと、離れてから:これらの5つのシナリオは、クラスが初期化されたトリガするために、仮想マシンの仕様は非常に強力な修飾子を使用しています加えて、クラスメソッドへのすべての参照は、パッシブリファレンスとして知られている、初期化をトリガしていません。

検証実施例3、4

public class Parent {
    static {
        System.out.println("parent 初始化了...");
    }
}

サブクラス

public class Child extends Parent {

    static {
        System.out.println("child 初始化...");
    }

    public static void main(String[] args) {

    }

}

出力:

分析:実行クラスは、)(主に初期化サブクラス中のクラス(第4条)を、初期化含まれ、親クラスは親クラスが初期化され、初期化されていないことを見出した(第3条)

 

2.ケースは、初期化をトリガしません。

ケース:静的フィールドは、サブカテゴリー別の親クラスを参照するには、サブカテゴリが初期化されることはありません

public class Parent {

    static {
        System.out.println("parent 初始化了...");
    }

    public static int num = 10;
}

サブクラス

public class Child extends Parent {

    static {
        System.out.println("child 初始化...");
    }

}

実行クラス

public class Main {

    public static void main(String[] args) {
        System.out.println(Child.num);
    }
}

親クラス、サブクラスを初期化しますが、初期化されていません。

 

。Bケース:のアレイによって定義されたクラスを参照します

いくつかの変更、サブクラス親クラスを行うには、上記のコードの実行クラスは変更されません。

実行クラス

public class Main {

    public static void main(String[] args) {
        Child[] children = new Child[10];
    }
}

サブクラスとスーパークラスを初期化できません。

 

ケースC:定数コールクラス。

実行クラスとサブクラスは、親クラスが変更されていない変更します。

サブクラス

public class Child extends Parent {

    public static final int a = 20;

    static {
        System.out.println("child 初始化...");
    }

}

実行クラス

public class Main {

    public static void main(String[] args) {
        System.out.println(Child.a);
    }
}

 

第二に、クラスのロード処理

ローディングの1クラスローディングプロセス---

「ロード」は「クラスローダ」段階(クラスローディング)プロセスであり、ロード・フェーズでは、仮想マシンは、次の3つのことを完了させる必要があります。

1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

三点デマンド仮想マシンの仕様は、特定のではありません。だから、仮想マシンの実装と柔軟性、特定のアプリケーションが非常に大きいです。

たとえば、最初:クラスファイルから指定されていないバイナリバイトストリームは、単純に取得する方法を、取得する場所を示していない正確な取得します。例えば:

1.从ZIP包中读取(典型场景:JAR、EAR、WAR)。
2.从网络中获取(典型场景:Applet)。
3.运行时计算生成(典型场景:动态代理技术,*$Proxy)。
4.由其它文件生成(典型场景:JSP应用)。
5.从数据库读取(典型场景:中间服务器)。

        特殊配列クラス:配列自体はJava仮想マシンで直接作成されたクラスローダによって作成されますが、要素型の配列クラスは、最終的にはクラスに依存するため、クラスやクラスローダの配列はまだ、非常に密接な関係を持っていません次のように配列の作成プロセスを作成するためのローダは次のとおりです。

1.如果数组的组件类型是引用类型,那就递归采用类加载来加载。
2.如果数组的组件类型不是引用类型,Java虚拟机会把数组标记为引导类加载器关联。
3.数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public。

クラスがロードされたときにクラスローダ「最初の使用」クラスまで待つ必要、Java仮想マシン仕様は、システムがいくつかのクラスを事前にロードすることができません。

        ローディング段階が完了した後、仮想マシンの外部フォーマット領域の方法で仮想マシンを格納するために必要なバイトのバイナリストリームによれば、面積法は、仮想マシン自体によって定義されたデータ記憶フォーマットを実装し、仮想マシンの仕様では、小領域を所定のありません特定のデータ構造。

その後、メモリ内に格納されjava.lang.Classクラスのメソッド領域のオブジェクト、データのこれらのタイプのプログラム領域として外部インタフェースアクセス方法をインスタンス化します。

       負荷位相と接続相の一部(例えば、ファイル形式のバイトコード検証動作の一部)が接続フェーズが開始した可能性があり、ローディング段階が完了していない、交差を行っているが、それらは動作段階に巻き込まれ、ステージがまだ接続されていますコンテンツの開始時間は、これらの2つの段階が固定された順序を維持しました。

2.クラスローディング検証プロセス---

最初のステップは、接続フェーズを確認することです、この段階の目的は、クラスに含まれる情報のバイトストリームファイルが自分自身の安全を危うくしません、現在の仮想マシンと仮想マシンの要件を満たしていることを確認することです。

I。検証ファイル形式

バイトストリーム準拠のクラスファイル形式かどうかを検証する第一段階、及び(バイナリバイトストリームに基づいて)仮想マシンプロセスの現在のバージョンであることができます。このフェーズでは、次の確認ポイントを含んでいてもよいです。

1.是否以魔数0xCAFEBABE开头。
2.主、次版本号是否在当前虚拟机处理范围之内。
3.常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
4.指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
5.CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
6.Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
……

この検証の主な目的は、適切に解析し、ゾーン法を格納することができるバイトストリームの入力段は、Javaは情報フォーマット要件の種類の説明に合うことを保証することです。

II。メタデータの検証

第二段階は、Java言語仕様(記憶領域の方法に基づいて、コンフィギュレーション)に記載の要件を満たしている情報のことを確実にするためにバイトコード記述情報セマンティック分析です。この相は、検証ポイントを含んでいてもよい以下の通りです。

1.这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
2.这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
3.如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
4.类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合 规则的方法重载,例5.如方法参数都一致,但返回值类型却不同等)。
……

       認証の第二段階の主な目的は、存在しないメタデータ情報は、Java言語仕様に準拠していないことを確実にするために、メタデータ情報のような意味論的検証です。

III。バイトコード検証

仕上がりのチェックのデータ型内のメタデータ情報の後、プロセスのこの段階は、第二段階での分析クラスのボディを検証する、実行時にメソッドのバリデーションクラスを確保害仮想マシンのセキュリティをすることはありません例えばイベント(構造ベースのストレージエリア法)。

1.保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。
2.保证跳转指令不会跳转到方法体以外的字节码指令上。
3.保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。
……

第三段階は、全体の検証プロセスで最も複雑な段階であり、主な目的は、プログラムのセマンティクスを決定するために、データ・フローと制御フロー解析であり、正当な論理的です。

シンボリック参照の静脈。検証

シンボリック参照の検証は、型自体(様々なシンボル定数プール参照)整合性チェック以外の情報として見ることができ、通常、次のことを確認する必要があります。

1.符号引用中通过字符串描述的全限定名是否能找到对应的类。
2.在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
3.符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。
……

場合シンボル参照最終段チェックが直接参照に、仮想マシンで発生し、接続の第3段階における変換動作 - 分析フェーズが生じます。

         目的は、適切に行うことができるシンボリック参照の分析動作の検証を確実にすることである。シンボルを参照することによって検証されない場合、それはそのようなjava.lang.IllegalAccessError、java.lang.NoSuchFieldErrorの、Javaなどの異常java.lang.IncompatibleClassChangeErrorサブクラスをスローします.lang.NoSuchMethodErrorのように。

3.準備クラスローディングプロセス---

準備フェーズは、正式にクラス変数にメモリを割り当て、必要なメモリは、メソッド領域に配置されるこれらの変数のステージクラス変数の初期値が設定されています。

public static int value = 1024;

その時点ではまだJavaメソッドを実行開始していないので、0の代わりに1024の初期値の準備段階後にその変数の値と、クラスのコンストラクタに格納されたプログラムをコンパイルした後putstatic指令1024に割り当てられた値、<clinit>間()メソッド1024の前のアクションに割り当てられた値は、初期化段階で実行されるように。

すべてのデータ型のため、実質的にゼロ値

特殊なケース:現在のフィールド属性テーブル一定値クラスの属性フィールドの場合、および準備段階で変数の値が一定値プロパティで指定された値に初期化されています。

public static final int value = 1024;

编译时Javac会为value生产ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为1024。

4.类加载的过程---解析

解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程。

符号引用:

        符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机实现的布局无关,引用的目标并不一定已经加载到内存中。

直接引用:

       直接引用可以是直接指向目标的指针,相对偏移量或是一个能间接定义到目标的句柄,直接引用是和虚拟机内存布局相关,引用的布标必定在内存中存在。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符。

分别对应于常量池的

CONSTANT_Class_info

CONSTANT_Fieldref_info

CONSTANT_Methodref_info

CONSTANT_InterfaceMethodref_info

CONSTANT_MethodType_info

CONSTANT_MethodHandle_info

CONSTANT_InvokeDynamic_info 7种常量类型

5.类加载的过程---初始化

       类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段可以自定义类加载器参与之外,其余都是由虚拟机主导与控制,到了初始化阶段,才真正开始执行类中定义的Java程序代码(字节码)。

从另一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。

       如果执行<clinit>()方法那条线程退出后,其它线程唤醒之后也不会再次进行<clinit>()方法。同一个类加载器下,一个类型只会初始化一次。

三、类加载器

类加载器:通过一个类的全限定名来获取描述此类的二进制字节流。

1.类与类加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。

比较两个类是否相等,需要这两个类是由同一个类加载器加载的前提之下才有意义。否则哪怕是同一个Class文件,这两个类一定不相等。

相等指的是包括Class对象的equals()方法,isAsssignableFrom()方法,isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。
  • i. 自定义类加载器

  1. 定义一个类,继承ClassLoader
  2. 重写loadClass方法
  3. 实例化Class对象
  • ii. 自定义类加载器的优势

  1. 类加载器是Java语言的一项创新,也是Java语言流行的重要原因之一,最初的设计是为了满足Java Applet的需求而开发出来的。
  2. 高度的灵活性
  3. 通过自定义类加载器可以实现热部署
  4. 代码加密
  • iii. 自定义类加载器案例

package com.kevin.jvm.classloader;

import java.io.IOException;
import java.io.InputStream;

/**
 * @author caonanqing
 * @version 1.0
 * @description     测试自定义类加载器
 *      1.定义一个类,继承ClassLoader
 *      2.重写loadClass方法
 *      3.实例化Class对象
 * @createDate 2019/8/2
 */
public class ClassLoaderDemo {

    public static void main(String[] args) throws Exception {

        // 自定义类加载器
        ClassLoader mycl = new ClassLoader() {

            /**
             *  加载当前包下的所有类,其他的类委派父类加载器
             * @param name  类的全限定名
             * @return
             * @throws ClassNotFoundException   找不到类的时候会抛出这个异常
             */
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                // com.kevin.jvm.classloader.ClassLoaderDemo
                String filename = name.substring(name.lastIndexOf(".") + 1) + ".class";
                InputStream ins = getClass().getResourceAsStream(filename); // 加载
                if(ins == null) {   // 没有找到,让父类加载器来加载
                    return super.loadClass(name);
                }
                try {
                    byte[] buff = new byte[ins.available()];
                    ins.read(buff);
                    return defineClass(name,buff,0,buff.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException();
                }
            }
        };
        // 有main方法的类,被会应用程序加载器加载一次,加上我们自定义的加载器,所以会被加载两次
        Object c = mycl.loadClass("com.kevin.jvm.classloader.ClassLoaderDemo").newInstance();
        System.out.println(c.getClass());
        // 其实c是被应用程序加载类所加载的,所以他们并不是被同一个类加载器加载的、
        System.out.println(c instanceof ClassLoaderDemo);

    }
}

2.双亲委派模型

从Java虚拟机的角度来讲,只存在两种不同的类加载器:

1.一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分。
2.另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

从Java开发人员的角度来看,大部分Java程序会使用到以下3中系统提供的类加载器:

1.启动类加载器(Bootstrap ClassLoader):加载lib下或被-Xbootclasspath路径下的类。
2.扩展类加载器(Extension ClassLoader):加载lib/ext或java.ext.dirs系统变量所指定的路径中的所有类库。
3.应用程序类加载器(Application ClassLoader):ClassLoader负责,加载用户路径(ClassPath)上所指定的类库,一般也成为系统类加载器,默认使用这个。

        一般我们的应用程序都是这3种类加载器互相配合进行加载的,如果有必要还可以加入我们自定义的类加载器。这些类加载器之间的关系,称为类加载的双亲委派模型。

如图所示:

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。

这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用类加载器的代码。

工作流程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器。只有父类加载器无法完成时子类加载器才会尝试加载。

a.案例

package java.lang;

/**
 * @author caonanqing
 * @version 1.0
 * @description         测试双亲类加载器
 * @createDate 2019/8/2
 */
public class Object {

    public static void main(String[] args) {
        Object obj = new Object();
        System.out.println("Hello World");
    }
}

这个类其实并没有被加载。

b.双亲类加载器的案例

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        //检查该类是否已经加载过
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果该类没有加载,则进入该分支
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //当父类的加载器不为空,则通过父类的loadClass来加载该类
                    c = parent.loadClass(name, false);
                } else {
                    //当父类的加载器为空,则调用启动类加载器来加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //非空父类的类加载器无法找到相应的类,则抛出异常
            }

            if (c == null) {
                //当父类加载器无法加载时,则调用findClass方法来加载该类
                long t1 = System.nanoTime();
                c = findClass(name); //用户可通过覆写该方法,来自定义类加载器

                //用于统计类加载器相关的信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            //对类进行link操作
            resolveClass(c);
        }
        return c;
    }
}

3.破坏双亲委派模型

双亲委派模型并不是强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。

        线程上下文类加载器(Thread Context ClassLoader),这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还没设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:

1.将以java.*开头的类委派给父类加载器加载。
2.否则,将委派列表名单内的类委派给父类加载器加载。
3.否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4.否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5.否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的 类加载器加载。
6.否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7.否则,类查找失败。 上面的查找顺序中只有开头两点仍然符合双亲委派规则,其余的类查找都是在平级的类 加载器中进行的。

 

おすすめ

転載: blog.csdn.net/qq1021979964/article/details/98214894