1.java虚拟机的生命周期:
Java虚拟机的生命周期 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。程序开始执行时他才运行,程序结束时他就停止。你在同一台机器上运行三个程序,就会有三个运行中的Java虚拟机。 Java虚拟机总是开始于一个main()方法,这个方法必须是公有、返回void、直接受一个字符串数组。在程序执行时,你必须给Java虚拟机指明这个包换main()方法的类名。 Main()方法是程序的起点,他被执行的线程初始化为程序的初始线程。程序中其他的线程都由他来启动。Java中的线程分为两种:守护线程(daemon)和普通线程(non-daemon)。守护线程是Java虚拟机自己使用的线程,比如负责垃圾收集GC的线程就是一个守护线程。当然,你也可 以把自己的程序用setDeamon设置为守护线程。包含Main()方法的初始线程不是守护线程。 只要Java虚拟机中还有普通的线程在执行,Java虚拟机就不会停止。如果有足够的权限,你可以调用exit()方法终止程序。
1.1 结束一个JVM生命周期的方式
- System.exit
- Normal finish done
- Encounter the error or exception
- Crash
- OS problems and others
1.2类加载的过程
类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。
其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。
1)加载
加载阶段主要完成下面三件事情:
通过一个类的全限定名来获取定义此类的二进制字节流。
将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构(这里可以理解为字节流的格式是虚拟机规范规定的,而每个虚拟机中对Java类的数据结构有自己的规定,这一步将外部的二进制字节流转换为虚拟机需要的格式存储在方法区之中)。
在内存中共生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据结构的访问入口。
上面三个阶段中的第一个步骤”通过一个类的全限定名来获取定义该类的二进制字节流”是开发人员可控性最强的,因为虚拟机规范并没有规定一定要从Class文件中获取,所以可以通过定义自己的类加载器来完成(通过重写一个类加载器的loadClass()方法),可以实现从jar、zip、war等压缩包中读取,也可以同网络中获取,甚至可以在运行是计算生成(例如java的动态代理就是利用ProxyGenerator.generateProxyClass()来为特定接口生成代理类的二进制字节流)。
2)验证
验证是连接阶段的第一步,这一步的目的是为了确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会威胁虚拟机自身的安全。其实如果纯粹是从Java源码编译得到的Class文件,自身是可以确保安全的,但是因为Class文件可以由任何途径产生(甚至可以由十六进制编辑器直接编写来产生Class文件),所以虚拟机很有必要对输入的字节流进行验证以维护自身的安全。
验证阶段需要完成四个阶段的校验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
文件格式验证主要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,这个阶段是基于二进制字节流进行的,验证的目的是为了保证输入的字节流符合Class文件规范能够正确的解析并存储于方法区内,通过这个阶段的校验之后字节流才能进入到内存的方法区中进行存储,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。元数据验证和字节码验证主要是对字节码的语义分析和对数据流和控制流进行分析,做一些确保代码逻辑的验证工作;最后的符号引用验证发生在虚拟机将符号引用转换为直接引用的时候,这个转换动作将在链接的第三个阶段–解析阶段。符号引用验证可以看成是对类自身以外的信息进行匹配性校验的过程,简单的来说就是看下符号引用通过字符串形式描述的类是否存在并且类的定义是否规范。
3)准备
准备阶段是正式为类实例变量分配内存并且设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里的类变量指的是被static修饰的变量,不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在java堆中;另外这里的初始值不是代码中指的初始值而是变量数据类型对应的零值(int为0,String为null等)。
4)解析
解析阶段简单的来说就是虚拟机将常量池内的符号引用替换为直接引用的过程。具体的过程牵涉到Class文件格式中符号引用的规定,在这里就不展开了。
5)初始化
初始化是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序通过自定义类加载器参与之外,其余动作完全由虚拟机主导。到了初始化过程,才会真正开始执行类中定义的Java程序代码。这一步主要就是执行静态变量的初始化,包括静态变量的赋值和静态初始化块的执行。这里需要引入一个类构造器<clinit>()方法,下面对其进行简单的介绍:
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态初始化块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态初始化块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量静态语句块中可以赋值但是不能访问(这里有一点需要注意,静态初始化块和静态变量赋值语句执行顺序是按定义顺序来的,并不是说初始化块一定会在静态赋值语句之后执行)。
<clinit>()方法与类的构造函数不同,它不需要显示的调用父类的<clinit>()方法,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
<clinit>()方法对于类或接口来说不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
接口中不能使用静态初始化块,但是仍有static变量的赋值操作,所以也会有<clinit>()方法,但是接口执行<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用到时,才会执行<clinit>()方法。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其它线程都需要阻塞等待。
类加载器的任务就是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例。
BootstrapClassLoader、ExtClassLoader和AppClassLoader
defineClass方法将字节码的byte数组转换为一个类的class对象实例,如果希望在类被记载到JVM时就被链接,那么可以调用resolveClass方法。
自定义类加载器需要继承抽象类ClassLoader,实现findClass方法,该方法会在loadClass调用的时候被调用,findClass默认会抛出异常。
findClass方法表示根据类名查找类对象
loadClass方法表示根据类名进行双亲委托模型进行类加载并返回类对象
defineClass方法表示跟根据类的字节码转换为类对象
1.3 主动使用的分类
- new,直接使用
- 访问某个类或者接口的静态变量,或者对该静态变量进行赋值操作
- 调用静态方法
- 反射某个类
- 初始化一个类
- 启动类,比如:java HelloWorld
除了上述六个以外,其余的都是被动使用,不会导致类的初始化。
public class Singleton {
public static int x = 0;
public static int y;
private static Singleton instance = new Singleton();
private Singleton(){
x++;
y++;
}
public static Singleton getInstance(){
return instance;
}
public static void main(String[] args) {
Singleton singleton = getInstance();
System.out.println(singleton.x);
System.out.println(singleton.y);
}
}
1
1
public class Singleton {
private static Singleton instance = new Singleton();
public static int x = 0;
public static int y;
private Singleton(){
x++;
y++;
}
public static Singleton getInstance(){
return instance;
}
public static void main(String[] args) {
Singleton singleton = getInstance();
System.out.println(singleton.x);
System.out.println(singleton.y);
}
}
0
1
网友评论