jvm之类加载技术

我会带着你远行 2023-01-19 04:39 259阅读 0赞

一:jvm之类加载技术

1、ClassLoader作用

Java程序在运行的时候,JVM通过类加载机制(ClassLoader)把class文件加载到内存中,只有class文件被载入内存,才能被其他class引用,使程序正确运行起来。

二、ClassLoader的分类

Java中的ClassLoader有三种:Bootstrap ClassLoader 、Extension ClassLoader、App ClassLoader。

1. Bootstrap ClassLoader

由C++写的,由JVM启动.

启动类加载器,负责加载java基础类,对应的文件是%JRE_HOME/lib/ 目录下的rt.jar、resources.jar、charsets.jar和class等

2.Extension ClassLoader

Java类,继承自URLClassLoader 扩展类加载器,

对应的文件是 %JRE_HOME/lib/ext 目录下的jar和class等

3.App ClassLoader

Java类,继承自URLClassLoader 系统类加载器,

对应的文件是应用程序classpath目录下的所有jar和class等

三、ClassLoader的加载机制

Java的加载机制是双亲委派机制来加载类

为什么要使用这种方式?这个是为了保证 如果加载的类是一个系统类,那么会优先由Bootstrap ClassLoader 、Extension ClassLoader先去加载,而不是使用我们自定义的ClassLoader去加载,保证系统的安全!

这三种类加载器存在父子关系,App ClassLoader的父类加载器是Extension ClassLoader,Extension ClassLoader的父类加载器是Bootstrap ClassLoader,要注意的一点是,这里的父子并不是继承关系.

7ab4dcf8468ad34568ba0cc4c6aa13e6.png

  1. ClassLoaderParentMain实体类:
  2. package com.example.springbootparam.demo;
  3. /**
  4. * @author lizhangyu
  5. * @date 2021/6/19 16:08
  6. */
  7. public class ClassLoaderParentMain {
  8. public static void main(String[] args) {
  9. ClassLoader classLoader = ClassLoaderParentMain.class.getClassLoader();
  10. ClassLoader parentClassLoader = classLoader.getParent();
  11. ClassLoader pparentClassLoader = parentClassLoader.getParent();
  12. System.out.println(classLoader);
  13. System.out.println(parentClassLoader);
  14. System.out.println(pparentClassLoader);
  15. }
  16. }

运行结果为:

  1. sun.misc.Launcher$AppClassLoader@18b4aac2
  2. sun.misc.Launcher$ExtClassLoader@2a84aee7
  3. null
  4. Process finished with exit code 0

当这三者中的某个ClassLoader要加载一个类时,会先委托它的父类加载器尝试加载,一直往上,如果最顶级的父类加载器没有找到该类,那么委托者则亲自到特定的地方加载

  1. protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
  2. synchronized (getClassLoadingLock(name)) { // 首先,检测是否已经加载
  3. Class<?> c = findLoadedClass(name);
  4. if (c == null) {
  5. long t0 = System.nanoTime();
  6. try {
  7. if (parent != null) { //父加载器不为空则调用父加载器的loadClass
  8. c = parent.loadClass(name, false);
  9. } else { //父加载器为空则调用Bootstrap Classloader
  10. c = findBootstrapClassOrNull(name);
  11. }
  12. } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader
  13. }
  14. if (c == null) { // If still not found, then invoke findClass in order // to find the class.
  15. long t1 = System.nanoTime(); //父加载器没有找到,则调用findclass
  16. 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();
  17. }
  18. }
  19. if (resolve) { //调用resolveClass()
  20. resolveClass(c);
  21. }
  22. return c;
  23. }
  24. }

先加载Extension ClassLoader ,如果没有加载到,那么使用Bootstrap ClassLoader去加载,如果都没有,那么使用App ClassLoader去加载。如果没找到,那么就抛出异常ClassNotFoundException.

方法原理很简单,一步一步解释一下:

1、第5行,首先查找.class是否被加载过

2、 第6行~第12行,如果.class文件没有被加载过,那么会去找加载器的父加载器。如果父加载器不是null(不是Bootstrap ClassLoader),那么就执行父加载器的loadClass方法,把类加载请求一直向上抛,直到父加载器为null(是Bootstrap ClassLoader)为止

3、第13行~第17行,父加载器开始尝试加载.class文件,加载成功就返回一个java.lang.Class,加载不成功就抛出一个ClassNotFoundException,给子加载器去加载

4、第19行~第21行,如果要解析这个.class文件的话,就解析一下,解析的作用类加载的文章里面也写了,主要就是将符号引用替换为直接引用的过程

我们看一下findClass这个方法:

  1. protected Class<?> findClass(String name) throws ClassNotFoundException {
  2. throw new ClassNotFoundException(name);
  3. }

