JVM学习之类加载

1类加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

1.1加载.class文件的方式

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件(动态代理生成的及jsp转换为servlet)

1.2类加载过程

从类的生命周期而言,一个类包括如下阶段:
![在这里插入图片描述](https://img-blog.csdnimg.cn/img_convert/7b4341c2994298dd571aec231ba47ff7.png

加载、验证、准备、初始化和卸载这个5个顺序是确定的,类加载过程必须按照这种顺序进行。而解析阶段则不一定,它在某些情况下可能在初始化阶段后面开始,因为Java支持运行时绑定。
加载(loading)阶段,java虚拟机规范中没有进行约束,但初始化阶段,java虚拟机严格规定了有且只有如下5种情况必须立即进行初始化(初始化前,必须经过加载、验证、准备阶段):
(1)使用new实例化对象时,读取和设置类的静态变量、静态非字面值常量(静态字面值常量除外)时,调用静态方法时。
(2)对内进行反射调用时。
(3)当初始化一个类时,如果父类没有进行初始化,需要先初始化父类。
(4)启动程序所使用的main方法所在类
(5)当使用1.7的动态语音支持时。
如上5种场景又被称为主动引用,除此之外的引用称为被动引用,被动引用有如下3种常见情况

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  • 定义对象数组和集合,不会触发该类的初始化
  • 类A引用类B的static final常量不会导致类B初始化(注意静态常量必须是字面值常量,否则还是会触发B的初始化)
    类加载方式
    这里的类加载不是指类加载阶段,而是指整个类加载过程,即类加载阶段到初始化完成。
    (1)隐式加载
  • 创建类对象
  • 使用类的静态域
  • 创建子类对象
  • 使用子类的静态域
  • 在JVM启动时,BootStrapLoader会加载一些JVM自身运行所需的class
  • 在JVM启动时,ExtClassLoader会加载指定目录下一些特殊的class
  • 在JVM启动时,AppClassLoader会加载classpath路径下的class,以及main函数所在的类的class文件
    (2)显式加载
  • ClassLoader.loadClass(className),只加载和连接、不会进行初始化
  • Class.forName(String name, boolean initialize,ClassLoader loader); 使用loader进行加载和连接,根据参数initialize决定是否初始化。

双亲委派
作用:

  • 出于安全考虑
  • 防子重复加载

源码:
findInCache–>parent.loadClass–>findClass()

**LazyLoading **
严格将应该叫lazyInitiazing;Java规范并没有规定何时加载;但是严格规定了什么时候必须初始化

  • new getstatic putstatic invokestatic指令,访问final变量除外
  • java.lang.reflect对类进行反射调用时
  • 初始化子类的时候,父类首先初始化
  • 虚拟机启动时,被执行的主类必须初始化
  • 动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化

JVM的三种执行模式:

  • 混合执行: -Xmixed 默认模式,开始解释执行,启动速度较快对,对热点代码实行检测和编译
    混合使用解释器+热点代码编译
    起始阶段采用解释执行
    热点代码
    1 多次被调用的方法(方法计数器:监测方法执行频率)
    2 多次被调用的循环(循环计数器:检测循环执行频率)
  • 编译执行:-Xint 使用解释模式,启动很快,执行稍慢
  • 解释执行:-Xcomp使用纯编译模式,执行很快,启动很慢

连接阶段
验证:确保被加载的类的符合JVM规范
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段是非常重要的,但不是必须的。可以采用-Xverify:none参数来关闭大部分的类验证措施。
准备:为类的静态变量分配内存,并将其赋默认值
解析:将常量池中的符号引用替换为直接引用(内存地址)的过程
初始化:为类的静态变量赋初值(只有对类的主动使用才会导致类的初始化)
在编译生成class文件时,编译器会产生两个方法加于class文件中,一个是类的初始化方法clinit, 另一个是实例的初始化方法init
clinit
clinit指的是类构造器,主要作用是在类加载过程中的初始化阶段进行执行,执行内容包括静态变量初始化和静态块的执行。

  1. 如果类中没有静态变量或静态代码块,那么clinit方法将不会被生成。
  2. 在执行clinit方法时,必须先执行父类的clinit方法。
  3. clinit方法只执行一次。
  4. static变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定。
    init
    init指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行。
    1. 如果类中没有成员变量和代码块,那么clinit方法将不会被生成。
    2. 在执行init方法时,必须先执行父类的init方法。
    3. init方法每实例化一次就会执行一次。
    4. init方法先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块。

在这里插入图片描述
示例一

public class T004_ClassLoadingProcedure {
    public static void main(String[] args) {
        System.out.println(T.count);
    }
}

class T{
    public static T t = new T();
    public static int count = 2;

    private T(){
        count++;
    }
}

示例一:
1先进行准备阶段 此时T t = new T(),t赋默认值 null;count赋默认值0;
2在进行初始化时 先调用T的构造方法;同时执行count++;此时count = 1
3初始化完成后t指向new出来堆中的对象,count 赋值初始值 即 count = 2
在这里插入图片描述
示例二

public class T005_ClassLoadingProcedure {
    public static void main(String[] args) {
        System.out.println(T.count);
    }
}

class T{
    public static int count = 2;
    public static T t = new T();

    private T(){
        count++;
    }
}

示例二:
1先进行准备 此时T t = new T(),t赋默认值 null;count赋默认值0;
2在进行初始化时赋初始化值 先 count 赋值初始值 即 count = 2 ;然后 调用T的构造方法;同时执行count++;此时count = 3
3初始化完成后t指向new出来堆中的对象,count = 3
在这里插入图片描述
卸载:执行System.exit()方法

结合下图理解
在这里插入图片描述

1.3类的加载的方式

1.由关键字new创建一个类的实例
2.通过Class.forName()方法动态加载
3.通过ClassLoader.loadClass()方法动态加载

三者的区别:
1和2使用的类加载器是相同的,都是当前类加载器。(即:this.getClass.getClassLoader)。
3由用户指定类加载器。如果需要在当前类路径以外寻找类,则只能采用第3种方式。第3种方式加载的类与当前类分属不同的命名空间。
另外,1是静态加载,2、3是动态加载

两个异常(exception)
静态加载的时候如果在运行环境中找不到要初始化的类,抛出的是NoClassDefFoundError,它在JAVA的异常体系中是一个Error
动态态加载的时候如果在运行环境中找不到要初始化的类,抛出的是ClassNotFoundException,它在JAVA的异常体系中是一个checked异常

Class.forName与ClassLoader.loadClass区别
Class的装载包括3个步骤:加载(loading),连接(link),初始化(initialize).
Class.forName(className)实际上是调用Class.forName(className, true, this.getClass().getClassLoader())。第二个参数,是指Class被loading后是不是必须被初始化
ClassLoader.loadClass(className)实际上调用的是ClassLoader.loadClass(name, false),第二个参数指Class是否被link
Class.forName(className)装载的class已经被初始化,而ClassLoader.loadClass(className)装载的class还没有被link。一般情况下,这两个方法效果一样,都能装载Class。但如果程序依赖于Class是否被初始化,就必须用Class.forName(name)了。
例如,在JDBC编程中,常看到这样的用法,Class.forName(“com.mysql.jdbc.Driver”).如果换成了getClass().getClassLoader().loadClass(“com.mysql.jdbc.Driver”),就不行。
com.mysql.jdbc.Driver的源代码如下:

// Register ourselves with the DriverManager
static {
	try {
		java.sql.DriverManager.registerDriver(new Driver());
	} catch (SQLException E) {
		throw new RuntimeException(Can’t register driver!);
	}
}

原来,Driver在static块中会注册自己到java.sql.DriverManager。而static块就是在Class的初始化中被执行。所以这个地方就只能用Class.forName(className)。

1.3.1用关键字new

代码示例

public class Demo4 {
    public static void main(String[] args) {
        System.out.println("主线程执行...");
        newTest();
    }
    public static void newTest(){
        new ServiceTest();
    }
}

class ServiceTest {
    public static String pro1;
    static {
        pro1 = "aaaa";
        System.out.println("ServiceTest静态块");
    }
    public ServiceTest(){
        System.out.println("ServiceTest构造方法");
    }
}

打印信息
在这里插入图片描述

1.3.2 Class.forName()

代码示例

public class Demo4 {
    public static void main(String[] args) {
        System.out.println("主线程执行...");
        classForNameTest();
    }
    public static void classForNameTest(){
        try {
            //调用forName方法时传递的是true,即会对类进行初始化
            Class c = Class.forName("classloader.ServiceTest");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class ServiceTest {
    public static String pro1;
    static {
        pro1 = "aaaa";
        System.out.println("ServiceTest静态块");
    }
    public ServiceTest(){
        System.out.println("ServiceTest构造方法");
    }
}

打印信息
在这里插入图片描述

1.3.3 ClassLoader.loadClass()

代码示例

public class Demo4 {
    public static void main(String[] args) {
        System.out.println("主线程执行...");
        classLoaderTest();
    }

    public static void classLoaderTest() {
        try {
            //默认只是进行类加载,不会进行连接(link),当然更不会进行类的初始化
            Class c = ServiceTest.class.getClassLoader().loadClass("classloader.ServiceTest");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class ServiceTest {
    public static String pro1;
    static {
        pro1 = "aaaa";
        System.out.println("ServiceTest静态块");
    }
    public ServiceTest(){
        System.out.println("ServiceTest构造方法");
    }
}

打印信息
在这里插入图片描述

2类加载器

在这里插入图片描述
boot启动类加载的目录:
代码示例

public static void main(String[] args) {
    bootClassLoaderLoadingPath();
    //extClassLoaderLoadingPath();
    //appClassLoaderLoadingPath();
}

public static void bootClassLoaderLoadingPath(){
    //获取启动类加载器加载的目录
    String bootClassPath = System.getProperty("sun.boot.class.path");
    List<String> bootLoadingPathList = Arrays.asList(bootClassPath.split(";"));
    for (String s :bootLoadingPathList) {
        System.out.println("启动类加载器---加载的目录"+s);
    }
}

打印信息
在这里插入图片描述
ext扩展类启动器
代码示例

public static void main(String[] args) {
    //bootClassLoaderLoadingPath();
    extClassLoaderLoadingPath();
    //appClassLoaderLoadingPath();
}

public static void extClassLoaderLoadingPath(){
    //获取扩展类加载器加载的目录
    String extClassPath = System.getProperty("java.ext.dirs");
    List<String> extLoadingPathList = Arrays.asList(extClassPath.split(";"));
    for (String s :extLoadingPathList) {
        System.out.println("扩展类加载器---加载的目录"+s);
    }
}

打印信息
在这里插入图片描述
App应用加载器
代码示例

public static void main(String[] args) {
    //bootClassLoaderLoadingPath();
    //extClassLoaderLoadingPath();
    appClassLoaderLoadingPath();
}

public static void appClassLoaderLoadingPath(){
    //获取app类加载器加载的目录
    String appClassPath = System.getProperty("java.class.path");
    List<String> appLoadingPathList = Arrays.asList(appClassPath.split(";"));
    for (String s :appLoadingPathList) {
        System.out.println("app类加载器---加载的目录"+s);
    }
}

打印信息
在这里插入图片描述

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }

    Thread.currentThread().setContextClassLoader(this.loader);
    String var2 = System.getProperty("java.security.manager");
    if (var2 != null) {
        SecurityManager var3 = null;
        if (!"".equals(var2) && !"default".equals(var2)) {
            try {
                var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
            } catch (IllegalAccessException var5) {
                ;
            } catch (InstantiationException var6) {
                ;
            } catch (ClassNotFoundException var7) {
                ;
            } catch (ClassCastException var8) {
                ;
            }
        } else {
            var3 = new SecurityManager();
        }
        if (var3 == null) {
            throw new InternalError("Could not create SecurityManager: " + var2);
        }
        System.setSecurityManager(var3);
    }
}

3双亲委派源码体现

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded—首先检查该类是否已经被加载
        Class<?> c = findLoadedClass(name);
        if (c == null) { 
            long t0 = System.nanoTime();
            try {
                if (parent != null) {//如果存在父类加载器,就委派给父类加载器加载
                    c = parent.loadClass(name, false);
                } else {//如果不存在父类加载器,就检查是否由boot启动类加载器加载,通过调用本  
                		//地方法native class findBootstrapClass(String name)
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class. 如果还没有找到(父类加载器和启动类加载器都不能完成加载  //任务),调用自身的加载器去找类
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

4自定义类加器

1、继承ClassLoader;
2、写一个loaderClassData()方法,将文件变为byte数组;
3、重新ClassLoader中的findClass();
findClass(){
loaderClassData();
defineClass();
}

自定义类加载器

package classloader;

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

public class MyClassLoader extends ClassLoader{
     private final static String fileSuffixExt = ".class";
     private String classLoaderName;
     private String loadPath;
     public void setLoadPath(String loadPath){
         this.loadPath = loadPath;
     }

     public MyClassLoader(ClassLoader parent,String classLoaderName){
         //指定当前类加载器的父类加载器
         super(parent);
         this.classLoaderName = classLoaderName;
     }

    public MyClassLoader(String classLoaderName){
         //使用appClassLoader加载器 作为本类加载器
         super();
         this.classLoaderName = classLoaderName;
    }

    public MyClassLoader(ClassLoader classLoader){
        super(classLoader);
    }

    /**
     * 创建我们的class的二进制文件
     */
    private byte[] loadClassData(String name) {
        byte[] data = null;
        ByteArrayOutputStream baos = null;
        InputStream is = null;
        try{
            name = name.replace(".","\\");
            String fileName = loadPath +name +fileSuffixExt;
            File file = new File(fileName);
            is = new FileInputStream(file);
            baos = new ByteArrayOutputStream();
            int ch;
            while(-1!=(ch=is.read())){
                baos.write(ch);
            }
            data = baos.toByteArray();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try{
                if(null != baos){
                    baos.close();
                }
                if(null != is){
                    is.close();
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return data;
    }

    public Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassData(name);
        System.out.println("MyClassLoader 加载我们的类:====》"+name);
        return defineClass(name, b, 0, b.length);
     }
}

测试类

public class TestMyClassLoader {
    public static void main(String[] args) {
        MyClassLoader myClassLoader = new MyClassLoader("myClassLoader");
        myClassLoader.setLoadPath("D:\\loadertest\\classes\\");
        try{
            Class<?> clazz = myClassLoader.loadClass("jvm.Student");
            System.out.println(clazz.getClassLoader());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

打印信息:
当类路径下有对应的class文件时 是app加载器
在这里插入图片描述
当去掉类路径下的calss文件时:
在这里插入图片描述
总结:
1、这里传递的文件名需要是类的全限定性名称,即jvm.Student格式的,因为 defineClass 方法是按这种格式进行处理的。
2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
3、这类Student类本身可以被 AppClassLoader 类加载,因此我们不能把C:\Users\sangfor\IdeaProjects\jwt\target\classes\jvm\Student.class 放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader 加载,而不会通过我们自定义类加载器来加载。

5类加载器的命名空间

代码示例

package classloader;

import java.lang.reflect.Method;
/**
 * 类加载器的命名空间
 * 类加载器的命名空间是指类加载器本身以及所有父加载器所加载出来的binary name(full class name)组成
 * ①在同一个命名空间里,不允许出现两个完全一样的binary name。
 * ②在不同的命名空间中,可以出现两个相同的binary name。此时两者相互是无感知的,
 *   也就是说Class对象的类型是不一样的
 * ③子加载器的命名空间中的binary name对应的类中可以访问父加载器的命名空间中的binary name
 *   对应的类,反之不行(子可以访问父,父不可以访问子)印证了双亲委派的安全,不然相同的对象不  *   可访问
 */
public class TestMyClassLoader {
    public static void main(String[] args) {
        MyClassLoader myClassLoader1 = new MyClassLoader("myClassLoader");
        myClassLoader1.setLoadPath("D:\\loadertest\\classes\\");

        MyClassLoader myClassLoader2 = new MyClassLoader("myClassLoader");
        myClassLoader2.setLoadPath("D:\\loadertest\\classes\\");
        try{
            Class<?> clazz1 = myClassLoader1.loadClass("jvm.Student");
            System.out.println("通过classLoader1加载student"+clazz1.getClassLoader());

            Class<?> clazz2 = myClassLoader2.loadClass("jvm.Student");
            System.out.println("通过classLoader2加载student"+clazz2.getClassLoader());

            System.out.println("clazz1 == clazz2:"+(clazz1 == clazz2));
            //模拟问题
            Object student1 =clazz1.newInstance();
            Object student2 =clazz2.newInstance();
            Method method = clazz1.getMethod("setStudent",Object.class);
            method.invoke(student1,student2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

打印信息
java.lang.ClassCastException:
jvm.Student
cannot be cast to
jvm.Student

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值