前言:要秋招了,复习一下应对秋招,纠结该先看啥,最后决定先学习《Java高并发编程详解》,此博客为看书所写的笔记,因为是笔记,所以会只记比较重要的东西,不适合初学者。
参考:
https://blog.csdn.net/Hellowenpan/article/details/101389330
目录
第一章 类的加载过程
ClassLoader的主要职责就是负责加载各种class文件到JVM中,ClassLoader是一个抽象的的class,给定一个class的二进制文件名,ClassLoader会尝试加载并且在JVM中生成构成这个类的各个数据结构,下面我们将要它的加载过程。
1.1 类的加载过程简介
类的加载过程一般分为三个比较大的阶段,分别是加载阶段、连接阶段和初始化阶段。
加载阶段:负责查找并读取类的二进制文件
连接阶段:
(1)验证,确保类文件的正确性。
(2)准备:为静态变量分配内存,并且为其初始化默认值。
(3)把类中的符号引用转换为直接引用。
初始化阶段:为类的静态变量赋予正确的初始值。
注:符号引用与直接引用区别;
现在我要在A类中引用到B类,符号引用就是我只要知道B类的全类名是什么就可以了,而不用知道B类在内存中的那个具体位置(有可能B类还没有被加载进内存呢)。直接引用就相当于是一个指针,能够直接或者间接的定位到内存中的B类的具体位置。将符号引用转换为直接引用简单来说就是:在A类中可以通过使用B类的全类名转换得到B类在内存中的具体位置。
1.2 类的主动使用和被动使用
一般来说,每个类或者接口被Java程序首次主动使用时才会对其进行初始化
六种主动使用场景:
- new关键字会导致类的初始化
- 访问类的静态变量、包括读取和更新会导致类的初始化
- 访问类的静态方法会导致类的初始化
- 对类进行反射操作会导致类的初始化
- 初始化子类会导致父类的初始化
- 启动类即执行main函数所在的类会导致该类的初始化
除了上述六种情况,其余的都是被动使用,不会导致类的加载和初始化:
- 构造某个类的数组并不会导致该类的初始化,此操作只不过是在堆内存开辟了一段连续的地址空间
- 引用类的静态常量不会导致类的初始化
1.3 类的加载过程详解
1.3.1 类的加载阶段
类的加载就是class文件中的二进制数据读取到内存之中,然后将该字节流所代表的静态存储结构转换为方法区中运行时的数据结构,并且在堆内存中生成一个该类的java.lang.Class对象,作为方法区数据结构的入口。
类加载的最终产物就是堆内存中的class对象,对于同一个ClassLoader来讲,不管这个类被加载了多少次,对应到堆内存中的class对象始终是一个。
虚拟机规范中指出类的加载是通过一个全限定类名(报名+类名)来获取二进制数据流,实际并没有限定必须通过某种方式去获得。
- 通过网络获取类的二进制流
- 读取zip文件获得类的二进制流
- 运行时生成class文件,并且动态加载
1.3.2 类的连接阶段
类的连接阶段分成验证、准备、解析三个小阶段。
验证:
确保字节流中包含的内容符合当前JVM的规范要求,并且不会出现危害JVM自身安全的代码。
1.验证文件格式
验证文件格式,确定文件是不是class文件
主次版本号
常量池中的常量是否存在不支持类型
指向常量的引用中是否指到了不存在的常量或者该常量的类型不被支持
2.元数据的验证,其实就是对class的字节流进行语义分析的过程,整个语义分析就是为了确保class字节流符合JVM规范的要求。
3.字节码验证,主要是验证程序的控制流程,循环、分支等,如确保程序计数器中的指令不会跳转到不合法的字节码指令中去。
4.符号引用验证,在类加载的过程中,有些阶段是交叉进行的,比如在加载阶段尚未结束之前,连接阶段可能已经开始工作了,主要作用是验证符号引用转换为直接引用时的合法性。
准备:
当一个class字节流通过了所有的验证过程之后,就开始为该对象的类变量,也就是静态变量分配内存并且设置初始值了,类变量的内存会被分配到方法区中。
解析:
在常量池中寻找类、接口、字段和方法的符号引用,并将这些符号引用替换成直接引用的过程,具体包括类接口解析、字段解析、类方法解析、接口方法解析。
1.3.3类的初始化阶段
类的初始化阶段是整个类加载过程中的最后一个阶段,在初始化阶段做的最主要的一件事情就是执行<clinit>()方法的过程,在<clinit>()方法中所有的类变量都会被赋予正确的值,也就是在程序编写时候指定的值。<clinit>()方法不需要调用构造函数,虚拟机会保证<clinit>()方法在构造函数前先执行。
注:静态语句块只能对后面的变量进行赋值,但是不能对其访问。
1.4 例题
public class Singleton {
private static Singleton instance = new Singleton();
private static int x=0;
private static int y;
private Singleton(){
x++;
y++;
}
public static Singleton getInstance()
{
return instance;
}
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.x);
System.out.println(singleton.y);
}
}
输出值为:
0
1
解析:
在准备阶段中,每一个类变量都被赋予了相应的初始值:
instance=null,x=0,y=0
然后在初始化阶段,首先进入构造函数中,instance=Singleton@2f99bd52,x=1,y=1
然后,为x,y初始化,x赋值为0,y由于没给定值,所以构造函数中计算所得的值就是所谓的正确赋值。
所以最后是
0
1
第二章 JVM类加载器
类加载器就是负责类的加载职责,对于任意一个class,都需要加载它的类加载器和这个类本身确立其在JVM中的唯一性。
2.1 JVM内置三大类加载器
2.1.1 根类加载器介绍
根类加载器是最顶层的加载器,由C++编写,主要负责虚拟机核心类库的加载,比如整个java.lang包都是由根加载器所加载的。
2.1.2 扩展类加载器
扩展类加载器的父加载器是根加载器,它主要用于JAVA_HOME下的jre/lb/ext子目录里面的文件,扩展类加载器是由纯Java语言实现的,它的完整类名是sun.misc.Launcher$ExtClassLoader
2.1.3 系统类加载器
系统加载器是一种常见的类加载器,其负责加载classpath下的类库资源,我们进行项目开发时引入的第三方jar包,同时它也是自定义类加载器的默认父加载器,系统类加载器的加载路径一般通过-classpath或者-cp指定,同样也可以通过系统属性java.class.path获取。
2.2 自定义类加载器
所有自定义类加载器都是ClassLoader的直接子类或者间接子类,ClassLoader是一个抽象类,但其中并没有抽象方法,但务必实现findClass方法,否则会抛出
2.2.1 自定义类加载器
核心思想就是通过读取类的编译过后的class二进制流,然后调用ClassLoader的defineClass方法,来加载一个class,这里定义class位置是将包名的中的.换成了/,值得注意是仅仅defineClass的话是不会初始化类的,而仅仅是加载。
补:这里本人思考了一个问题,类加载器是怎么通过类的全包名定位到要加载的类呢,按照上面的把全包名中的.换成/的方法,那加载的时候理应考虑相对路径啥的,但是这我看了一些代码发现并没有,直接把全类名传进去就行了,所以这里本人有个推测,当程序运行时,所有的class都会被放在一个地方,这个地方遵循包的路径配置,然后到时候需要加载哪个class直接根据包的路径去加载就好了,在内存中,查找类都会使用报名.类的全类名(内部、匿名可以的全类名可以自行百度)。
2.2.2 双亲委托机制详细介绍
具体机制:
从当前类已加载的类缓存中根据类的全路径查询是否已存在该类,如果存在直接返回。
如果没加载该类,则调用父加载器进行加载。
如果当前类不存在父加载器,则直接调用根加载器进行加载。
如果当前类的所有父加载器都没有成功加载class,则尝试调用当前类加载器对其进行加载。
如果最后类被加载,则做一些性能数据的统计。
如果我们想让自定义加载器加载某类,而不是使用内置的某类,我们可以:
设置我们的自定义加载器的父类是扩展加载器,扩展加载器是找不到我们的类的,然后自然会使用我们的自定义加载器。
设置我们自定义加载器的没有父类,这样会直接调用根,然后找不到自然会使用我们的自定义加载器。
2.2.3 破坏双亲委托机制
上面提到的打破双亲委托机制,都是采用操作父加载器实现的,本质上的加载还是双亲委托,如果想要绕过双亲委托,我们可以通过重写loadClass方法来打破双亲委托机制
- 首先执行的loadClass方法需要根据类的全路径名称进行加锁,确保每个类在多线程的情况下只能被加载一次。
- 然后到已加载类的缓存中查看该类是否已被加载,如果已加载直接返回
- 若缓存中没被已加载的类,则需要对其进行加载,如果类的全路径以java和javax开头,则委托给系统类加载器对其进行加载。
- 如果不是以java和javax开头,则尝试用我们自定义的类加载器进行加载。
- 如果依旧没完成加载,则抛出异常
2.2.4类加载命名空间、运行时包、类的卸载
类加载命名空间:
每一个类加载器实例都有各自的命名空间,命名空间是由该加载器以及其所有父加载器所构成的,因此在每个类加载器中同一个class都是独一无二的,但是使用不同的类加载器,或者同一个类加载器的不同实例,去加载同一个class,则会在堆内存中产生多个class对象。
运行时包:
类的全限定名称:包名+类名
运行时包:类加载器的命名空间+类的全限定的名称
JVM规定了在不同运行时包下的类彼此之间是不可以进行访问的,那为什么我们在开发程序中可以访问呢?如我们自定义类可以访问java.lang包下的类。因为当类C被类加载器CL加载之后,那么CL成为C的初始加载器,JVM为每一个类加载器维护了一个列表,该列表中记录了将该类加载哎作为初始类加载器的所有class,JVM使用这些列表判断类是否已经被加载过了,是否要首次加载。
根据JVM规定,在类的加载过程中,所有参与的类加载器,即使没有亲自加载过该类,也都会被标识为该类的初始类加载器,比如java.lang.String首先经过了我们的自定义加载器BrokerDelegateClassLoader,然后依次又经过了系统加载器、扩展类加载器、根类加载器,这些类加载器都是java.lang.String的初始类加载器,JVM会为每一个类加载器维护的列表中添加该class类型,所以根据该维护表就能访问对应的class对象,所以就能访问了。
类的卸载:
在JVM启动时JVM会加载很多的类,在运行期间也会加载很多的类,我们都知道某个对象在堆内存中如果没有其它地方引用则会在垃圾回收器线程进程GC的时候被回收,他们类在堆内存中的Class对象以及Class在方法区中的数据结构何时会被回收呢?
JVM规定只有在满足下面三个条件的时候才会被GC回收,也就是类被卸载:
- 该类所有的实例都已经被GC
- 加载该类的ClassLoader被回收
- 该类的class实例没有在其它地方被引用
第三章 线程上下文加载器
线程上下文方法是从JDK1.2开始引入的,getContextClassLoader()和setContextClassLoader(ClassLoader c1) 分别用于获取和设置当前线程的上下文类加载器,如何当前线程没有设置上下文类加载器,那么它将和父线程保持相同的类加载器。
3.1 为什么需要线程上下文类加载器
这与JVM类加载器双亲委托机制自身的缺陷有关,JDK核心类库提供了很多SPI,常见的SPI包包括JDBC、JCE、JNDI等,JDK只规定了这些接口之间的逻辑,但不提供具体的实现,具体的实现需要第三方厂商类提供。
作为Java程序员或多或少地都写过JDBC的程序,在编写JDBC程序时几乎百分之百的都在与java.sql包下的类打交道。不管数据库类型如何切换,应用程序只要替换JDBC的驱动jar包,以及数据库的驱动名即可。
这样的好处是JDBC提供了高度抽象,应用程序只需要面向接口编程即可,不用关心各大数据库厂商的具体实现,但问题在于java.lang.sql中的所有接口都由JDK提供,加载这些接口的类加载器是根加载器,第三方厂商提供的类库驱动则是由系统类加载器加载的,比如Connections、Statement、RowSet等都由根加载器加载,第三方驱动包中的实现不会被加载。
上面的内容本人能看懂文字意思,但是不是很理解为什么不行,比如本人提个下面的猜想,感觉也可以啊,从网上找了找也没找到答案,已经在向别人问了。
如java.sql中有Connection,第三方jar包实现了Connection,那么在没有线程上下文类加载器的情况下,加载第三方jar包中的Connection时顺序是什么样的呢?
我这样的猜想为什么不行呢,加载第三方Connection,因为第三方Connection实现了java.sql中有Connection,根据加载父类要先加载子类的原则,要先加载java.sql中有Connection,然后j因为ava.sql中有Connection已由由根加载器加载,所以两个类都加载完毕然后直接用就行了啊。
3.2 数据库驱动的初始化源码分析
简略写一下了,不是特别理解
过程发生在DriverManager类里的getConnection方法中
(1)首先获取到了线程的上下文类加载器,该类就是调用Class.forName(“xx”)所在线程的线程上下文类加载器,通常是系统加载器。
(2)然后在遍历已在DriverManager中注册的驱动类,验证该数据库驱动是否可以被指定的类加载器加载。
(3)然后调用Class.forName,使线程上下文类加载器进行数据库驱动的加载以及初始化。