阅读《Java高并发编程详解》后的笔记。
类加载过程
1、类加载的过程
- 类加载阶段
- 类连接阶段
(1)验证
文件格式,元数据验证、字节码验证、符号引用验证。
(2)准备
为类变量(静态变量)分配内存并设置初始值。
(3)解析
类接口解析、字段解析、类方法解析、接口方法解析。
- 类初始化阶段
如果某各类没有静态代码块、静态变量,那就不会生成<clinit>(),接口中只有变量的初始化操作才会生成<clinit>()。
执行<clinit>()方法,该方法包含了所有类变量的赋值动作和静态语句块的执行代码,所有的类变量被赋予正确的值,即编写程序指定的值。
保证顺序性,父类<clinit>()方法最先执行,父类的静态变量总是得到优先赋值。
以下代码中,静态语句块只能对后面的静态变量进行赋值,但不能对其进行访问。
触发类的初始化会调用<clinit>()方法,JVM保证了该方法在多线程的执行环境下的同步语义。若有多线程同时访问这个方法,只能有一个线程执行到静态代码块中的内容,并且静态代码块仅仅只会被执行一次:
public class TicketRunnable {
static{
try {
System.out.println("111111");
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String args[]){
//jdk1.8新特性
IntStream.range(0,5).forEach(i->new Thread(TicketRunnable::new));
}
}
看一段程序,分析:
连接阶段准备时:x=0,y=0,ticketRunnable=null
连接阶段初始化执行<clinit>(): x=0,y=0,ticketRunnable=new TicketRunnable();
执行类的构造方法:x=1,y=1
public class TicketRunnable {
//(1)
private static int x = 0;
private static int y;
private static TicketRunnable ticketRunnable= new TicketRunnable();//(2)
public TicketRunnable(){
x++;
y++;
}
public static TicketRunnable getInstance(){
return ticketRunnable;
}
public static void main(String args[]){
TicketRunnable ticketRunnable = TicketRunnable.getInstance();
System.out.println(ticketRunnable.x);
System.out.println(ticketRunnable.y);
}
}
当(1)和(2)交换位置后:
连接阶段准备时:ticketRunnable=null,x=0,y=0,
连接阶段初始化:赋予正确的初始值
执行构造函数: ticketRunnable=TicketRunnable@2cdf8d8a,x=1,y=1,
然后为x初始化,x没有显式赋值,x=0,为y初始化,由于没有给定初始值,构造函数中赋的值才是正确的,y=1
最后结果:ticketRunnable=TicketRunnable@2cdf8d8a,x=0,y=1。
2、类的主动使用和被动使用
6种主动使用类的场景:
- new
- 访问类静态变量
- 访问类静态方法
- 对某个类进行反射操作
- 初始化子类导致父类初始化
- 启动类,执行main函数所在的类导致该类初始化
除以上6种,其余叫被动调用,不会导致类加载和初始化:
- 构造某各类数组:Simple[] s = new Simple[10];
- 引用类的静态常量 public static final int MAX;
JVM类加载器
1、JVM内置三大类加载器
2、自定义加载器:是classLoader的直接子类或间接子类。
3、双亲委托机制
当一个类加载器背地哦啊用了LoadClass之后,它并不会直接将其加载,而是交给当前类加载器的父加载器直到最顶层的父加载器,然后依次向下进行加载。
破坏双亲委托机制:
应用:热部署 运行时进行某个模块的升级,不停止服务增加某个功能
- 绕过系统类加载器,将扩展类加载器作为父加载器;
- 构造指定其父加载器为null
4、类加载命名空间、运行时包、类的卸载
(1)每一个类加载器实例都有各自的命名空间,该命名空间是由该加载器及其所有父加载器构成的。
- 使用不同类加载器加载同一个class
- 同一个类加载器的不同实例,加载同一个class
这两种情况在对内存和方法区会产生多个class对象。
(2)JVM规定不同的运行时包下的类,彼此间不可进行访问。
初始类加载器:自定义SImple class 由 自定义classLoader --BrokerDelegateLoader加载,能访问不同的运行时包下的类,如String。原因:
(3)类的卸载
一个class被GC回收
- 该类的所有实例已被GC,如Simple class中所有实例被回收;
- 加载该类的classLoader的实例被GC;
- 该类的class实例没有在其他地方被调用。
线程上下文类加载器
JDK核心类库提供了很多SPI(Service Provider Interface),常见的SPI包括JDBC,JNDI等。JDK只规定了这些接口之间的逻辑关系,不提供具体实现。如JDBC,不管数据库怎么切换,应用程序只需要更换JDBC的驱动jar包以及数据库驱动名称。
这里应用程序只需要面向接口编程,但是java.lang.sql所有接口由JDK提供,加载这些接口的类加载器是根加载器,第三方厂商提供的类库驱动是由系统类加载器加载的,由于JVM类加载器的双亲委托机制,如Connections,Statement,Rowset等都由根加载器加载,第三方的JDBC驱动包中的具体实现不会被启动类加载器(根加载器)加载,于是JDK提供了线程上下文类加载器,这时启动类加载器(根加载器)委托子类加载器加载厂商提供的SPI具体实现。