一.原理
上一节说到了JVM的内存结构。JVM将class文件字节码文件加载到内存中, 并将这些静态数据转换成方法区中的运行时数据结构,在堆(并不一定在堆中,HotSpot在方法区中)中生成一个代表这个类的java.lang.Class 对象,作为方法区类数据的访问入口。
二.具体过程
1.加载
- 官方描述:加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口
Java的反射机制就是通过这一步产生的class对象来获取对象实例的各种信息的。
2.验证
当 JVM 加载完 Class 字节码文件并在方法区创建对应的 Class 对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。比如判断文件是不是以0x CAFEBABE开头的(这是java class文件的开头标识)
*3.准备
当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。要注意内存分配对象的类型
Java 中的变量有 类变量 和 类成员变量 两种类型,类变量 指的是被 static 修饰的变量,而其他所有类型的变量都属于 类成员变量 。我的理解是,static修饰的变量总是和“类“联系在一起,而其他的都是和类的实例联系在一起。
public static int factor = 3;
public String lzj = "6324";
初始化时,只会为factor这个类变量分配内存,而分配的值是赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。所以factor值为0。 但如果被final修饰的话,那么会直接给它想要的值,如果加一个final,factor值为3。
4.解析
当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。这也不是我们学习的重点。
*5.初始化
到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化,初始化有以下几个规则:
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
public class Book {
public static void main(String[] args)
{
System.out.println("Hello ShuYi.");
}
Book()
{
System.out.println("书的构造方法");
System.out.println("price=" + price +",amount=" + amount);
}
{
System.out.println("书的普通代码块");
}
int price = 110;
static
{
System.out.println("书的静态代码块");
}
static int amount = 112;
}
输出结果是什么???
可以这么分析,先根据上面第四条原则,找到Main函数的位置,发现它在Book这个类中,而要初始化这个类,就要执行其中的static 标注的代码块和变量
static
{
System.out.println("书的静态代码块");
}
static int amount = 112;
也就是先初始化这个部分,然后发现main中有一条输出语句,执行该语句,最终结果为:
书的静态代码块
Hello ShuYi.
为什么没有输出Book(){}其中那堆? 因为这是类的对象初始化方法,当我们创造Book实例对象才会调用,比如new Book()
6.使用
当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。
7.卸载
当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存
三.双亲委派机制
1.双亲委派工作流程:
如果一个类加载器收到了类加载器的请求.它首先不会自己去尝试加载这个类.而是把这个请求委派给父加载器去完成.每个层次的类加载器都是如此.因此所有的加载请求最终都会传送到Bootstrap类加载器(启动类加载器)中.只有父类加载反馈自己无法加载这个请求(它的搜索范围中没有找到所需的类)时.子加载器才会尝试自己去加载。
2.类加载器的分类:
- 启动类加载器(Bootstrap):这个加载器主要负责将存放在 <JAVA_HOME>的lib 目录下的,或者被–Xbootclasspath参数所指定的路径中的,并且被虚拟机识别的(比如rt.jar).名字不符合的类库即使放在lib目录下也会被加载。
- 扩展类加载器(Ext):这个加载器主要负责加载存放在 <JAVA_HOME>/lib/ext 目录下的java类库,或者而被java.ext.dirs系统变量所指定的路径的所有类库,开发者可以直接使用扩展类加载器
- 应用程序加载器(Application):这个类加载器负责加载用户类路径上所指定的类库,如果程序中没有定义过自己的类加载器,那么一般情况下这个就是程序中默认的类加载器
3.双亲委派模型的优点:
例如类java.lang.Object,它存放在rt.jart之中.无论哪一个类加载器都要加载这个类.最终都是双亲委派模型最顶端的Bootstrap类加载器去加载.因此Object类在程序的各种类加载器环境中都是同一个类. 这样能避免有些用户用自己编写的类替换系统的类。
原因:
任意一个类都是由这个类本身和加载这个类的类加载器来确定这个类在JVM中的唯一性
3.JDBC例外:
- 在rt里面定义了这个SPI(Service Provider Interface),那mysql有mysql的jdbc实现,oracle有oracle的jdbc实现,反正我java不管你内部如何实现的,反正你们都得统一按我这个来,这样我们java开发者才能容易的调用数据库操作
- Bootstrap ClassLoader就得委托子类来加载数据库厂商们提供的具体实现。因为它的手只能摸到<JAVA_HOME>\lib中,其他的它无能为力。这就违反了自下而上的委托机制了
解决:Java就搞了个线程上下文类加载器,通过setContextClassLoader()默认情况就是应用程序类加载 通过Thread.current.currentThread().getContextClassLoader()获得类加载器来加载
4.补充一些注意点:
- 双亲委派模型在JDK1.2后被引入,但它并不是一个强制性的约束模型,而是java设计者推荐给开发者的一种类加载器的实现方式。
四.什么是类加载器?
类加载器就是把类加载阶段中的”通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部来实现的代码模块。 类加载器的分类: 启动类加载器、扩展类加载器、应用类加载器(系统类加载器)、用户自定义类加载器。
启动类加载器:这个类负责将存放在JAVA_HOME/lib目录或者被-Xbootclasspath参数所指定的路径中的并且是虚拟机内存中。
扩展类加载器:负责加载JAVA_HOME/lib/ext目录中或者被java.ext.dirs系统变量指定路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器:负责加载用户类路径上指定的类加载器,一般情况下就是程序中默认的类加载器。