JVM是伴随Java这门语言的诞生而存在的,Java的最大特点就是跨平台性,即我们常常说的一次编译,到处运行,这个特性其实就是JVM的功劳,JVM不仅仅是一个虚拟机,更是一种规范,所以任何符合JVM虚拟机规范的语言都可以跑在JVM中,包括Scala、Grooy、Kotlin等
JDK:
JDK(Java Development Kit) 是Java语言的软件开发工具包(SDK)。在JDK的安装目录下有一个jre目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib合起来就称为jre。
JRE:
JRE(Java Runtime Environment,Java运行环境),包含JVM标准实现及Java核心类库。JRE是Java运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器和调试器)
JVM:
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
下面一张图可以形象的反应他们之间的关系
类加载过程:
将内存中的二进制字节码加载到JVM中的过程
1、验证,确保类文件遵从Java类文件的固定格式
2、准备,为类的静态变量分配内存,并将其初始化为默认值
3、解析,把类中的符号引用转换为直接引用
为类的静态变量赋予正确的初始值
使用
卸载
java程序对类的使用分为主动使用和被动使用
主动使用:
所有的java虚拟机实现必须在每个类或接口被java程序首次主动使用时才初始化他们,主动使用大致可以分为以下6种情况:
public class MyTest1 {
public static void main(String[] args) {
/*
* 结果为:
* Father static block
* hello world
* 疑问?如果str1加了final 则不打印Father static block
*/
System.out.println(Child1.str2);
}
}
class Father1{
static final String str1 = "hello world";
static {
System.out.println("Father static block");
}
}
class Child1 extends Father1 {
static String str2 = "welcome";
static {
System.out.println("Child1 static block");
}
}
执行结果如下图所示:
由于加载信息太多,上图中截取的只是部分重要的ClassLoader加载信息,其中截图中可以看到加载了MyTest1的二进制码文件以及Father1及Child1的文件信息,如下图显示
可以看到不仅打印了子类静态块中的信息,而且还打印了父类静态块中的信息,而且父类打印是在子类之前,说明了子类调用自己的静态变量时(子类初始化),必须先初始化父类,当子类调用父类的静态变量时即Child1.str1时,此时JVM加载了子类文件,但是只打印了父类的静态块却并没有打印子类的静态块,说明:只有静态变量所属的类 调用时才会被初始化,如图三所示
要追踪加载信息需要在VM option➕上
-XX:+TraceClassLoading
JVM参数配置格式如下
-XX:+
接下来看另外一个代码:
/*
助记符:
ldc:将int float String等类型的常量从常量池中推送到栈顶
bipush表示将单字节(-128-127)的常量值推送至栈顶
sipush表示将一个短整型常量值(-32768-32767)推送至栈顶
iconst_1表示将int类型1推送至栈顶 (iconst_1 - iconst_5)有待验证
*/
public class MyTest2 {
public static void main(String[] args) {
//结果是哈喽 并没有打印Demo1 static block
// System.out.println(Demo1.s);
System.out.println(Demo1.j);
}
}
class Demo1{
static final String s = "哈喽";
static final int j = 0;
static final int k = 128;
static{
System.out.println("Demo1 static block");
}
}
跟上面的代码有所不同的地方在于,这里的静态变量加了final,也就是静态常量,此时执行Demo1.j得出的结果是哈喽,如下图
可以看到并没打印Demo1的静态代码块,结论:常量(s)会被调用常量的方法(main)所在的类(MyTest)加载到类的常量池中,本质上:调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化,注意:MyTest2将常量存到了常量池中,此时MyTest2与Demo1本质上已经没有任何关系了
实验:将生成的Demo1的class文件删除 仍然能打印出哈喽
反编译Demo1.class文件,进入到classpath文件目录下,输入 javap -c MyTest2.class
,如图所示
main方法下涉及到的助记符:
ldc:将int float String等类型的常量从常量池中推送到栈顶
bipush:表示将单字节(-128-127)的常量值推送至栈顶
sipush:表示将一个短整型常量值(-32768-32767)推送至栈顶
iconst_m:表示将int类型-1推送至栈顶 (iconst_0 - iconst_5)+ iconst_m
接下来我们再看一个类型的代码:
public class MyTest3 {
public static void main(String[] args) {
//结果
// Demo2 static block
// a5d9eb5a-5057-4ce4-9e0d-431198105439
System.out.println(Demo2.s);
}
}
class Demo2 {
static final String s = UUID.randomUUID().toString();
static {
System.out.println("Demo2 static block");
}
}
代码跟上面的版本不同之处获取了随机UUID,但是打印结果打印了Demo2的静态代码块,如下图所示,什么原因?
结论:常量非编译期间能确定的,则常量不会存放在调用该常量的方法所在的类的常量池中
,这时在常量程序运行时会导致主动的使用这个常量所在的类 显然会导致这个类被初始化
最后在看一版代码
public class MyTest4 {
public static void main(String[] args) {
//结果 Demo4 static block
Demo4 demo4 = new Demo4();
//结果 没有任何打印
Demo4[] demo4s = new Demo4[2];
System.out.println(demo4s.getClass());
System.out.println(demo4s.getClass().getSuperclass());
int[] ints = new int[1];
System.out.println(ints.getClass());
System.out.println(ints.getClass().getSuperclass());
char[] chars = new char[2];
System.out.println(chars.getClass());
}
}
class Demo4 {
static {
System.out.println("Demo4 static block");
}
}
区别是类数组代替了普通类,发现new了数组并没有初始化类的静态代码块,如下图所示.
相信经过上面多个例子,这个代码的结果的原因大家都应该猜到了吧,结论:对于数组来说 其类型是由JVM在运行期动态生成的,表示为[Lcom.zh.classloader.Demo4;这种形式 动态生成的类型,其父类型是Object
对于数组来说,JavaDoc经常将构成素组的元素为Component,实际上就是将数组降低一个维度后的类型
助记符:
anewarray 表示创建一个引用类型的(如类、接口、数组)数组并将其引用值压入栈顶
newarray 表示创建一个指定的原始类型 如int float char的数组并将其引用值压入栈顶