《深入理解JVM》-类的加载-张龙

第一部分:类加载 

java中类型的加载、连接和初始化都是在程序运行期间完成的:

  • 加载:查找并加载类的二进制数据
  • 连接:
  1. 验证:确保被加载的类的正确性
  2. 准备:为类的静态变量分配内存,并将其初始化为默认值
  3. 解析:把类中的符号引用转换为直接引用
  • 初始化:为类的静态变量赋予正确的初始值(会执行静态代码块),每一个类只会被初始化一次

类的生命周期的五个阶段:加载、连接、初始化、使用、卸载

java程序对类的使用方式分为两种:主动使用和被动使用

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

主动使用(七种):

  1. 创建类的实例
  2. 访问某个类或接口的静态变量(getstatic),或者对该静态变量赋值(putstatic)
  3. 调用类的静态方法(invokestatic)
  4. 反射
  5. 初始化一个类的子类
  6. java虚拟机启动时被标明为启动类的类(包含main)
  7. jdk1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化

除了上方七种情况,其他使用java类的方式都被看做是对类的被动使用,都不会导致类的初始化

 类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象用来封装类在方法区内的数据结构

java虚拟机结束生命的几种方式:

  • 执行Sytem.exit()方法
  • 程序正常执行结束
  • 程序执行过程中遇到异常或者错误
  • 由于操作系统出现错误而导致JVM进程终止
package shengsiyuan.capter01;

/**
 * 1.静态代码块只有在类被初始化的时候会执行
 * 2.对于静态字段而言,使用静态字段时,只有直接定义了该静态字段的类才会被初始化
 * 3.当一个类被初始化的时候,要求其父类都已经初始化完毕
 */
public class Test1 {
    public static void main(String[] args) {
        //Parent1初始化,Child1不会初始化
        System.out.println(Child1.name);
    }
}
class Parent1{
    public static String name = "parent";
    static {
        System.out.println("Parent1 init");
    }
}
class Child1 extends Parent1{
    static {
        System.out.println("Child1 init");
    }
}
package shengsiyuan.capter01;

/**
 * 常量(final)在编译阶段会存入到这个常量池的方法所在的类的常量池中
 * 本质上,调用类并没有直接引用到这个定义常量的类,因此不会触发定义常量
 * 类的初始化,
 * 注意:这里指的是将常量存放到了Test2的常量池中,只有的Test2与Parent2没有任何关系,甚至,我们可以将Parent2的class文件删除
 */
public class Test2 {
    public static void main(String[] args) {
        System.out.println(Parent2.parentName);
    }
}

class Parent2{
    final static String parentName = "parent";
    static {
        System.out.println("Parent1 init");
    }
}

 

package shengsiyuan.capter01;

import java.util.UUID;

/**
 * 当一个常量的值并非编译期间可以确定的,那么其值不会放到调用方法的调用类的常量池中
 * 这时在程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类被初始化
 */
public class Test3 {
    public static void main(String[] args) {
        System.out.println(Parent3.parentName);
    }
}

class Parent3{
    final static String parentName = UUID.randomUUID().toString();
    static {
        System.out.println("Parent1 init");
    }
}

 

package shengsiyuan.capter01;

/**
 * 输出的值是1,0
 * 原因:当调用Singleton.getInstance()是主动使用类,对Singleton类进行加载,连接,初始化
 * 在连接的准备阶段,会对静态变量进行赋初始值:count1=0,singleton=null,count2=0,
 * 然后进行初始化进行赋值阶段,当执行到new Singleton()时,会执行构造方法,执行完构造方法后count1=1,count2=2,
 * 然后执行到count2=0进行赋值,导致1被覆盖成0
 */
public class Test6 {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println(Singleton.count1);
        System.out.println(Singleton.count2);
    }
}

class Singleton{

    public static int count1;

    private static Singleton singleton = new Singleton();

    public static int count2 = 0;
    private Singleton(){
        count1++;
        count2++;
    }
    public static Singleton getInstance(){
        return singleton;
    }
}

 

静态变量的声明语句,以及静态代码块都被看做类的初始化语句,java虚拟机会按照初始化语句在类文件中的先后顺序来依次执行它们。

package shengsiyuan.capter01;

/**
 * 调用classloader的loadClass方法只会加载类,不会对类进行初始化
 * 使用Class.forName()加载类会对类进行初始化
 */
