类加载
类加载机制概述
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
懒加载机制:用的时候就打开,不用就不打开。 第一次使用需要等待时间,但节省系统资源。
加载开始后,连接开始;加载结束后,连接才可以结束
类加载过程
加载
- 通过一个类的全限定名来获取定义此类的二进制流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的Class对象,作为这个类的各种数据的访问入口
加载源
文件:class文件,jar文件
网络
计算生成一个二进制流 $Proxy
其他文件生成:jps
数据库
连接
验证
验证是连接的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
- 文件格式验证 魔数,java版本信息等
- 元数据验证
- 字节码验证
- 符号引用验证
准备
- 准备阶段正式为类变量分配内存并设置变量的初始值。这些变量使用的内存都将在方法区中进行分配。
- 这里的初始值并非我们指定的值,而是默认值;但是如果是被final修饰,那么在这个过程中,常量值会被一起指定
解析
- 解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程
- 类或者接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
初始化
类初始化阶段是类加载过程的最后一步,前面类加载的过程中除了在加载阶段用户应用程序可以通过自定义类加载器参与以外,其余动作完全由虚拟机主导与控制。到了初始化阶段,才是真正执行类中定义的Java程序代码。
在准备阶段,变最已经赋过一次系统要求的初始值,。而在初始化阶段,则根据开发者通过程序控制制定的主观计划去初始化类变量和其他资源。
- 初始化是类加载的最后一步
- 初始化是执行方法的过程
初始化相关注意项一类
初始化触发条件
- 遇到new(实例化对象)、getstatic(获取类变量的值,被final修饰的除外,他的值在编译器时放到了常量池)、putstatic(给类变量赋值)或invokestatic(调用静态方法)这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
- 使用Java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化(接口除外)。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
package com.jvm.bytecode.java02;
public class Parent {
static {
System.out.println("Parent 初始化了。。。。。");
}
}
package com.jvm.bytecode.java02;
/*
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化(接口除外)。
当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
*/
public class Child extends Parent {
static {
System.out.println("Child 初始化了。。。。");
}
public static void main(String[] args) {
}
}
//--------------
Parent 初始化了。。。。
Child 初始化了。。。。
不会初始化
- 通过子类引用父类的静态字段,子类不会被初始化
- 通过数组定义来引用类
- 调用类的常量
package com.jvm.classloader;
/* 通过数组定义来引用类,不会初始化
对于数组实例来说,其类型是由JVs在运行期动态生成的,表示为[Lcom.shengsiyuan.jvm.classloader.MyParent4这种形式。
动态生成的类型,其父类型就是object。
对于数组来说,JavaDoc经常将构成数组的元素为component,实际上就是将数组降低一个维度后的类型。
助记符:
anewarray:表示创建一个引用类型的(如类、接口、数组)数组,并将其引用值压入栈顶
newarray:表示创建一个指定的原始类型(如int、float、char等)的数组,并将其引用值压入栈顶
*/
public class MyTest4 {
public static void main(String[] args) {
// MyParent4 myParent4 = new MyParent4();
//输出 MyParent4 static code --主动初始化
MyParent4[] myParent4s = new MyParent4[1];
//无输出 -未初始化
System.out.println(myParent4s.getClass());
MyParent4[][] myParent4s1 = new MyParent4[1][1];
//无输出 -未初始化
System.out.println(myParent4s1.getClass());
/*
class [Lcom.jvm.classloader.MyParent4;
class [[Lcom.jvm.classloader.MyParent4;
*/
System.out.println(myParent4s.getClass().getSuperclass());
System.out.println(myParent4s.getClass().getSuperclass());
/*
class java.lang.Object
class java.lang.Object
*/
System.out.println("=======================");
int[] ints = new int[1];
System.out.println(ints.getClass());
System.out.println(ints.getClass().getSuperclass());
char[] chars= new char[1];
System.out.println(chars.getClass());
System.out.println(chars.getClass().getSuperclass());
boolean[] booleans = new boolean[1];
System.out.println(booleans.getClass());
/*
class [I
class java.lang.Object
class [C
class java.lang.Object
class [Z
*/
}
}
class MyParent4{
static {
System.out.println("MyParent4 static code");
}
}
初始化相关注意项一执行顺序
- 编译器收集的顺序是由语句在源文件中出现的顺序决定的
静态语句块中只能访问到定义在静态语句块之前的变量;定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问
代码如下:编译器提示“非法向前引用”
- 初始化方法执行的顺序
虚拟机会保证在子类的初始化方法执行之前,父类的初始化方法已经执行完毕,因此在虚拟机中第一个被执行的类初始化方法一定是java.lang.Object。另外,也意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
初始化相关注意项一静态字段
- 对于静态字段来说,只有直接定义了该字段的类才会被初始化
//对于静态字段来说,只有直接定义了该字段的类才会被初始化
public class MyTest1 {
public static void main(String[] args) {
System.out.println(MyChild1.str);
/*
MyParent static blokc 执行父类静态代码块
hello word 执行父类 str代码块
MyChild1.str -- str为父类MyParent1,主动调用了MyParent1 ;而MyChild1,没有主动调用
*/
}
class MyParent1{
public static String str = "hello word";
static {
System.out.println("MyParent static blokc");
}
}
class MyChild1 extends MyParent1{
public static String str2 = "welcome";
static {
System.out.println("MyChild1 static blokc");
}
}
- final 静态字段,与类无关,不会初始化该类
/package com.jvm.classloader;
/*
常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中,
本质上,调用类并没有直接引用到定义常量的类,因此不触发,定义常量的类的初始化 !!!
注意:这里是指将常量存放到 MyTest2 的常量池中,之后MyTest2与MyParent2 就没有任何关系了;甚至 我们可以将MyParent2的class文件删除!
助记符:
ldc表示将int,float或是string类型的常量值从常量池中推送至栈顶
bipush表示将单字节(-128_~ 127)的常量值推送至栈顶
sipush表示将一个短整型常量值(-32768 ~32767)推送至栈顶
iconst_1表示将int类型1推送至栈顶(iconst_1 - iconst_5) 6 ——> bipush
*/
public class MyTest2 {
public static void main(String[] args) {
System.out.println(MyParent2.str);
System.out.println(MyParent2.s);
System.out.println(MyParent2.i);
System.out.println(MyParent2.m);
//不会输出 MyParent2 static block
}
}
class MyParent2 {
//final 常量
public static final String str = "hello world";
public static final short s = 127;
public static final int i = 128;
public static final int m = 1;
static {
System.out.println("MyParent2 static block");
}
}
- final 动态常量 --会初始化类
package com.jvm.classloader;
//编译常量与运行常量区别
/*
当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池中,
这时在程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类初始化。
*/
import java.util.UUID;
public class MyTest3 {
public static void main(String[] args) {
System.out.println(MyParent3.str);
/*
MyParent3 static code
198b96ce-bb59-4981-9974-4f70060547cf
*/
}
}
class MyParent3{
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("MyParent3 static code");
}
}
初始化相关注意项一接口
接口中不能使用静态语句块,但仍然有变量初始化的操作,因此接口与类一样都会生成clinit()方法,但与类不同的是,执行接口的初始化方法之前,不需要先执行父接口的初始化方法。只有当父接口中定义的变量使用时,才会执行父接口的初始化方法。另外,接口的实现类在初始化时也一样不会执行接口的clinit()方法。
package com.jvm.classloader;
/*
当一个接口在初始化时,并不要求其父接口都完成了初始化
只有在真正使用到父接口的时候(如引用接口中所定义的常量时),才会初始化
*/
import java.util.Random;
public class MyTest5 {
public static void main(String[] args) {
System.out.println(MyChild5.b);
}
}
interface MyParent5 {
public static final int a = new Random().nextInt(2); //随机值不能确定;使用会初始化
}
interface MyChild5 extends MyParent5 {
// public static int b = 6;
public static final int b = 5; //使用不初始化
}
package com.jvm.classloader;
/*
当一个接口在初始化时,并不要求其父接口都完成了初始化
只有在真正使用到父接口的时候(如引用接口中所定义的常量时),才会初始化
*/
import java.util.Random;
public class MyTest5 {
public static void main(String[] args) {
// System.out.println(MyChild5.b);
System.out.println(MyParent5_1.thread);
//在初始化一个接口时,并不会先初始化它的父接口。
/*
MyParent5_1 invoked
Thread[Thread-0,5,main] --MyParent5_1 toString结果的线程名
*/
}
}
interface MyGrandpa {
public static Thread thread = new Thread() {
{
System.out.println("MyGrandpa invoked");
}
};
}
interface MyParent5 extends MyGrandpa{
public static Thread thread = new Thread() {
{
System.out.println("MyParent5 invoked");
//初始化:为静态代码块赋予正确的值 ;如果执行,则实例化此接口。
// 证明:当一个接口在初始化时,并不要求其父接口都完成了初始化!!!
// 若将 接口 interface 都修改为 class;会按照顺序输出三个结果
}
};
}
interface MyChild5 extends MyParent5 {
public static int b = 5;
}
//
interface MyGrandpa5_1{
public static Thread thread = new Thread() {
{
System.out.println("MyGrandpa5_1 invoked");
}
};
}
interface MyParent5_1 extends MyGrandpa5_1{
public static Thread thread = new Thread() {
{
System.out.println("MyParent5_1 invoked");
}
};
}
初始化相关注意项一多线程
虚拟机会保证一个类的clinit()方法在多线程环境中被正确的加锁、同步,
如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,直到活动线程执行类初始化方法完毕。
package com.jvm.bytecode.java02;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class DemoThread {
static class Hello {
static {
System.out.println(Thread.currentThread().getName() + "init");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(20);
int i = 0;
while (i++ < 20) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "start...");
Hello h = new Hello();
System.out.println(Thread.currentThread().getName() + "end...");
}
});
}
}
}
//---------------------------------------------
pool-1-thread-2start...
pool-1-thread-5start...
pool-1-thread-7start...
pool-1-thread-13start...
pool-1-thread-17start...
pool-1-thread-1start...
pool-1-thread-6start...
pool-1-thread-12start...
pool-1-thread-20start...
pool-1-thread-11start...
pool-1-thread-10start...
pool-1-thread-3start...
pool-1-thread-4start...
pool-1-thread-19start...
pool-1-thread-15start...
pool-1-thread-8start...
pool-1-thread-14start...
pool-1-thread-9start...
pool-1-thread-2init //--只有一个 init
pool-1-thread-18start...
pool-1-thread-16start... //等5s后执行end
pool-1-thread-3end...
pool-1-thread-16end...
pool-1-thread-5end...
pool-1-thread-18end...
pool-1-thread-2end...
pool-1-thread-9end...
pool-1-thread-14end...
pool-1-thread-17end...
pool-1-thread-20end...
pool-1-thread-15end...
pool-1-thread-11end...
pool-1-thread-10end...
pool-1-thread-12end...
pool-1-thread-13end...
pool-1-thread-4end...
pool-1-thread-6end...
pool-1-thread-19end...
pool-1-thread-1end...
pool-1-thread-8end...
pool-1-thread-7end...
如果发现线程阻塞的问题,可能是clinit()方法出现如下问题
package com.jvm.bytecode.java02;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class DemoThread {
static class Hello {
static { //这里面出问题
if (true) {
System.out.println(Thread.currentThread().getName() + "init");
while (true) {}
}
}
}
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(20);
int i = 0;
while (i++ < 20) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "start...");
Hello h = new Hello();
System.out.println(Thread.currentThread().getName() + "end...");
}
});
}
}
}
//-------------------------
pool-1-thread-16start...
pool-1-thread-7start...
pool-1-thread-8start...
pool-1-thread-4start...
pool-1-thread-13start...
pool-1-thread-10start...
pool-1-thread-18start...
pool-1-thread-6start...
pool-1-thread-17start...
pool-1-thread-20start...
pool-1-thread-5start...
pool-1-thread-2start...
pool-1-thread-3start...
pool-1-thread-12start...
pool-1-thread-15start...
pool-1-thread-11start...
pool-1-thread-14start...
pool-1-thread-9start...
pool-1-thread-1start...
pool-1-thread-19start...
pool-1-thread-16init //后面没执行