-
2022-01-20 17:46:07
复用类
复用代码是Java中众多引人注目的功能之一,也是程序员需要掌握的一项能力。
组合语法
组合语法就是将一个对象的引用放在新的类中。
class WaterSource{ private String s; WaterSource(){ System.out.println("WaterSource()"); s = "Constructed"; } public String toString(){ return s; } } public class SprinklerSystem(){ private String v1,v2,v3,v4; private WaterSource source = new WaterSource(); private int i; private float f; public String toString(){ return "v1="+v1+""+ "v2="+v2+""+ "v3="+v3+""+ "v4="+v4+""+ "i="+i+""+"f="+f+""+ "source="+source; } public void static main(){ SprinklerSystem s = new SprinklerSystem(); System.out.println(s) } }
我们知道类中域为基本类型是能够自动被初始化零,对象引用被初始化为null。
编译器并不是简单地为每一个引用都创建默认对象,如果真要这么左,就会在很多情况下增加不必要的负担。
可以在代码中初始化引用的位置:
class Soap(){ private String s; Soap(){ //1.在类构造器中 System.out.println("Soap()"); s="Constructed"; } } public class Bath(){ //2.在定义对象的地方 private String s1="Happy",s2="Happy",s3,s4; private Soap castille; private int i; private float toy; public Bath(){ //在类构造器中 System.out.println("Bath()"); s3="joy"; toy=3.14f; castille = new Soap(); } //3.使用实例初始化,匿名代码块每次创建对象的时候都会运行 { i = 47; } public String toString(){ // 4.在正要使用对象之前 if(s4 == null ) s4 = "Joy"; ....... } ..... }
在正要使用对象之前初始化这种方式就称为惰性初始化,这样可以减少额外的负担。
继承
Java中除非已经明确指出从其他类中继承,否则就是隐式地从Java的标准根类Object进行继承
子类会自动得到父类所有的域和方法,所以为了继承,一般会将所有的数据成员都指定为private,将所有的方法指定为public。
从外部来看,子类就像是一个与父类具有相同接口的新类,或许还会有一些额外的方法和域,继承并不只是复制父类的接口。
当我们创建了一个子类的对象时,这个对象还包含了一个父类的子对象。这个子对象与我们用父类直接创建对象是一样的,区别就在后者来自于外部,而父类的子对象被包装在子类的内部。所以Java会自动在子类的构造器中插入对父类构造器的调用。
protected关键字
访问权限控制4个关键字
public>protected>default>private
protected关键字,简单说就是对于类用户,它是private的,但是对于继承此类的子类或其它任何在同一个包内的类来说,它是public的
向上转型
父类与子类的关系,可以用**“子类是父类的一种类型”**来加以概括。
class Instrument(){ public void play(){} static void tune(Instrument i){ i.play(); } } public class Wind extends Instrument{ public static void main(String[] args){ Wind flute = new Wind(); // tune方法中的参数类型是Instrument 而flute是Wind,将子类型引用转换为父类型引用,称为向上转型 Instrument.tune(flute); } }
因为传统的类继承图是将根置于页面顶部,然后逐渐向下,所以称作向上转型。
所以说,子类是父类的一个超集,它可能比父类含有更多的方法,但是它必须至少具备父类中所含有的方法。
final关键字
可能使用到final 的三种情况:数据、方法和类。
final数据
许多编程语言中都有某种方法来告诉编译器一块数据是恒定不变的。恒定不变有时是很有用的。比如:
- 一个永不改变的编译时常量。
- 一个在运行时被初始化的值,而我们不希望它被改变。
在Java中,这类常量必须是基本数据类型,并且以fianl表示。在对这个常量定义时,必须对其进行赋值。
一个即使static又是final 的域只占据一段不能改变的存储空间。
对于基本类型,final使数值恒定不变,而用于对象引用,final使引用恒定不变,一旦引用被初始化执行一个对象。就无法再把它改为执行另一个对象。然而对象自身却是可以被修改的。
按照惯例,既是static又是final的域(即编译时常量)将用大写表示,并使用下划线来分割各个单词:
class Value{ int i; public Value(int i ){this.i = i;} } public class FinalData{ private static Random rand = new Random(47); private String id; public FinalData(String id){this.id = id;} //编译时常量,final是必需的,staic不是必需的,static强调只有一份,final则说明它是一个常量 private final int valueOne = 9; private static final int VALUE_TWO = 99; //并不是所有的final的基本数据类型都是在编译时就可以知道它的值的 private final int i4 = rand.nextInt(20); private final Value v2 = new Value(22); private final int[] a = {1,2,3,4,5}; public static void main(String[] args){ FinalData fd1 = new FinalData("fd1"); // 错误,valueOne不能改变数值 fd1.valueOne++; //正确,fd1.v2不能指向新的对象,但是可以更改对象里的数据 fd1.v2.i++; //错误 v2不能指向新的对象 fd1.v2 = new Value(9); for(int i=0;i<fd1.a.length;i++){ //正确,理由同v2 fd1.a[i]++ } ..... } }
空白final
所谓空白final,是指被声明为final但又为给定初值的域。无论什么情况,编译器都确保空白final在使用必须被初始化。
这样一个类中的final域可以做到根据对象而有所不同,却又保持恒定不变的特性。
class Poppet{ private int i; Poppet(int ii){ i=ii; } } public class BlankFinal{ private final int i =0; //空白final private final int j; //空白final 引用 private final Poppet p; //空白final必须在构造器中初始化 public BlankFinal(){ j=1; p = new Poppet(1); } public BlankFinal(int x){ j=x; p=new Poppet(x); } public static void main(String[] args){ new BlankFinal(); new BlankFinal(47); } }
final 参数
Java允许在参数列表中以声明的方式将参数指明为final。这意味着我们无法在方法中更改参数引用所指向的对象。
class Gizmo{ public void spin(){} } public class FinalArguments{ void with(final Gizmo g){ //错误 g = new Gizmo(); } void g(final int i){ //错误 return i++; } int gi(final int i ){ //正确,这里只读取了并没有修改,这一个特性主要用来向匿名内部类传递数据 return i+1; } }
final方法
使用final方法有两个原因,第一个原因是方法锁定,以防止任何继承类修改它的含义。另一个原因是因为效率,不过在我们应该让编译器和JVM去处理效率问题,只有在想要明确禁止覆盖的时候,才将方法设置为final
类中所有的private方法都隐式的指定为final,如果尝试覆盖一个private方法,似乎是奏效的,而且编译器也不会给出错误信息。
class TestPrivate{ private void print(){ System.out.println("hello"); } } public class PrivateFinal extends TestPrivate{ // 并没有覆盖,只是创建了一个新的方法。 private void print(){ System.out.println("yes"); } public static void main(String[] args) { PrivateFinal p = new PrivateFinal(); p.print(); } }
覆盖只有在某个方法是父类的接口的一部分时才会出现,即,必须能将一个对象向上转型为它的基本类习惯并调用相同的方法。如果一个方法是private,那它就不是父类接口的一部分。
final类
当某个类是final时,那么就是不希望它有子类。
final类中所有的方法都是隐式为final的。
初始化以及类的加载
Java中采用了一种独特的加载方式,因为Java中所有的事务都是对象。每个类的编译代码都存在于它自己的独立文件中,该文件只有在需要实用程序代码的时候才会被加载。可以说,类的代码在初次使用时才加载。这通常是指加载发生于创建类的第一个对像时,但是当访问static域或static方法时,也会发生加载。所有的static对象和static代码段都会在加载时按程序中的顺序依次初始化。
继承与初始化
class Insect{ private int i =9; protected int j; Insect(){ System.out.println("i="+i+",j="+j); j=39; } private static int x1= printInit("static Insect.x1 initialized"); static int printInit(String s) { System.out.println(s); return 47; } } public class Beetle extends Insect{ private int k = printInit("Beetle.k initialized"); public Beetle(){ System.out.println("k="+k); System.out.println("j="+j); } private static int x2= printInit("static Beetle.x2 initialized"); public static void main(String[] args) { System.out.println("Beetle constructor"); Beetle b = new Beetle(); } }
在Beetle上运行Java,所发生的第一件事情就是试图访问Beetle.main(),于是加载器开始启动并找出Beetle类的编译代码(在Beetle.class文件中)。在对它进行加载的过程中,编译器注意到它有一个父类(由extends得知),于是继续加载父类。不管我们是否打算产生一个父类对象,这都要发生(可以尝试将对象创建代码Beetle b = new Beetle();注释掉来证明)。
如果这个父类还有父类,那么第二个父类就会被加载,如此类推。接下来,根基类中的static初始化就会被执行,然后是下一个子类,以此类推。因为子类的static初始化可能会依赖于父类成员能否被正确初始化。
至此,必要的类都已经加载完毕,对象就可以被创建了。首先,对象中所有的基本类型都会被设为默认值,对象引用为设为null,然后父类的构造器就会被调用。在父类构造器完成之后,子类实例变量按其次序被初始化。最后,子类构造器的其余部分被执行。
更多相关内容 -
接口和实现必须使用相同的类加载器_JVM学习(五)类加载器
2020-11-21 08:33:48类加载考点类的加载的生命周期有哪些?加载过程具体流程?加载触发的时机?加载过程的Class对象是否唯一还是随创建方法数量而定?连接过程具体流程?连接触发的时机?类的静态变量赋值经历的过程?类的初始化具体...类加载
考点
- 类的加载的生命周期有哪些?
- 加载过程具体流程?加载触发的时机?
- 加载过程的Class对象是否唯一还是随创建方法数量而定?
- 连接过程具体流程?连接触发的时机?
- 类的静态变量赋值经历的过程?
- 类的初始化具体流程?初始化触发的时机(何为主动加载/被动加载)?
- <clinit>方法怎样才会导致静态变量值覆盖?
- 为什么单例安全模式会使用静态单例方法?
- 什么是双亲委派机制?
- SPI是怎么打破双亲委派机制的?
- 何时进行类的加载?
类加载的七个周期
针对类的加载时机,jvm没有强制的约束,jvm堆类的初始化由明确且严格的规定,当然执行类的初始化说明类的加载和连接需要在此前开始。
七个周期只是保证了开始时间的固定顺序,并没有保证结束时间,也没有保证各阶段是独立进行的。
加载
加载过程需要完成以下三件事情
- 通过一个类的全限定名来获取其定义的二进制字节流。
- 将这个字节流转化为方法区的运行时数据结构。(要通过验证阶段才可以)
- 在Java堆中生成一个代表这个类的java.lang.Class对象(唯一),作为对方法区中这些数据的访问入口。
它没有指明二进制字节流必须要从class文件中获取,因此可以从zip包,网络,运行时动态生成,数据库中读取都可以,这也是加载过程最大的特点
连接
连接过程具体流程
验证-》准备-》解析
验证
保证class字节流中包含的信息符合jvm规范要求,包括(平时点运行就报错就是验证通不过):
- 文件格式验证:保证能正常解析和储存方法区,只有通过该验证,才能进入方法区存储,后面的三个验证都是基于方法区上的结构进行。
- 元数据验证:类的元数据信息(语法级别)验证,如父类是否可以继承,方法重载是否满足
- 字节码验证:方法体(语法级别)验证,如操作数类型是否保持一致,跳转指令是否不会跳转到方法体以外的字节码指令上
- 符号引用验证:对类自身以外的信息进行匹配性校验,即检查符号引用的类或方法是否能找到,以及符号引用的类或方法可访问性是否可被当前类访问
验证阶段是一个非常重要,却不是必须执行的阶段。如果你能保证自己的代码不会被篡改且正确,可以在生产环境使用-Xverify:none来关闭检查。因为验证工作在类加载过程占了相当大的比重
准备
为类的静态变量(即被static修饰的变量)在方法区分配内存,并赋默认初值(0值或null值)。
如static int a = 100;(不会执行静态代码块,因为不会执行语句)
如果存在final的类变量,则会在此时赋值,不会需要类的初始化的时候再赋值
解析
它是将类的二进制数据中的符号引用换为直接引用。
在解析阶段,虚拟机会把类或接口,字段,类方法,接口方法,方法类型,方法句柄,调用点限定符这七类符号引用替换为具体的内存地址或偏移量,也就是直接引用(因为你写代码的时候并不清楚内存结构)
如果出现NoXXX找不到类找不到方法错误,就是这个阶段抛出的。
类的初始化
这时候,虚拟机才真正开始执行类中编写的java程序代码
- 类构造器<clinit>由所有类变量(静态变量)的赋值动作和静态代码块的语句合并产生,合并的顺序与代码的编写顺序一致。
- 静态变量的值先,静态语句块的值后,导致被覆盖
- 静态语句块值先,静态变量的值后,导致静态语句块非法前向引用
- jvm保证子类<clinit>执行前,父类<clinit>已执行(也就是主动引用中的一种)
- JVM保证该方法是线程安全,因此可以用来实现静态单例
初始化时机(也叫主动引用和被动引用)
- 遇到new,getstatic,putstatic,invokestati这四条字节码指令时
- 使用new关键字实例化对象时
- 读取或设置一个静态字段(非final)时
- 调用一个静态方法时
- 使用java.lang.reflect包的方法进行反射调用的时候
- 初始化类时,如果发现父类没有初始化则需要先触发父类的初始化
被动引用(不会触发<clinit>方法)
- final修饰的静态常量(连接过程中就会执行赋值并存入常量池中)。
- 通过数组定义来引用,Obj[] array = new Obj[10],不会触发Obj的初始化,数组类本身不通过类加载器创建,他是直接在内存中动态创建
- 当访问一个静态域时,只有真正声明这个域的类才会被初始化,如子类访问父类的静态变量,子类不会初始化,父类会初始化
使用
使用既是所需要的对象开始被调用。
卸载
对象被jvm回收,JVM中的class满足一下3个条件才能被GC回收
- 该类所有实例已经被GC
- 加载该类的ClassLoader实例被GC
- 该类的java.lang.class对象没有被引用
GC的时机是不可控的,同样对于class的卸载也是不可控的
参考来源
类加载器ClassLoader
- 任务:根据一个类的全限定名来读取此类的二进制字节流到JVM中,是类加载的第一阶段。
- 特点:每个类加载器由自己特有的加载路径,因此不同位置的包需要不同的加载器
类架构体系:三层类加载器,双亲委派机制
三层类加载器时系统内置的,无法进行new,只能通过getClassLoader获取
ClassLoader
父委托机制/双亲委托机制
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。(先↑再↓的过程)
protected
SPI打破双亲机制
首先要理解双亲机制为什么需要被打破?
双亲委派机制很好的维护了基础类型一致性问题,保证像String,Object这样的类不会被第三方恶意篡改。然而没有完美的事情,双亲委派模型,父加载器是拿不到通过子加载器加载的类的,也就意味着父类加载器加载的接口,没有办法拿到子类加载器加载的实现类。(当接口是Bootstrap ClassLoader,子类是Application ClassLoader)
举一个例子就是
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。
因此java设计团队提供了一个线程上下文类加载器,该类加载器可以通过java.lang.Thread.setContextClassLoader()设置,默认是Application ClassLoader,父类加载器会先通过ContextClassLoader(),如果没有实现类,再执行自己的加载。该过程就叫做打破双亲委托机制。
委派给默认系统类加载器的时候系统类加载器会不会重新向上委托?
会,但是此时它需要加载的用户自定义类,父加载器加载不了的
代码层面理解
打破双亲委托机制在父类加载器使用下面函数进行类加载
public
public
- 应用程序调用ServiceLoader.load方法 ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,包括:
- loader(ClassLoader类型,类加载器)
- acc(AccessControlContext类型,访问控制器)
- providers(LinkedHashMap<String,S>类型,用于缓存加载成功的类)
- lookupIterator(实现迭代器功能)
- 应用程序通过迭代器接口获取对象实例 ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。 如果没有缓存,执行类的装载,实现如下:
- (1)读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件,具体加载配置的实现代码如下:
try
- (2) 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
- (3) 把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象。
相当于IOC容器,通过配置进行加载;
相当于自定义了一个类加载器,在双亲委派机制之上通过特定的规则去加载;
SPI具体举例
-
JavaSE之类加载器
2022-01-27 17:13:51创建类的实例(对象);调用类的类方法(静态方法);访问类或者接口的类变量(静态变量),或者为该类变量赋值 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象;初始化某个类的子类;直接使用java....
类加载器
概念
Java文件被编译成 .class文件(字节码文件),类加载器就是负责将.class文件(存储的物理文件)加载在到内存中
类加载时机
-
创建类的实例(对象)
-
调用类的类方法(静态方法)
-
访问类或者接口的类变量(静态变量),或者为该类变量赋值
-
使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
-
初始化某个类的子类
-
直接使用java.exe命令来运行某个主类
用到就加载,不用不加载
类加载的过程
(1)加载
通过一个类的全限定名来获取此类的二进制字节流
将这个字节流所代表的静态存储结构转换为运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象(任何类被使用时,系统都会未知创建一个java.lang.Class对象)
简单的说就是:
-
通过包名 + 类名,获取这个类,准备用流进行传输
-
在这个类加载到内存中
-
加载完毕创建一个class对象
(2)链接
验证
确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全
(文件中的信息是否符合虚拟机规范有没有安全隐患)
准备
负责为类的类变量(被static修饰的变量)分配内存,并设置默认初始化值(null)
(初始化静态变量)
解析
将类的二进制数据流中的符号引用替换为直接引用
(本类中如果用到了其他类,此时就需要找到对应的类)
如下案例,有一个Student对象,要将其加载到内存,地址值假设为0x0011,String是一个引用数据类型
所以在之前的加载过程中,会将String标记成符号,而在解析过程中,会对该符号进行解析,假设String的Class类对象是在内存的0x0022的位置
就会把之前在该位置的符号引用变成直接引用,即将&&&变成0x0022。
(3)初始化
根据程序员通过程序制定的主观计划去初始化类变量和其他资源
(静态变量赋值以及初始化其他资源)
之前定义的静态变量在准备阶段的值为null,附上程序设定的初始值为 "华南理工大学"
static String school = "华南理工大学";
类加载器的类型
-
Bootstrap class loader:虚拟机的内置类加载器,底层是用C++实现的,没有父加载器
-
Platform class loader:平台类加载器,负责加载JDK中一些特殊的模块
-
System class loader:系统类加载器,负责加载用户类路径上所指定的类库
System的父加载器为Platform,Platform的父加载器为Bootstrap
即:启动类加载器是平台类加载器的父加载器;平台类加载器是系统类加载器的父加载器;系统类加载器是的自定义类加载器的父加载器。
注意这里的父子关系并不是代码中的extends的关系,而是逻辑上的父子。
注意Java9之前的版本关于类加载器的说法是有不同的!!!这里上述是讲Java9的类加载器
Java9类加载器
https://zhuanlan.zhihu.com/p/97650770
JDK8与JDK9在类加载器上的区别
Java8的类加载机制如下
- 引导类加载器(boostrap class loader):虚拟机内置的类加载器,通常以
null
表示,从引导类路径加载。 - 扩展类加载器(extension class loader):从扩展目录加载类。它是JDK 1.2中引入的扩展机制的产物。它的父类加载器为引导类加载器。
- 应用类加载器(application class loader):从应用的CLASSPATH中加载类。它的父类加载器为扩展类加载器。
Java9的类加载机制如下
Java 9仍然保留了三层类加载器结构,不过为了支持模块系统,对它们做了一些调整。扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform class loader)。可以通过
ClassLoader
的新方法getPlatformClassLoader()
来获取。Java 9中,平台类加载器和系统类加载器不再是
URLClassLoader
类的对象。这会影响一个常见的用来在运行时向系统类加载器的查找路径中添加条目的hack。在下面的代码中,该hack把系统类加载器转型成URLClassLoader
并调用其addURL()
方法。该hack在Java 9无法工作,因为转型为URLClassLoader
会失败。双亲委派模型
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
Coding演示
Java8版本的
public class demo { public static void main(String[] args) { ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); ClassLoader classLoader1 = systemClassLoader.getParent(); ClassLoader classLoader2 = classLoader1.getParent(); System.out.println(systemClassLoader); System.out.println(classLoader1); System.out.println(classLoader2); } }
而Java9版本的
详细的区别见下
Java9类加载器
https://zhuanlan.zhihu.com/p/97650770
ClassLoader 中的两个方法
方法名 说明 public static ClassLoader getSystemClassLoader() 获取系统类加载器 public InputStream getResourceAsStream(String name) 加载某一个资源文件 Coding
public class Demo{ public static void main(String[] args) throws IOException { //static ClassLoader getSystemClassLoader() 获取系统类加载器 //InputStream getResourceAsStream(String name) 加载某一个资源文件 //获取系统类加载器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); //利用加载器去加载一个指定的文件 //参数:文件的路径(放在src的根目录下,默认去那里加载) //返回值:字节流。 InputStream is = systemClassLoader.getResourceAsStream("prop.properties"); Properties prop = new Properties(); prop.load(is); System.out.println(prop); is.close(); } }
-
-
Java类加载器和双亲委派模型
2019-12-08 21:55:52Java类加载器类的生命周期类型的加载类加载器的分类、职责以及层级结构类加载器加载的目录 类的生命周期 加载:查找并加载类的二进制数据.class, 通过一个类的全限定名来获取此类的二进制字节流。 将这个字节...Java类加载器和双亲委派模型
这里讲了类加载器和双亲委派模型。
讲到的很多东西都是很底层的东西,一些源码的实现。
如果只是想简单了解双亲委派模型的,可以看看类加载器的分类和层级关系,然后直接去看最后的总结。最后的图把整个流程都描述了。类的生命周期
- 加载:查找并加载类的二进制数据.class,
- 通过一个类的全限定名来获取此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在java方法区中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口。
- 连接
- 验证:确保加载的类的正确性,保证Class文件的字节流中包含的信息符合当前虚拟机的要求,并且确定不会危害虚拟机自身的安全。
- 准备:为类的静态变量分配内存,并将其初始化为默认值。
正式为类变量分配内存并设置类变量初始值(各数据类型的默认值)的阶段,这些内存将在方法区中进行分配。
但是如果类字段的字段属性表中存在常量属性,那在准备阶段变量值就会初始化为常量属性指定的值。 - 解析:把类中的符号引用转为直接引用,在虚拟机将常量池内的符号引用替换为直接引用的过程。
- 初始化:为类的静态变量赋予正确的初始值,执行静态代码块。
类型的加载
类型就是指我们Java源代码通过编译后的class文件,类型的来源有哪些?
- 本地磁盘
- 网络下载的.class文件
- war/jar包下载的.class文件
- 从专门的数据库中读取的.class文件(少见)
- 将Java源文件动态编译成class文件:
- 典型的就是动态代理,通过运行期生成class文件。
- 我们的jsp会被转换成servlet,而我们的servlet是一个java文件,会被编译成class文件。
class文件经过类加载器加载到JVM内存中的方法区的数据区。
方法区:1.7叫方法区,1.8叫元空间。
数据区:用来存储class数据的结构,class文件的数据结构都存储在数据区。
在元空间中生成的一个个class对象都有一个引用指向数据区的数据结构,class对象可以访问到数据区的数据结构。
在元空间中生成的一个个class对象,比如AClass对象,在1.7中存放在方法区,1.8中没有说明具体放在哪(有的虚拟机可能存放在堆内存),根据不同JVM的规范来实现。
在HotSpot虚拟机中,jdk1.8中Class对象还是存放在方法区。类加载器的分类、职责以及层级结构
- 系统级别:
- 启动类加载器 --> C++语言实现
- 扩展类加载器
- 系统类加载器(App类加载器)
扩展类加载器和App类加载器是通过Java来实现,有统一的父类(ClassLoader) - 用户级别的:用户自定义类加载器(继承ClassLoader)。
这里的层级关系并不是说下层的加载器去继承上层的加载器,而是当前类加载器中的parent引用指向父加载器。
类加载器加载的目录
每个加载器的职责不一样,加载的目录也不一样。
- 启动类加载器(BootStrapClassLoader)
是java应用体系中最顶层的类加载器,负责加载JVM需要的一些类库。是一个由C++编写的类加载器。
系统变量"sun.boot.class.path"表示启动类加载器的加载目录:System.getProperty(“sun.boot.class.path”)。
我们可以把class文件放到D:\java\jdk1.8\jre\classes 目录下,这样我们自己的类也能通过启动类加载器来进行加载,(本地的D:\java\jdk1.8\jre目录下载jdk时就已经存在,classes文件夹需要我们自己创建)。
- 扩展类加载器(ExtClassLoader)
系统变量"java.ext.dirs"表示扩展类加载器的加载目录:System.getProperty(“java.ext.dirs”)。
- App类加载器
系统变量"java.class.path"表示App类加载器的加载目录:System.getProperty(“java.class.path”)。
App类加载器加载classpath目录下的class文件,也就是平常我们自己写的代码/引入的jar都是由App类加载器来加载。
Launcher类
sun.misc.Launcher类位于rt.jar包中,由启动类加载器来加载,sun.misc.Launcher类是java程序的的入口,在启动java应用的时候会首先创建Launcher类,创建Launcher类的时候会准备应用程序运行中需要的类加载器。
Laucher类是由JVM创建的,它类加载器是系统类加载器(BootStrapClassLoader)。
查看Laucher类的源码会发现,ExtClassLoader和AppClassLoader是Launcher类的静态内部类。
在Launcher类的构造方法中,会获取AppClassLoader和ExtClassLoader,然后把ExtClassLoader指定为AppClassLoader的父加载器(AppClassLoader类中的parent引用指向ExtClassLoader,)。在Launcher类的构造方法中,还会把AppClassLoader设置为当前线程上下文的ClassLoader,Thread.currentThread().setContextClassLoader(this.loader);
然后可以在线程中随时获取线程上下文的类加载器:Thread.currentThread().getContextClassLoader();ClassLoader和loadClass方法
AppClassLoader和ExtClassLoader继承了URLClassLoader,URLClassLoader继承了SecureClassLoader,SecureClassLoader继承了ClassLoader,通过调用ClassLoader类中的loadClass方法来加载类。
当getClassLoader()为null就表示加载器是BootStrapClassLoader了。
loadClass方法
在loadClass中,会先去得到当前类加载器的父加载器,如果parent不为null,则交给父加载器来进行类加载,parent.loadClass()。如果父类加载器为null,则说明父类加载器为启动类加载器,则使用启动类加载器来进行类加载,c = findBootstrapClassOrNull(name);
if(c == null) 则说明这个类的.class文件不在启动类加载器加载的目录下,启动类加载器不能加载。则会让子加载器自己去加载。下面是ClassLoader类的loadClass源码:
c = findClass(name); 真正的去加载class,调用的是其直接父类的findClass方法,也就是URLClassLoader类的findClass方法。
findClass方法中就是去该加载器的加载路径下查找有没有该类的class文件,方法中有这么一行代码:Resource res = ucp.getResource(Path, false);比如我们自己写了一个Teacher类,在ExtClassLoader的加载路径下肯定是找不到Teacher.class文件的,Resource
为null。父加载器ExtClassLoader加载不了,则会交给AppClassLoader自己来加载,在classpath下能找到Teacher.class,所以AppClassLoader能加载成功。
什么时候会触发类加载器去加载类?
类的主动使用会触发类的初始化,初始化一定会有类的加载,类加载不一定会初始化。
类的主动使用:
- 调用类的静态字段
- 调用类的静态方法
- 执行main方法(执行A类中的main方法就是主动使用A类,会触发A类的初始化)
- new指定 :new一个类的对象
- Class.forname("") 反射,输入一个类的全限定名。
- 子类初始化一定会触发父类初始化
自定义类加载器
我们自定义的类加载的父类一般都是AppClassLoader,自定义类加载器时会指定该类加载器的加载路径。
ClassLoader的源码中的文档给出了自定义加载器的例子
自定义加载器需要去继承ClassLoader类,然后重写findClass方法。还需要再写一个loadClassData方法:给定一个class文件的全类名,把.class文件读取成一个byte[]。然后通过一个JVM底层的方法defineClass()把byte[]转成一个Class对象。defineClass方法:本地的native方法,c++实现。
package com.zlin.jvm.myClassLoader; import java.io.*; /** * 自定义的类加载器 */ public class MyClassLoader extends ClassLoader{ private final static String fileSuffixExt = ".class"; // 加载器的名称 private String classLoaderName; // 加载器的加载路径 private String loadPath; public void setLoadPath(String loadPath) { this.loadPath = loadPath; } public MyClassLoader(ClassLoader parent, String classLoaderName) { // 指定当前类加载器的父类加载器 super(parent); this.classLoaderName = classLoaderName; } public MyClassLoader(String classLoaderName) { // 不指定父类加载器,则默认使用AppClassLoader加载器作为父类加载器 super(); this.classLoaderName = classLoaderName; } public MyClassLoader(ClassLoader classLoader) { super(classLoader); } /** * 方法实现说明 * @param name: 类的二进制名称 */ private byte[] loadClassData(String name) { byte[] data = null; ByteArrayOutputStream byteArrayOutputStream = null; InputStream inputStream = null; try { name = name.replace(".", "\\"); String fileName = loadPath + name + fileSuffixExt; File file = new File(fileName); inputStream = new FileInputStream(file); byteArrayOutputStream = new ByteArrayOutputStream(); int ch; while (-1 != (ch = inputStream.read())) { byteArrayOutputStream.write(ch); } data = byteArrayOutputStream.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { if (byteArrayOutputStream != null) { byteArrayOutputStream.close(); } if (inputStream != null) { inputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } return data; } protected Class<?> findClass(String name) { byte[] data = loadClassData(name); System.out.println("MyClassLoader加载的类:" + name); return defineClass(name, data, 0, data.length); } }
E:\zlin\路径下存在Student.class, AppClassLoader的加载路径下也存在,根据双亲委托模型,会使用AppClassLoader来加载Student类。当把classpath下的Student.class文件删掉后, AppClassLoader的加载路径下找不到Student.class ,才会使用MyClassLoader来进行加载。
类加载器的命名空间
类加载器的命名空间:是由类加载器本身以及所有父加载器所加载出来的binary name(full class name) 组成。
- 在同一个命名空间里,不允许出现两个完全一样的binary name。
- 在不同的命名空间中,可以出现两个相同的binary name。
- 子加载器的命名空间中的binary name对应的类中可以访问父加载器命名空间中的binary name对应的类,反之不行。
子加载器加载的类能访问父加载器加载的类,父加载器加载的类不能访问子加载器加载的类。比如:我们自定义一个Person类(使用AppClassLoader加载),
在Person类中可以使用String/Object类(使用启动类加载器加载),但是在String/Object类中不能访问Person类。同一个Person.class文件被不同的类加载器加载,则我们的JVM内存中会生成多个Person的Class对象,而且这两个对应的Class对象是相互不可见的(通过Class对象反射创建的实例对象相互是不能兼容的,不能相互转型)。
下面看一个例子:
Student类package com.zlin.jvm.myClassLoader; public class Student { private Student student; public Student getStudent() { return student; } public void setStudent(Object student) { this.student = (Student) student; } }
package com.zlin.jvm.myClassLoader; import java.lang.reflect.Method; public class DifferentClassLoaderNameSpaceTest { public static void main(String[] args) throws Exception{ MyClassLoader myClassLoader1 = new MyClassLoader("myClassLoader1"); myClassLoader1.setLoadPath("E:\\zlin\\"); MyClassLoader myClassLoader2 = new MyClassLoader("myClassLoader2"); myClassLoader2.setLoadPath("E:\\zlin\\"); // 通过myClassLoader1加载Student Class<?> clazz1 = myClassLoader1.loadClass("com.zlin.jvm.myClassLoader.Student"); System.out.println("class1的类加载器" + clazz1.getClassLoader()); // 通过myClassLoader2加载Student Class<?> clazz2 = myClassLoader2.loadClass("com.zlin.jvm.myClassLoader.Student"); System.out.println("class2的类加载器" + clazz2.getClassLoader()); System.out.println("class1 == class2 :" + (clazz1 == clazz2)); // 当classpath目录下Student.class存在时,clazz1和clazz2都是AppClassLoader来进行加载 // 当把classpath目录下的Student.class删除后,clazz1使用myClassLoader1进行加载,class2使用myClassLoader2进行加载 // 两个类的命名空间不同 // java.lang.ClassCastException: com.zlin.jvm.myClassLoader.Student cannot be cast to com.zlin.jvm.myClassLoader.Student // 模拟问题 Object student1 = clazz1.newInstance(); Object student2 = clazz2.newInstance(); Method method = clazz1.getMethod("setStudent", Object.class); method.invoke(student1, student2); } }
当classpath目录下Student.class存在时,clazz1和clazz2都是AppClassLoader来进行加载,setStudent方法能正常执行。
当把classpath目录下的Student.class删除后,clazz1使用myClassLoader1进行加载,class2使用myClassLoader2进行加载,两个类的命名空间不同,执行setStudent抛出java.lang.ClassCastException: com.zlin.jvm.myClassLoader.Student cannot be cast to com.zlin.jvm.myClassLoader.Student
当你在生产环境中遇见这种问题, java.lang.ClassCastException: com.zlin.jvm.myClassLoader.Student cannot be cast to com.zlin.jvm.myClassLoader.Student,相同的两个类不能相互转换,则可能就是使用不同的类加载器加载到不同的命名空间中了。双亲委派模型的好处
双亲委派模型的好处:核心包下的类不能使用我们自定义的类加载器去加载。
如果没有双亲委派模型,比如我们能自定义类加载器去加载Object类,则JVM中会存在多个Object的Class对象且它们之间不可见,不能相互转型(这是非常危险的)。打破双亲委派模型
举一个例子:我们的规范类会使用到数据库厂商jar包中的类,比如DriverManager需要注册具体数据库厂商的驱动。
但是根据全盘委托模型,启动类加载器无法加载classpath目录下的jar包。
– 怎么解决?
JVM中规定了,在启动类加载器(DriverManager)中可以访问到当前线程上下文类加载器所加载的类,我们的线程上下文类加载器是AppClassLoader。
还记得上面提到的在Launcher类的构造方法中会把AppClassLoader设置为当前线程上下文的类加载器。这里会涉及到SPI(服务扩展机制)的一些东西,如果不懂SPI可能看不懂,有兴趣的小伙伴可以看看这篇博客SPI服务扩展机制
- Class.forName(“com.mysql.jdbc.Driver”):
只会注册一个驱动com.mysql.jdbc.Driver,在Driver类的静态代码块中: DriverManager.registerDriver(new Driver());来注册驱动。 - Connection connection = DriverManager.getConnection(“jdbc:mysql://localhost:3306/test”, “root”, “admin”);
DriverManager的构造方法中的loadInitialDrivers()会通过SPI服务把mysql-connector-java-8.0.18.jar下META-INF/services/java.sql.Driver文件中的所有驱动都注册(包含"com.mysql.jdbc.Driver")。
通过SPI服务扩展机制:Iterator遍历将mysql-connector-java-8.0.18.jar下META-INF/services/java.sql.Driver文件中的类 来进行Class.forName("");
查看源码会发现,在getConnection()中会去获取线程上下文的类加载器,然后能读取和使用获取线程上下文的类加载器加载的类。
当把当前线程上下文的ClassLoader改为ExtClassLoader后
- Class.forName(“com.mysql.jdbc.Driver”);任能注册一个驱动com.mysql.jdbc.Driver (反射 ==> 会触发类的初始化 ==> 执行静态代码块)
因为在Driver类的静态代码块中: DriverManager.registerDriver(new Driver()); - 但是Connection connection = DriverManager.getConnection(“jdbc:mysql://localhost:3306/test”, “root”, “admin”);
不能加载mysql-connector-java-8.0.18.jar下META-INF/services/java.sql.Driver文件中的类,因为ExtClassLoader的加载路径不包含classpath/第三方jar, classpath/引入的第三方jar属于AppClassLoader的加载路径。不能注册驱动。
package com.zlin.jvm.classLoadNameSpace; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class TestDemo { public static void main(String[] args) throws ClassNotFoundException, SQLException { Class.forName("com.mysql.jdbc.Driver"); // 只会注册一个驱动com.mysql.jdbc.Driver Thread.currentThread().setContextClassLoader(TestDemo.class.getClassLoader().getParent()); // 任然能注册Driver驱动 Class.forName("com.mysql.jdbc.Driver"); // 不能注册驱动 Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "admin"); } }
这里可能不是很好理解,最好自己能去看看源码,把各个类,执行流程打断点走一遍,就很好理解,印象也会很深。
这里总的来说就是:在启动类加载器中可以访问到当前线程上下文类加载器所加载的类。DriverManager由启动类加载器加载,“com.mysql.jdbc.Driver"mysql驱动的jar由AppClassLoader加载,Launcher类的构造方法中把AppClassLoader设置为当前线程上下文的类加载器,在getConnection方法中通过获取当前线程上下文的类加载器来使用"com.mysql.jdbc.Driver”。
当把当前线程上下文的类加载器修改为ExtClassLoader时,mysql驱动的jar的路径不在ExtClassLoader的加载目录,ExtClassLoader不会加载mysql驱动的jar,获取当前线程上下文的类加载器不能获取"com.mysql.jdbc.Driver"。所以不能加载驱动。总结
双亲委派模型也叫双亲委托模型/ 双亲委任模型。
双亲委派模型:类加载时,会由下向上依次判断是否加载过,加载过则直接返回,如果到顶层还没有加载。则启动类加载器会尝试加载,当class文件不在启动类加载器的加载目录下,则启动类加载器不能加载,再交给ExrClassLoader来加载,这样依次下去,如果到最底层也不能加载成功,则抛出异常ClassNotFoundException。
下面的图很详细了
上面涉及到的代码地址:https://github.com/zhonglinliu123/MyBlogCode/tree/master/JVM/classLoader%E7%B1%BB%E5%8A%A0%E8%BD%BD -
探秘类加载器和类加载机制
2019-09-21 10:33:17在面向对象编程实践中,我们通过众多的类来组织一个复杂的系统,这些类之间相互关联、调用使他们的关系形成了一个复杂紧密的网络。当系统启动时,出于性能、资源...这就要靠类加载器来完成了。 什么是类加载器 类... -
JVM的艺术—类加载器篇(二)
2020-10-31 22:08:36定义类加载器:假设我们的某一个类是由ExtClassLoader加载的,那么ExtClassLoader称为该类的定义类加载器 初始化加载器:能够返回Class对象引用的都叫做该类的初始类加载器,比如类A是由我们的ExtClassLoader加载,... -
类加载和类加载器部分讲解
2020-09-10 15:22:35测试:使用两个不同类加载器加载同一个类文件 package classloader; import java.io.IOException; import java.io.InputStream; public class Test { public static void main(String[] args) throws Exception { ... -
类加载机制深入
2021-08-06 13:20:45Tomcat-正统的类加载器架构 对于一个web服务器如tomcat 需要实现如下功能: 1 不同web应用所使用的类库需要相互隔离,不能互相影响,比如webapp1使用spring3.1依赖库,webapp2使用了spring3.2的依赖库,那么这两... -
Java 类加载、调用构造器、执行方法的过程
2021-02-12 23:07:51所以这里可能会存在很多不够详细甚至错误的理解,准备翻一遍编程思想再来更新,欢迎评论交流当JVM执行到这么一句代码: new Person().setName("superzhao")它会做这么几件事类加载JVM会用类加载器加载xxx.Person这个... -
每周 10 道 Java 面试题 : 面向对象, 类加载器, JDBC, Spring
2021-03-13 00:21:30Java 类加载器有: 引导类加载器(bootstrap class loader):只加载 JVM 自身需要的类,包名为 java、javax、sun 等开头。 扩展类加载器(extensions class loader):加载 JAVA_HOME/lib/ext 目录下或者由系统... -
Java 之路 (八) -- 多态(向上转型、多态、绑定、构造器与多态、再再谈初始化和类的加载、协变返回类型、向...
2018-08-09 00:12:01允许不同类的对象对同一消息做出响应 同一个行为具有多个不同表现形式或形态的能力 只有在运行时才会知道引用变量所指向的具体实例对象 封装:通过合并特征和行为创建新的数据类型。 实现隐藏:通过将细节... -
自定义类加载器的使用实例
2015-01-02 00:19:49自定义的类加载器必须继承ClassLoader抽象类,并覆盖findClass方法。 测试的需要被加密使用的类: package com.franky.classloader; import java.util.Date; /** * @描述 加密的测试类,需要用自定义的类加载器... -
Java类加载过程
2019-05-23 18:46:533.1 启动类加载器(Bootstrap) 3.2 扩展类加载器(Extension) 3.3 系统加载器 3.4 自定义加载器 四、加载模式-双亲委派 4.1 什么是双亲委派模式 4.2 双亲委派优势 五、加载器主要方法 5... -
【Java虚拟机】《深入理解Java虚拟机》| 虚拟机类加载机制(二)
2019-03-05 22:19:23类加载器对判断类是否相等的影响 Java的类加载器机制 Java中类加载器的类型 类加载器的详细介绍 双亲委派模型 双亲委派模型的破坏者 | 线程上下文类加载器 Tomcat的类加载器机制 Tomcat是个web容器, 那么它要... -
JavaEnhance——类加载器
2015-05-17 13:55:49类加载器的定义所有Java类在使用的时候都必须通过类加载器加载到内存。Java虚拟机可以安装多个类加载器,系统默认的,有三个主要的类加载器:BootStrap, ExtClassLoader, AppClassLoader。它们分别负责加载特定位置... -
Java类加载、调用构造器、执行方法的过程
2021-03-14 15:12:55注:整理到这部分知识点时候发现有很多问题...JVM会用类加载器加载xxx.Person这个class文件 加载(class){ if(class有父类){ 加载(superclass); } 1.静态域申明,默认初始化为0或者0.0,false,null 2.按照申明顺序(从上 -
向上转型(thinking in Java 第七章)
2019-09-25 23:34:14组合和继承都允许在新的类中放置子对象,组合是显示地这样做,而继承则是隐式地做。 组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形。即,在新类中嵌入某个对象,让其实线所需要的功能,但新类的... -
Java类加载器——热替换
2017-12-19 14:21:42Java类加载器——热替换 -
类加载机制总结
2019-01-26 08:25:25而虚拟机把描述类的数据从class文件中加载到内存,并对数据进行校验、转换解析、初始化,最终形成可以被虚拟机直接使用的java类型,这就是类加载机制,他在运行期间完成。 JVM加载class文件到内存有两种方式: 隐式... -
jvm在什么时候加载类,反射获取类对象的机制是什么?
2021-02-27 10:57:35作者:KeenYeh来源:博客园虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终成为被虚拟机直接使用的Java对象,这就是JVM的类加载机制。Java天生的可动态扩展的语言特性就是... -
《深入理解Java虚拟机》第七章–虚拟机类加载机制-总结
2020-11-25 14:48:07)三、类加载过程3.1加载(Loading)3.2验证3.2.1文件格式验证3.2.2元数据验证3.2.3字节码验证3.2.4符号引用验证3.3准备3.4解析3.5初始化四、类加载器4.1类与类加载器4.2双亲委派机制4.2.1三种类加载器4.2.2类加载器... -
Java 之路 (七) -- 复用类(组合、继承、代理、向上转型、final、再谈初始化和类的加载、方法覆盖)
2018-08-08 14:39:24向上转型 final 关键字 1. 组合 1.1 组合的概念 在新的类中产生现有类的对象,由于新的类是由现有类的对象所组成,所以这种方法成为组合。 1.1.1 组合的写法及样例 只需要在当前类中声明另一个类的... -
00..java基础注解-反射-代理-加载器
2021-08-15 18:09:25文章目录网络注解反射代理静态代理动态代理ClassLoader类加载器双亲委派 网络 Internet地址 IP–>Internet Protocol网络协议 所有连接到Internet的设备都看做一个节点(node),计算机节点叫主机(host),每个节点... -
总结Java里类的加载顺序_面试高频考点之一
2021-05-20 22:41:49java里面类的加载顺序(秋招必考的知识点) -
java转型和加载
2016-07-31 08:34:06Java入门记(二):向上转型与向下转型 在对Java学习的过程中,对于转型这种操作比较迷茫,特总结出了此文。例子参考了《Java编程思想》。 目录 几个同义词 向上转型与向下转型 例一:向上转型,调用指定的... -
JVM学习(一)类加载子系统
2020-06-16 10:26:05类加载器子系统 idea安装插件jclasslib Bytecode viewer,能方便查看字节码 什么是类的加载? 虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验转换解析和初始化最终形成可以被虚拟机直接使用的java类...