public class Test7 {
    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        Class<?> clazz =  classLoader.loadClass("shengsiyuan.capter01.GL");
        System.out.println(clazz);
        clazz = Class.forName("shengsiyuan.capter01.GL");
        System.out.println(clazz);
    }
}
class GL{
    static {
        System.out.println("invoke static block");
    }
}

ClassLoader中对于数组类型的加载解释:

Class objects for array classes are not created by class loaders, but are created automatically as required by the Java runtime. The class loader for an array class, as returned by Class.getClassLoader() is the same as the class loader for its element type; if the element type is a primitive type, then the array class has no class loader.

package shengsiyuan.capter01;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * 自定义类加载器,具体加载过程参见ClassLoader java doc文档
 */
public class MyClassLoader extends ClassLoader{

    private String classLoaderName;
    private static final String extention = ".class";

    public MyClassLoader(String classLoaderName){
        super();
        this.classLoaderName = classLoaderName;
    }

    public MyClassLoader(String classLoaderName,ClassLoader parent){
        super(parent);
        this.classLoaderName = classLoaderName;
    }

    @Override
    public String toString() {
        return "MyClassLoader{" +
                "classLoaderName='" + classLoaderName + '\'' +
                '}';
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] b = this.loadClassData(name);
        return this.defineClass(name,b,0,b.length);
    }

    private byte[] loadClassData(String name){
        // com.shengsiyuan.Test-->com/shengsiyuan/Test.class
        String path = name.replace(".","/")+extention;
        byte[] data = null;
        FileInputStream inputStream = null;
        ByteArrayOutputStream outputStream = null;
        try{
            inputStream = new FileInputStream(new File(path));
            int d ;
            while (-1 !=(d = inputStream.read())){
                outputStream.write(d);
            }
            data = outputStream.toByteArray();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(null!=inputStream){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(null!=outputStream){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return data;
    }

    public void test(ClassLoader classLoader)throws Exception{
        Class<?> loadClass = classLoader.loadClass("shengsiyuan.capter01.Test1");
        Object instance = loadClass.newInstance();
        System.out.println(instance);
        ((Test1)instance).test();
    }

    public static void main(String[] args)throws Exception {
        MyClassLoader classLoader = new MyClassLoader("test1");
        classLoader.test(classLoader);
    }
}

关于命名空间的重要说明:

1.子加载器所加载的类能够访问父加载器所加载的类

2.父加载器所加载的类无法访问子加载器所加载的类

类加载器的双亲委托模型的好处:

1.可以确保java核心库的类型安全:所有java应用都至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object这个类会被加载到java虚拟机中;如果这个加载过程是由java应用自己的类加载器所完成的,那么很可能就会在JVM中存在多个版本的java.lang.Object类,而且这些类之间还是不兼容的,相互不可见的(正是命名空间在发挥作用)。

借助与双亲委托机制,java核心类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了java应用所使用的都是同一个版本的java核心类库,他们之间是相互不兼容的。

2.可以确保java核心类库所提供的类不会被自定义的类所替代。

3.不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间,相同名称的类可以并存在java虚拟机中,只需要用不用的类加载器来加载他们即可,不同类加载器所加载的类之间是不兼容的,这就相当于在java虚拟机内部创建了一个有一个相互兼容的java类空间,这类技术在很多框架中得到了实际应用。

在运行期,一个java类是由该类的完全限定名(binary name,二进制名)和用于加载该类的定义类加载器(defining loader)所共同决定的。如果同样名字(即相同的完全限定名)的类是由两个不同的加载器所加载,那么这些类就是不同的,即便.class文件的字节码完全一样,并且从相同的位置加载亦如此

内建于JVM中的启动类加载器会加载java.lang.ClassLoader以及其他java平台类,当JVM启动时,一块特殊的机器码会运行,他会加载扩展类加载器与系统类加载器,这块特殊的机器码叫做启动类加载器。 启动类加载器并不是java类,而其他的加载器则都是java类,启动类加载器是特定于平台的机器指令,它负责开启整个加载过程。 所有类加载器(除了启动类加载器)都被实现为java类。不过,总归要有一个组件来加载第一个java类加载器,从而让整个加载过程能够顺利进行下去,加载第一个纯java类加载器就是启动类加载器的职责。 启动类加载器还会负责加载供JRE正常运行所需要的基本组件,这包括java.util与java.lang包中的类等等

package shengsiyuan.capter01;

/*
    当前类加载器(Current Classloader)

    每个类都会使用自己的额类加载器(即加载自身的类加载器)来去加载其他类(指
    的是所依赖的类),如果ClassX引用了ClassY,那么ClassX的类加载器就会去加载ClassY(前提是ClassY尚未被加载)
    
    线程上下文类加载器(Context Classloader)
    
    线程上下文类加载器是从JDK1.2开始引入的,类Thread中的getContextClassloader()与setContextClassLoader(ClassLoader cl)分
    别用来获取和设置上下文类加载器
    
    如果没有通过setContextClassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文类加载器。java应用运行时的初始线
    程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过该类加载器来加载类与资源。
    
    线程上下文类加载器的重要性:
    
    SPI(Service Provider Interface)
    
    父ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的ClassLoader加载的类。这就改变的父
    ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的ClassLoader加载的类的情况,即改变了双亲委托模型。
    
    线程上下文类加载器就是当前线程的Current ClassLoader
    
    在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是java核心库所提供的,
    而java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的jar包(厂商提供,如JDBC),java的启动类加载器是不会加载
    其他来源的jar包,且类加载器加载的类下层能访问上层,但是上层加载的类无法访问到下层类加载器加载的类,这样传统的双亲委派模型就
    无法满足SPI的要求。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对接口实现类的加载

线程上下文类加载器的一般使用模式(获取-使用-还原)
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try{
    Thread.currentThread.setContextClassLoader(targetThreadContextClassLoader);
    myMethod()
}finally{
    Thread.currentThread.setContextClassLoader(classLoader);
}
myMethod里面则调用了Thread.currentThread().getContextClassLoader(),获取当前线程的上下文类加载器做某些事情。
如果一个类由类加载器A加载,那么这个类的依赖类也是由相同的类加载器加载的(如果该依赖类之前没有被加载过的话)。
ContextClassLoader的作用就是为了破坏java的类加载委托机制。
当高层提供了统一的接口让低层去实现,同时又要在高层加载(或实例化)低层类的时候,就必须要通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。

java的双亲委托机制限制了只能沿着类加载层级由下向上访问,线程上下文类加载相当于提供了由上向下访问的入口


 */
public class Test8 {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getContextClassLoader());
        System.out.println(Thread.class.getClassLoader());
    }
}

