类加载机制之ClassLoader

1,类加载

每个编写的”.java”拓展名类文件都存储着需要执行的程序逻辑,这些”.java”文件经过Java编译器编译成拓展名为”.class”的文件,”.class”文件中保存着Java代码经转换后的虚拟机指令

当需要使用某个类时,虚拟机将会加载它的”.class”文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载,这里我们需要了解一下类加载的过程,如下:

创建的对象存储在java 堆内存,对象的引用存储在java 虚拟机栈  class 对象中包含的方法,属性存储在方法区(JDK1.8 之前叫叫做永久区)

Jvm执行class文件

Loading:

step1: 类加载器将.class文件的二进制数据从外部存储器(如光盘,硬盘)调入内存中,在方法区生成方法区中的运行数据,生成java.lang.Class 对象(存储在java 堆)

step2: CPU再从内存中读取指令和数据进行运算,并将运算结果存入内存中(方法的返回结果也是存在方法区中) 。直接给CPU处理,而由于CPU的处理速度远远大于调入数据的速度,容易造成数据的脱节,所以需要内存起缓冲作用

每个类都对应有一个Class类型的对象,Class类的构造方法是私有的,只有JVM能够创建。因此Class对象是反射的入口,使用该对象就可以获得目标类所关联的.class文件中具体的数据结构。

Linking:

验证:确保加载的类信息符合JVM规范,没有安全方面的问题
准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配
解析:虚拟机常量池的符号引用替换为字节引用过程

initilization:初始化

当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化

初始化过程:

类构造器<clinit>()方法是由编译器自动收藏类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生,代码从上往下执行。

也就是说这个方法,在类初始化阶段,会将类中所有的静态变量收集到一块并且分配空间,顺序是从上往下执行,若一个静态变量多次赋值,静态函数里面若没有类型也是可以的,会默认初始化:

static {
        agent = 0;
    }

private static int agent = 9;

2,类加载器常用方法
loadClass(String)
该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作。
正如loadClass方法所展示的,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载(关于findClass()稍后会进一步介绍)。从loadClass实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass("className"),这样就可以直接调用ClassLoader的loadClass方法获取到class对象。
findClass(String)
在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的(稍后会分析)
defineClass(byte[] b, int off, int len)
defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象
resolveClass(Class≺?≻ c)
使用该方法可以使用类的Class对象创建完成也同时被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。

3,热部署

对于Java应用程序来说,热部署就是在运行时更新Java类文件。

通俗的说,项目在运行过程中,已经部署到服务器的项目,发现逻辑有问题,需要修改代码,但是又不想在修改完毕之后重新部署,这时候就需要热部署的方法。

热部署就是直接修改.class 文件,但是修改之后,并不会生效,因为之前版本的.class 文件已经在项目启动的时候被类加载器读取到内存中,而且这个过程只会发生一次(除非重新部署),所以我们要实现热部署需要做的就是:

1,销毁之前的ClassLoader加载的对象,让System.gc(),提醒垃圾回收

2,更新字节码文件

3,创建新的自定义ClassLoader 读取字节码文件

Java热部署与热加载的联系

1.不重启服务器编译/部署项目

2.基于Java的类加载器实现

Java热部署与热加载的区别

部署方式

热部署在服务器运行时重新部署项目

热加载在运行时重新加载class

实现原理

热部署直接重新加载整个应用

热加载在运行时重新加载class

使用场景

热部署更多的是在生产环境使用

热加载则更多的实在开发环境使用

4,例子:

创建一个对象HelloWorld 类,里面打印Hello World 1,用记事本创建和HelloWorld类一样的java 文件,打印的是Hello World 2,Hello World2 的类用javac 编译,生成.class文件,在项目运行中先加载之前的class 文件,再用自定义的classLoader自动的去更新字节码文件,查看输出是否有变化。

package com.hella.hotswap;

public class HelloWorld {
    
    public void search(){
        System.out.println("Hello World 1");
    }

}

创建对象:

package com.hella.hotswap;

public class HelloWorld {
    
    public void search(){
        System.out.println("Hello World 2");
    }

}

//自定义ClassLoader

public class ClassLoaderDemo extends ClassLoader {

    // name 是一个全路径 如 com.baidu.dev.HelloWorld
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 文件名称
            String fileName = "/"+ name.replace(".", "/")+".class";
            // 获取文件输入流
            InputStream is = this.getClass().getResourceAsStream(fileName); //加 / 代表从classes 的根目录下开始找
            // 读取字节
            byte[] b = new byte[is.available()];
            is.read(b);
            // 将byte字节流解析成jvm能够识别的Class对象
            return defineClass(name, b, 0, b.length);
        } catch (IOException e) {
            throw new ClassNotFoundException();
        }
    }

}

测试:

import java.io.File;
import java.lang.reflect.Method;

public class App {
        public static void main(String[] args) throws Exception {
            loadObject();
            System.gc();
            Thread.sleep(1000);// 等待资源回收
            // 需要被热部署的class文件
            File file1 = new File("C:\Users\caich5\Desktop\test\HelloWorld.class");
            // 之前编译好的class文件
            File file2 = new File(
                    "C:\Users\caich5\workspace\JvmWeb\target\classes\com\hella\hotswap\HelloWorld.class");
            boolean isDelete = file2.delete();// 删除旧版本的class文件
            if (!isDelete) {
                System.out.println("热部署失败.");
                return;
            }
            file1.renameTo(file2);
            System.out.println("update success!");
            loadObject();
            
            
        }

        private static void loadObject() throws  Exception {
            ClassLoaderDemo classLoaderDemo = new ClassLoaderDemo();
            Class<?> clazz = classLoaderDemo.findClass("com.hella.hotswap.HelloWorld");
            Object object = clazz.newInstance();        
            //通过反射机制获取方法
            Method method = clazz.getMethod("search");
            //反射机制对象执行方法
            method.invoke(object);
            
            
        }
    }

打印:

     Hello World 1
     update success!
     Hello World 2

 
原文地址:https://www.cnblogs.com/pickKnow/p/11132774.html