前言
JVM偏理论,请坚持。
一些相关工具有:jconsole,jvisualvm.exe,jmap,jps,jstack,,在控制台直接用即可。
JVM是?
- 与其说Java是一个非常不错的语言,且设计的非常不错,不如说是JVM的平台设计的非常好。
- 因为它不仅支持Java这样优秀的语言,它还支持其它语言,如Kotlin、Scala。。
- 也就是说Java和JVM并不是一种紧密的绑定关系。
- 也就是说在JVM上运行的并不是语言,而是class文件,即字节码文件,任何语言只要能翻译成JVM说能识别和运行的字节码,它就可以在JVM上运行。
- HotSpot 是sun公司的JVM实现,还有一些商用的JVM的实现。
- JVM是有规范的,只要根据JVM的规范你就能实现你自己的JVM。
类加载(ClassLoading)
在Java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的
-
类型:是我们平时写代码时定义的class,interface,枚举等,称之为类型。并不是对象!!!
- 类型的加载是在运行期间(Runtime)完成的。把运行期间的类型有机的装配在看一起。
- Java是静态的语言,但却有动态语言的一些特性。
- 因为它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。
- 比如Java的反射机制,就可以体现其动态性。
-
类的加载一般分三个阶段完成的 :类型的加载、连接与初始化。
-
典型的类型加载,就是将磁盘中的class文件加载到内存中。(并不是唯一的方式)
-
连接:有很多步骤,简单来说就是,确定类与类之间的关系,以及字节码的校验之类的,还有类与类之间的调用关系,比如符号引用转换为直接引用等,就在这个阶段完成的。
你可能会说字节码有啥好校验的,只要Java代码没错,字节码怎么会错?一般情况下不会错,但我手动打开class文件,修改里面的内容,它就可能出错了,所以需要校验。
-
初始化:就是给类中的静态变量赋值。
并不是严格的按照以上三个步骤进行,有可能在类型的加载过程中进行连接操作,不同的JVM实现会有一些差别。
类加载器(ClassLoader)
就是将类型加载进JVM内存的工具,所有的类型想进入JVM内存就要通过类加载器。
JVM与程序的生命周期
在如下几种情况下,Java虚拟机将结束生命周期
- 执行了
System.exit()
方法程序正常执行结束。 - 程序在执行过程中遇到了异常或错误而异常终止。
- 就是写代码时没有catch异常,导致异常传递到了main方法处,导致JVM和程序终止。
- 由于操作系统出现错误而导致Java虚拟机进程终止。
- 这是不可抗力的。
类的加载、连接、初始化、使用和卸载
类的加载
- 查找并加载类的二进制数据
连接
- 验证: 确保被加载的类的正确性。
- 准备: 为类的静态变量分配内存,并将其初始化为默认值。(int 0,boolean false)
public static int a = 1;//a并不直接等于1,而是会给其一个默认值0。
- 解析:把类中的符号引用转换为直接引用。
a = classA; c = a;//c引用符号a,在解析时c 会直接指向classA。并不会管a是什么东西。 //或者说,a和c都是符号,是一种字面量,在运行时要确定具体的内存地址或偏移量。 //就是将符号变成真正的具体的内存地址或偏移量。
初始化
- 为类的静态变量赋予正确的初始值。
public static int a = 1;//在连接阶段赋予的默认值0,此时会被正确的赋予初始值1
使用
- 比如说根据类创建对象。
- 调用类的相关方法。
- 等等
卸载
- 将内存中的类给清除掉,也就不能在创建对象了。
更进一步
类的加载
类的加载是?
- 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区(元空间)内,然后在内存中创建一个java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用来封装类在方法区内的数据结构。
- 无论生成多少个实例,最终的Class对象只有一份。
- Class对象就像一面镜子,可以洞悉到Class里面的所有内容和结构,其实这就是反射的根源。
类的加载方式是?
- 从本地系统中直接加载。
- 通过网络下载.class文件。
- 从zip,jar等归档文件中加载.class文件。
- 从专有数据库中提取.class文件。
- 将Java源文件动态编译为.class文件。
- 比如动态代理
- 比如运行jsp时,jsp会被翻译成servlet进一步成为.class
初始化
在说之前, Java程序对类的使用方式有?
- 主动使用。
- 被动使用。
什么时候发生初始化?
- 所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们。
- 换句话说,被动使用时不初始化类。
主动使用指的是?
- 创建类的实例。
new Thread();
- 访问某个类或接口的静态变量,或者对该静态变量赋值。
System.out.println(StaticClass.value);//访问静态变量。 StaticClass.value = "hello world";//给静态变量赋值。
- 调用类的静态方法。
StaticClass.function();
- 反射
Class.forName("com.test.Test")
- 初始化一个类的子类。
- Java虚拟机启动时被标明为启动类的类
- 最常见的main方法所在的类。
- 还有单元测试,Junit等。
- JDK1.7开始提供的动态语言支持:
- java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化。(较罕见)。
被动使用指的是?
- 除了上述讲的7种主动使用,其它都是被动使用。
用一些代码来说明类的初始化
众所周知,静态代码块在没有new的情况下,是会自动运行的。
- 代码
public class InitTest {
public static void main(String[] args) {
System.out.println(Child.str);
}
}
//static 代表着Class
class Parent{
public static String str = "hello world";
static {
System.out.println("Parent static block");
}
}
class Child extends Parent{
static {
System.out.println("Child static block");
}
}
- 结果
- 可以发现子类的静态代码块并没有运行。
- 可以发现子类并没有满足 “首次主动使用” 的条件,它没有被初始化。
- 父类满足了“主动使用”的条件---访问静态变量(在main方法中访问了str)。
- 就进一步得出结论,静态代码块是在类加载中的初始化后执行的。
- 对于静态字段来说,只有直接定义了该字段的类才会被初始化。
- 小小的修改一下代码,访问子类静态变量,让其父类初始化
public class InitTest {
public static void main(String[] args) {
System.out.println(Child.string);//访问子类静态变量,导致父类初始化
}
}
//static 代表着Class
class Parent{
public static String str = "hello world";
static {
System.out.println("Parent static block");
}
}
class Child extends Parent{
public static String string = "Welcome to China";
static {
System.out.println("Child static block");
}
}
- 结果
- 当一个类在初始化时,要求其父类全部都已经初始化完毕了。
对于没有被初始化的类,它有被加载吗?
- 从第一个代码示例可知,只有在类初始化时,才会导致静态代码块的执行。
- Child类的静态代码块没有执行,也就没有被初始化,那如何知道它有没有被加载嘞?
- 可以用JVM的运行参数来获取类是否被加载。
-XX:+TraceClassLoading
用于追踪类的加载信息并打印出来。
VM选项
- 运行结果
- 学到了,类在被初始化时,才会导致静态代码块的执行。
- 即使类没有被初始化也会被加载。
关于VM参数
- 形式是固定的
-XX:+<option>//表示开启option选项
-XX:-<option>//表示关闭option选项
/*
还有常见的调整JVM内存大小的参数,形状如下:
*/
-XX:<option>=<value>//表示将option选项的值设置为value