是的,没有具体实现,只抛了一个异常,而且是protected的,这充分证明了:这个方法就是给开发者重写用的

这里画张图来表示下:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3NDY5MDU1_size_16_color_FFFFFF_t_70

只有被同一个类加载器实例加载并且文件名相同的class文件才被认为是同一个class

四、自定义ClassLoader

自定义类加载器

从上面对于java.lang.ClassLoader的loadClass(String name, boolean resolve)方法的解析来看,可以得出以下2个结论:

1、如果不想打破双亲委派模型,那么只需要重写findClass方法即可

2、如果想打破双亲委派模型,那么就重写整个loadClass方法

当然,我们自定义的ClassLoader不想打破双亲委派模型,所以自定义的ClassLoader继承自java.lang.ClassLoader并且只重写findClass方法。

第一步,自定义一个实体类Person.java,我把它编译后的Person.class放在D盘根目录下:

  1. Person实体类:
  2. package com.example.springbootparam.demo;
  3. /**
  4. * @author lizhangyu
  5. * @date 2021/6/19 16:56
  6. */
  7. public class Person {
  8. private String name;
  9. public Person() {
  10. }
  11. public Person(String name) {
  12. this.name = name;
  13. }
  14. public String getName() {
  15. return name;
  16. }
  17. public void setName(String name) {
  18. this.name = name;
  19. }
  20. public String toString() {
  21. return "I am a person, my name is " + name;
  22. }
  23. }

第二步,自定义一个类加载器,里面主要是一些IO和NIO的内容,另外注意一下 defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class——只要二进制字节流的内容符合Class文件规 范。我们自定义的MyClassLoader继承自java.lang.ClassLoader,就像上面说的,只实现findClass方法:

  1. MyClassLoader实体类:
  2. package com.example.springbootparam.demo;
  3. import java.io.ByteArrayOutputStream;
  4. import java.io.File;
  5. import java.io.FileInputStream;
  6. import java.nio.ByteBuffer;
  7. import java.nio.channels.Channels;
  8. import java.nio.channels.FileChannel;
  9. import java.nio.channels.WritableByteChannel;
  10. /**
  11. * @author lizhangyu
  12. * @date 2021/6/19 16:46
  13. */
  14. public class MyClassLoader extends ClassLoader {
  15. public MyClassLoader() {
  16. }
  17. public MyClassLoader(ClassLoader parent) {
  18. super(parent);
  19. }
  20. @Override
  21. protected Class<?> findClass(String name) throws ClassNotFoundException {
  22. File file = getClassFile(name);
  23. try {
  24. byte[] bytes = getClassBytes(file);
  25. Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
  26. return c;
  27. } catch (Exception e) {
  28. e.printStackTrace();
  29. }
  30. return super.findClass(name);
  31. }
  32. private File getClassFile(String name) {
  33. File file = new File("E:/Person.class");
  34. return file;
  35. }
  36. private byte[] getClassBytes(File file) throws Exception {
  37. // 这里要读入.class的字节,因此要使用字节流
  38. FileInputStream fis = new FileInputStream(file);
  39. FileChannel fc = fis.getChannel();
  40. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  41. WritableByteChannel wbc = Channels.newChannel(baos);
  42. ByteBuffer by = ByteBuffer.allocate(1024);
  43. while (true) {
  44. int i = fc.read(by);
  45. if (i == 0 || i == -1)
  46. break;
  47. by.flip();
  48. wbc.write(by);
  49. by.clear();
  50. }
  51. fis.close();
  52. return baos.toByteArray();
  53. }
  54. }

第三步,Class.forName有一个三个参数的重载方法,可以指定类加载器,平时我们使用的Class.forName(“XX.XX.XXX”)都是使用的系统类加载器Application ClassLoader。写一个测试类:

  1. TestMyClassLoader实体类:
  2. package com.example.springbootparam.demo;
  3. /**
  4. * @author lizhangyu
  5. * @date 2021/6/19 17:01
  6. */
  7. public class TestMyClassLoader {
  8. public static void main(String[] args) throws Exception{
  9. MyClassLoader mcl = new MyClassLoader();
  10. Class<?> c1 = Class.forName("com.example.springbootparam.demo.Person", true, mcl);
  11. Object obj = c1.newInstance();
  12. System.out.println(obj);
  13. System.out.println(obj.getClass().getClassLoader());
  14. }
  15. }

运行结果:

  1. I am a person, my name is null
  2. com.example.springbootparam.demo.MyClassLoader@a09ee92
  3. Process finished with exit code 0

最容易出问题的点是第二行的打印出来的是”sun.misc.Launcher$AppClassLoader”。造成这个问题的关键在于idea是自动编译的,Person.java这个类在ctrl+S保存之后或者在Person.java文件不编辑若干秒后,idea会帮我们用户自动编译Person.java,并生成到CLASSPATH也就是bin目录下。在CLASSPATH下有Person.class,那么自然是由Application ClassLoader来加载这个.class文件了。解决这个问题有两个办法:

