• 类加载机制与反射(一)


    1、类的加载、连接和初始化
      当调用java命令运行某个Java程序时,该命令将会启动一个Java虚拟机进程,不管该Java程序有多么复杂,改程序启动了多少个线程,它们都处于该Java虚拟机进程里。同一个JVM的所有线程、所有变量都处于同一个进程里,它们都使用该JVM进程的内存区。当系统出现以下几种情况时,JVM进程将被终止
      程序运行到最后正常结束;
      程序运行到使用System.exit()或Runtime.getRuntime().exit()代码处结束程序;
      程序执行过程中遇到未捕获的异常或错误而结束。
      程序所在平台强制结束了JVM进程。
     
      当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化三个步骤来对该类进行初始化。
      类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
      通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源
      从本地文件系统加载class文件;
      从JAR包加载class文件,这种方式也是很常见的,如JDBC编程用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
      通过网络加载class文件。
      把一个Java源文件动态编译,并执行加载。
     
      类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。
    类的连接
      当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下三个阶段:
      (1)验证: 验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致;
      (2) 准备: 类准备阶段则负责为类的类变量分配内存,并设置默认初始值;
      (3) 解析: 将类的二进制数据中的符号引用替换成直接引用。
    类的初始化
      在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对类变量进行初始化。在Java类中对类变量指定初始值有两种方式:
      (1)声明类变量时指定初始值;
      (2)使用静态初始化块为类变量指定初始值。
    public class Test {
            static {
                    //使用静态初始化块为变量b指定初始值
                    b = 6;
            }   
            //声明变量a时指定初始值
            static int a = 5;
            static int b = 9;        //
            static int c;
            
            public static void main(String[] args) {
                    System.out.println(Test.b);
            }
    }
      现在静态初始化块中为变量b赋值,此时类变量b的值为6;接着程序向下执行,执行到号代码处,这行代码也属于该类的初始化语句,所以程序再次为类变量b赋值。也就是说,当Test类初始化结束后,该类的类变量b的值为9.
      JVM初始化一个类包含如下几个步骤:
      (1)假如这个类还没有被加载和连接,则程序先加载并连接该类;
      (2)假如该类的直接父类还没有被初始化,则先初始化其直接父类;
      (3)假如类中有初始化语句,则系统依次执行这些初始化语句。
    类初始化的时机
      当Java程序首次通过下面6种方式来使用某个类或接口时,系统就会初始化该类或接口。
      (1)创建类的实例。为某个类创建实例的方式包括:使用new操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例。
      (2)调用某个类的类方法(静态方法);
      (3)访问某个类或接口的类变量,或为该类变量赋值;
      (4)使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。例如代码:Class.forName("Person"),如果系统还未初始化Person类,则这行代码将会导致Person类被初始化,并返回Person类对应的java.lang.Class对象。
      (5)初始化某个类的子类。当初始化某个类的子类时,该子类的所有父类都会被初始化。
      (6)直接使用java.exe命令来运行某个主类。当运行某个主类时,程序会先初始化该主类。
      对于一个final型的类变量,如果该类变量的值在编译时就可以确定下来,那么这个类变量相当于“宏变量”。Java编译器会在编译时直接把这个类变量出现的地方替换成它的值,因此即使程序使用该静态类变量,也不会导致该类的初始化如:
    public class CompileConstantTest {
            
            public static void main(String[] args) {
                    System.out.println(MyTest.compileConstant);
            }
    
    }
    class MyTest{
            static {
                    System.out.println("静态初始化块...");
            }
            //使用一个字符串直接量为static final 的类变量赋值
            static final String compileConstant = "crazy java";
    }
      程序中使用compileConstant 的地方都会在编译时被直接替换成它的值,所以不会导致初始化MyTest类。
      当某个类变量(页脚静态变量)使用了final修饰,而且它的值可以在编译时就确定下来,那么程序其他地方使用该类变量时,实际上并没有使用该类变量,而是相当于使用常量
      反之,如果final修饰的类变量的值不能再编译时确定下来,则必须等到运行时才可以确定该类变量的值,如果通过该类来访问它的类变量,则会导致该类被初始化。如:
    static final String currentTime = System.currentTimeMillis() + "";
      定义的currentTime 类变量的值必须在运行时才可以确定,将导致类被初始化。
      当使用ClassLoader类的loadClass()方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。使用Class的forName()静态方法才会导致强制初始化该类。如:
    public class ClassLoaderTest {
            public static void main(String[] args) throws ClassNotFoundException {
                    ClassLoader cl = ClassLoader.getSystemClassLoader();
                    cl.loadClass("com.turing.classload.jvm.Tester");
                    System.out.println("系统加载Tester类");
                    Class.forName("com.turing.classload.jvm.Tester");
            }
    }
    
    class Tester{
            static {
                    System.out.println("Tester类的静态初始化块...");
            }
    }

    outputs:

    系统加载Tester类
    Tester类的静态初始化块...
    2、类加载器
      类加载器负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的java.lang.Class对象。
      类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。
      当JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构
      (1)Bootstrap ClassLoader: 根类加载器;
      (2)Extention ClassLoader: 扩展类加载器;
      (3)System ClassLoader: 系统类加载器。
      Bootstrap ClassLoader被称为引导(也称为原始或根)类加载器,它负责加载Java的核心类。在Sun的JVM中,当执行java.exe命令时,使用-Xbootclasspath选项或使用-D选项指定sun.boot.class.path系统属性值可以指定加载附加的类。
    根类加载器非常特殊,它并不是java.lang.ClassLoader的子类,而是由JVM自身实现的
      Extention Classloader被称为扩展类加载器,它负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext或者由java.ext.dirs系统属性指定的目录)中JAR包的类
      通过这种方式,就可以为Java扩展核心类以外的新功能,只要把自己开发的类打包成JAR文件,然后放入JAVA_HOME/jre/lib/ext路径即可。
      System ClassLoader被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以类加载器作为父加载器
     
    类加载机制
      JVM的类加载机制主要有如下三种:
      (1)全盘负责。所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
      (2)父类委托。所谓父类委托,就是先让parent类加载器试图加载该Class.只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
      (3)缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因
    public class ClassLoaderPropTest {
            public static void main(String[] args) throws IOException {
                    // 获取系统类加载器
                    ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
                    System.out.println("系统类加载器: " + systemLoader);
                    /*
                     * 获取系统类加载器的加载路径--通常由CLASSPATH环境变量指定
                     * 如果操作系统没有指定CLASSPATH环境变量,则默认以当前路径作为系统类加载器的加载路径
                     */
                    Enumeration<URL> eml = systemLoader.getResources("");
                    while (eml.hasMoreElements()) {
                            System.out.println(eml.nextElement());
                    }
                    // 获取系统类加载器的父类加载器,得到扩展类加载器
                    ClassLoader extentionLoader = systemLoader.getParent();
                    System.out.println("扩展类加载器: " + extentionLoader);
                    System.out.println("扩展类加载器的加载路径: " + System.getProperty("java.ext.dirs"));
                    System.out.println("扩展类加载器的parent: " + extentionLoader.getParent());
            }
    }

    Outputs:

    系统类加载器: sun.misc.Launcher$AppClassLoader@73d16e93
    file:/E:/workspace/test/CrazyJava/bin/
    扩展类加载器: sun.misc.Launcher$ExtClassLoader@15db9742
    扩展类加载器的加载路径: C:Program FilesJavajdk1.8.0_91jrelibext;C:WindowsSunJavalibext
    扩展类加载器的parent: null
      系统类加载器的加载路径是程序运行的当前路径,扩展类加载器的加载路径是C:Program FilesJavajdk1.8.0_91jrelibext;但此处看到扩展类加载器的父加载器是null,并不是根类加载器。这是因为根类加载器并没有继承ClassLoader抽象类,所以扩展类加载器的getParent()方法返回null.但实际上,扩展类加载器的父类加载器是根类加载器,只是根类加载器并不是Java实现的。
      从运行结果可以看出,系统类加载器是AppClassLoader的实例,扩展类加载器是ExtClassLoader的实例。实际上,这两个类都是URLClassLoader类的实例
      JVM的根类加载器并不是Java实现的,而且由于程序通常无须访问根类加载器,因此访问扩展类加载器的父类加载器时返回null
      使用自定义的类加载器,可以实现如下常见功能:
      执行代码前自动验证数字签名;
      根据用户提供的密码解密代码,从而可以实现代码混淆器来避免反编译*.class文件;
      根据用户需求来动态的加载类;
      根据应用需求把其他数据以字节码的形式加载到应用中。
    3、通过反射查看类信息
      获得Class对象
      在Java程序中获得Class对象通常有如下三种方式:
      使用Class类的forName(String clazzName)静态方法。该方法需要传入字符串参数,该字符串参数的值是某个类的全限定类名(必须添加完整包名)
      调用某个类的class属性来获取该类对应的Class对象。例如,Person.class将会返回Person类对应的Class对象;
      调用某个对象的getClass()方法。该方法是java.lang.Object类中的一个方法,所以所有的Java对象都可以调用该方法,该方法将会返回该对象所属类对应的Class对象。
      对于第一种方式和第二种方式都是直接根据类来取得该类的Class对象,相比之下,第二种方式有如下两种优势:
      代码更安全。程序在编译阶段就可以检查需要访问的Class对象是否存在;
      程序性能更好。因为这种方式无须调用方法,所以性能更好。
     
    Java8新增的方法参数反射
      Java8在java.lang.reflect包下新增了一个Executable抽象基类,该对象代表可执行的类成员,该类派生了Constructor、Method两个子类。
      Executable基类提供了大量方法来获取修饰该方法或构造器的注解信息;还提供了isVarArgs()方法用于判断该方法或构造器是否包含数量可变的形参,以及通过getModifiers()方法来获取该方法或构造器的修饰符。除此之外,Executable提供了如下两个方法来获取该方法或参数的形参个数及形参名。
    使用javac命令编译Java源文件时,默认生成的class文件并不包含方法的形参名信息,因此调用isNamePresent()方法将会返回false,调用getName()方法也不能得到该参数的形参名。如果希望javac命令编译Java源文件时可以保留形参信息,则需要为该命令指定-parameters选项。
    import java.lang.reflect.Method;
    import java.lang.reflect.Parameter;
    import java.util.List;
    
    public class MethodParameterTest {
            public static void main(String[] args) throws Exception {
                    //获取string的类
                    Class<Test> clazz = Test.class;
                    //获取string类的带两个参数的replace()方法
                    Method replace = clazz.getMethod("replace", String.class, List.class);
                    //获取指定方法的参数个数
                    System.out.println("replace方法参数个数: " + replace.getParameterCount());
                    //获取replace的所有参数信息
                    Parameter[] parameters = replace.getParameters();
                    int index = 1;
                    //遍历所有参数
                    for(Parameter p : parameters) {
                            if (p.isNamePresent()) {
                                    System.out.println("---第" + index + "个参数信息---");
                                    System.out.println("参数名: " + p.getName());
                                    System.out.println("形参类型: " + p.getType());
                                    System.out.println("泛型类型: " + p.getParameterizedType());
                            }
                    }
                    
            }
    }
    
    class Test{
            public void replace(String str, List<String> list) {
                    
            }
    }

     
  • 相关阅读:
    Java多态
    推荐TED演讲:20岁光阴不再来(Why 30 is not the new 20)
    HDU 5305 Friends (DFS)
    C#高级编程八十一天----捕获异常
    Amazon EC2安装mysql多实例并配置主从复制
    python coding style guide 的高速落地实践
    Tomcat 在win7/win8 系统下tomcat-users.xml.new(拒绝访问)解决方法
    jsp+oracle实现数据库内容以表格形式在前台显示(包含分页)
    JSP/SERVLET入门教程--Servlet 使用入门
    解决系统打开CHM文件无法正常显示
  • 原文地址:https://www.cnblogs.com/ycyoes/p/6197378.html
Copyright © 2020-2023  润新知