赞
踩
图片存疑:new Customer( )实例的类型指针是指向该对象的Class类对象,而不是直接指向方法区的元数据信息。
一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding)
对象头:
普通对象:分为两部分
Mark Word:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等
hash(25) + age(4) + lock(3) = 32bit #32位系统
unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统
Klass Word:类型指针,指向该对象的 Class 类对象的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;在 64 位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 Java 中的一个引用的大小)
|-----------------------------------------------------|
| Object Header (64 bits) |
|---------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|---------------------------|-------------------------|
数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度(12 字节)
|-------------------------------------------------------------------------------|
| Object Header (96 bits) |
|-----------------------|-----------------------------|-------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|-----------------------|-----------------------------|-------------------------|
实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来
对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全
32 位系统:
一个 int 在 java 中占据 4byte,所以 Integer 的大小为:
private final int value;
# 需要补位4byte
4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte
int[] arr = new int[10]
# 由于需要8位对齐,所以最终大小为56byte`。
4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte
浅堆(Shallow Heap):对象本身占用的内存,不包括内部引用对象的大小,32 位系统中一个对象引用占 4 个字节,每个对象头占用 8 个字节,根据堆快照格式不同,对象的大小会同 8 字节进行对齐
JDK7 中的 String:2个 int 值共占 8 字节,value 对象引用占用 4 字节,对象头 8 字节,对齐后占 24 字节,为 String 对象的浅堆大小,与 value 实际取值无关,无论字符串长度如何,浅堆大小始终是 24 字节
private final char value[];
private int hash;
private int hash32;
保留集(Retained Set):对象 A 的保留集指当对象 A 被垃圾回收后,可以被释放的所有的对象集合(包括 A 本身),所以对象 A 的保留集就是只能通过对象 A 被直接或间接访问到的所有对象的集合,就是仅被对象 A 所持有的对象的集合
深堆(Retained Heap):指对象的保留集中所有的对象的浅堆大小之和,一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间
对象的实际大小:一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小
下图显示了一个简单的对象引用关系图,对象 A 引用了 C 和 D,对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身,A 的实际大小为 A、C、D 三者之和,A 的深堆大小为 A 与 D 之和,由于对象 C 还可以通过对象 B 访问到 C,因此 C 不在对象 A 的深堆范围内
内存分析工具 MAT 提供了一种叫支配树的对象图,体现了对象实例间的支配关系
基本性质:
对象 A 的子树(所有被对象 A 支配的对象集合)表示对象 A 的保留集(retained set),即深堆
如果对象 A 支配对象 B,那么对象 A 的直接支配者也支配对象 B
支配树的边与对象引用图的边不直接对应
左图表示对象引用图,右图表示左图所对应的支配树:
比如:对象 F 与对象 D 相互引用,因为到对象 F 的所有路径必然经过对象 D,因此对象 D 是对象 F 的直接支配者
参考文章:https://www.yuque.com/u21195183/jvm/nkq31c
尽量使用基本数据类型
满足容量前提下,尽量用小字段
尽量用数组,少用集合,数组中是可以使用基本类型的,但是集合中只能放包装类型,如果需要使用集合,推荐比较节约内存的集合工具:fastutil
一个 ArrayList 集合,如果里面放了 10 个数字,占用多少内存:
private transient Object[] elementData;
private int size;
Mark Word 占 4byte,Klass Word 占 4byte,一个 int 字段占 4byte,elementData 数组占 12byte,数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte(深堆)
时间用 long/int 表示,不用 Date 或者 String
JVM 是通过栈帧中的对象引用访问到其内部的对象实例:
句柄访问:Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息
优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改
直接指针(HotSpot 采用):Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的对象地址
优点:速度更快,节省了一次指针定位的时间开销
缺点:对象被移动时(如进行 GC 后的内存重新排列),对象的 reference 也需要同步更新
参考文章:https://www.cnblogs.com/afraidToForget/p/12584866.html
在 Java 中,对象的生命周期包括以下几个阶段:
创建阶段 (Created):
应用阶段 (In Use):对象至少被一个强引用持有着
不可见阶段 (Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用
不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括 GC Root 的强引用
收集阶段 (Collected):垃圾回收器对该对象的内存空间重新分配做好准备,该对象如果重写了 finalize() 方法,则会去执行该方法
终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完 finalize() 方法后仍然处于不可达状态时进入该阶段
对象空间重分配阶段 (De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配
参考文章:https://blog.csdn.net/sodino/article/details/38387049
类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类
Java 对象创建时机:
使用 new 关键字创建对象:由执行类实例创建表达式而引起的对象创建
使用 Class 类的 newInstance 方法(反射机制)
使用 Constructor 类的 newInstance 方法(反射机制)
public class Student {
private int id;
public Student(Integer id) {
this.id = id;
}
public static void main(String[] args) throws Exception {
Constructor<Student> c = Student.class.getConstructor(Integer.class);
Student stu = c.newInstance(123);
}
}
使用 newInstance 方法的这两种方式创建对象使用的就是 Java 的反射机制,事实上 Class 的 newInstance 方法内部调用的也是 Constructor 的 newInstance 方法
使用 Clone 方法创建对象:用 clone 方法创建对象的过程中并不会调用任何构造函数,要想使用 clone 方法,我们就必须先实现 Cloneable 接口并实现其定义的 clone 方法
使用(反)序列化机制创建对象:当反序列化一个对象时,JVM 会创建一个单独的对象,在此过程中,JVM 并不会调用任何构造函数,为了反序列化一个对象,需要让类实现 Serializable 接口
从 Java 虚拟机层面看,除了使用 new 关键字创建对象的方式外,其他方式全部都是通过转变为 invokevirtual 指令直接创建对象的
创建对象的过程:
判断对象对应的类是否加载、链接、初始化
为对象分配内存:指针碰撞、空闲链表。当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量,即使从隐藏变量也会被分配空间(继承部分解释了为什么会隐藏)
处理并发安全问题:
初始化分配的空间:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值
设置对象的对象头:将对象的所属类(类的元数据信息)、对象的 HashCode、对象的 GC 信息、锁信息等数据存储在对象头中
执行 init 方法进行实例化:实例变量初始化、实例代码块初始化 、构造函数初始化
实例变量初始化与实例代码块初始化:
对实例变量直接赋值或者使用实例代码块赋值,编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后(Java 要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前
构造函数初始化:
Java 要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到 Object 类。然后从 Object 类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数
一个实例变量在对象初始化的过程中会被赋值几次?一个实例变量最多可以被初始化 4 次
JVM 在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值;在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值;在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值;;在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值
类的初始化过程与类的实例化过程的异同?
类的初始化是指类加载过程中的初始化阶段对类变量按照代码进行赋值的过程;类的实例化是指在类完全加载到内存中后创建对象的过程(类的实例化触发了类的初始化,先初始化才能实例化)
假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?(经典案例)
public class StaticTest { public static void main(String[] args) { staticFunction();//调用静态方法,触发初始化 } static StaticTest st = new StaticTest(); static { //静态代码块 System.out.println("1"); } { // 实例代码块 System.out.println("2"); } StaticTest() { // 实例构造器 System.out.println("3"); System.out.println("a=" + a + ",b=" + b); } public static void staticFunction() { // 静态方法 System.out.println("4"); } int a = 110; // 实例变量 static int b = 112; // 静态变量 }
2
3
a=110,b=0
1
4
static StaticTest st = new StaticTest();
:
实例实例化不一定要在类初始化结束之后才开始
在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此在实例化上述程序中的 st 变量时,实际上是把实例化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置,这就导致了实例初始化完全发生在静态初始化之前,这也是导致 a 为 110,b 为 0 的原因
代码等价于:
public class StaticTest {
<clinit>(){
System.out.println("2"); // 实例代码块
a = 110; // 实例变量
System.out.println("3"); // 实例构造器中代码的执行
System.out.println("a=" + a + ",b=" + b); // 实例构造器中代码的执行
类变量st被初始化
System.out.println("1"); //静态代码块
类变量b被初始化为112
}
}
类是在运行期间第一次使用时动态加载的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存,加载的类信息存放于一块成为方法区的内存空间
包括 7 个阶段:
加载(Loading) 加载就是寻找类/接口的二进制标识,并且根据二进制表示创建类/接口
Loading is the process of finding the binary representation of a class or interface type with a particular name and creating a class or interface from that binary representation
链接:验证(Verification)、准备(Preparation)、解析(Resolution):组合类/接口让JVM执行
Linking is the process of taking a class or interface and combining it into the run-time state of the Java Virtual Machine so that it can be executed.
初始化(Initialization): 更多参考 init
Initialization of a class or interface consists of executing the class or interface initialization method <clinit>
使用(Using)
卸载(Unloading)
加载是类加载的其中一个阶段,注意不要混淆;完整的类加载包括加载、链接、初始化
加载过程完成以下三件事:
其中二进制字节流可以从以下方式中获取:
将字节码文件加载至方法区后,会在堆中创建一个 java.lang.Class 对象,用来引用位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象。
方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构:
_java_mirror
即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 Klass 暴露给 Java 使用_super
父类、_fields
成员变量、_methods
方法、_constants
常量池、_class_loader
类加载器、_vtable
虚方法表、_itable
接口方法表加载过程:
创建数组类有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建,创建数组类的过程:
类加载过程中链接阶段的可以大致分为三个阶段:1)验证 2)准备 3)解析
确保 Class 文件的字节流中包含的信息是否符合 JVM 规范,保证被加载类的正确性,不会危害虚拟机自身的安全
主要包括四种验证:
文件格式验证
语义检查,但凡在语义上不符合规范的,虚拟机不会给予验证通过
是否所有的类都有父类的存在(除了 Object 外,其他类都应该有父类)
是否一些被定义为 final 的方法或者类被重写或继承了
非抽象类是否实现了所有抽象方法或者接口方法
是否存在不兼容的方法
字节码验证,试图通过对字节码流的分析,判断字节码是否可以被正确地执行
符号引用验证,Class 文件在其常量池会通过字符串记录将要使用的其他类或者方法
准备阶段为静态变量分配内存并设置初始值,使用的是堆的内存:
说明:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次
类变量初始化:
Java的静态变量存储位置?
public class test10 {
static int a; // 1) 准备阶段仅仅进行空间分配
static int b = 10; // 2) 准备阶段完成赋值并初始化
static final int c = 20; // 3) 准备阶段完成赋值并初始化
static final String d = "hello"; // 4) 准备阶段完成赋值并初始化
static final Object e = new Object(); // 5) 准备阶段仅仅进行空间分配,初始化阶段完成赋值,通过 <cinit> 构造函数实现
}
将常量池中类、接口、字段、方法的符号引用替换为直接引用(内存地址)的过程:
例如:在 com.demo.Solution
类中引用了 com.test.Quest
,把 com.test.Quest
作为符号引用存进类常量池,在类加载完后,用这个符号引用去方法区找这个类的内存地址
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等
public class Load2 {
public static void main(String[] args) throws Exception{
ClassLoader classloader = Load2.class.getClassLoader();
// cloadClass 加载类方法不会导致类的解析和初始化,也不会加载D
Class<?> c = classloader.loadClass("cn.jvm.t3.load.C");
// new C();会导致类的解析和初始化,从而解析初始化D
System.in.read();
}
}
class C {
D d = new D();
}
class D {
}
使用HSDB工具
D:\Java\bin>java -cp ../lib/sa-jdi.jar sun.jvm.hotspot.HSDB
ClassLoader classloader = Load2.class.getClassLoader();
// cloadClass 加载类方法不会导致类的解析和初始化,也不会加载D
Class<?> c = classloader.loadClass("cn.jvm.t3.load.C");
创建实例对象会导致类的解析和初始化,从而解析初始化D
new C();
初始化阶段才真正开始执行类中定义的 Java 程序代码,在准备阶段,类变量已经赋过一次系统要求的初始值;在初始化阶段,通过程序制定的计划去初始化类变量和其它资源,执行
在编译生成 class 文件时,编译器会产生两个方法加于 class 文件中,一个是类的初始化方法 clinit,另一个是实例的初始化方法 init
类构造器 () 与实例构造器 () 不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器最多被虚拟机调用一次,而实例构造器则会被虚拟机调用多次,只要程序员创建对象
():类构造器,由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的
作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块
线程安全问题:
特别注意:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问
public class Test {
static {
//i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法,两者不同的是:
原则:类的初始化是懒惰的,只有在首次使用时才会被装载,JVM 不会无条件地装载 Class 类型,Java 虚拟机规定,一个类或接口在初次使用前,必须要进行初始化
主动引用:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列情况必须对类进行初始化(加载、验证、准备都会发生):
被动引用:所有引用类的方式都不会触发初始化,称为被动引用
class A {
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}
验证(实验时请先全部注释,每次只执行其中一个)
public class Load3 { static { System.out.println("main init"); } public static void main(String[] args) throws ClassNotFoundException, IOException { // // 1. 静态常量不会触发初始化 // System.out.println(B.b); // // 2. 类对象.class 不会触发初始化 // System.out.println(B.class); // // 3. 创建该类的数组不会触发初始化 // System.out.println(new B[0]); // 4. 不会初始化类 B,但会加载 B、A // ClassLoader cl = Thread.currentThread().getContextClassLoader(); // cl.loadClass("cn.itcast.jvm.t3.load.B"); // // 5. 不会初始化类 B,但会加载 B、A // ClassLoader c2 = Thread.currentThread().getContextClassLoader(); // Class.forName("cn.itcast.jvm.t3.load.B", false, c2); System.in.read(); // // 1. 首次访问这个类的静态变量或静态方法时 // System.out.println(A.a); // // 2. 子类初始化,如果父类还没初始化,会引发 // System.out.println(B.c); // // 3. 子类访问父类静态变量,只触发父类初始化 // System.out.println(B.a); // // 4. 会初始化类 B,并先初始化类 A // Class.forName("cn.itcast.jvm.t3.load.B"); } }
懒惰初始化的单例模式(类的懒惰初始化应用)
// 内部类中保存单例
// 这里利用了 Singleton类加载过程中在初始化阶段的懒惰初始化的特性,当首次访问类的静态方法getInstance时,触发类的初始化。
// 注意:单例模式实现是线程安全的,静态代码只在class的加载过程中执行一次,JVM提供的类加载器(C实现)需要确保多个线程进行初始化操作的时候,只有一个线程实现初始化
// 双亲委派机制是推荐使用类加载器的方式,而类加载器是JVM底层实现用于加载类的代码,二者要注意区分。
class Singleton {
private Singleton() {}
private static class LazyHolder{
private static final Singleton SINGLETON = new Singleton();
static {
System.out.println("lazy holder init");
}
}
public static Singleton getInstance() {
return LazyHolder.SINGLETON;
}
}
init 指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行
实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行
类实例化过程:父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数
时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java虚拟机进程终止
卸载类即该类的 Class 对象被 GC,卸载类需要满足3个要求:
1. 该类所有的实例已经被回收
2. 加载该类的ClassLoder已经被回收
3. 该类对应的java.lang.Class对象没有任何对方被引用
在 JVM 生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,这些类始终是可及的。
总结:由Java虚拟机自带的三种类加载加载的类在虚拟机的整个生命周期中是不会被卸载的,由用户自定义的类加载器所加载的类才可以被卸载,
类加载方式:
类的唯一性:
命名空间:
基本特征:
类加载器是 Java 的核心组件,用于加载字节码到 JVM 内存,得到 Class 类的对象
从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器:
从 Java 开发人员的角度看:
JAVA_HOME/jre/lib
或 sun.boot.class.path
目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类,并且是虚拟机识别的类库加载到虚拟机内存中JAVA_HOME/jre/lib/ext
或者被 java.ext.dir
系统变量所指定路径中的所有类库加载到内存中java.class.path
指定路径下的类库名称 | 加载的类(管理区域) | 说明 |
---|---|---|
Bootstrap ClassLoader (启动类加载器) | JAVA_HOME/jre/lib | 无法直接访问,C++代码实现 |
Extension ClassLoader (扩展类加载器) | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap |
Application ClassLoader (应用类加载器,最常见) | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
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@610455d6 //获取其上层 获取不到引导类加载器 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);//null }
补充两个类加载器:
ClassLoader 类,是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器)
获取 ClassLoader 的途径:
clazz.getClassLoader()
Thread.currentThread.getContextClassLoader()
ClassLoader.getSystemClassLoader()
DriverManager.getCallerClassLoader()
ClassLoader 类常用方法:
getParent()
:返回该类加载器的超类加载器loadclass(String name)
:加载名为 name 的类,返回结果为 Class 类的实例,该方法就是双亲委派模式findclass(String name)
:查找二进制名称为 name 的类,返回结果为 Class 类的实例,该方法会在检查完父类加载器之后被 loadClass() 方法调用findLoadedClass(String name)
:查找名称为 name 的已经被加载过的类,final 修饰无法重写defineClass(String name, byte[] b, int off, int len)
:将字节流解析成 JVM 能够识别的类对象resolveclass(Class<?> c)
:链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析InputStream getResourceAsStream(String name)
:指定资源名称获取输入流用 Bootstrap 类加载器加载类:
public class F {
static {
System.out.println("bootstrap F init");
}
}
public class Load10 {
// -XX:+TraceClassLoading
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
Class<?> aClass = Class.forName("c_04.F");
System.out.println(aClass.getClassLoader());
}
}
(通过JVM参数指定)
-Xbootclasspath // 设置启动类加载的路径
java -Xbootclasspath:<new bootclasspath>
java -Xbootclasspath/a:<追加路径>
java -Xbootclasspath/p:<追加路径>
D:\StudyJava\AboutDiKnown\study-08-JVM\target\classes>
java -Xbootclasspath/a:. c_04.Load10
执行结果:
用 扩展类加载器加载类:
public class G {
static {
System.out.println("ext G init");
//System.out.println("classpath G init");
}
}
使用扩展类加载器需要打包成Jar ,放入 ext目录下:
D:\StudyJava\AboutDiKnown\study-08-JVM\target\classes>
jar -cvf my.jar c_04\G.class
/**
* 演示 扩展类加载器
* 在 D:\Java\jre\lib\ext 下有一个 my.jar
* 里面也有一个 G 的类,观察到底是哪个类被加载了
*/
public class Load10_2 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("c_04.G");
System.out.println(aClass.getClassLoader());
}
}
在 JVM 中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制
全盘加载:当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入
双亲委派:先让父类加载器加载该 Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载
缓存机制:会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区(方法区)中
双亲委派模型(Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它类加载器都要有父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)
工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载
双亲委派机制的优点:
可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证全局唯一性
Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一
保护程序安全,防止类库的核心 API 被随意篡改
例如:在工程中新建 java.lang 包,接着在该包下新建 String 类,并定义 main 函数
public class String {
public static void main(String[] args) {
System.out.println("demo info");
}
}
此时执行 main 函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心 API 库。出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法
双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类(可见性)
所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则
具体流程可以参考上图 ↑
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 这里的类加载通过synchronized来保证安全性 synchronized (getClassLoadingLock(name)) { // 1. 调用当前类加载器的 findLoadedClass(name),检查当前类加载器是否已加载过指定 name 的类 Class c = findLoadedClass(name); // 当前类加载器如果没有加载过 if (c == null) { long t0 = System.nanoTime(); try { // 判断当前类加载器是否有父类加载器 if (parent != null) { // 2.如果当前类加载器有父类加载器,则调用父类加载器的loadClass(name,false) c = parent.loadClass(name, false); } else { // 3. 如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { //4. 如果调用父类的类加载器无法对类进行加载,则用自己的 findClass() 方法进行加载 // 可以自定义 findClass() 方法 long t1 = System.nanoTime(); c = findClass(name); // 5.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) { // 链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 resolveClass(c); } return c; } }
假设loadclass方法首次加载应用程序类
执行流程为:
AppClassLoader
//1 处, 开始查看已加载的类,结果没有AppClassLoader
// 2 处,委派上级ExtClassLoader.loadClass()
ExtClassLoader
// 1 处,查看已加载的类,结果没有ExtClassLoader
// 3 处,没有上级了,则委派 BootstrapClassLoader查找BootstrapClassLoader
是在 JAVA_HOME/jre/lib
下找 H 这个类,显然没有ExtClassLoader
// 4 处,调用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext
下找 H 这个类,显然没有,同时会抛出ClassNotFoundException
异常,该异常被应用类加载器给捕获,不做任何处理引申问题:多线程环境下,是如何保证类加载的唯一性?
synchronized (getClassLoadingLock(name))
// 这个加锁的过程用到了concurrentHashMap,key是类的名称,value是object对象,加载类的时候会对这个类的object对象进行加锁
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;
}
JDBC中的双亲委派机制的打破
实例:在使用 JDBC 时,都需要加载 Driver 驱动**,com.mysql.jdbc.Driver 是如何被正确加载的**?
Applications no longer need to explicitly load JDBC drivers using Class.forName(). Existing programs which currently load JDBC drivers using Class.forName() will continue to work without modification.
// DriverManger.java文件中的注解,为什么之前需要 Class.forName()加载驱动,现在不需要呢?)
public static void main(String[] args) {
System.out.println(DriverManager.class.getClassLoader());
}
代码执行结果:
null
DriverManger.java的部分源码
step1:在静态代码块中调用loadInitialDrivers();
public class DriverManager {
// List of registered JDBC drivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
...
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
...
}
step2: 调用loadInitialDrivers()方法
关键点:1)使用SPI加载驱动类 2)使用应用程序类加载器加载驱动类
private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } // 1)使用 ServiceLoader 机制加载驱动,即 SPI AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); println("DriverManager.initialize: jdbc.drivers = " + drivers); // 2)使用 jdbc.drivers 定义的驱动名加载驱动 if (drivers == null || drivers.equals("")) { return; } /*这里就是String数组,里面每个String都是需要加载驱动类的名称*/ String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); // 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器 Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } }
先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载
再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称
JDBC的驱动类加载总结:
备注:整个过程貌似是通过SPI获取到需要加载的类的类的名称,然后再调用应用程序类加载器加载(不是太确定)
定义: SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。
mysql-connector-java在service目录下建立名称为java.sql.Driver的文件。
使用SPI必须遵循上述约定
按照上述约定并将接口类实现,可通过ServiceLoader来得到实现类(如下所示),体现的是【面向接口编程+解耦】的思想
ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
iter.next();
}
SPI思想在其他框架的体现:
JDBC
Servlet 初始化器
Spring 容器
Dubbo(对 SPI 进行了扩展)
背景:SPI实现扩展功能的类加载的时候,使用了线程上下文类加载器,分析源码可以发现线程上下文类加载器就是应用程序类加载器。
线程上下文类加载器定义:当前线程使用的类加载器,默认就是应用程序类加载器 ,在每一个Thread启动的时候,JVM都会为该线程初始化一个类加载器
private ClassLoader contextClassLoader; // Thread源码中包含有contextClassLoader这个属性
ServiceLoader.java的源码内容:
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); // 传入两个参数: 1)class对象 2)类的加载器 return ServiceLoader.load(service, cl); } public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){ return new ServiceLoader<>(service, loader); } ========================================================================================= private ServiceLoader(Class<S> svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null"); // 确保类加载器不为空 loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; reload(); } public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); // 创建一个发现service的迭代器 } ========================================================================================= /* nextService方法中会获取*/ private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { // loader就是传递进来的线程上下文加载器,本质上就是应用程序类加载。 c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen }
总结:从上面的源码中看到SPI机制并没有遵循双亲委派机制去加载类即没有适用ClassLoader.loadClass方法加载类,而是通过Java的反射机制(@CallerSensitive)即使用Class.forName方法指定应用程序类加载器去加载class。
Class.forName()加载类和使用ClassLoader.loadClass加载类的区别(重要)?
/*方式1:使用ClassLoader.loadClass进行类加载*/
Class<?> aClass1 = test13.class.getClassLoader().loadClass("part3.test13");
/*方式2:使用Class.forName进行类加载*/
Class.forName(String name, boolean initialize,ClassLoader loader)
关于ClassLoader.loadClass
关于Class.forName()
可以看出Class.forName这个方法加载类更加的灵活,不要遵循双亲委派机制,可以自由的去加载类。
(打破了双亲委派机制)
Class.java文件中Class.forName的源码
@CallerSensitive public static Class<?> forName(String name, boolean initialize,ClassLoader loader) throws ClassNotFoundException{ Class<?> caller = null; SecurityManager sm = System.getSecurityManager(); if (sm != null) { // Reflective call to get caller class is only needed if a security manager // is present. Avoid the overhead of making this call otherwise. caller = Reflection.getCallerClass(); if (sun.misc.VM.isSystemDomainLoader(loader)) { ClassLoader ccl = ClassLoader.getClassLoader(caller); if (!sun.misc.VM.isSystemDomainLoader(ccl)) { sm.checkPermission( SecurityConstants.GET_CLASSLOADER_PERMISSION); } } } return forName0(name, initialize, loader, caller); }
双亲委派模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者的类加载器实现方式
破坏双亲委派模型的方式:
自定义 ClassLoader
引入线程上下文类加载器
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类:
JDK 开发人员引入了线程上下文类加载器(Thread Context ClassLoader),这种类加载器可以通过 Thread 类的 setContextClassLoader 方法进行设置线程上下文类加载器,在执行线程中抛弃双亲委派加载模式,使程序可以逆向使用类加载器,使 Bootstrap 加载器拿到了 Application 加载器加载的类,破坏了双亲委派模型
实现程序的动态性,如代码热替换(Hot Swap)、模块热部署(Hot Deployment)
IBM 公司主导的 JSR一291(OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换,在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构
当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索:
热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,热替换的关键需求在于服务不能中断,修改必须立即表现正在运行的系统之中
沙箱机制(Sandbox):将 Java 代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏
沙箱限制系统资源访问,包括 CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样
对于自定义类加载器的实现,只需要继承 ClassLoader 类,覆写 findClass 方法即可
作用:
1)想加载非 classpath 随意路径中的类文件
2)都是通过接口来使用实现,希望解耦时,常用在框架设计
3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤:
1. 继承 ClassLoader 父类
2. 要遵从双亲委派机制,重写 findClass 方法(不是重写 loadClass方法,否则不会走双亲委派机制)
3. 读取类文件的字节码
4. 调用父类的 defineClass 方法来加载类
5. 使用者调用该类加载器的 loadClass 方法
示例:
编译生成 class文件
public class MapImpl1 {
static {
System.out.println("MapImpl1");
}
}
public class MapImpl2 {
static {
System.out.println("MapImpl2");
}
}
自定义类加载器,加载的路径为class文件的文件夹
/** * @date 自定义类加载器 * 1. 继承 ClassLoader 父类 */ class MyClassLoader extends ClassLoader { /** * 2. 要遵从双亲委派机制,重写 findClass 方法(不是重写 loadClass方法,否则不会走双亲委派机制) */ @Override public Class<?> findClass(String name) throws ClassNotFoundException { //3.确定加载路径 String path = "D:\\StudyJava\\AboutDiKnown\\study-08-JVM\\target\\classes\\" + name +".class"; try { //4. 读取类文件的字节码 ByteArrayOutputStream os = new ByteArrayOutputStream(); Files.copy(Paths.get(path),os); //5.字节码文件转二进制数组 byte[] bytes = os.toByteArray(); //6. 调用父类的 defineClass 方法来加载类 return defineClass(name, bytes, 0, bytes.length); } catch (IOException e) { e.printStackTrace(); throw new ClassNotFoundException("类文件未找到", e); } } }
同一个类对象的需要同名、同包、通加载器加载
public class Load7 {
public static void main(String[] args) throws Exception {
//6. 使用者调用该类加载器的 loadClass 方法
MyClassLoader classLoader = new MyClassLoader();
Class<?> c1 = classLoader.loadClass("MapImpl1");
Class<?> c2 = classLoader.loadClass("MapImpl1");
System.out.println(c1 == c2);
MyClassLoader classLoader2 = new MyClassLoader();
Class<?> c3 = classLoader2.loadClass("MapImpl1");
System.out.println(c1 == c3);
c1.newInstance();
}
}
为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动:
扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取
JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数个 JMOD 文件),其中 Java 类库就满足了可扩展的需求,那就无须再保留 <JAVA_HOME>\lib\ext
目录,此前使用这个目录或者 java.ext.dirs
系统变量来扩展 JDK 功能的机制就不需要再存在
启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader
{
//4. 读取类文件的字节码
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path),os);
//5.字节码文件转二进制数组
byte[] bytes = os.toByteArray();
//6. 调用父类的 defineClass 方法来加载类
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException(“类文件未找到”, e);
}
}
}
同一个类对象的需要同名、同包、通加载器加载 ```java public class Load7 { public static void main(String[] args) throws Exception { //6. 使用者调用该类加载器的 loadClass 方法 MyClassLoader classLoader = new MyClassLoader(); Class<?> c1 = classLoader.loadClass("MapImpl1"); Class<?> c2 = classLoader.loadClass("MapImpl1"); System.out.println(c1 == c2); MyClassLoader classLoader2 = new MyClassLoader(); Class<?> c3 = classLoader2.loadClass("MapImpl1"); System.out.println(c1 == c3); c1.newInstance(); } }
为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动:
扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取
JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数个 JMOD 文件),其中 Java 类库就满足了可扩展的需求,那就无须再保留 <JAVA_HOME>\lib\ext
目录,此前使用这个目录或者 java.ext.dirs
系统变量来扩展 JDK 功能的机制就不需要再存在
启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。