类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行一系列处理,形成可以被虚拟机直接使用的Java类型,称为虚拟机的类加载机制。
1. 类的加载过程
JVM加载一个类共分为3个步骤:加载,链接,初始化。其中链接又分为3个步骤:验证,准备,解析。
- 加载
共分为3步:1)获取定义此类的二进制字节流;2)将字节流代表的静态存储结构转化为方法区的运行时数据结构;3)在堆中实例化一个这个类的Class对象,作为方法区这个类的入口。
其实加载阶段用一句话来说就是:把代码数据加载到内存中。
- 链接
(1)验证
主要是确认字节流包含的信息符合虚拟机的要求,不含有害信息危害虚拟机安全,主要有4个阶段:文件格式验证,元数据验证,字节码验证,符号引用验证。
(2)准备
为类中的静态变量赋初始值(零值),但如果是静态常量(即被final修饰),在准备阶段直接就它设置为真正的初始值了。
注:Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。
(3)解析
将常量池中的符号引用替换成直接其在内存中的直接引用。 - 初始化
到了初始化阶段,用户定义的 Java 程序代码才真正开始执行,即虚拟机执行类构造器 < clinit >() 方法的过程。 < clinit >()执行所有类变量的赋值和静态代码块的执行(按照源代码的顺序执行)。一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化:
(1)遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
(2)使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
(3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
(4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
(5)当使用 JDK1.7 动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic ,REF_putstatic ,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
https://blog.csdn.net/qq_22938671/article/details/89926724
https://mp.weixin.qq.com/s/YTa0h4FSjqvbKDuGYjHjHw
相关问题
- 极为重要:https://mp.weixin.qq.com/s/YTa0h4FSjqvbKDuGYjHjHw
- 补充,java对象创建时机:
(1)使⽤new关键字创建对象
(2)使⽤Class类的newInstance⽅法(反射机制)
(3)使⽤Constructor类的newInstance⽅法(反射机制)
(4)使⽤Clone⽅法创建对象
(5)使⽤(反)序列化机制创建对象
2.类加载器与双亲委派模型
2.1.类加载器
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。
- JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:
(1)根类加载器(bootstrap class loader):它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
(2)扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
(3)系统类加载器(system class loader):被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。 - 类加载器加载Class大致要经过如下8个步骤:
(1)检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
(2)如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
(3)请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
(4)请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
(5)当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
(6)从文件中载入Class,成功后跳至第8步。
(7)抛出ClassNotFountException异常。
(8)返回对应的java.lang.Class对象。
2.2.双亲委派模型
- 定义:双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。
- 优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
相关问题
https://www.sxkawzp.cn/archives/jdbctomcatclassloaderdestroy
- Java给数据库操作提供一个Driver接口,并提供一个DriverManager来管理这些Driver的具体实现。
- 在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个,然后使用的时候就直接这样就可以了:
DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "123");
- DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,但是DriverManager由BootStrap类加载器加载,而其Driver接口的实现类是位于服务商提供的 Jar 包的,所以BootStrap类加载器无法加载Driver实现类,这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类。
- 通过线程上下文类加载器解决这个问题,线程上下文类加载器让父级类加载器能通过调用子级类加载器来加载类,这就打破了双亲委派模型的原则。
3.补充问题
3.1.关于clinit 和init
- clinit:https://www.jianshu.com/p/8a14ed0ed1e9
- init:编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后。Init没有非法的向前引用问题。
//例子:
public class Test {
private String a = "s1";
{
b = 20;//这个地方没有发生编译错误
}
private int b = 10;
{
a = "s2";
}
public Test(String a, int b)
{
this.a = a; this.b = b;
}
public Test()
{
System.out.println("我是无参构造!");
}
@Override
public String toString() {
return "Test{" +
"a='" + a + '\'' +
", b=" + b +
'}';
}
public static void main(String[] args) {
System.out.println(new Test());//Test{a='s2', b=10}
}
}
3.3.方法类型
公有成员方法为invokevirtual,非final的static方法为invokevstatic,其余的都是invokespecial。
3.4.final题
黑马pdf:3_类加载和字节码技术中p43~49
3.5.多态
补:
字节码指令:https://www.cnblogs.com/longjee/p/8675771.html