JVM是什么?
1. 类加载(Class Loading)
在 Java 代码中, 类型(如Class Interface Enum等,是Object这个类本身,而不是Object的对象)的加载、连接与初始化过程都是在程序运行期间完成的;
提供了更大的灵活性,增加了更多的可能性;
- 加载:查找并加载类的二进制数据(就是把类的字节码文件加载到内存中);
- 连接:
- 验证:确保被加载的类的正确性;
- 准备:为类的静态变量分配内存,并将其初始化为默认值;
- 解析:把类中的符号引用转换为直接引用;
- 初始化:为类的静态变量赋予正确的初始值;
- 使用
- 卸载
请看一坨新鲜的例子
Class Test{
public static int a = 1;
/**
以上,程序使用类 Test时,完成 加载,在执行 连接时
JVM会先为静态变量 a 分配内存,并将默认值初始化为0,所有的整数类型初始化值都为 0
在 初始化 阶段会将 正确的初始值 1 赋予静态变量 a
*/
}
其实在初始化后,会有一个类实例化的过程。
类实例化:java编译器为它编译的每一个类都至少生成一个实例初始化方法。在Java的class文件中,这个实例初始化方法被称为 init。针对源代码中每一个类的构造方法。Java编译器都产生一个init方法
1.1 类的加载
- 类的加载的最终产品是位于内存中的Class对象;
- Class对象封装了类在方法区内的数据结构,并且向Java程序与那提供了访问方法区内的数据结构的接口;
类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区中,然后在内存中创建一个java.lang.Class对象(规范并说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中) 用来封装类在方法区内的数据结构;
加载 .class文件的方式
- 从本地系统中直接加载;
- 通过网络下载 .class文件;
- 从 zip, jar等归档文件中加载 .class文件;
- 从专有数据库中提取 .class文件;
- 将 Java源文件动态编译为 .class文件;
1.1.1 类加载器
类加载器用来把类加载到 Java虚拟机中, 从 JDK 1.2版本开始, 类的加载过程采用父亲委托机制,这种机制能更好的保证 Java平台的安全, 在此委托机制中, 除了Java虚拟机自带的根类加载器以外, 其余的类加载器都有且只有一个父加载器. 当 Java 程序请求加载器 loader1 加载 Sample类时, loader1 首先委托自己的父加载器去加载Sample类, 若父加载器能加载, 则由父加载器完成家在任务,否则由加载器 loader1 本身加载 Sample类(是一种双亲委托机制);
java 里面每一个类型,最终其 数据结构纳入到JVM的管辖范围内即进入到JVM内存当中,该过程就是由类加载器来实施完成的。
类加载器并不需要等到某个类被“首次主动使用”时再加载它。
其实也不难理解,因为某个类被 “首次主动使用” 的时候,进行的时它的初始化。
- JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了 .class文件缺失或者存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)
- 如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
Java虚拟机与程序的生命周期
在如下几种情况下,Java虚拟机将结束生命周期:
- 执行了 System.exit() 方法;
- 程序正常执行结束;
- 程序在执行过程中遇到了异常或错误而异常终止;
- 由于操作系统出现错误而导致Java虚拟机进程终止;
1.1.1.1 类加载器的类型
- Java 虚拟机自带的加载器
- 根类加载器(Bootstrap)
- 扩展类加载器(Extension)
- 系统(应用)类加载器(System)
- 用户自定义的类加载器
- java.lang.ClassLoader的子类
- 用户可以定制类的加载方式
1.2 类的连接
类被加载后,就进入连接阶段。连接就是将已经读入到内存的二进制数据合并到虚拟机的运行时环境中去。
类的连接有分为三个阶段:验证,准备,解析
1.2.1 类的验证
类的验证的内容
- 类文件的结构检查
- 语义检查
- 字节码验证
- 二进制兼容性的验证
1.2.2 类的准备
在准备阶段, Java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如对于以下 Sample类,在准备阶段将 int类型的静态变量 a 分配4个字节的内存空间,并且赋予默认值 0,为 long 类型的静态变量分配8个字节的内存空间,并且赋予默认值 0.
public class Sample{
private static int a = 1;
public static long b;
static{
b = 2;
}
...
}
1.2.3 类的初始化
1. 在初始化阶段, Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。
在程序中,静态的初始化有两种途径:
- 在静态变量的声明处进行初始化;
- 在静态代码块中进行初始化;
例如:在下面代码中,静态变量 a 和 b 都被显式初始化,而静态变量 c 没有被显式初始化,它将保持默认值 0;
public class Sample{
private static int a = 1; //在静态变量的声明处进行初始化
public static long b;
public static long c;
static{
b = 2; //在静态代码块中进行初始化;
}
}
2. 静态变量的声明语句,以及静态代码块都被看做类的初始化语句。Java 虚拟机 会按照初始化语句在类文件中的先后顺序来依次执行它们。
例如:当以下 Sample类被初始化后,它的静态变量 a 的取值 为4;
public class Sample{
static int a = 1;
static{
a = 2;
}
static{
a = 4;
}
public static void main(String args[]){
System.out.println("a=",a);
}
}
1.2.3.1 类的初始化步骤
- 假如这个类还没有被加载和连接,那就先进行加载和连接;
- 假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类;
- 假如类中存在初始化语句,那就依次执行这些初始化语句;
1.2.3.2 类的初始化时机
- 主动使用
所有的 Java虚拟机实现必须在每个类或接口被Java程序 “首次主动使用” 时才初始化他们;
主动使用的七种情况:
* 创建类的实例;
* 访问某个类或接口的静态变量,或者对该静态变量赋值;
* 调用类的静态方法;
* 反射;
* 初始化一个类的子类(其父类也会被初始化);
* Java虚拟机启动时被标明为启动类的类;
* JDK1.7开始提供的动态语言支持(如在Java中调用JS代码);
java.lang.invoke.MethodHandle实例的解析结果 REF_getStatic, REF_putStatic,
REF_invokeStatic句柄对应的类没有初始化,则初始化;
-
除了上述七种情形,其他使用 Java类的方式都被看作是被动使用,不会导致类的初始化;
-
关于接口:当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口
- 在初始化一个类时,并不会先初始化它所实现的接口;
- 在初始化一个接口时,并不会先初始化他的父接口;
因此,一个父接口并不会因为他的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化;
只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用;
调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化;
问题1,阅读以下代码,对于System.out.println(MyChild1.str);
System.out.println(MyChild1.str2);
这两个打印的内容分别会是什么?
/**主动使用,被动使用
* @author:徐凯
* @date:2019/12/9,16:58
* @what I say:just look,do not be be
*/
public class MyTest1 {
public static void main(String[] args) {
System.out.println(MyChild1.str);
System.out.println(MyChild1.str2);
}
}
class MyParent1{
public static String str = "hello world";
static {
System.out.println("MyParent1 static block");
}
}
class MyChild1 extends MyParent1{
public static String str2 = "welcome";
static {
System.out.println("MyChild1 static block");
}
}
-
执行程序我们能看到,
System.out.println(MyChild1.str);
结果是
but why?解析,要知道,对于静态字段来说,只有直接定义了该字段的类才会被初始化! 直接定义该字段的类, MyChild1.str ,字段
str
定义在类MyParent1中,因此 MyChild1.str 只会对MyParent1进行初始化, 也就是说对MyParent1进行了主动使用;
因此会先加载静态代码块的内容, 再加载静态字段的值; -
执行程序我们能看到,
System.out.println(MyChild1.str2);
的结果是
but why again?解析
要知道, 当一个类在初始化时, 要求其父类全部都已经初始化完毕, 也就是我们在上面说的主动使用中的一种情况: 初始化一个类的子类,这个时候会先初始化完成其父类,如果父类有父类会先初始化父类的父类,依次直到基类Object
因此,有以上打印结果我们也就不难理解了, 因为我们没有直接访问父类的静态字段,因此不会被打印.
问题2:
问题1中, 类MyChild1没有被初始化, 那么有没有被加载呢(上述类加载过程中,加载,连接,初始化,使用,卸载 五步中的加载)?
在这里我们有一个 虚拟机参数-XX:TraceClassLoading
-XX:TraceClassLoading
:它用于追踪类的加载信息并打印
那么该如何使用它呢?
以上
运行我们的代码,看一下控制台,这是会有很多很多的信息打印,它是按照 虚拟机在当前启动情况下,它所加载的类的信息都会给呈现出来。我们头尾截取一下
可以看到,即便我们没有初始化类MyChild1, 虚拟机在启动过程中,也会将类MyChild1加载
JVM参数 格式
- -XX:+option 表示开启option选项
- -XX:-option 表示关闭option选项
- -XX:option=value 表示将option选项的值设置为 value
问题3:
看下面这段代码,请问控制台会打印什么内容
public class MyTest2 {
public static void main(String[] args) {
System.out.println(Myparent2.str);
}
}
class Myparent2{
public static final String str = "hello world";
static {
System.out.println("MyParent2 static block");
}
}
打印结果
why?
解析:
我们知道 关键字
final
修饰的变量其实就相当于定义了一个常量,无法被修改的变量,如果final修饰的是一个基本数据类型的变量,那么这个变量的值就定了,不能变了,而如果修饰的是一个引用变量,那么该变量存的是一个内存地址,该地址就不能变了,但是该内存地址所指向的那个对象还是可以变的,就像你记住了人家的门牌号,但你不能管人家家里人员数量
上面代码中,
final
修饰的是一个字符串,那也就是定义了一个常量。常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中(换种说法就是,在哪一个类中调用了这个常量),本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化
注意:这里指的是将常量存放到了 MyTest2的常量池中,之后MyTest2与MyParent2就没有任何关系了
问题4:
看以下代码,请问控制台会打印什么内容
public class MyTest3 {
public static void main(String[] args) {
System.out.println(MyParent3.str);
}
}
class MyParent3{
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("myParent3 static code");
}
}
打印结果
why?
解析:
当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池中,
这时在程序运行时, 会导致主动使用这个常量所在的类, 显然会导致这个类被初始化;
关于接口:
类的加载,不会加载该类所实现的接口(如果该类实现了接口)
当一个接口在初始化时, 并不要求其父接口都完成了初始化;
只有在真正使用到父接口的时候(如引用接口中所定义的常量时),才会初始化;
问题5:
看以下代码,请问控制台会打印什么内容
public class MyTest5 {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("counter1: " + Singleton.counter1);
System.out.println("counter2: " + Singleton.counter2);
}
}
class Singleton {
public static int counter1;
private static Singleton singleton = new Singleton();
private Singleton() {
counter1 ++;
counter2 ++;
}
public static int counter2 = 0;
public static Singleton getInstance(){
return singleton;
}
}
解析:
程序运行时,会完成类的加载,连接(验证,准备,解析),初始化过程 .在连接的准备阶段,会对类的静态变量分配空间并且赋初值;代码中三个静态变量在准备阶段的初值分别为0,null,0 . 也就是说,在调用
getInstance()
方法的时候,静态变量都是有值的. 后面根据代码的执行顺序, 就很容易得出输出结果为 1和0
1.3 普及几个常见的 助记符
public class MyTest2 {
public static void main(String[] args) {
System.out.println(Myparent2.str);
}
}
class Myparent2{
public static final String str = "hello world";
static {
System.out.println("MyParent2 static block");
}
}
以上代码我们在编译,运行之后,通过反编译,来看一下助记符的使用
进入字节码所在文件夹内, 通过
javap -c xxx.class
命令来查看反编译信息
构造函数的代码信息我们先不看,我们看一下main方法的 Code:
助记符
- getstatic ☞ 调用静态的属性或方法
- invokevirtual ☞ 调用对象的实例方法
- ldc: 表示将 int,float或是String类型的常量值从常量池中推送至栈顶
除了ldc,还有以下类似的助记符
- bipush:表示将单字节(-128 ~ 127) 的常量值推送至栈顶
- sipush:表示将一个短整型常量值(-32768 ~ 32767) 推送至栈顶
- iconst_1:表示将int类型1推送至栈顶 (iconst_1 - iconst_5)