/*
    此处体现了使用当前线程上下文classLoader加载数据库接口实现类资源,如果设置当前线程的类加载器是扩展类加载器,则不会从classpath的jar包中查找实现类,则获取到的Driver接口实现类为空,如果使用系统类加载器进行加载,则默认会加载classpath的jar包中的实现类,则能够加载到mysql驱动实现类
*/
class Test8 {
    public static void main(String[] args) {
//        Thread.currentThread().setContextClassLoader(Test8.class.getClassLoader().getParent());
        ServiceLoader<Driver> load = ServiceLoader.load(Driver.class);
        Iterator<Driver> iterator = load.iterator();
        while (iterator.hasNext()){
            System.out.println();
            Driver driver = iterator.next();
            System.out.println("driver: "+driver.getClass()+", load: "+driver.getClass().getClassLoader());
        }
        System.out.println("当前线程上下文加载器:"+Thread.currentThread().getContextClassLoader());
        System.out.println("serviceLoader的类加载器:"+load.getClass().getClassLoader());
    }
}

研究mysql加载过程:

Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("");

JVM设置参数规则:

-XX:+<option>   表示开启option选项

-XX:-<option>    表示关闭option选项

-XX:<option>=<value>  表示将option选项的值设置为true

JVM常见参数设置:

-XX:+TraceClassLoading,用于追踪类的加载信息并打印出来

-XX:+TraceClassUnloading,用户追踪类的卸载信息并打印出来

 JVM中常用命令:

javap -c xxxx.class,反编译class文件

JVM中助记符:

idc表示将int、float或是String类型的常量值从常量池中推送至栈顶

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

sipush表示将一个短整型常量值(-32768~32767)推送至栈顶

iconst_1表示将int类型1对推送至栈顶(iconst_1~iconst_5)

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

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章