本文主要是介绍JVM - 1.类加载子系统,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
1.类加载子系统
1.作用
- 1.负责从文件系统或网络中加载
字节码
(.class
)文件,即将物理磁盘上的字节码
文件加载到内存中,生成供程序使用的类对象
- 2.
字节码
文件要求在文件开头有特定的文件标识(CA FE BA BE
)- 3.
类加载器
(ClassLoader
)只负责字节码
文件的加载,是否可运行,由执行引擎(Execution Engine)
决定- 4.
类加载器
是指特定的加载器,而类加载子系统
是一个系统流程的统称- 5.加载生成的的类信息存放在称为
方法区
的内存空间中- 6.除了类的信息外,
方法区
还会存放运行时常量池(字节码文件中的Constant pool在运行时加载到内存中称为运行时常量池)
信息,可能还包括字符串字面量
和数字常量
,参考附录1
2.角色
- 1.
Car
类通过编译器(javac)
编译后生成Car.calss
字节码文件并存在于本地磁盘上- 2.程序执行时
字节码文件
通过类加载器
加载到JVM
中,生成一个类对象
- 3.通过该
类对象
可获取到类的构造器,根据该构造器可实例化出多个实例,通过实例的getClass
方法也可以获取类对象
本身
- 4.
字节码文件
加载到JVM
中,被称为DNA元数据模板
,放在方法区
- 5.
.class文件
->JVM
->元数据模板
,该过程通过类加载器
(Class Loader
)实现- 6.物理磁盘上的
字节码
文件通过二进制流
的方式加载到内存
中
3.类的加载过程
1.加载
- 1.通过一个类的
全限定名
获取定义此类的二进制字节流
- 2.将这个字节流所代表的静态存储结构转化为
方法区
的运行时数据结构
- 3.
方法区
:抽象概念;落地实现:1.7
及以前叫永久代
,之后叫元空间
- 4.并在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区
该对象各种数据的访问入口
2.链接
1.验证(Verify)
- 1.确保
class
文件的字节流包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机的自身安全- 2.主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证
- 3.例:文件标识验证(
CA FE BA BE
),使用Binary Viewer
工具进行查看
2.准备(Prepare)
- 1.为
类变量/静态变量
分配内存空间
并且设置该类变量
的默认初始值
- 1.
成员变量
:定义在方法体和语句块之外,不属于任何一个方法,作用域是整个类
- 1.静态变量/类变量:用
static
修饰的成员变量- 2.全局变量/实例变量:无
static
修饰的成员变量- 2.
局部变量
:定义在方法或代码块中的变量,作用域是其所在的代码块- 2.例如:
- 1.
private static int a = 1
,准备阶段会赋默认初始值为0,即a=0
,然后在初始化(initial
)阶段会赋值a = 1
- 3.注意
- 1.不同类型的类变量默认初始值不同
- 2.这里不包含
final
修饰的static
类变量,因为final
修饰的是常量而不是变量,常量后期不会再被修改,所以在编译阶段就已经分配值,准备阶段只是显示初始化- 3.这里不会为
实例变量
默认初始化,因为当前还没创建对象,只是加载过程,类变量
会分配在方法区
中,而实例变量
是会随着对象一起分配到Java堆
中
3.解析(Resolve)
- 1.将
常量池
内的符号引用转换为直接引用的过程- 2.事实上
解析
操作往往会在JVM
执行完初始化
后再执行- 3.
符号引用
:一组符号来描述所引用的目标,符号引用的字面量形式明确定义在《java虚拟机规范》的class
文件格式中- 4.
直接引用
:直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄- 5.解析动作主要针对类或接口,字段,类方法,接口方法,方法类型等
- 6.通过反编译可以查看
class
文件中的符号引用和直接引用package com.java;public class HelloApp {private static int a = 1;public HelloApp() {}public static void main(String[] args) {System.out.println(a);} }
如下如示:Constant pool是常量池,其中以#开头的是符号引用,其余的是直接引用F:\文档\笔记\代码\JVMDemo\out\java>javap -v HelloApp.class Classfile /F:/文档/笔记/代码/JVMDemo/out/java/HelloApp.classLast modified 2022-11-9; size 608 bytesMD5 checksum 5964d34bba8f8bf4e817be8fe95a17feCompiled from "HelloApp.java" public class com.java.HelloAppminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER Constant pool:#1 = Methodref #6.#23 // java/lang/Object."<init>":()V#2 = Fieldref #24.#25 // java/lang/System.out:Ljava/io/PrintStream;#3 = Fieldref #5.#26 // com/atguigu/java/HelloApp.a:I#4 = Methodref #27.#28 // java/io/PrintStream.println:(I)V#5 = Class #29 // com/atguigu/java/HelloApp#6 = Class #30 // java/lang/Object <=符号引用 #7 = Utf8 a <=直接引用#8 = Utf8 I#9 = Utf8 <init>#10 = Utf8 ()V#11 = Utf8 Code#12 = Utf8 LineNumberTable#13 = Utf8 LocalVariableTable#14 = Utf8 this#15 = Utf8 Lcom/atguigu/java/HelloApp;#16 = Utf8 main#17 = Utf8 ([Ljava/lang/String;)V#18 = Utf8 args#19 = Utf8 [Ljava/lang/String;#20 = Utf8 <clinit>#21 = Utf8 SourceFile#22 = Utf8 HelloApp.java#23 = NameAndType #9:#10 // "<init>":()V#24 = Class #31 // java/lang/System#25 = NameAndType #32:#33 // out:Ljava/io/PrintStream;#26 = NameAndType #7:#8 // a:I#27 = Class #34 // java/io/PrintStream#28 = NameAndType #35:#36 // println:(I)V#29 = Utf8 com/atguigu/java/HelloApp#30 = Utf8 java/lang/Object#31 = Utf8 java/lang/System#32 = Utf8 out#33 = Utf8 Ljava/io/PrintStream;#34 = Utf8 java/io/PrintStream#35 = Utf8 println#36 = Utf8 (I)V {public com.java.HelloApp();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 7: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcom/atguigu/java/HelloApp;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic #2 // Field >java/lang/System.out:Ljava/io/PrintStream;3: getstatic #3 // Field a:I6: invokevirtual #4 // Method >java/io/PrintStream.println:(I)V9: returnLineNumberTable:line 12: 0line 13: 9LocalVariableTable:Start Length Slot Name Signature0 10 0 args [Ljava/lang/String;static {};descriptor: ()Vflags: ACC_STATICCode:stack=1, locals=0, args_size=00: iconst_11: putstatic #3 // Field a:I4: returnLineNumberTable:line 8: 0 } SourceFile: "HelloApp.java"
3.初始化
- 1.初始化阶段就是执行
类构造器方法
(<clinit>()
)的过程,可使用jclasslib
查看(下载地址)- 2.此方法不需要定义,是
javac
编译器自动收集类中的所有类变量的赋值动作
(类变量的显式赋值)和静态代码块中的语句
合并而来,注意不包含静态方法
- 3.类构造器方法中指定按语句在源文件中出现的顺序执行
- 4.
<clinit>()
不同于类的构造器,类的构造器方法对应的是init()
方法- 5.若该类具有父类,
JVM
会保证子类的<clinit>()
执行前,父类的<clinit>()
已经执行完毕- 6.虚拟机必须保证一个类的
<clinit>()
方法在多线程下被同步加锁(多线程下如果有一个线程加载,则其他加载线程会被阻塞;即保证类只会被加载一次,加载后的类对象保存在方法区)- 7.
<clinit>()
只有在类中有对静态变量
,静态代码块
操作时才会有,其他情况不会存在(已测试静态方法不会存在)public class HelloApp2 {private static int num = 1; //prepare:num = 0 ---> initial : num = 1 ---> num = 2static {num = 2;number = 20;}private static int number = 10; //prepare:number = 0 ---> initial : number = 20 ---> number = 10public static void main(String[] args) {System.out.println(num); // num = 2System.out.println(number); // number = 10} }
- 8.非法的前项引用,可以提前赋值,但不是不能提前引用
4.类加载器
- 1.JVM支持两种类型的类加载器:
- 1.引导类加载器(
Bootstrap ClassLoader
)- 2.自定义类加载器(
User-Defined ClassLoader
)- 2.一般来说
自定义类加载器
指的是程序中由开发人员自定义的类加载;但是《Java虚拟机规范
》中将所有派生于抽象类ClassLoader
的类加载器都划分为自定义类加载器
- 3.程序中最常见的三个类加载器
- 1.
Bootstrap Class Loader
:引导类加载器- 2.
Extension Class Loader
:扩展类加载器- 3.
System Class Loader
:系统类加载器
- 4.以上四者之间(引导,扩展,系统,自定义)是包含关系,不是上层下层,也不是子父类的继承关系
- 5.引导类加载器通过
C/C++
语言编写无法直接获取;扩展类加载器包含系统类加载器public class ClassLoaderTest {public static void main(String[] args) {//获取系统类加载器ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2//获取其上层:扩展类加载器ClassLoader extClassLoader = systemClassLoader.getParent();System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19//获取其上层:获取不到引导类加载器ClassLoader bootstrapClassLoader = extClassLoader.getParent();System.out.println(bootstrapClassLoader);//null//对于用户自定义类来说:默认使用系统类加载器进行加载ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2//String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载ClassLoader classLoader1 = String.class.getClassLoader();System.out.println(classLoader1);//nullClassLoader classLoader2 = Integer.class.getClassLoader();System.out.println(classLoader2);//null} }
- 7.通过下列代码可以动态获取到引导类,扩展类,系统类加载器负责加载的类,其中越底层能加载的类就越多
public class ClassLoaderTest1 {public static void main(String[] args) {System.out.println("**********引导类加载器**************");//获取BootstrapClassLoader能够加载的api的路径URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();for (URL element : urLs) {System.out.println(element.toExternalForm());}//从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器ClassLoader classLoader = Provider.class.getClassLoader();System.out.println(classLoader);System.out.println("***********扩展类加载器*************");String extDirs = System.getProperty("java.ext.dirs");for (String path : extDirs.split(";")) {System.out.println(path);}//从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器ClassLoader classLoader1 = CurveDB.class.getClassLoader();System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19dSystem.out.println("***********系统类加载器*************");String appDirs = System.getProperty("java.class.path");for (String path : appDirs.split(";")) {System.out.println(path);}//从上面的路径中随意选择一个类,来看看他的类加载器是什么:系统类加载器ClassLoader classLoaderTest = ClassLoaderTest1.class.getClassLoader();System.out.println(classLoaderTest);//sun.misc.Launcher$AppClassLoader@18b4aac2} }
1.引导类加载器(Bootstrap ClassLoader)
- 1.该类加载器使用
C/C++
语言实现,是JVM
的一部分,通过Java
代码是无法获取的- 2.该类加载器用来加载
Java
的核心库,提供JVM
自身需要的类
- 1.
JAVA_HOME/jre/lib
目录下的rt.jar
和resources.jar
- 2.
sun.boot.class.path
路径下的内容- 3.该类加载器并不继承自
java.lang.ClassLoader
,没有父加载器- 4.该类加载器也用来加载扩展类和系统类加载器,并指定为他们的父类加载器
- 5.出于安全考虑,引导类加载器只加载包名为
java
,javax
,sun
等开头的类
2.扩展类加载器(Extension ClassLoader)
- 1.
Java
语言编写,由sun.misc.Launcher$ExtClassLoader
实现(内部类),该加载器是JVM
自带的- 2.
ExtClassLoader
派生于ClassLoader
抽象类- 3.该类加载器的父类加载器为引导类加载器
- 4.从
java.ext.dirs
系统属性所指定的目录中加载类库或从JDK
的安装目录的jre/lib/ext
子目录(扩展目录)下加载类库- 5.如果用户创建的
jar
包放在此目录(jre/lib/ext
)下,也会自动由扩展类加载类加载,主要用来加载核心包外的扩展目录下的jar包
3.系统类加载器(System Class Loader)
- 1.Java语言编写,由
sun.misc.Launcher$AppClassLoader
实现(内部类),该加载器是JVM
自带的- 2.
AppClassLoade
派生于ClassLoader
抽象类- 3.该类加载器的父类加载器为扩展类加载器
- 4.该类加载器负责加载环境变量
classpath
或系统属性java.class.path
指定路径下的类库- 5.该类加载器是程序中默认的类加载器,一般来说
Java
应用的类都是由它来完成加载- 6.通过
ClassLoader.getSystemClassLoader()
方法可以获取到该类加载器
4.用户自定义类加载器
- 1.
Java
开发可以自定义类加载器,定制类的加载方式- 2.自定义类加载器的优势
- 1.隔离加载类(不同中间件的加载是隔离的,确保加载
jar包
时相同名称的路径不会冲突)- 2.修改类加载的方式(修改为需要的时候动态的加载)
- 3.扩展加载源(本地磁盘,网络,扩展其他加载源)
- 4.防止源码泄露(自定义类加载器实现加密解密)
- 3.实现步骤
- 1.通过继承抽象类
java.class.ClassLoader
的方式,实现自定义类加载器- 2.
JDK1.2
之前,自定义类加载器会去继承ClassLoader
类并重写loadClass()
方法,从而实现自定义的类加载类- 3.
JDK1.2
之后,不建议覆盖loadClass()
方法,建议把自定义的类加载逻辑写在findClass()
方法中- 4.编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承
URLClassLoader
类,这样可以避免去编写findClass()
方法以及获取字节码流的方式,使自定义类加载器编写更加简洁package com.java;import java.io.FileNotFoundException;/*** 自定义用户类加载器*/ public class CustomClassLoader extends ClassLoader {@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {byte[] result = getClassFromCustomPath(name);if(result == null){throw new FileNotFoundException();}else{//如果不为null则继续调用该方法return defineClass(name,result,0,result.length);}} catch (FileNotFoundException e) {e.printStackTrace();}throw new ClassNotFoundException(name);}//根据指定路径已二进制流的方式读入到内存中形成字节数组private byte[] getClassFromCustomPath(String name){//从自定义路径中加载指定类:细节略//如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。return null;}public static void main(String[] args) {CustomClassLoader customClassLoader = new CustomClassLoader();try {Class<?> clazz = Class.forName("One",true,customClassLoader);Object obj = clazz.newInstance();System.out.println(obj.getClass().getClassLoader());} catch (Exception e) {e.printStackTrace();}} }
5.ClassLoader
- 1.
ClassLoader
是一个抽象类,除引导类加载器其余的类加载器都继承自ClassLoaser
- 2.
sun.misc.Launcher
是JVM
的入口应用,ExtClassLoader
和AppClassLoader
都是Launcher
的内部类- 3.加载类生成
Class
的方式:
- 1.通过
loadClass
方法传入一个想要加载的路径然后返回Class
的类实例- 2.通过
findClass
和defineClass
配合传入一个想要加载的路径然后返回Class
的类实例
1.获取ClassLoader的途径
6.双亲委派机制
- 1.
Java虚拟机
对class文件
采用的是按需加载
的方式,当需要使用该类时才会将它的class文件
加载到内存生成Class
对象- 2.
Java虚拟机
加载某个类的class文件
时,采用的双亲委派机制
,即把请求交由父类处理,它是一种任务委派模式
1.工作原理
- 1.如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
- 2.如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的引导类加载器
- 3.如果父类加载器可以完成类加载任务,则成功返回,如果父类加载器无法完成此加载任务,子类才会尝试自己去加载,这就是双亲委派机制
2.实例
- 1.本地创建一个和
Java.lang.String
同样包级的String
类,如果该类被加载,则会输出静态代码块中的内容package java.lang;public class String {static{System.out.println("我是自定义的String类的静态代码块");} }
- 2.创建一个测试类,调用
Java.lang.String
类public class StringTest {public static void main(String[] args) {java.lang.String str = new java.lang.String();System.out.println("hello");StringTest test = new StringTest();System.out.println(test.getClass().getClassLoader()); //自定义的类加载器一般是系统类加载器System.out.println(str.getClass().getClassLoader()); //核心类库加载器是引导类加载器} }
- 3.测试结果发现自动调用的是核心类库中
String
类而不是本地中的String
类(只限测试使用),因此可防止恶意攻击导致项目崩溃
- 4.上述自定义
String
类添加main
方法并执行,因为双亲委派机制,最后本地String
类的加载交给引导类加载器去加载核心类库中的String
类,而该类不存在main
方法,所以会报错
- 5.系统类接口由引导类加载器加载,实现其功能的第三方的
jar包
加载一般是通过线程上下文类加载器
加载,默认为系统类型加载器
3.优势
- 1.避免类的重复加载
- 2.保护程序的安全,防止核心
API
被随意篡改,防止自定义的类使JVM
崩溃
- 例:自定义
类java.lang.String
- 3.禁止自定义包名与系统包名冲突,因为该包名下的类由引导类加载器加载,但是该类并不存在于引导类加载器要加载的路径中
4.沙箱安全机制
- 1.沙箱:是一个限制程序运行的环境
- 2.沙箱机制:是将
Java
代码限定在虚拟机JVM
特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏,可参考Java沙箱机制- 3.简单来说
- 1.上述自定义
String
类,加载的时候会优先使用引导类加载器
加载- 2.
引导类加载器
在加载的过程中会先加载jdk
自带的文件(rt.jar
包中java\lang\String.class
)- 3.上述报错信息说没有
main
方法就是因为加载的是rt.jar
包中的String
类,这样可以保证对java
核心源代码的保护,这就是沙箱安全机制
5.判断两个Class对象是否为同一个类
- 1.
JVM
中表示两个Class
对象是否为同一个类存在两个必要条件
- 1.类的完整类名必须一致,包括包名
- 2.加载这个类的
classLoader
(指:ClassLoader
实例对象)必须相同(例:上述两个String
类不同,因为类加载器不同)- 2.
JVM
中即使两个类对象(Class
对象)来源同一个class
文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader
实例对象不同,那么这两个对象也是不相等的- 3.
JVM
必须知道一个类型是由引导类加载器加载还是由自定义类加载器加载的- 4.如果一个类型是由自定义类加载器加载的,
JVM
会将这个类加载器的一个引用作为类型信在这里插入代码片
息的一部分保存在方法区中,引导类除外因为该类标识为null
- 5.当解析一个类型到另一个类型的引用的时候,
JVM
需要保证这两个类型的类加载器是相同的(动态链接)
6.类的主动使用和被动使用
- 1.
Java
程序对类的使用方式分为:主动使用和被动使用
- 1.主动使用:分为七种情况
- 1.创建类的实例
- 2.访问某个类或接口的静态变量,或者对该静态变量赋值
- 3.调用类的静态方法
- 4.反射(Class.forName等)
- 5.初始化一个类的子类
- 6.
Java
虚拟机启动被标明启动类的类- 7.
JDK7
开始提供的动态语言支持:
- 1.
java.lang.invoke.MethodHandle
实例的解析结果- 2.
REF_getStatic,REF_putStatic,REF_invokeStatic
句柄对应的类没有初始化,则初始化- 2.被动使用
- 1.除以上七种情况,其他使用
Java
类的方式都被看作是对类的被动使用,都不会导致类的初始化
- 2.不管是主动使用还是被动使用都会被加载,只要使用都会被加载,但是只有主动使用会执行
类加载的初始化
步骤
这篇关于JVM - 1.类加载子系统的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!