jdk版本:Java HotSpot™ 64-Bit Server VM (build 25.131-b11, mixed mode)
JVM虚拟机的学习可能更偏向于理论,但是当你理解了JVM虚拟机后,你将会写出更好的代码,性能更好的代码。
今天先介绍JVM对类的处理,总共分为加载
,连接
,初始化
,下面我们慢慢介绍
首先,我们先看java虚拟机的退出情况
java的虚拟机和程序的生命周期
在如下的情况下,java虚拟机会结束生命周期
- 执行了System.exit()方法
- 程序正常结束
- 程序在执行中遇到异常或者错误而异常终止
- 由于操作系统出现错误,导致java虚拟机退出
下面我们来详细了解下java虚拟机中类的加载过程
加载
描述:
类的加载是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法内,然后在内存中保存一个java.lang.Class
对象,用来封装类在方法区内的数据结构
可以多种类型文件:
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从数据库中提取.class文件
- 将java源代码编译成.class二进制文件
连接
- 验证 :确保被加载类的正确性
- 准备 :为类的
静态变量
分配内存,并把它初始化为默认值
- java中一些变量有一些默认值:
- int = 0
- java中一些变量有一些默认值:
//默认是把i赋值为0
public final static int i = 0;
- 解析 :把类中的符号引用转换成直接引用
- 符号表示:间接引用
- 直接引用:直接指针指向某个类的方法
初始化
把类的静态变量赋值为正确的初始值
//默认是把i赋值为0
public final static int i = 2;
注意:所有的JAVA虚拟机实现必须在每个类或者接口被java程序首次主动使用
时才会初始化他们
使用
正常使用类 ,比如 new String();
java程序对类的使用分为2种
- 主动使用 (7种)
- 创建类的实例
public class Test1 { public static void main(String[] args) { //这里创建实例 MyParent myParent = new MyParent(); System.out.println(myParent); } public static class MyParent {} }
- 访问某个类或者接口的
静态变量
,或者对该静态变量
进行赋值 这里其实并不会去加载MyParent,这里留了个坑哦???,详细请看下一章
public class Test1 { public static void main(String[] args) { //调用静态变量 System.out.println(MyParent.str); } public static class MyParent { public static final String str = "123"; public MyParent() { System.out.println("myParent init"); } } }
- 调用类的静态方法
- 反射
- 初始化一个类的子类
public class Test1 { public static void main(String[] args) { MyChild myChild = new MyChild(); } public static class MyParent { public MyParent() { System.out.println("myParent init"); } } public static class MyChild extends MyParent{ public MyChild() { System.out.println("MyChild init"); } } }
- java虚拟机启动时,被标记为启动类的类 如:Java Test
- Jdk1.7 开始提供的动态语言支持
- 被动使用
除了主动使用,其他都是被动使用
卸载
把加载到java虚拟机中的class类删除掉
下面3种情况会导致类的卸载
-
类加载器没有被引用
-
类对象没有被引用
-
没有该类的实例对象存在
扩充:
JVM的参数介绍
-XX:+TraceClassLoading
: 配置参数,可以显示类的加载过程
-XX:+TraceClassUnloading
: 配置参数,可以显示类的卸载的过程
参数形式的解析:
-XX :规定的
+:说明是开启的参数
-:说明是关闭的参数
助记符
iconst_*
基础类型(int) -1~5
通过iconst_*指令把常量压到栈顶
指令 | 值 |
---|---|
iconst_m1 | -1 |
iconst_0 | 0 |
iconst_1 | 1 |
iconst_2 | 2 |
iconst_3 | 3 |
iconst_4 | 4 |
iconst_5 | 5 |
-
bipush
基础类型(int) -128~127
通过bipush指令把常量压到栈顶
-
sipush
基础类型(int) -32768~32767
通过sipush指令把常量压到栈顶
-
ldc
基础类型(int) -2147483648~2147483647
通过ldc指令把常量压到栈顶
截图中看到,超过这个值就已经报错了
getstatic
static关键字
加强理解
理解链接中的准备阶段和初始化阶段
首先我们先准备个例子。
public class Test2 {
public static void main(String[] args) {
MyParent instance = MyParent.getInstance();
System.out.println(instance.num1);
System.out.println(instance.num2);
}
}
class MyParent {
public static int num1;
public static int num2 = 0;
private static MyParent myParent = new MyParent();
private MyParent() {
num1++;
num2++;
}
public static MyParent getInstance() {
return myParent;
}
}
这个就是一个跟简单的单例模式。运行后输出为
1
1
然后我们把这个例子里面的位置改变下
public class Test2 {
public static void main(String[] args) {
MyParent instance = MyParent.getInstance();
System.out.println(instance.num1);
System.out.println(instance.num2);
}
}
class MyParent {
public static int num1;
private static MyParent myParent = new MyParent();
private MyParent() {
num1++;
num2++;
}
//把num2迁移到这里
public static int num2 = 0;
public static MyParent getInstance() {
return myParent;
}
}
我们再执行,发现变成了
1
0
咦,这是为什么呢????
我们根据连接中的3个步骤来看下
- 验证阶段,验证MyParent这个类是否是否能正确被加载
- 准备阶段呢? 为类的静态变量分配内存,并且设置为默认值,问题就在这。。
- jvm把num1 设置为了0
- jvm把myParent 设置为了Null
- jvm把num2 设置为了0
- 解析阶段,就是把类之间的引用关系给转换成直接引用
到这里 连接步骤已经完成了。
连接执行完了,就开始初始化了(jvm是按照类中的静态变量的顺序进行一个个初始化的)
- 把num1设置了0,因为是默认值嘛
- 把myParent 创建实例的时候,就会调用
MyParent()
构造方法- 在
MyParent()
构造方法中- 把num1 和num2进行+1
- 这个时候 num1 = 1 , num2 = 1
- 在
- 继续把num2 设置为0
到这里,初始化的步骤以及完成了。
所以说最后打印的结果就是num1=1,num2=0
为了验证是否是和想的一样,我们MyParent()
构造函数打印出num1和num2
private MyParent() {
num1++;
num2++;
System.out.println("myParent init "+num1);
System.out.println("myParent init "+num2);
}
继续执行。
myParent init 1
myParent init 1
1
0
会发现在构造函数里面打印1和1。
相信看到这里,小伙伴对连接和初始化阶段已经很明白了吧。