JVM的整体结构
类加载子系统
类的加载指的是将类的.class文件中二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。类加载的最终产品是位于堆区中的class对象,Class对象封装了类在方法区内的数据结构,并向Java程序员提供了访问方法区内的数据机构的接口. 我们可以通过类名.class来获取一个类的类型的引用,通过new 类名().getClass()来获取一个实例变量的类的引用
类加载子系统分为三个阶段
- 加载阶段
- 链接阶段
- 初始换阶段
加载阶段
类的加载机制
从JDK1.2开始类加载采用双亲委派机制。除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当Java程序加载器加载某个类时,加载器会首先委托自己的父加载器去加载该类,若父加载器能加载,则由父加载器完成加载任务,否则才由自加载器去加载。
类加载器分类
-
引导(启动)类加载器(Bootstrap Class Loader):
- Java的核心类库(JAVA_HOME/jre/lib/rt.jar, resources.jar 或 sun.boot.class.path路径下的内容),用于提供JVM自身所需要的类。
- 不继承ClassLoader,没有父类加载器。
- 而且只加载包名位java,javax,sun等开头的类。
-
自定义类加载器(User-Defined Class Loader):
-
扩展类加载器(Extend Class Loader):
- 派生于ClassLoader类,父类加载器为启动类加载器
-
系统类加载器(App Class Loader):
- 该类加载是程序中默认的类加载器,Java应用的类都是由它来完成加载。派生于ClassLoader类,父类加载器为扩展类加载器
-
链接阶段
验证
确保class字节流中包含的信息符合虚拟机的加载要求,保证加载的字节流文件不会损害虚拟机
准备
为变量分配内存和设置类变量的初始值,对于static类型变量在这个阶段会为其赋值为默认值,比如public static int v=5,在这个阶段会为其赋值为v=0,而对于static final类型的变量,在准备阶段就会被赋值为正确的值
解析
1.将常量池内的符号引用转换为直接引用。
原来的符号引用仅仅是一个字符串,而引用的对象不一定被加载,直接引用只的是将引用对象的指针或者地址偏移量指向真正的对象,将字符串所指向的对象加载到内存中。
2.显示赋值, 即将类变量的初始值替换为设置的值
初始化阶段
初始化阶段就是执行类构造器方法()的过程。此方法是初始化阶段,jvm主要完成对静态变量的初始化,静态块执行等工作。它在多线程中被同步加锁,一次只初始化一个类。
而类的构造器则是虚拟机视角下的()方法
当一个类被主动使用时,Java虚拟就会对其初始化,如下六种情况为主动使用:
1、 创建类的实例
2、访问某个类或接口的静态变量,或者对该静态变量赋值
3、调用类的静态方法
4、反射(Class.fotName)
5、初始化一个类的子类
6、Java虚拟机启动时被标明为启动类的类(面方法所在的类)
Java编译器会收集所有的类变量初始化语句和类型的静态初始化器,将这些放到一个特殊的方法中:clinit。
注意:1、当Java虚拟机初始化一个类时,要求他的所有父类都已经被初始化,但是这条规则并不适合接口。在初始化一个类或接口时,并不会先初始化它所实现的接口。
2、只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用。如果静态方法或变量在parent中定义,从子类进行调用,则不会初始化子类。
public class Singleton {
private static Singleton singleton = new Singleton();
public static int count1;
public static int count2 = 0;
public static Singleton getInstance() {
return singleton;
}
//构造方法私有化
private Singleton(){
count1++;
count2++;
}
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.count1);
System.out.println(singleton.count2);
}
}
输出结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210111150434491.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxMTM1MDk3NTY5NA==,size_16,color_FFFFFF,t_70)
public class Singleton {
public static int count1;
public static int count2 = 0;
private static Singleton singleton = new Singleton();
//构造方法私有
public static Singleton getInstance() {
return singleton;
}
private Singleton(){
count1++;
count2++;
}
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.count1);
System.out.println(singleton.count2);
}
}
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210111150311471.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxMTM1MDk3NTY5NA==,size_16,color_FFFFFF,t_70)
在main方法中调用单例模式创建类的实例时,是对类的主动使用。
-
在第一种情况下,首先执行的是类的构造方法
private Singleton(){ count1++; count2++; }
执行完构造方法之后,JVM会对静态变量进行赋值,所以按顺序执行,count1没有被赋值,而count2通过
public static int count2 = 0;
又被赋值为0. -
在第二种情况下是先赋值,再执行构造方法,所以结果为1,1.
这个小例子说明类在初始化时,类里面的静态变量赋值语句和构造方法执行是顺序执行的。
双亲委派机制
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210111153825580.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxMTM1MDk3NTY5NA==,size_16,color_FFFFFF,t_70)
1)如果一个类加载器收到了类加载请求,它会先将这个请求委托给父类的加载器去执行。
2)如果父类加载其还存在父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。
3)如果父类加载器可以完成加载类的任务则加载该类,完成任务,倘若完不成则会让子类加载器自己去尝试加载。
举个例子:士兵缴获了战利品,不会自己先使用,而是会先问上级领导需不需要,上级领导会去问总部需不需要。若总部需要则该战利品由总部处理,不会再给领导,同样的若是总部不需要而领导需要,则不会再给士兵,若领导不需要则会给士兵自行分配。
双亲委派机制的优势
1)避免类的重复加载,类一旦被加载则会返回。
2)保护程序的安全,防止核心API被随意篡改。
我们自己创建一个与JAVA核心API一样路径的String类:
然后创建一个Test类去调用它,如果我们能够调用自己所创建的String。
如果输出的是:String类的静态代码块
,这就意味着我们自己所创建的这个类代替了JAVA中String。
如果输出的是:ssss
,那么就意味着我们自己创建的String类中的静态代码块没有被执行,我们调用的还是JAVA中的String。
输出结果:
很显然,JAVA的开发者已经想到了这一步。
沙箱安全机制
那么我们自己在自己创建的String类中执行主方法会怎么样?
沙箱安全机制就是对Java核心源代码的保护,你自定义的java.lang.String并不能使用,这就是体现。