{"blogColumn":{"blogSum":0,"id":1,"imageType":"png","name":"语言基础"},"blogTagList":[{"blogSum":0,"id":1,"name":"Java"},{"blogSum":0,"id":3,"name":"JVM"}],"content":"
Java 虚拟机对 class 文件采用的是按需加载的方式。也就是说,我们所编写的 .java
文件被编译成 .class
文件后,只有当需要使用该类时,JVM 才会将它的 .class
文件加载(Load)到内存中生成 class 对象,然后通过链接(Link)将类的二进制数据合并到 JRE 中,最后将类的静态变量初始化(Initialize)为正确的值。
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个 java.lang.Class 类的实例对象。一旦一个类被加载进 JVM 中,同一个类就不会被再次载入了。
\n此外,正如一个对象有一个唯一的标识一样,一个载入 JVM 的类也有一个唯一的标识。在 Java 中,一个类用其全限定类名(包名 + 类名)作为标识;但在 JVM 中,一个类由其全限定类名和其类加载器一同作为唯一标识。
\n类加载的具体过程如下:
\n加载(Load):通过一个类的全限定名获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转换成方法区的运行时数据结构,然后在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区中这个类的各种数据的访问入口。
\n链接(Link):将 Java 类的二进制代码合并到 JVM 的运行状态之中的过程。
\n初始化(Initialize):执行类构造器 <clinit>()
方法的过程。
此方法不需要定义,它是由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。如果没有这些语句,那么编译器便不会自动为此类添加这样一个方法。
\n方法中的指令按照语句在源文件中出现的顺序执行。
\n此方法不等同于该类的构造方法。对于 JVM 而言,构造方法被视为 <init>()
方法。
若该类具有父类,JVM 会保证在该类的 <clinit>()
方法执行之前,父类 <clinit>()
先被执行完毕。
虚拟机会保证一个类的 <clinit>()
方法在多线程环境中被同步加锁。
初始化的时机:
\n类的主动引用(一定会发生初始化)
\nmain()
方法所在的类类的被动引用(不会发生初始化)
\n示例:
\npackage jvm;\n\nclass Parent {\n static {\n System.out.println("父类被加载");\n }\n\n static int n = 400;\n}\n\nclass Child extends Parent{\n static {\n System.out.println("子类被加载");\n m = 300;\n }\n\n static int m = 100;\n\n static final int M = 200;\n}\n\npublic class Demo01 {\n\n static {\n System.out.println("main 方法类被加载");\n }\n\n public static void main(String[] args) throws ClassNotFoundException {\n\n /*\n 主动引用,单独执行本句会输出如下内容:\n main 方法类被加载\n 父类被加载\n 子类被加载\n */\n Child child = new Child();\n\n /*\n 主动引用,单独执行本句会输出如下内容:\n main 方法类被加载\n 父类被加载\n 子类被加载\n */\n Class aClass = Class.forName("jvm.Child");\n\n /*\n 主动引用,单独执行本句会输出如下内容:\n main 方法类被加载\n 父类被加载\n 子类被加载\n 100\n */\n System.out.println(Child.m);\n\n /*\n 被动引用,单独执行本句会输出如下内容:\n main 方法类被加载\n */\n Child[] children = new Child[10];\n\n /*\n 被动引用,单独执行本句会输出如下内容:\n main 方法类被加载\n 父类被加载\n 400\n */\n System.out.println(Child.n);\n\n /*\n 被动引用,单独执行本句会输出如下内容:\n main 方法类被加载\n 200\n */\n System.out.println(Child.M);\n }\n}\n
\nJVM 中三种预置的类加载器自顶向下可依次分为:
\n除此之外,我们还可以使用自定义类加载器(即继承自 ClassLoader 抽象类的类)。严格来讲,类加载器只分为引导类加载器和自定义类加载器两类,因为拓展类加载器和系统类加载器都继承自 ClassLoader 抽象类,而引导类加载器是通过 C 语言实现的。
\n当然,我们也可以自己编写类去继承 ClassLoader 抽象类。如果没有特别指定,则用户自定义的类加载器都以系统类加载器作为上层加载器。
\nJVM 在加载某个类的 class 文件时,采用的是双亲委派机制,即把请求委派给父类处理。这是一种任务委派模式,该模式的具体工作流程如下:
\n简而言之,其核心思想可以概括成一句话:“向上委派,向下加载。”
\n现在思考一个问题:当我们自己创建一个 String 类,并将它放在我们自己创建的 java.lang 包下时,会发生什么呢?
\n示例一:静态代码块
\n假设我们在这个新建的 String 类中添加一个静态代码块:
\npackage java.lang;\n\npublic class String {\n static{\n System.out.println("自定义的 String 类");\n }\n}\n
\n我们在另一个地方创建 String 类的对象实例,然后运行代码,观察输出结果:
\npublic class StringTest {\n\n public static void main(String[] args) {\n java.lang.String str = new java.lang.String();\n System.out.println("这句应当输出在静态代码块的下面");\n }\n}\n
\n输出结果:
\n这句应当输出在静态代码块的下面\n
\n然而,我们却并没有看到想象中的输出结果,那句静态代码块中的输出语句并没有被执行。这就说明,这里调用的仍然是 JDK 中的 String 类,而不是我们自定义的 String 类。那么,为什么会是这样一种结果呢?
\n这是因为,我们自定义的 String 类本应使用系统类加载器,但它并不会自己先加载,而是把这个请求向上委托给父类的加载器去执行,即扩展类加载器。而到了扩展类加载器时,它同样选择了委托给父类加载器,即引导类加载器。这时,引导类加载器发现要调用的是 java.lang 包下的 String 类,而这事恰好就归引导类加载器管,因此它不再向下加载,而是自己去加载了 JDK 自带的那个 String 类。
\n示例二:main 方法
\n我们继续在 String 类中按照如下方式添加一个 main 方法:
\npackage java.lang;\n\npublic class String {\n static{\n System.out.println("自定义的 String 类");\n }\n\n public static void main(String[] args) {\n System.out.println("这里是 main 方法里的输出结果");\n }\n}\n
\n运行这个 main 方法,可以看到以下输出结果:
\n错误:在类 java.lang.string 中找不到 main 方法,请将 main 方法定义为:\n\tpublic static void main(String[] args)\n否则 JavaFX 应用程序类必须扩展 javafx.application.Application\n
\n可见,这里报错了,说在 String 类里面找不到 main 方法。可我们明明自定义了一个 main 方法,为什么会报找不到的错误呢?
\n这是因为,根据双亲委派机制,首先找到的是 JDK 自带的 String 类,而不是我们自定义的 String 类,因此加载的也会是 JDK 里面的 String 类。然而,引导类加载器发现,JDK 里的 String 类中并没有 main 方法,因此才会报出这样的错误。
\n示例三:自定义类
\n我们试着在这个 java.lang 包下面随意自定义一个类:
\npackage java.lang;\n\npublic class TestNewClass {\n public static void main(String[] args) {\n System.out.println("hello!");\n }\n}\n
\n运行之后,可以看到报出了如下异常:
\njava.lang.SecurityException: Prohibited package name: java.lang\n at java.lang.classLoader.preDefineclass(classLoader.java:662)at java.lang.classLoader.defineclass(classLoader.java:761)\n at java.security.secureclassLoader.defineclass(SecureclassLoader.java:142)at java.net.URLClassLoader.defineclass(URLClassLoader.java:467)\n at java.net.URLClassLoader.access$100(URLClassLoader.java:73)at java.net.URLClassLoader$1.run(URLClassLoader.java : 368)\n at java.net.URLClassLoader$1.run(URLClassLoader.java:362) <1 internal call>at java.net.URLClassLoader.findclass(URLClassLoader.java:361)\n at java.lang.classLoader.loadclass(classLoader.java:424)\n at sun.misc.Launcher$AppClassLoader.loadclass(Launcher.java : 335)at java.lang.classLoader.loadclass(classLoader.java : 357)\n at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)\nError: A JNI error has occurred,please check your installation and try again\nException in thread "main"\n
\n事实上,出于保护机制,java.lang 包下面不允许我们自定义类。
\n通过上面的例子,我们可以得知,双亲委派机制可以避免类的重复加载,同时还可以防止核心 API(诸如 String 类)被随意篡改。
\n","createTime":"2021-09-20 03:38:18","id":50,"redisKeyWithId":"mxblog:view:blog:50","shield":{"id":1,"name":"正常"},"summary":"Java 虚拟机对 class 文件采用的是按需加载的方式。也就是说,我们所编写的 .java 文件被编译成 .class 文件后,只有当需要使用该类时,JVM 才会将它的 .class 文件加载到内存中生成 class 对象,然后通过链接将类的二进制数据合并到 JRE 中,最后将类的静态变量初始化为正确的值。类加载器负责加载所有的类,其为所有被载入内存中的类生成一个 java.lang.Class 类的实例对象。一旦一个类被加载进 JVM 中,同一个类就不会被再次载入了。此外,正如一个对象有一个唯一的标识一样,一个载入 JVM 的类也有一个唯一的标识,由它的全限定类名和其类加载器一同组成。","title":"JVM(二) 类加载器","type":{"id":1,"name":"原创"},"user":{"avatarType":"png","email":"3297705066@qq.com","id":1,"username":"妙霄"},"views":125}