首先举个例子
final static变量会在jvm启动的时候编译好
final 实例变量会在获取实例的时候被初始化好
class Grandpa {
static {
System.out.println("爷爷在静态代码块");
}
}
class Father extends Grandpa {
static {
System.out.println("爸爸在静态代码块");
}
public static int factor = 25;
public Father() {
System.out.println("我是爸爸~");
}
}
class Son extends Father {
public static int factor2 = 25;
static {
System.out.println("儿子在静态代码块");
}
public Son() {
System.out.println("我是儿子~");
}
}
public class InitializationDemo {
public static void main(String[] args) {
System.out.println("爸爸的岁数:" + Son.factor); //入口
}
}
Son.factory 首先会调用父类的类构造器(<clinit>),执行静态代码快和静态方法,但是因为调用的是父类的属性factor,所以Son自己不会调用类构造器
所以这边顺序是爷爷在静态代码块,爸爸在静态代码块,factor (执行了这个)
如果我们换成System.out.println("爸爸的岁数:" + Son.factor2); ,执行结果就是,爷爷在静态代码块,爸爸在静态代码块,factor (执行了这个),factor 2,儿子在静态代码块
类加载的时机
- 类的生命周转:加载,验证,准备,解析,初始化,使用和卸载。其中验证,准备和解析可以成为连接。
- 加载,验证,准备,初始化和卸载他们的顺序是固定的,即初始化必须在卸载后面。
- 解析有可能在初始化之后
- 对于什么时候加载类,jvm并没有规定,但是jvm规定了初始化,而初始化则必定代表已经加载。
初始化的四种情况
-
1.遇到new,获取或者设置静态属性(final属性除外),或者调用静态方法时候如果这个类没有进行过初始化,则触发器初始化。
-
2.是用来reflect包的方法对类进行反射调用 也会触发初始化
-
3.如果一类需要初始化,但是其父类没有初始化,则先初始化其父类
-
4.虚拟机会初始化执行main方法的主类
-
注意如果只是定义了某个类的数组也不会初始化这个类
-
接口是无法定义静态代码块的,但是jvm还是会为接口生成类构造器,用于执行静态变量
-
接口初始化的时候并不会初始化父类接口
类加载的过程--加载,验证,准备,解析和初始化
加载
- 通过类的全限定名来获取定义此类的二进制字节流(即把class文件转化为二进制流)
- 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构(把类的相关信息放入方法区)
- 在堆中生成一个class对象,作为方法区这些数据的访问入口
验证
- 确保class文件的字节流包含的信息符合虚拟机的要求,且不会危害虚拟机自身的安全
- 主要检验文件格式,元数据,字节码验证和符号引用验证
- 文件格式:验证是否符合class文件格式规范,以及当前的jvm是否可以处理这个版本的jvm
- 元数据验证:类是否有父类(除了object),类的父类是否寄出了不允许被寄出的类,是否实现了抽象类的方法等。主要是java语义的验证
- 字节码验证:保证操作数栈的数据类型与指令代码序列能配合工作(比如放了一个int数据在操作数栈,但是却按照ling类型来加载到本地变量表),保证跳转指令不会跳转到方法体以外的字节码指令
- 符号引用验证:即将符号引用转换为直接引用的时候发生的验证,验证符号引用字符串描述的全限定名是否能找到对应的类,在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段,符号引用的类和字段以及方法的访问性是否可悲当前类访问。
准备
- 为类变量分配内存并设置类变量的初始值(该初始值是默认初始值而不是我们程序写的值比如static int a=3 那么a被赋值为0 只有该类初始化的时候 a被赋值3)
- final值会在编译阶段直接变为constantValue,在准备阶段直接被赋值为程序设置的值
解析
- 就是将常量池的符号引用替换为直接引用的过程。
- 符号引用是以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,只要可以定位到目标即可,所引用的目标并不一定已经架子啊到内存中
- 直接引用:是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄
- 虚拟机只是规定了13个字节码执行前符号引用需要解析成对应的直接引用,所以符号引用解析的工作可以在类加载的时候或者13个字节码执行前执行
- 13个字节码:anewarry,checkcast ,getfield,getstatic,instanceof,inokeinterface,invokespecial,invokestatic,invokevirtual,multianewarry,new,putfield和putstatic
- 虚拟机可能会对符号引用的第一次解析结果进行缓存,即在运行常量池记录直接引用,把常量标识位已解析,所以如果第一次解析失败了后续的同样解析可能会发挥同样的异常
初始化
- 是执行类构造器的<clinit>方法
- 如果类中没有静态变量和静态代码块
- 该方法执行会加锁,避免多线程同时初始化该类
类加载器
- 确保类的唯一性就是classloader+类的全限定名
整个loadclass 就是按照双亲委派机制干活的
resolve=true的时候就是会去初始化类,而我们loadclass 不允许初始化类
jvm规定了四种初始化类的情况
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
检测jvm中这个classloader和其父类中的命名空间是否已经包含该名称的class
Class<?> c = findLoadedClass(name);
如果没有开始按照双亲委派机制去加载类
if (c == null) {
long t0 = System.nanoTime();
try {
首先查看父类是否存在,如果存在就调用父类的loadClass
if (parent != null) {
c = parent.loadClass(name, false);
} else {
否则有可能是顶级加载器bootStrap
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
如果还未加载到就尝试自己去加载类
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
更新下加载的耗时和次数等
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
这边至今不太理解,好像是jvm类加载的链接那一步,即验证,链接,解析
if (resolve) {
resolveClass(c);
}
return c;
}
}
三种类加载器
- 启动类加载器:bootstrap classloader,加载java_home/lib下面的目录或者通过jvm参数-Xboostrapclasspath指定加载路径,但是名称必须是rt.jar。且该classloader无法被引用
- 扩展类加载器:extension classloader 属于Launcher类中的ExtClassLoader,继承了URLClassLoader。加载java_home/lib/ext下面的目录或被系统参数java.ext.dirs指定的目录下所有jar包,不需要指定名称
- 应用程序类加载器:application classloader 属于Launcher类中的AppClassLoader,继承了URLClassLoader。可以通过classloader.getSystemClassloader获取该加载器,所以我们也称呼其为系统类加载器可以通过java.class.path的系統属性获取该目录下jar进行加载或者是加载我们classpath下面的类。如果我们没用在系统中定义过类加载器,那么这就是默认加载器。我们的线程上下文中的加载器也是这个
类的加载有个默认机制 如果我们在某个类使用了 Aclassloader去加载该类,那么在该类中涉及到的其他类加载也默认使用该类
破坏双亲委派加载模式
-
覆盖loadclass方法
-
SPI机制问题(类和接口放在rt.jar),各个厂商的实现放在classpath下面,这样当我们使用某个类似于SPI服务,当JDK规定的类放在rt.jar,我们采用bootstrap获取到了类,这个时候我们需要在这个类中获取各个厂商的实现但是因为其放在classpath下面,而此时bootstrap无法委派子类去加载,所以只能通过线程上下文的类加载器去加载类,从而得到厂商的class。主要是创建一个ServiceLoader,传递一个我们需要寻找的Class以及线程上下文其就会去META-INF/services/接口全限定的文件下面 得到实现类的的class。
jdbc就是利用serviceLoader各个实现类通过serviceloader加载(创建serviceloader 可以传递我们设置的classloader),当各个实现子类被加载的时候会触发他们的类构造器执行方法即把自身new出来放入drvicermanager中的一个copyonwrite的list中.
利用serviceloader的好处是我们可以指定类加载器,也可以不需要在classpath下面迭代寻找所有类,只需要去文件中找寻即可发现实现子类。
类 com.example.Outer引用了类com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。即加载Outer的加载器会按照双亲委派机制去加载Inner,这也就是为什么我们如果使用driverManager直接去加载我们的com.mysql.driver。会出现classnotfoundexception,所以采用线程上下文 -
osgi
为什么dubbo不采用jdk的spi
- jdk的spi会在一次实例化所有实现,可能会比较耗时,而且有些可能用不到的实现类也会实例化,浪费资源而且没有选择。也可以不在文件中配置具体的实现类
- 另外dubbo的spi增加了对扩展点IOC和AOP的支持,一个扩展点可以直接setter注入其他扩展点。这是jdk spi不支持的。
- dubbo中的spi 是一个文件中可以支持多个接口和实现 以key=value而jdk则是一个接口 一个文件 如果接口较多则需要多个文件
网友评论