类加载器
类加载器用于加载类的工具,将类加载到内存中。 Java虚拟机与程序生命周期,在如下几种情况,Java虚拟机将结束生命周期。
-
执行了System.exit()方法
-
程序正常执行结束
-
程序在执行过程中遇到异常或错误而被异常终止
-
操作系统出现错误导致Java虚拟机进程终止
类加载五个过程
在Java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的,不在编译阶段。类型指的是Class、Interface或Enum,并不指对象。 提供了更大的灵活性,增加了更多的可能性。
类型的加载
将已经编译完成的.class文件从硬盘上加载到内存上。查找并加载类的二进制数据。
类型的连接
将类与类之间的关系确立好,并且对字节码的验证校验。
连接验证阶段:确保被加载的类的正确性,没有被篡改,遵循JVM规范。
连接的准备阶段:为类的静态变量分配内存,并将其初始化为默认值。
连接的解析阶段:把类中的符号引用转换为直接引用。
类型的初始化
为类的静态变量赋于正确的初始值。
类的使用
类的使用是我们在程序中最常见的。
类的卸载(使用很少)
类的卸载指把类从内存中剔除
类加载过程
举个栗子:
private static int i =1; //i为静态变量
在连接验证阶段为i分配内存空间,并赋予初始值0,在类型的初始化时,为类赋予正确的初始值1。一定要分清i的变化阶段
类的使用方式
Java程序对类的使用方式可分为二中
-
主动使用
-
被动使用
所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们
主动使用(七种)
创建类的实例,也就是new一个对象。
访问某个类或者接口的静态变量(getstatic),或者对该静态变量赋值(putstatic)。
调用类的静态方法(invokestatic)
反射(如Class.forName("com.test.Test"))
初始化一个类的子类,当子类初始化时父类也会初始化
Java虚拟机启动时被标明为启动类的类
JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REFgetStatic,REFputStatic,REF_invokeStatic句柄对应的类没有初始化,则去初始化
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化,不会导致类型的初始化这个动作产生。
类的加载
类的加载指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用来封装类在方法区内的数据结构。
加载.class文件的方式
-
从本地系统中直接加载
-
通过网络下载.class文件
-
从zip,jar等归档文件中加载.class文件
-
从专有数据库中提取.class文件
-
将Java源文件动态编译为.class文件(动态代理,JSP页面文件)
主动使用栗子
-
程序1
package com.gd.jvm.classloader;
/**
* @date 2019/3/31 23:50
*/
public class Initiative {
public static void main(String[] args) {
System.out.println(Mychild1.str);
}
}
class Myparent {
public static String str = "hello workd";
static {
System.out.println("Myparent static block");
}
}
class Mychild1 extends Myparent{
static {
System.out.println("Mychild1 static block");
}
}
结果:
Myparent static block
hello workd
-
程序2
package com.gd.jvm.classloader;
/**
* @date 2019/3/31 23:50
*/
public class Initiative {
public static void main(String[] args) {
System.out.println(Mychild1.str2);
}
}
class Myparent {
public static String str = "hello workd";
static {
System.out.println("Myparent static block");
}
}
class Mychild1 extends Myparent{
public static String str2 = "welcome";
static {
System.out.println("Mychild1 static block");
}
}
结果:
Myparent static block
Mychild1 static block
welcome
总结:
程序1,父类的代码块执行了,程序1中str是被父类定义的,因此是对Myparent主动使用,而没有对Mychild1主动使用。
程序2,父类和子类的代码块都执行了,str2是在Mychild1中定义的,所以Mychild1的代码块一定会被执行,但Mychild1继承了Myparent,看主动调用的第5种,当初始化一个类的子类,子类初始化时父类也会初始化,因此会先调用Myparent的代码块。
对于静态字段调用,只有直接定义了该字段的类才会被初始化,当一个类在初始化时,要求其父类全部都已经初始化了。
-
程序3
package com.gd.jvm.classloader;
/**
* @date 2019/4/1 20:47
*/
public class Initiative2 {
public static void main(String[] args) {
System.out.println(MyPParent2.str);
}
}
class MyPParent2{
public static final String str="hello world";
static {
System.out.println("MyPParent2 static block");
}
}
结果:
hello world
总结:
常量在编译阶段会存入到调用这个常量的方法所在的类(Initiative2)的常量池中,本质上,调用类(Initiative2)并没有直接引用到定义常量的类(MyPParent2),因此并不会触发定义常量的类(MyPParent2)的初始化。
这里指的是将常量存放到了Initiative2的常量池中,之后Initiative2和MyPParent2就没有任何关系了
-
程序4
package com.gd.jvm.classloader;
import java.util.UUID;
/**
* @date 2019/4/1 20:47
*/
public class Initiative3 {
public static void main(String[] args) {
System.out.println(MyPParent3.str);
}
}
class MyPParent3{
public static final String str= UUID.randomUUID().toString();
static {
System.out.println("MyPParent3 static block");
}
}
结果:
MyPParent3 static block
0a01d4a5-8605-47a1-b5e5-2b7f017c7f2b
总结:
当一个常量的值并非在编译期间可以确定的,那么其值就不会被放到调用类(Initiative3)的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类(MyPParent3),导致这个类被初始化。
-
程序5
package com.gd.jvm.classloader;
public class Initiative4 {
public static void main(String[] args) {
System.out.println(Mychild4.b);
}
}
interface MyPParent4{
public static int a=5;
}
interface Mychild4 extends MyPParent4{
public static int b=6;
}
将生成的Myparent4.class删除结果:
6
总结:
当一个接口在初始化时,并不要求父接口都完成了初始化。只有真正使用父接口的时候(如引用接口所定义的常量时)
--------------想看更多内容请关注公众号--------------