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.class
Last modified 2022 -11-9; size 608 bytes
MD5 checksum 5964d34bba8f8bf4e817be8fe95a17fe
Compiled from "HelloApp.java"
public class com.java.HelloApp
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
{
public com.java.HelloApp( ) ;
descriptor: ( ) V
flags: ACC_PUBLIC
Code:
stack = 1 , locals = 1 , args_size = 1
0 : aload_0
1 : invokespecial
4 : return
LineNumberTable:
line 7 : 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/atguigu/java/HelloApp;
public static void main( java.lang.String[ ] ) ;
descriptor: ( [ Ljava/lang/String; ) V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack = 2 , locals = 1 , args_size = 1
0 : getstatic
3 : getstatic
6 : invokevirtual
9 : return
LineNumberTable:
line 12 : 0
line 13 : 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [ Ljava/lang/String;
static { } ;
descriptor: ( ) V
flags: ACC_STATIC
Code:
stack = 1 , locals = 0 , args_size = 0
0 : iconst_1
1 : putstatic
4 : return
LineNumberTable:
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 ;
static {
num = 2 ;
number = 20 ;
}
private static int number = 10 ;
public static void main ( String [ ] args) {
System . out. println ( num) ;
System . out. println ( number) ;
}
}
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) ;
ClassLoader extClassLoader = systemClassLoader. getParent ( ) ;
System . out. println ( extClassLoader) ;
ClassLoader bootstrapClassLoader = extClassLoader. getParent ( ) ;
System . out. println ( bootstrapClassLoader) ;
ClassLoader classLoader = ClassLoaderTest . class . getClassLoader ( ) ;
System . out. println ( classLoader) ;
ClassLoader classLoader1 = String . class . getClassLoader ( ) ;
System . out. println ( classLoader1) ;
ClassLoader classLoader2 = Integer . class . getClassLoader ( ) ;
System . out. println ( classLoader2) ;
}
}
7.通过下列代码可以动态获取到引导类,扩展类,系统类加载器负责加载的类,其中越底层能加载的类就越多public class ClassLoaderTest1 {
public static void main ( String [ ] args) {
System . out. println ( "**********引导类加载器**************" ) ;
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) ;
System . 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) ;
}
}
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 {
@Override
protected Class < ? > findClass ( String name) throws ClassNotFoundException {
try {
byte [ ] result = getClassFromCustomPath ( name) ;
if ( result == null ) {
throw new FileNotFoundException ( ) ;
} else {
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
崩溃
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.不管是主动使用还是被动使用都会被加载,只要使用都会被加载,但是只有主动使用会执行类加载的初始化
步骤