1、删除CLASSPATH下的Person.class,CLASSPATH下没有Person.class,Application ClassLoader就把这个.class文件交给下一级用户自定义ClassLoader去加载了。

2、TestMyClassLoader类的第5行这么写”MyClassLoader mcl = new MyClassLoader(ClassLoader.getSystemClassLoader().getParent());”, 即把自定义ClassLoader的父加载器设置为Extension ClassLoader,这样父加载器加载不到Person.class,就交由子加载器MyClassLoader来加载了。

ClassLoader.getResourceAsStream(String name)方法作用

ClassLoader中的getResourceAsStream(String name)其实是一个挺常见的方法,所以要写一下。这个方法是用来读入指定的资源的输入流,并将该输入流返回给用户用的,资源可以是图像、声音、.properties文件等,资源名称是以”/“分隔的标识资源名称的路径名称。

不仅ClassLoader中有getResourceAsStream(String name)方法,Class下也有getResourceAsStream(String name)方法,它们两个方法的区别在于:

1、Class的getResourceAsStream(String name)方法,参数不以”/“开头则默认从此类对应的.class文件所在的packge下取资源,以”/“开头则从CLASSPATH下获取

2、ClassLoader的getResourceAsStream(String name)方法,默认就是从CLASSPATH下获取资源,参数不可以以”/“开头

其实,Class的getResourceAsStream(String name)方法,只是将传入的name进行解析一下而已,最终调用的还是ClassLoader的getResourceAsStream(String name),看一下Class的getResourceAsStrea(String name)的源代码:

  1. /**
  2. * Returns an input stream for reading the specified resource.
  3. *
  4. * <p> The search order is described in the documentation for {@link
  5. * #getResource(String)}. </p>
  6. *
  7. * @param name
  8. * The resource name
  9. *
  10. * @return An input stream for reading the resource, or <tt>null</tt>
  11. * if the resource could not be found
  12. *
  13. * @since 1.1
  14. */
  15. public InputStream getResourceAsStream(String name) {
  16. URL url = getResource(name);
  17. try {
  18. return url != null ? url.openStream() : null;
  19. } catch (IOException e) {
  20. return null;
  21. }
  22. }
  23. /**
  24. * Finds the resource with the given name. A resource is some data
  25. * (images, audio, text, etc) that can be accessed by class code in a way
  26. * that is independent of the location of the code.
  27. *
  28. * <p> The name of a resource is a '<tt>/</tt>'-separated path name that
  29. * identifies the resource.
  30. *
  31. * <p> This method will first search the parent class loader for the
  32. * resource; if the parent is <tt>null</tt> the path of the class loader
  33. * built-in to the virtual machine is searched. That failing, this method
  34. * will invoke {@link #findResource(String)} to find the resource. </p>
  35. *
  36. * @apiNote When overriding this method it is recommended that an
  37. * implementation ensures that any delegation is consistent with the {@link
  38. * #getResources(java.lang.String) getResources(String)} method.
  39. *
  40. * @param name
  41. * The resource name
  42. *
  43. * @return A <tt>URL</tt> object for reading the resource, or
  44. * <tt>null</tt> if the resource could not be found or the invoker
  45. * doesn't have adequate privileges to get the resource.
  46. *
  47. * @since 1.1
  48. */
  49. public URL getResource(String name) {
  50. URL url;
  51. if (parent != null) {
  52. url = parent.getResource(name);
  53. } else {
  54. url = getBootstrapResource(name);
  55. }
  56. if (url == null) {
  57. url = findResource(name);
  58. }
  59. return url;
  60. }

.class和getClass()的区别

最后讲解一个内容,.class方法和getClass()的区别,这两个比较像,我自己没对这两个东西总结前,也常弄混。它们二者都可以获取一个唯一的java.lang.Class对象,但是区别在于:

1、.class用于类名,getClass()是一个final native的方法,因此用于类实例

2、.class在编译期间就确定了一个类的java.lang.Class对象,但是getClass()方法在运行期间确定一个类实例的java.lang.Class对象。

发表评论

表情:
评论列表 (有 0 条评论,259人围观)

还没有评论,来说两句吧...

相关阅读

    相关 JVM之类过程

    当我们在Java代码中写下new String()的时候,我们理所当然认为java会返回给我们一个String对象,但是在JVM背后做了很多事情,包括类的加载、对象内存的分配等

    相关 JVM 之 类

    1.类加载 虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,解析和初始化, 最终形成可以被虚拟机直接使用的java类型。 2.加载机

    相关 JVM之类

    类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们开始的顺序如下图所示: ![classonloa