浅析类装载

原创
2019/03/08 00:09
阅读数 165

要使用类,必须有两个步骤,一个是加载类,然后是初始化。

创建一个类的实例可以使用到的方法:

  • 使用new关键字
  • 通过反射
  • 克隆
  • 反序列化

在什么情况下使用类,不需要初始化类

通过子类调用父类的静态变量,子类不会被初始化,但是子类会被加载。

public class Parent {
    static {
        System.out.println("Parent init");
    }
    public static int v = 100;
}
public class Child extends Parent {
    static {
        System.out.println("Child init");
    }
}
public class UseParent {
    public static void main(String[] args) {
        System.out.println(Child.v);
    }
}

通过JVM参数-XX:+TraceClassLoading运行得到以下结果

[Loaded com.guanjian.Parent from file:/E:/classload/out/production/classload/]
[Loaded com.guanjian.Child from file:/E:/classload/out/production/classload/]
Parent init
100

由结果可知,只有父类被初始化了,子类没有被初始化,但是被加载了。

直接调用某个类的final修饰的静态变量,不但不会初始化该类,连加载都不会。

public class FinalFieldClass {
    public static final String constString = "CONST";
    static {
        System.out.println("FinalFieldClass init");
    }
}
public class UseFinalField {
    public static void main(String[] args) {
        System.out.println(FinalFieldClass.constString);
    }
}

通过JVM参数-XX:+TraceClassLoading运行得到以下结果

[Loaded java.net.URI$Parser from C:\Program Files\Java\jdk1.8.0_91\jre\lib\rt.jar]
[Loaded java.lang.Class$MethodArray from C:\Program Files\Java\jdk1.8.0_91\jre\lib\rt.jar]
[Loaded java.lang.Void from C:\Program Files\Java\jdk1.8.0_91\jre\lib\rt.jar]
CONST

由结果可知,不但没有打印FinalFieldClass init,连Loaded FinalFieldClass的日志都没有。这是因为final常量直接存放到了常量池中,因此FinalFieldClass类不会被加载。

加载类的步骤

我们都知道,java在编译类后并不是产生固有机器的机器码,而是一段字节码,这段字节码可以存放于任何地方,如.class文件,jar包中,可以通过网络传输。JVM虚拟机在拿取到这段二进制数据流字节码后,就会处理这些数据,并最终转换成一个java.lang.Class的实例(注意,这里并不是类本身的实例)。java.lang.Class实例是访问类型元数据的接口,也是实现反射的关键数据。通过Class类提供的接口,可以访问一个类型的方法,字段等信息。

public class ClassTest {
    public static void main(String[] args) throws ClassNotFoundException {
        Class clzStr = Class.forName("java.lang.String");
        //获取该类的所有方法
        Method[] ms = clzStr.getDeclaredMethods();
        for (Method m:ms) {
            //获取该方法的修饰符
            String mod = Modifier.toString(m.getModifiers());
            //打印方法的修饰符,方法名跟起始括号
            System.out.print(mod + " " + m.getName() + " (");
            //获取方法的所有参数类型
            Class<?>[] ps = m.getParameterTypes();
            //如果没有参数,直接打印结束括号
            if (ps.length == 0) {
                System.out.print(')');
            } else {
                //取出所有的参数类型名称,以逗号分隔
                for (int i = 0; i < ps.length; i++) {
                    char end = i == ps.length - 1 ? ')' : ',';
                    System.out.print(ps[i].getSimpleName() + end);
                }
            }
            System.out.println();
        }
    }
}

运行结果:

public equals (Object)
public toString ()
public hashCode ()
public compareTo (String)
public volatile compareTo (Object)
public indexOf (String,int)
public indexOf (String)
public indexOf (int,int)
public indexOf (int)
static indexOf (char[],int,int,char[],int,int,int)
static indexOf (char[],int,int,String,int)
public static valueOf (int)
public static valueOf (long)
public static valueOf (float)
public static valueOf (boolean)
public static valueOf (char[])
public static valueOf (char[],int,int)
public static valueOf (Object)
public static valueOf (char)
public static valueOf (double)
public charAt (int)
private static checkBounds (byte[],int,int)
public codePointAt (int)
public codePointBefore (int)
public codePointCount (int,int)
public compareToIgnoreCase (String)
public concat (String)
public contains (CharSequence)
public contentEquals (CharSequence)
public contentEquals (StringBuffer)
public static copyValueOf (char[])
public static copyValueOf (char[],int,int)
public endsWith (String)
public equalsIgnoreCase (String)
public static transient format (Locale,String,Object[])
public static transient format (String,Object[])
public getBytes (int,int,byte[],int)
public getBytes (Charset)
public getBytes (String)
public getBytes ()
public getChars (int,int,char[],int)
 getChars (char[],int)
private indexOfSupplementary (int,int)
public native intern ()
public isEmpty ()
public static transient join (CharSequence,CharSequence[])
public static join (CharSequence,Iterable)
public lastIndexOf (int)
public lastIndexOf (String)
static lastIndexOf (char[],int,int,String,int)
public lastIndexOf (String,int)
public lastIndexOf (int,int)
static lastIndexOf (char[],int,int,char[],int,int,int)
private lastIndexOfSupplementary (int,int)
public length ()
public matches (String)
private nonSyncContentEquals (AbstractStringBuilder)
public offsetByCodePoints (int,int)
public regionMatches (int,String,int,int)
public regionMatches (boolean,int,String,int,int)
public replace (char,char)
public replace (CharSequence,CharSequence)
public replaceAll (String,String)
public replaceFirst (String,String)
public split (String)
public split (String,int)
public startsWith (String,int)
public startsWith (String)
public subSequence (int,int)
public substring (int)
public substring (int,int)
public toCharArray ()
public toLowerCase (Locale)
public toLowerCase ()
public toUpperCase ()
public toUpperCase (Locale)
public trim ()

Class.forName可以得到代表String类的Class实例,它是反射中最重要的方法。

二进制数据流字节码被加载到虚拟机之后,会进行一系列的验证检查,主要步骤如下。

  • 格式检查:包括魔数检查(识别文件类型开头的几个十六进制数,每一种文件类型都不同),版本检查,长度检查
  • 语义检查:是否继承final,是否有父类,抽象方法是否有实现
  • 字节码验证:跳转指令是否指向正确位置,操作数类型是否合理
  • 符号引用验证:符号引用的直接引用是否存在

以上比较复杂,暂略过。

当一个类验证通过时,虚拟机就会进入准备阶段。分配内存空间,分配初始值。如果类存在常量字段,如果被final修饰,就会被直接放入常量池中。如果没有final修饰,就会在初始化中赋值,而不是直接放入常量池。

准备阶段完成后,就是解析类,解析类就是把字节码中的类,接口,字段,方法放入JVM虚拟机实际内存的地址中,方便程序可以真正执行,比如说类的方法会有一个方法表,当需要调用一个类的方法时,就要知道这个方法在方法表中的偏移量,直接调用该方法。

类的初始化是类装载的最后一个阶段。初始化的重要工作就是执行类的初始化方法<clinit>。方法<clinit>是由编译器自动生成的,它是由类静态成员的赋值语句以及static语句合并产生的。类似代码如下

public class SimpleStatic {
    public static int id = 1;
    public static int number;
    static {
        number = 4;
    }
}

在<clinit>函数中,先后对id和number两个成员变量进行赋值。但如果成员变量是final修饰的,则不在此阶段赋值,而是在准备阶段赋值的。

 

 

展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部