[Java内存]Java类加载过程

动机

对于Java的内存管理,我认为可以分为2类:
内存分配:这里也可以理解为类加载(ClassLoader)
内存释放:也就是Java GC
本文主要介绍类加载,了解类加载的过程对Java类可以有更好的理解,比如static块里面的代码为什么比构造方法还先执行。

基础知识

首先咱们先了解Java代码的执行过程。
[Java内存]Java类加载过程_第1张图片
从这个框图很容易大体上了解 java 程序工作原理。首先,你写好 java 代码,保存到硬盘当中。然后你在命令行中输入

javac YourClassName.java

此时,你的 java 代码就被编译成字节码(.class)。此时的 class 文件依然是保存在硬盘中,因此,当你在命令行中运行

java YourClassName

就完成了上面红色方框中的工作。JRE 的来加载器从硬盘中读取 class 文件,载入到系统分配给 JVM 的内存区域–运行数据区(Runtime Data Areas). 然后执行引擎解释或者编译类文件,转化成特定 CPU 的机器码,CPU 执行机器码,至此完成整个过程。

而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。

类加载的过程

在上一小节中介绍了整个过程,这里主要看上面红框框里面的部分。
JVM将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(Initialize)。这和我们在操作系统中所学的知识很相似哦。
[Java内存]Java类加载过程_第2张图片

  • 装载:查找并加载类的二进制数据
  • 链接:1.验证:确保被加载类信息符合JVM规范、没有安全方面的问题。2.准备:为类的静态变量分配内存,并将其初始化为默认值。3.解析:把虚拟机常量池中的符号引用转换为直接引用。
  • 初始化:为类的静态变量赋予正确的初始值。

一些简单的说明:

  • 在解析过程中,虚拟机为每个加载的类维护一个常量池(不同于字符串常量池,这个常量池只是该类的字面值(例如类名、方法名)和符号引用的有序集合。 而字符串常量池,是整个JVM共享的),而解析过程就是把它转换成指向堆中的对象地址的相对地址。
  • 在初始化的过程中,就会加载静态的代码块与静态的变量,而对于实例化的引用,只有在实例化时才被加载。

类加载器(ClassLoader

说了这么就,那么什么是ClassLoader呢?

A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a “class file” of that name from a file system.

class loader用于把一个类的二进制的名称,去定位这个类的数据,并为这个类构造一个定义。其中的二进制名字也就是类的完整名字(包名+类名,如”java.lang.String”)。

JDK提供了以下的ClassLoader

  • Bootstrap ClassLoader:Bootstrap加载器是用C++语言写的,它是在Java虚拟机启动后初始化的,它主要负责加载%JAVA_HOME%/jre/lib以及%JAVA_HOME%/jre/classes中的类,是最顶级的ClassLoader。
  • Extensions ClassLoader:Extensions ClassLoader是用java写的,且它的父加载器是Bootstrap。Extensions ClassLoader主要加载%JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中的类库。
  • System ClassLoader:系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。它的父加载器为Extensions ClassLoader。

[Java内存]Java类加载过程_第3张图片

除了JDK提供的ClassLoader还可以使用开发人员自己定义的类加载器。
就像上图所表面的一样。
ClassLoader里面有如下的方法:

protected ClassLoader(ClassLoader parent); // Creates a new class loader using the specified parent class loader for delegation.

public Class loadClass(String name) throws ClassNotFoundException // Loads the class with the specified binary name.重写这个方法,不遵从委托模型


protected Class findClass(String name)
                      throws ClassNotFoundException // Finds the class with the specified binary name,遵守委托模型,重写这个方法就好。

protected final Class defineClass(String name,
                                     byte[] b,
                                     int off,
                                     int len)
                              throws ClassFormatError // Converts an array of bytes into an instance of class Class. Before the Class can be used it must be resolved.即把数组b中的内容转换成Java 类。

委托模型

The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine’s built-in class loader, called the “bootstrap class loader”, does not itself have a parent but may serve as the parent of a ClassLoader instance.

ClassLoader类使用一种委托模型来查找类和资源。每个ClassLoader实例都会关联1个父ClassLoader。当需要查询类和资源的时候,一个ClassLoader实例在查询类或资源之前会先委托给它的父ClassLoader去查询。Bootstrap ClassLoader是最顶层的加载器,并且可以作为其它ClassLoader实例的父ClassLoader。

由此看见,这个“委托模型”的安全性是很高的,Bootstrap是最顶层的加载器,这样比如加载 java.lang.String 的时候,永远都会被Bootstrap加载(Bootstrap ClassLoader会加载%JAVA_HOME%/jre/lib中rt.jar里的String类)。 这样用户自定义的java.lang.String永远都不会被加载,这样就避免了多个java.lang.String造成的混乱现象。

ClassLoader源码里面就可以看到这样的代码:

 // Java 7
 protected synchronized Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    // First, check if the class has already been loaded
    Class c = findLoadedClass(name); //先查找这个类是否已经加载过,每个加载器都有自己的缓存
    if (c == null) {
        try {
        if (parent != null) { //父加载器存在的话先使用父加载器加载
            c = parent.loadClass(name, false);
        } else {
            c = findBootstrapClassOrNull(name); //没有父加载器的话使用bootstrap加载
        }
        } 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.
            c = findClass(name); //如果父加载器没有找到,那么自身查找
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
    }

在Java8里面,我们可以看到,synchronized的位置不一样了。锁的粒度变小了。原来是整个方法,现在只是每个要加载的类。

// Java 8
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 {
                        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;
        }
    }

     protected Object getClassLoadingLock(String className) {
        Object lock = this;
        if (parallelLockMap != null) {
            Object newLock = new Object();
            lock = parallelLockMap.putIfAbsent(className, newLock);
            if (lock == null) {
                lock = newLock;
            }
        }
        return lock;
    }

可以看到findClassloadClass方法。符合上文所说的委托模型。

自定义类加载器

1:要被加载的类

public class Obj {
    @Override
    public String toString() {
        return "Hello,World!";
    }

}

2:类加载器

public class WQHClassLoader extends ClassLoader {
    private String directory;

    public WQHClassLoader(String directory) {
        this.directory = directory;
    }

    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        byte[] clsBytes = getClassBytes(name);
        if (clsBytes == null)
            throw new ClassNotFoundException();
        return defineClass(name, clsBytes, 0, clsBytes.length);
    }

    private byte[] getClassBytes(String name) {
        String location = getClassLoc(name);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(location);
            byte[] buffer = new byte[4096];
            int readLen = 0;
            while ((readLen = fis.read(buffer)) != -1) {
                baos.write(buffer, 0, readLen);
            }
            baos.flush();
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    private String getClassLoc(String name) {
        return this.directory + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
    }

}

3.Main

public class Main {
    @Test
    public void test() {
        WQHClassLoader fscl = new WQHClassLoader("/classloader");
        try {
            System.out.println(fscl.loadClass("Obj").newInstance());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

结果:

HelloWorld!

注意

  • Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的
  • ClassLoader有什么用呢?热部署,代码加密等领域。加载jar里面的class文件就是用了动态加载啦。可以参考这里:知乎-Java 类加载器(ClassLoader)的实际使用场景有哪些?

参考:
1:浅析java类加载器ClassLoader
2:官网

你可能感兴趣的:(Java)