类加载-学习笔记

概念

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

提供了更大的灵活性,增加了更多的可能性

Java编译原理–类加载过程

java虚拟机的生命周期

java虚拟机的生命周期
几种结束虚拟机生命周期的情况

  • 调用System.exit(),命令结束生命周期
  • 程序执行完成
  • 程序抛出异常到最上层给予虚拟机一直未处理
  • 操作系统发生错误导致jvm虚拟机进程关闭

System.exit()执行 finally就不会执行,因为程序已经退出

在连接的准备阶段,为静态变量设置默认值。
在初始化阶段为静态变量赋值为正确的值,所以在初始化中静态变量实际上值从默认值到设置的值

jvm参数

参数格式:-XX固定格式

  1. -XX:+ 表示开启参数
  2. -XX:- 表示关闭参数
  3. -XX:= 将option选项值设置为value

有趣的final

User.OUT有final和没有final输出不一样,查看类加载也可以看到jvm.User from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/] 被加载

package jvm;

/**
 * Created by LiRui on 2019-07-25.
 *
 * @author LiRui
 * @version 1.0
 */
public class Main {
    public static void main(String[] args) {
        System.out.println(User.OUT);
    }
}

class User {
    public static String OUT = "Hello User!";

    static {
        System.out.println("I'm  User static");
    }

    public User() {
        System.out.println("I'm User");
    }
}

输出:
I'm  User static
Hello User!
package jvm;

/**
 * Created by LiRui on 2019-07-25.
 *
 * @author LiRui
 * @version 1.0
 */
public class Main {
    public static void main(String[] args) {
        System.out.println(User.OUT);
    }
}

class User {
    public static final String  OUT = "Hello User!";

    static {
        System.out.println("I'm  User static");
    }

    public User() {
        System.out.println("I'm User");
    }
}

输出:
Hello User!

问题答案

答案就在编译后class

package jvm;

public class Main {
    //没带final关键字编译结果
    public Main() {
    }

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



package jvm;

public class Main {
    //带final关键字编译结果
    public Main() {
    }

    public static void main(String[] args) {
        System.out.println("Hello User!");
    }
}

原因:
因为声明字符串为final说明不可修改,不可变。在编译阶段编译器直接将字符串移动到调用位置成为普通常量(放到当前调用处的常量池中),所以就不会调用User类,User类也就不会初始化

从这里看出来常量类全部定义成为final也可以少加载一个类,可以节约资源。在书中也有记载在类、方法明确定义为final时就不会去寻找加载父类,也算是节约资源。但是也缺失了延展性

final 使用影响类加载另一种情况

package jvm;

import java.util.UUID;

/**
 * Created by LiRui on 2019-07-29.
 *
 * @author LiRui
 * @version 1.0
 */
public class OutUUID {
    public static void main(String[] args) {
        System.out.println(UUIDOut.UUID_OUT);
    }
}

class UUIDOut{
    public static final String UUID_OUT = UUID.randomUUID().toString();
    static {
        System.out.println("I'm UUIDOut");
    }
}


输出结果:
I'm UUIDOut
ad790d0d-8c47-4f37-9079-03d7ef7ace17

在打印加载类时发现确实加载了UUIDOut类

查看编译class


package jvm;

public class OutUUID {
    public OutUUID() {
    }

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

原因:在编译期不能确定常量值时该变量就不会放到调用处的常量池中,所以在调用时也会加载初始化UUIDOut类

结论

当一个常量的值并非编译期可以确定的,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,显然导致这个类被初始化

反编译工具

命令:java -c xxxx地址
code: 前面是助记符
Java助记符

localhost:jvm LiRui$ javap -c out/production/jvm/jvm/Main.class 
Compiled from "Main.java"
public class jvm.Main {
  public jvm.Main();
    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 Hello User!
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

助记符也是通过jdk的类进行操作的查看:com.sun.org.apache.bcel.internal.generic包下

创建数组

