JVM- 类的加载过程、类加载器(付示例代码)

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: JVM- 类的加载过程、类加载器(付示例代码)

一、类的加载过程

类从加载到内存中开始,到卸载出内存位置,为类的生命周期。

包括加载(loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initiazation)、使用(Using)、卸载(Unloading)7个阶段。其中验证、准备、连接统称为连接(linking)。


其中加载、验证、准备、初始化和卸载这5个阶段的顺序是一定的;类的加载过程必须按照这个顺序按部就班的开始,而解析阶段不一定;

解析阶段在某些情况下,可以在初始化之后解析,支持java语言的运行时绑定(也就是动态绑定或晚期绑定)

1、Loading加载

(1)同过全限定名获取定义此类的二进制流;

(2)将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内;

(3)然后在内存中创建一个java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在方法区中)用来封装类在方法区的数据结构,并作为方法区这个类的各种数据的访问入口;

(4)加载.class的方式:

本地系统直接加载;

从zip包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础;常见的应用程序在服务器上部署就是从JAR包中读取.class文件;

从网络中获取,这种场景最典型的就是Applet(小程序);

运行时计算生成,这种场景使用的最多的就是动态代理,在java.lang.reflect.Proxy,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流;;

从专有数据库中提取.class文件;例如:中间件服务器(SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发;

有其他文件生成,如:JSP应用,即有JSP文件生成对应的Class类

其他

2、Verification验证(Linking连接的第一阶段)

验证是连接阶段的第一步,包含:文件格式验证、元数据验证、字节码验证、符号引用验证。

验证阶段非常总要,但不一定必要,可以在实施阶段使用-Xverify:none来关闭大部分的验证措施。


(1)文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。

通过文件格式验证后,字节流会进入内存的方法区中进行存储;

其余3个验证阶段都是基于方法区的存储结构进行的,不会再直接操作字节流;

验证点包括:

是否以魔数0xCAFEBABE开头;

主、次版本号是否在当前虚拟机的处理范围内;

常量池中的常量类型是否有不被指出的常量类(检查常量tag标志);

指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量;

CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据;

Class文件中各个部分及文件本身是否有被删除的或附加的其他信息;

(2)元数据验证

对类的元数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息。

验证点包括:


这个类是否有父类(除了java.lang.Object之外,所有的类都应该有父类);

这个类的父类是否继承了不允许被继承的类(被final修饰的类);

如果这个类不是抽象类,是否继承了其父类或接口中要求实现的所有方法;

类中的字段、方法是否与父类产生矛盾(例如:覆盖了父类的final方法、不符合规则的方法重载,例如方法参数都一样,返回值类型却不同);

(3)字节码验证

通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的;

对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件;

(4)符号引用验证

对自身以外(常量池中的各种符号引用)的信息进行匹配行校验。


3、Preparation准备(Linking连接的第二阶段)

为类的静态变量在方法区中分配内存,设定初始值;

不包括实例变量,实例变量是随着对象实例化之后和对象一起分配在java堆中;

如:

    public static int value=3;


    在准备阶段过后初始值为0,而不是3。只有在初始化之后该内存中的值才会变成3.

    java中所有基本数据类型的零值:


    数据类型 零值

    int  0
    long  0L
    short  (short) 0
    char  ‘\u0000’
    byte  (byte) 0
    boolean  false
    float  0.0f
    double  0.0d
    reference  null

    特殊情况:


      public static final  int value = 3;

      编译时javac会为value生成ConstantValue属性,在准备阶段就会根据ConstantValue属性直接将value赋值为3;


      4、Resolution解析(Linking连接的第三阶段)

      将常量池中的符号引用替换为直接引用的过程;

      顺序不定,有可能在初始化之后解析,根据需要判断是在类加载器加载时解析还是在使用前解析;

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

      示例代码

      public class JVMClassResolutionTest {
        interface Interface0{
          int A=0;
        }
        interface Interface1 extends Interface0{
          int A=1;
        }
        interface Interface2{
          int A=2;
        }
        static class Parent implements Interface1{
          public static int A=3;
        }
        static class Son extends Parent implements Interface2{
          //public static int A=4;
        }
        public static void main(String[] args) {
          System.out.println(Son.A);
        }
      }




      报错:

        The field Son.A is ambiguous

        5、Initialization初始化

        初始化阶段是执行类构造器<clinit>()方法的过程。为类的静态变量赋予真正的初始值。


        <clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,收集顺序根据源文件顺序决定。静态语句块只能访问定义在静态语句块之前的变量,如:


        示例1


          public class Test16 {
          static {  
          i =1; 
          }
          static int i =0;
          }



          解析:代码正常编译通过,定义在静态语句块之后的变量,在静态语句中可以赋值;


          示例二

          public class Test16 {
            static {
               i =1;
               System.out.println("i===="+i); //代码编译报错Cannot reference a field before it is defined(非法向前引用)
            }
            static int i =0;
          }



          解析:定义在静态语句块之后的变量,在静态语句中可以赋值,不可以访问;


          示例三

          public class Test16 {
            static {
               i =1;
            }
            static int i =0;
            static {
              System.out.println("i===="+i);
            }
          }



          解析:定义在静态语句块之后的变量,在静态语句中可以赋值,在其之后可以访问;


          类的加载顺序。示例如下:


          public class ClassLoaderTest {
            public static void main(String[] args) {
                  son sons=new son();
              }
          }
          class parent{
              private static  int a=1;
              private static  int b;
              private   int c=initc();
              static {
                  b=1;
                  System.out.println("1.父类静态代码块:赋值b成功");
                  System.out.println("1.父类静态代码块:a的值"+a);
              }
              int initc(){
                  System.out.println("3.父类成员变量赋值:---> c的值"+c);
                  this.c=12;
                  System.out.println("3.父类成员变量赋值:---> c的值"+c);
                  return c;
              }
              public parent(){
                  System.out.println("4.父类构造方式开始执行---> a:"+a+",b:"+b);
                  System.out.println("4.父类构造方式开始执行---> c:"+c);
              }
          }
          class son extends parent{
              private static  int sa=1;
              private static  int sb;
              private   int sc=initc2();
              static {
                  sb=1;
                  System.out.println("2.子类静态代码块:赋值sb成功");
                  System.out.println("2.子类静态代码块:sa的值"+sa);
              }
              int initc2(){
                  System.out.println("5.子类成员变量赋值--->:sc的值"+sc);
                  this.sc=12;
                  return sc;
              }
              public son(){
                  System.out.println("6.子类构造方式开始执行---> sa:"+sa+",sb:"+sb);
                  System.out.println("6.子类构造方式开始执行---> sc:"+sc);
              }
          }







          运行结果

            1.父类静态代码块:赋值b成功
            1.父类静态代码块:a的值1
            2.子类静态代码块:赋值sb成功
            2.子类静态代码块:sa的值1
            3.父类成员变量赋值:---> c的值0
            3.父类成员变量赋值:---> c的值12
            4.父类构造方式开始执行---> a:1,b:1
            4.父类构造方式开始执行---> c:12
            5.子类成员变量赋值--->:sc的值0
            6.子类构造方式开始执行---> sa:1,sb:1
            6.子类构造方式开始执行---> sc:12


            解析:父类静态代码块>子类静态代码块>父类成员变量赋值>父类构造方式>子类成员变量赋值>子类构造器

            即验证了静态代码块优先初始化,有验证了在调用子类时,会优先初始化其父类。


            <clinit>()方法对于类和接口来说不是必须的,类中没有静态语句块和对变量的赋值操作,就不会生成<clinit>()方法;相同的接口中没有对变量的赋值操作也不会生成<clinit>()方法;

            接口和类的不同在于,子类执行<clinit>()方法之前其父类会先执行<clinit>()方法;子接口执行<clinit>()方法之前不会执行其父类接口的<clinit>()方法;

            <clinit>()方法在多线程环境中会被加锁、要求同步,多个线程同时初始化一个类,只有一个线程执行<clinit>()方法,其他线程阻塞等待。若<clinit>()方法执行时间过长会造成进行阻塞。实例:

            public class Test01 {
            public static void main(String[] args) {
              Runnable script = new Runnable() {
                public void run() {
                  System.out.println(Thread.currentThread()+"start");
                  JVMDeadLoopTest dlt = new JVMDeadLoopTest();
                  System.out.println(Thread.currentThread()+"run over");
                }
              };
              JVMDeadLoopTest dlt1 = new JVMDeadLoopTest();
              Thread thread1 = new Thread(script);
              Thread thread2 = new Thread(script);
              thread1.start();
              System.out.println("----");
              thread2.start();
            }




            ```

            public class JVMDeadLoopTest {
                static {
                  if(true) {
                    System.out.println(Thread.currentThread()+"init JVMDeadLoopTest");
                    //int i=0;
                    while(true) {
                      //i = i+1;
                      //if(i>3) { break; }
                    }
                  }
                }


            执行结果:

            Thread[Thread-1,5,main]start
            Thread[Thread-0,5,main]start
            Thread[Thread-1,5,main]init JVMDeadLoopTest


            分析:线程一直在初始化JVMDeadLoopTest类中的静态块部分,所以其他两个线程一直处于阻塞状态;

            放开代码中注释掉的部分,执行结果:


              Thread[Thread-1,5,main]start
              Thread[Thread-0,5,main]start
              Thread[Thread-1,5,main]init JVMDeadLoopTest
              Thread[Thread-0,5,main]run over
              Thread[Thread-1,5,main]run over


              未出现线程阻塞的情况


              6、Using使用

              7、Unloading卸载

              当一个类被加载、连接和初始化后,它的生命周期就开始了。当代表该类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,这个类在方法区内的数据也会被卸载,从而结束自己的生命周期

              一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期

              由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。Java虚拟机本身会始终引用这些类加载器,而这类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的;由用户自定义的类加载器所加载的类是可以被卸载的

              8、拓展:类实例化

              类的生命周期除了加载、连接、初始化之外,还有类实例化、垃圾回收和对象终结


              为新的对象分配内存;

              为实例变量赋予默认值;

              为实例变量赋予正确的初始值;

              java编译器为它编译的每一个类至少生成一个实例初始化方法,在java的class文件中,这个实例初始化方法被称为。针对源代码中的每一个类的构造方法,java编译器都会产生一个

              二、类的加载、连接、初始化代码示例

              1、类的使用方式

              java程序对类的使用分两种


              主动使用


              创建类的实例

              调用类的静态方法

              访问某个类或接口的静态变量

              初始化类的子类

              反射

              java虚拟机启动时被标明为启动类的类(包含main方法)

              JDK1.7开始提供的动态语言支持(java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化);

              被动使用


              除了主动使用的其中情况外,其他使用java类的方法都看做是对类的被动使用,都不会导致类的初始化;

              2、代码理解

              示例一:类的加载、连接和初始化过程

              代码一

                public class JVMTest01 {
                public static void main(String[] args) {   
                System.out.println(Child01.str); 
                }
                }
                class Father01{  
                public static String str = "我是父类01静态变量!"; 
                static {   
                System.out.println("Father01 static block");  
                }
                }
                  class Child01 extends Father01{
                    static {  
                    System.out.println("Child01 static block"); 
                    }
                    }


                  运行结果:


                    Father01 static block
                    我是父类01静态变量!


                    代码二

                    public class JVMTest01 {
                      public static void main(String[] args) {
                        System.out.println(Child01.str);
                      }
                    }
                    class Father01{
                      public static String str = "我是父类01静态变量!";
                      static {
                        System.out.println("Father01 static block");
                      }
                    }
                    class Child01 extends Father01{
                      public static String str = "我是子类类01静态变量!";
                      static {
                        System.out.println("Child01 static block");
                      }
                    }


                    运行结果:


                      Father01 static block
                      Child01 static block

                      我是子类类01静态变量!


                      分析:

                      代码一中,我们通过子类调用父类中的str,这个str是在父类中被定义的,对Father01主动使用,没有主动使用Child01,因此Child01中的静态代码没有执行,父类中的静态代码执行了。对于静态字段来说,只有直接定义了该字段的类才会被初始化。

                      代码二中,对Child01主动使用;根据主动使用的7中情况,调用类的子类时,其所有的父类都会被先初始化,所以Father01会被初始化。当一个类初始化时,要求其父类全部已经初始化完毕。

                      以上验证的是类的初始化情况,那么如何验证类的加载情况呢,可以通过在启动的时候配置虚拟机参数:-XX:+TraceClassLoading查看


                      运行代码一运行结果:

                      [Opened C:\Program Files\Java\jre1.8.0_221\lib\rt.jar]
                      [Loaded java.lang.Object from C:\Program Files\Java\jre1.8.0_221\lib\rt.jar]
                      [Loaded java.io.Serializable from C:\Program Files\Java\jre1.8.0_221\lib\rt.jar]
                      ......
                      [Loaded JVMTest.Father01 from file:/D:/work-space/TestCoJava/bin/]
                      [Loaded JVMTest.Child01 from file:/D:/work-space/TestCoJava/bin/]
                      Father01 static block
                      我是父类01静态变量!
                      [Loaded java.lang.Shutdown from C:\Program Files\Java\jre1.8.0_221\lib\rt.jar]
                      [Loaded java.lang.Shutdown$Lock from C:\Program Files\Java\jre1.8.0_221\lib\rt.jar]


                      可以看见控制台打印了very多的日志,第一个加载的是java.lang.Object类(不管加载哪个类,他的父类一定是Object类),后面是加载的一系列jdk的类,他们都位于rt包下。往下查看,可以看见Loaded classloader.Child01,说明即使没有初始化Child01,但是程序依然加载了Child01类。


                      拓展

                      所有的JVM参数都是以-XX:开头的;

                      如果形式是:-XX:+,表示开启option选项;

                      如果形式是:-XX:-,表示关闭option选项;

                      如果形式是:-XX:=,表示将option选项设置的值为value;

                      示例二:常量的本质含义

                      public class JVMTest02 {
                        public static void main(String[] args) {
                          System.out.println(Father02.str);
                        }
                      }
                      class Father02{
                        public static final String str="我是父类的静态常量!";
                        static {
                          System.out.println("Father02 static block");
                        }
                      }
                      


                      运行结果:


                      我是父类的静态常量!

                      1

                      分析:可以看到这段代码并没有初始化Father02类。这是因为final表示的是一个常量,在编译阶段常量就被存入调用这个常量的方法所在的类的常量池中,本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。在这段代码中,str被存入到JVMTest02的常量池中,之后JVMTest02和Father02没有任何关系,甚至可以删除Father的class文件。


                      反编译JVMTest02:

                      Compiled from "JVMTest02.java"
                      public class classloader.JVMTest02 {
                        public classloader.JVMTest02();
                          Code:
                             0: aload_0
                             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
                             4: return
                        public static void main(java.lang.String[]);
                          Code:
                             0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
                             3: ldc           #4                  // String 我是父类的静态常量!
                             5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
                             8: return
                      }


                      分析:


                      第一块是JVMTest02的构造方法,第二块是我们要看的main方法。可以看见“3: ldc #4 // String 我是父类的静态常量!”,此时这个值已经是“我是父类的静态常量!”了,而不是Father02.str,证实了上面说的在编译阶段常量就已经被存入调用常量的方法所在的类的常量池中了。


                      拓展(助记符):


                      Idc:表示int、float或String类型的常量值常量池中推送到栈顶;

                      bipush:表示将单字节(-128至127)的常量推送至栈顶;

                      sipush:表示将短整型(-32768至32767)的常量推送至栈顶;

                      =iconst_1:表示将int类型的1推送至栈顶(这类助记符只有iconst——m1-iconst_5七个;==

                      示例三:编译器常量与运行期常量的区别

                      public class JVMTest03 {
                        public static void main(String[] args) {
                          System.out.println(Father03.str);
                        }
                      }
                      class Father03{
                        public static final String str= UUID.randomUUID().toString();
                        static {
                          System.out.println("Father03 static block");
                        }
                      }


                      运行结果:



                      Father03 static block
                      2d38fe6a-9b12-4d6c-92d7-a015e24eb198


                      分析:

                      本代码与示例二的区别在于str的值是在运行时确认的,而不是编译时就确定好的,属于运行期常量,而不是编译期常量。当一个常量的值并非编译期间确定的,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,导致这个类被初始化。


                      示例四:数组创建的本质

                      代码一:

                      public class JVMTest04 {
                        public static void main(String[] args) {
                          Father04 father04_1 = new Father04();
                          System.out.println("--------------");
                          Father04 father04_2 = new Father04();
                        }
                      }
                      class Father04{
                        static {
                          System.out.println("Father04 static block");
                        }
                      }


                      运行结果:


                        Father04 static block
                        --------------



                        分析:


                        创建类的实例时,会初始化类;

                        所有的java虚拟机的实现,必须在每个类或接口被java程序“首次主动使用”时才初始化他们

                        代码二:

                        public class JVMTest04 {
                          public static void main(String[] args) {
                            Father04[] father04s = new Father04[1];
                            System.out.println(father04s.getClass());
                          }
                        }
                        class Father04{
                          static {
                            System.out.println("Father04 static block");
                          }
                        }
                        



                        运行结果:


                          class [LJVMTest.Father04;


                          分析:

                          创建数组对象不在主动使用的7中情况内,所以不会初始化Father04;

                          打印father04s的类型为[LJVMTest.Father04,这是虚拟机在运行期生成的。-> 对于数组示例来说,其类型是有JVM在运行期动态生成的,表示为[LJVMTest.Father04这种形式,动态生成的类型,其父类就是Object。

                          对于数组来说,javaDoc经常讲构成数组的元素为Component,实际上就是将数组降低一个维度后的类型;

                          反编译一下:

                          public static void main(java.lang.String[]);
                              Code:
                                 0: iconst_1
                                 1: anewarray     #2                  // class classloader/Father04
                                 4: astore_1
                                 5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
                                 8: aload_1
                                 9: invokevirtual #4                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
                                12: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
                                15: return
                          



                          分析:

                          anewarray:表示创建一个引用类型(如类、接口、数组)的数组,并将其引用值值压入栈顶

                          newarray:表示创建一个指定的原始类型(如int、float、char等)的数组,并将其引用值压入栈顶;

                          示例五:接口的加载与初始化

                          代码一

                          public class JVMTest05 {
                            public static void main(String[] args) {
                              System.out.println(Child05.i);
                              System.out.println(Child05.j);
                            }
                          }
                          interface Father05{
                            int i = 5;
                          }
                          interface Child05 extends Father05{
                            int j = 6;
                          }



                          编译后删除Father05.class文件和Child05.class文件,运行结果


                            5
                            6



                            分析


                            接口中定义的常量本身就是public、static、final的;

                            接口中的常量在编译阶段已经存在于JVMTest05类的常量池中了,此时Father05和Child05都不会被加载;

                            代码二

                            public class JVMTest05 {
                              public static void main(String[] args) {
                                System.out.println(Child05.j);
                              }
                            }
                            interface Father05{
                              int i = 5;
                            }
                            interface Child05 extends Father05{
                              //取0~8之间的随机数
                              int j = new Random().nextInt(8);
                            }


                            运行结果:


                              4
                              1

                              把Father05.class文件删除,运行结果:

                              Exception in thread "main" java.lang.NoClassDefFoundError: classloader/Father05
                                  at java.lang.ClassLoader.defineClass1(Native Method)
                                  at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
                                  at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
                                  at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
                                  at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
                                  at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
                                  at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
                                  at java.security.AccessController.doPrivileged(Native Method)
                                  at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
                                  at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
                                  at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
                                  at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
                                  at classloader.Test05.main(Test05.java:15)
                              Caused by: java.lang.ClassNotFoundException: classloader.Father05
                                  at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
                                  at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
                                  at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
                                  at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
                                  ... 13 more



                              分析:运行时的常量,

                              只有在真正使用到父接口的时候(如引用接口中定义的常量时),才会加载初始化Child05,并且初始化Child05的父类Father05.class,Father05.class已经被删除,所以报错。


                              代码三:

                              public class JVMTest06 {
                                public static void main(String[] args) {
                                  System.out.println(Child06.j);
                                }
                              }
                              interface Father06{
                                Thread thread = new Thread(){
                                  {
                                    System.out.println("Father05 code block");
                                  }
                                };
                              }
                              class Child06 implements Father06{
                                public static int j = 8;
                              }


                              运行结果


                                8


                                分析:在初始化一个类时,并不会先初始化他所实现的接口


                                代码四:

                                public class JVMTest07 {
                                    public static void main(String[] args) {
                                        System.out.println(Father07.thread);
                                    }
                                }
                                interface GrandFather {
                                    Thread thread = new Thread() {
                                        {
                                            System.out.println("GrandFather code block");
                                        }
                                    };
                                }
                                interface Father07 extends GrandFather{
                                    Thread thread = new Thread() {
                                        {
                                            System.out.println("Father07 code block");
                                        }
                                    };
                                }



                                运行结果:


                                  Father07 code block
                                  Thread[Thread-0,5,main]



                                  分析:初始化一个接口时,并不会初始化他的父接口


                                  示例六:类加载器的准备阶段和初始化阶段

                                  代码一

                                  public class JVMTest08 {
                                    public static void main(String[] args) {
                                      Singleton singleton = Singleton.getInstance();
                                      System.out.println("i:" +Singleton.i);
                                      System.out.println("j:" +Singleton.j);
                                    }
                                  }
                                  class Singleton{
                                    public static int i;
                                    public static int j = 0;
                                    private static Singleton singleton = new Singleton();
                                    private Singleton() {
                                      i++;
                                      j++;
                                    }
                                    public static Singleton getInstance() {
                                      return singleton;
                                    }
                                  }




                                  运行结果:


                                    i:1
                                    j:1


                                    分析:首先Singleton.getInstance()调用Singleton的getInstance方法,getInstance返回singleton实例,singleton的实例是==new Singleton();==出来的,因此调用了自定义的私有构造方法。在调用构造方法之前,给静态变量赋值,i默认为0,j显示的赋值为0,经过构造函数之后,值都为1。


                                    代码二

                                    public class JVMTest08 {
                                      public static void main(String[] args) {
                                        Singleton singleton = Singleton.getInstance();
                                        System.out.println("i:" +Singleton.i);
                                        System.out.println("j:" +Singleton.j);
                                      }
                                    }
                                    class Singleton{
                                      public static int i;
                                      private static Singleton singleton = new Singleton();
                                      private Singleton() {
                                        i++;
                                        j++;
                                      }
                                      public static int j = 0;
                                      public static Singleton getInstance() {
                                        return singleton;
                                      }
                                    }


                                    运行结果:


                                      i:1
                                      j:0



                                      分析:程序主动使用了Singleton类,准备阶段对类的静态变量分配内存,赋予默认值,下面给出类在连接及初始化阶段常量的值的变化

                                      i :0
                                      singleton:null
                                      j :0
                                      getInstance:初始化
                                      i:0
                                      singleton:调用构造函数
                                      i:1
                                      j:1
                                      j:0【覆盖了之前的1】

                                      因此返回值i的值为1,j的值为0;

                                      三、类加载器

                                      1、类加载器体系结构

                                      类加载器分为两种,一种是java虚拟机自带的类加载器,一种是用户自定义的类加载器。


                                      (1)java虚拟机再带的类加载器

                                      BootStrapClassLoader启动类加载器

                                      没有父加载器;依赖底层操作系统;

                                      没有继承java.lang.ClassLoader类,由C++编写;

                                      负责加载系统类(指的是内置类,像是String,对应于C#中的System类和C/C++标准库中的类,<JAVA_HOME>\lib目录中,或者-XbootclassPath参数指定路径下的javaAIP的核心类库,如java.lang.等;

                                      Bootstrap loader所做的初始工作中,除了一些基本的初始化动作之外,最重要的就是加载Launcher.java中的ExtClassLoader(扩展类装载器,负责加载扩展类,就是继承类和实现类),并设定其parent为null,代表其父类加载器为BootStrapLoader

                                      ExtClassLoader扩展类加载器

                                      由sun.misc.Launcher$ExtClassLoader实现,继承java.lang.ClassLoader,其父加载器为BootStrapClassLoader启动类装载器;

                                      负责加载<JAVA_HOME>\lib\ext目录下、java.ext.dirs系统变量指定路径中的的类库;如果用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载;

                                      负责加载扩展类,就是继承类和实现类

                                      AppClassLoader应用程序类加载器(系统类加载器)

                                      由sun.misc.Launcher$AppClassLoader实现,继承java.lang.ClassLoader,,父加载器为ExtClassLoader扩展类装载器;

                                      这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,故又称系统加载类;

                                      应用程序中默认的类加载器,是用户自定义类加载器的默认父加载器;

                                      从环境变量classpath或者系统或者系统属性java.class.path所指定的目录中加载类

                                      (2)用户自定义的类加载

                                      默认APPClassLoader为父加载器,java.lang.ClassLoader的子类;

                                      用户可以定义类的加载方式;

                                      2、类与加载器

                                      对于任意一个类,类和加载这个类的类加载器一同确立了其在虚拟机中的唯一性。每个类加载器在虚拟机中都有自己独立的命名空间。同一个类可能存在于多个命名空间。只要类的加载器不一样,同一个类也不相等,这里的相等包括equals方法、isAssignableFrom()方法、isInstance()方法的返回结果。


                                      代码待补充


                                      拓展:


                                      -命名空间:java虚拟机为每一个类装载器维护一个唯一标识的命名空间。一个java程序可以多次装载具有同一个权限命名的多个类。java虚拟机要确定这“多个类”的唯一性,因此,当多个类装载器都装载了同名的类时,为了唯一的标识这个类,还要在类名前加上装载该类的类装载器的标识(指出了类所位于的命名空间)。

                                      命名空间有助于安全的实现,因为你可以有效地在装入了不同命名空间的类之间设置一个防护罩。在Java虚拟机中,在同一个命名空间内的类可以直接进行交 互,而不同的命名空间中的类甚至不能察觉彼此的存在,除非显式地提供了允许它们进行交互的机制。一旦加载后,如果一个恶意的类被赋予权限访问其他虚拟机加 载的当前类,它就可以潜在地知道一些它不应该知道的信息,或者干扰程序的正常运行。

                                      3、双亲委派机制

                                      双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码。


                                      (1)工作原理

                                      -类加载器收到类加载请求,不会先自己去加载,而是请求委托给父加载器加载,如果还存在上一层,则继续向上委托,最终到达顶层的BootStrapClassLoader启动类加载器。只有当父类无法完成时,子类才会尝试自己去加载;示例如下:

                                      package test;   
                                        import java.net.URL;   
                                        import java.net.URLClassLoader;   
                                        public class ClassLoaderTest {   
                                            private static int count = -1;   
                                            public static void testClassLoader(Object obj) {   
                                                if (count < 0 && obj == null) {   
                                                   System.out.println("Input object is NULL";   
                                                   return;   
                                               }   
                                                ClassLoader cl = null;   
                                                if (obj != null && !(obj instanceof ClassLoader)) {   
                                                    cl = obj.getClass().getClassLoader();   
                                                } else if (obj != null) {   
                                                    cl = (ClassLoader) obj;   
                                                }   
                                                count++;   
                                                String parent = "";   
                                                for (int i = 0; i < count; i++) {   
                                                    parent += "Parent ";   
                                                }   
                                                if (cl != null) {   
                                                    System.out.println(   
                                                        parent + "ClassLoader name = " + cl.getClass().getName());   
                                                    testClassLoader(cl.getParent());   
                                                } else {   
                                                   System.out.println(   
                                                        parent + "ClassLoader name = BootstrapClassLoader";   
                                                   count = -1;   
                                                }   
                                           }   
                                            public static void main(String[] args) {   
                                                URL[] urls = new URL[1];   
                                                URLClassLoader urlLoader = new URLClassLoader(urls);   
                                                ClassLoaderTest.testClassLoader(urlLoader);   
                                           }   
                                      }



                                      以上例程的输出为:

                                      ClassLoader name = java.net.URLClassLoader
                                      Parent ClassLoader name = sun.misc.Launcher$AppClassLoader
                                      Parent Parent ClassLoader name = sun.misc.Launcher$ExtClassLoader
                                      Parent Parent Parent ClassLoader name = BootstrapClassLoader



                                      类装载器请求过程

                                      以上例程1为例.将main方法改为:


                                      ClassLoaderTest tc = new ClassLoaderTest();
                                      ClassLoaderTest.testClassLoader(tc);

                                      输出为:

                                      ClassLoader name = sun.misc.Launcher$AppClassLoader
                                      Parent ClassLoader name = sun.misc.Launcher$ExtClassLoader
                                      Parent Parent ClassLoader name = BootstrapClassLoader



                                      程序运行过程中,类路径类装载器发出一个装载ClassLoaderTest类的请求, 类路径类装载器必须首先询问它的Parent—扩展类装载器 —来查找并装载这个类,同样扩展类装载器首先询问启动类装载器。由于ClassLoaderTest不是 Java API(JAVA_HOME\jre\lib)中的类,也不在已安装扩展路径(JAVA_HOME\jre\lib\ext)上,这两类装载器 都将返回而不会提供一个名为ClassLoaderTest的已装载类给类路径类装载器。类路径类装载器只能以它自己的方式来装载 ClassLoaderTest,它会从当前类路径上下载这个类。这样,ClassLoaderTest就可以在应用程序后面的执行中发挥作用。

                                      在上例中,ClassLoaderTest类的testClassLoader方法被首次调用,该方法引用了Java API中的类 java.lang.String。Java虚拟机会请求装载ClassLoaderTest类的类路径类装载器来装载 java.lang.String。就像前面一样,类路径类装载器首先将请求传递给它的Parent类装载器,然后这个请求一路被委托到启动类装载器。但 是,启动类装载器可以将java.lang.String类返回给类路径类装载器,因为它可以找到这个类,这样扩展类装载器就不必在已安装扩展路径中查找 这个类,类路径类装载器也不必在类路径中查找这个类。扩展类装载器和类路径类装载器仅需要返回由启动类装载器返回的类java.lang.String。从这一刻开始,不管何时ClassLoaderTest类引用了名为java.lang.String的类,虚拟机就可以直接使用这个 java.lang.String类了。

                                      (2)一个经典的实例说明

                                        package java.lang; 
                                        public class String {  
                                        public static void main(String[] args){ 
                                        } 
                                        }



                                        大家发现什么不同了吗?对了,我们写了一个与JDK中String一模一样的类,连包java.lang都一样,唯一不同的是我们自定义的String类有一个main函数。我们来运行一下:


                                          java.lang.NoSuchMethodError: main  
                                          Exception in thread "main"



                                          这是为什么? 我们的String类不是明明有main方法吗?

                                          其实联系我们上面讲到的双亲委托模型,我们就能解释这个问题了。


                                          运行这段代码,JVM会首先创建一个自定义类加载器,不妨叫做AppClassLoader,并把这个加载器链接到委托链中:AppClassLoader -> ExtClassLoader -> BootstrapLoader。


                                          然后AppClassLoader会将加载java.lang.String的请求委托给ExtClassLoader,而 ExtClassLoader又会委托给最后的启动类加载器BootstrapLoader。


                                          启动类加载器BootstrapLoader只能加载JAVA_HOME\jre\lib中的class类(即J2SE API),问题是标准API中确实有一个java.lang.String(注意,这个类和我们自定义的类是完全两个类)。BootstrapLoader以为找到了这个类,毫不犹豫的加载了j2se api中的java.lang.String。


                                          最后出现上面的加载错误(注意不是异常,是错误,JVM退出),因为API中的String类是没有main方法的。


                                          结论:我们当然可以自定义一个和API完全一样的类,但是由于双亲委托模型,使得我们不可能加载上我们自定义的这样一个类。所以J2SE规范中希望我们自定义的包有自己唯一的特色(网络域名)。还有一点,这种加载器原理使得JVM更加安全的运行程序,因为黑客很难随意的替代掉API中的代码了。

                                          目录
                                          相关文章
                                          |
                                          3月前
                                          |
                                          安全 前端开发 Java
                                          【JVM的秘密揭秘】深入理解类加载器与双亲委派机制的奥秘!
                                          【8月更文挑战第25天】在Java技术栈中,深入理解JVM类加载机制及其双亲委派模型是至关重要的。JVM类加载器作为运行时系统的关键组件,负责将字节码文件加载至内存并转换为可执行的数据结构。其采用层级结构,包括引导、扩展、应用及用户自定义类加载器,通过双亲委派机制协同工作,确保Java核心库的安全性与稳定性。本文通过解析类加载器的分类、双亲委派机制原理及示例代码,帮助读者全面掌握这一核心概念,为开发更安全高效的Java应用程序奠定基础。
                                          91 0
                                          |
                                          2月前
                                          |
                                          安全 Java 应用服务中间件
                                          JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
                                          什么是类加载器,类加载器有哪些;什么是双亲委派模型,JVM为什么采用双亲委派机制,打破双亲委派机制;类装载的执行过程
                                          JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
                                          |
                                          1月前
                                          |
                                          缓存 前端开发 Java
                                          JVM知识体系学习二:ClassLoader 类加载器、类加载器层次、类过载过程之双亲委派机制、类加载范围、自定义类加载器、编译器、懒加载模式、打破双亲委派机制
                                          这篇文章详细介绍了JVM中ClassLoader的工作原理,包括类加载器的层次结构、双亲委派机制、类加载过程、自定义类加载器的实现,以及如何打破双亲委派机制来实现热部署等功能。
                                          43 3
                                          |
                                          1月前
                                          |
                                          小程序 Oracle Java
                                          JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
                                          这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
                                          41 0
                                          JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
                                          |
                                          2月前
                                          |
                                          Arthas Java 测试技术
                                          JVM —— 类加载器的分类,双亲委派机制
                                          类加载器的分类,双亲委派机制:启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器;JDK8及之前的版本,JDK9之后的版本;什么是双亲委派模型,双亲委派模型的作用,如何打破双亲委派机制
                                          JVM —— 类加载器的分类,双亲委派机制
                                          |
                                          1月前
                                          |
                                          前端开发 Java 应用服务中间件
                                          JVM进阶调优系列(1)类加载器原理一文讲透
                                          本文详细介绍了JVM类加载机制。首先解释了类加载器的概念及其工作原理,接着阐述了四种类型的类加载器:启动类加载器、扩展类加载器、应用类加载器及用户自定义类加载器。文中重点讲解了双亲委派机制,包括其优点和缺点,并探讨了打破这一机制的方法。最后,通过Tomcat的实际应用示例,展示了如何通过自定义类加载器打破双亲委派机制,实现应用间的隔离。
                                          |
                                          3月前
                                          |
                                          数据库 C# 开发者
                                          WPF开发者必读:揭秘ADO.NET与Entity Framework数据库交互秘籍,轻松实现企业级应用!
                                          【8月更文挑战第31天】在现代软件开发中,WPF 与数据库的交互对于构建企业级应用至关重要。本文介绍了如何利用 ADO.NET 和 Entity Framework 在 WPF 应用中访问和操作数据库。ADO.NET 是 .NET Framework 中用于访问各类数据库(如 SQL Server、MySQL 等)的类库;Entity Framework 则是一种 ORM 框架,支持面向对象的数据操作。文章通过示例展示了如何在 WPF 应用中集成这两种技术,提高开发效率。
                                          58 0
                                          |
                                          3月前
                                          |
                                          开发者 C# Windows
                                          WPF布局大揭秘:掌握布局技巧,轻松创建响应式用户界面,让你的应用程序更上一层楼!
                                          【8月更文挑战第31天】在现代软件开发中,响应式用户界面至关重要。WPF(Windows Presentation Foundation)作为.NET框架的一部分,提供了丰富的布局控件和机制,便于创建可自动调整的UI。本文介绍WPF布局的基础概念与实现方法,包括`StackPanel`、`DockPanel`、`Grid`等控件的使用,并通过示例代码展示如何构建响应式布局。了解这些技巧有助于开发者优化用户体验,适应不同设备和屏幕尺寸。
                                          86 0
                                          |
                                          1月前
                                          |
                                          存储 安全 Java
                                          jvm 锁的 膨胀过程?锁内存怎么变化的
                                          【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
                                          37 4
                                          |
                                          8天前
                                          |
                                          Arthas 监控 Java
                                          JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
                                          本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。