  • 数组类型是在运行期动态生成的[Ljvm.xxx类型 类。动态生成的类型,其父类是Object
    对于数组来说,javaDoc经常构成数组元素为Component,实际上就是将数组降低一个维度后的类型
package jvm;

import java.util.UUID;

/**
 * Created by LiRui on 2019-07-29.
 *
 * @author LiRui
 * @version 1.0
 */
public class OutUUID {
    public static void main(String[] args) {
        UUIDOut[][] outs = new UUIDOut[20][];
        System.out.println(UUIDOut.outs.getClass().toString());
    }
}

class UUIDOut {
    public static final String UUID_OUT = UUID.randomUUID().toString();
    public static final  UUIDOut[][] outs = new UUIDOut[20][];

    static {
        System.out.println("I'm UUIDOut");
    }
}

输出结果:
I'm UUIDOut
class [[Ljvm.UUIDOut;

  • 如输出结果可得,因为数组在编译器不可得,所以在调用数组时,也初始化UUIDOut类。即数组在UUIDOut的常量池中

数组加载器

数组类的加载器由数组元素的类加载器加载
注意:原生类型数组没有加载器,所以以下程序的两个null不同

package jvm;

/**
 * Created by LiRui on 2019-08-01.
 *
 * @author LiRui
 * @version 1.0
 */
public class TestMain0801 {
    /**
     * 输出结果:
     * sun.misc.Launcher$AppClassLoader@18b4aac2
     * sun.misc.Launcher$ExtClassLoader@610455d6
     * null
     * 输出null说明是启动类(根类)加载器
     *
     * @param args
     */
    public static void main(String[] args) {
        String[] strings = new String[10];
        System.out.println(strings.getClass().getClassLoader());
        TestMain0801[] main0801s = new TestMain0801[20];
        System.out.println(main0801s.getClass().getClassLoader());
        int[] ints  = new int[10];
        System.out.println(ints.getClass().getClassLoader());
    }
}
输出:
null
sun.misc.Launcher$AppClassLoader@18b4aac2
null

String类为根加载器加载所以输出为null
TestMain0801为系统加载器加载

接口

  • 在接口中定义的变量默认是public static final

  • 接口运行期能确定值

package jvm;

/**
 * Created by LiRui on 2019-07-30.
 *
 * @author LiRui
 * @version 1.0
 */
public class MainTest {
    public static void main(String[] args) {
        System.out.println(People.name);
    }
}

interface People extends Obj {
    String name = "name";
    int age = 20;
}

interface Obj {
    String ha = "ha";
}

编译文件:

package jvm;

public class MainTest {
    public MainTest() {
    }

    public static void main(String[] args) {
        System.out.println("name");
    }
}
  • 运行期不能确定值
package jvm;

import java.util.UUID;

/**
 * Created by LiRui on 2019-07-30.
 *
 * @author LiRui
 * @version 1.0
 */
public class MainTest {
    public static void main(String[] args) {
        System.out.println(People.name);
    }
}

interface People extends Obj {
    String name = UUID.randomUUID().toString();
    int age = 20;
}

interface Obj {
    String ha = "ha";
}


编译文件:

package jvm;

public class MainTest {
    public MainTest() {
    }

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

跟踪类加载:
[Loaded jvm.Obj from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/]
[Loaded jvm.People from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/]

总结

直接调用接口中的运行期可确定变量(public static final),该变量在编译期直接放到调用处的常量池中。调用运行期不可确定变量(在运行期确定),则会初始化接口。并且会初始化当前接口的父类接口。

在连接的准备阶段静态变量会被赋上初始化值,在初始化时再给赋程序值,所以static的顺序可能影响变量的值。

加载器

几种类加载器

static

静态块只有在初始化时第一次运行
{}语法,在每次初始化都会执行


class Child implements People{
    {
        System.out.println("每次实例化都会执行");
    }

    public Child() {
        System.out.println("实例化");
    }
}

每次实例化输出:
每次实例化都会执行
实例化

类初始化时机

  • 当虚拟机初始化一个类时,要求它的所有类都已经初始化,但是这条规则不适用于接口
  • 在初始化一个类时,并不会先初始化它所实现的接口
  • 在初始化一个接口时,并不会先初始化它的父接口
  • 因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定的静态变量时,才会导致该接口初始化
package jvm;

/**
 * Created by LiRui on 2019-07-30.
 *
 * @author LiRui
 * @version 1.0
 */
public class MainTest {
    public static void main(String[] args) {
        System.out.println(Child.child_name);
    }
}

interface People extends Obj {
    Thread thread = new Thread() {
        {
            System.out.println("初始化people接口");
        }
    };


}

interface Obj {
    String ha = "ha";
}


class Child implements People{
    public static  String child_name = "name";
}

输出结果:
name

类装载:
[Loaded jvm.Obj from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/]
[Loaded jvm.People from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/]
[Loaded jvm.Child from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/]

当Prople换成class

package jvm;

/**
 * Created by LiRui on 2019-07-30.
 *
 * @author LiRui
 * @version 1.0
 */
public class MainTest {
    public static void main(String[] args) {
        System.out.println(Child.child_name);
    }
}

class People implements Obj {
    public static Thread thread = new Thread() {
        {
            System.out.println("初始化people接口");
        }
    };


}

interface Obj {
    String ha = "ha";
}


class Child extends People{
    public static  String child_name = "name";
 
}

输出:
初始化people接口
name

类装载:
[Loaded jvm.Obj from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/]
[Loaded jvm.People from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/]
[Loaded jvm.Child from file:/Users/LiRui/IdeaProjects/jvm/out/production/jvm/]

结果

如上两段代码,People不管为接口还是类时都装载了,但是接口时并没有输出“初始化people接口”,说明接口时并没有初始化。

  1. 验证上面说法
  2. 说明装载不一定被初始化,这是两个过程一定要注意

自定义类加载器

Java自定义类加载器
在自定义加载器时,没有置空父加载器时。默认父加载器为系统加载器,由于加载器的双气委派机制,所以自定加载器默认情况下一般是不会调用到自己写的方法,直接由父加载器直接加载。

类加载器双亲委托模型作用

  • 确保Java核心库类型安全

    所有的Java应用都会引用java.long.Object类,也就是在运行期 java.long.Object类会被加载到虚拟机中。如果加载由自定义加载器完成,很可能在jvm中存在多个版本的java.long.Object类,而且这些类相互不兼容相互不可见(命名空间不同)。

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

  • 确保Java核心库的类不会被自定义类库替代

  • 不同的类加载器可以为相同名称的类创建额外的命名空间,相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载他们即可(命名空间不同)。不同类加载器加载的类之间是不兼容的,相当于在Java虚拟机内部创建了相互隔离的java类空间,这类技术在很多框架都得到了应用

摘抄牛客网回答:加载,JVM第一次使用到这个类时需要对,这个类的信息进行加载。一个类只会加载一次,之后这个类的信息放在堆空间,静态属性放在方法区。JVM类加载器从上到下一共分为三类 1.启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。 2. 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。 3. 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。 JVM通过双亲委派模型进行类的加载启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。 JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。

原始类加载器

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

线程上下文类

  /**
     * 输出
     * sun.misc.Launcher$AppClassLoader@18b4aac2
     * null
     *
     * @param args
     */
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getContextClassLoader());
        System.out.println(Thread.class.getClassLoader());
    }
    输出:
    sun.misc.Launcher$AppClassLoader@18b4aac2
null

解释:Thread为com.long包中,由根加载器加载。

当前类加载器

Thread.currentThread().getContextClassLoader()即获取当前类加载器

每个类都会使用自己的类加载器(即加载自身的类加器),来去加载其他类(指的是所依赖的类),如果ClassX引用了ClassY,那么ClassX的类加载器就会去加载ClassY(前提的ClassY尚未被加载)

线程上下文加载器(Context ClassLoader)

线程上下文类加载器是从JDK1.2开始引入。类Thread中的getContextClassLoader()与setContextClassLoader(ClasssLoader loader)分别用来获取和设置上下文类加载器

如果没有通过setContextClassLoader(ClasssLoader loader)进行设置的话,线程默认将继承其父线程的上下文的类加载器。Java应用的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过类加载器来加载资源。

上下文类加载器重要性

SPI(Sservice Provider Interface)
SPI机制详解

父ClassLoader可以使用当前线程Thread.currentThread.getContextLoader()所指定的classLoader加载的类。这就改变了父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的ClassLoader加载类的情况,即改变了双亲委托模型。

线程上下文类加载器就是当前线程的Current ClassLoader

在双亲委托模型下,类加载器是由上至下,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是Java核心库所提供的,而Java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不用的jar包(厂商提供),Java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求。而通过给当前线程设置上下文加载器,就可以由设置的上下文类加载器来实现对于接口类的加载

上下文类加载器的使用模板

上下文类加载器默认为系统类加载器

/**
 * 线程上下文类加载器一般是使用模式(获取-使用-还原)
 */
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
try {
    Thread.currentThread().setContextClassLoader(设置类加载器);
    执行逻辑,这里就可以通过当前线程获取上下文类加载器执行自己想加载的类
}finally {
    //还原上下文类加载器
    Thread.currentThread().setContextClassLoader(contextClassLoader);
}
   

上下文累类加载器主要作用是为了“破坏双亲委托机制”能够在SPI情况下,高层类加载器也能加载低层实现类

ServiceLoader

JDK中文文档

在ServiceLoader描述中:通过在资源目录 META-INF/services 中放置提供者配置文件 来标识服务提供者。文件名称是服务类型的完全限定二进制名称。

ServiceLoader在JDK1.6中加入,就是为了处理SPI情况下的类加载问题。

本身ServiceLoader在java.util中,由根加载器加载。由于SPI情况,根加载器不能加载服务厂商实现类,可以由系统类加载器进行加载,所以使用上下文类加载器来处理。

主要代码:

//默认使用上下文类加载器为系统类加载器
public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

文件加载主要实现在LazyIterator中:

private class LazyIterator
        implements Iterator<S>
    {

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;

        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }

        /**
         * 判断是否存在下一个服务实现
         */
        private boolean hasNextService() {
            //nextName为null说明调用next或者第一次调用
            if (nextName != null) {
                return true;
            }
            //判断是否存在配置资源
            if (configs == null) {
                try {
                    //配置位置"META-INF/services/" + "java.sql.Driver"
                    String fullName = PREFIX + service.getName();
                    //没有配置加载器,默认系统类加载器
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                    //使用配置加载器加载
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                //没有更多的元素
                if (!configs.hasMoreElements()) {
                    return false;
                }
                //解析文件里名称
                pending = parse(service, configs.nextElement());
            }
            //写入下一个实现类名称
            nextName = pending.next();
            return true;
        }

        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            //文件中需要加载的类名如:com.mysql.jdbc.Driver
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                //加载指定类,不用初始化,指定类加载器(这里为是上下文加载器即:系统类加载器)
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            // 判定此 Class 对象所表示的类或接口与指定的 Class 参数所表示的类或接口是否相同,或是否是其超类或超接口(做校验)
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                //将一个对象强制转换成此 Class 对象所表示的类或接口
                S p = service.cast(c.newInstance());
                //kv形式存储对象
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

        public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    }

修改上下文加载器

import java.sql.Driver;
import java.util.ServiceLoader;

/**
 * 线程上下文类加载器一般是使用模式(获取-使用-还原)
 */
public class Test20190809 {
    public static void main(String[] args) {
        //修改上下文加载器为扩展类加载器
        Thread.currentThread().setContextClassLoader(Test20190809.class.getClassLoader().getParent());
        //1.6版本引入
        ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class);
        for (Driver driver : serviceLoader) {
            System.out.println("driver: " + driver.getClass() + ", loader: " + driver.getClass().getClassLoader());
        }
        System.out.println("当前线程上下文类加载器: " + Thread.currentThread().getContextClassLoader());
        //输出null 由启动类负责加载
        System.out.println("serviceLoader的类加载器:" + ServiceLoader.class.getClassLoader());
    }
}

输出:
当前线程上下文类加载器: sun.misc.Launcher$ExtClassLoader@6a38e57f
serviceLoader的类加载器:null

由程序输出可以看到上下文类加载器为扩展类加载器,但是没有输出mysql jdbc的加载类,说明没有识别到文件

问题: 问题就在于这个hasNextService方法中configs = loader.getResources(fullName);,由于loader为扩展类加载器所以加载的根目录就变成了JAVA_HOME/jre/lib/ext/ 所有找不到对应的配置值JAVA_HOME/jre/lib/ext/META-INF/services/ java.sql.Driver 所以并没有输出

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

niubility锐

觉得有用的话鼓励鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值