JAVA虚拟机:
一、如上图所示,JAVA虚拟机运行时主要由以下三个部分构成:
A. 本地库接口
负责把描述类的数据从Class文件加载到内存,并对数据进行校验、装换解析、以及初始化,最终形成可以被虚拟机直接使用的Java类型。
在Java语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的。
B. 运行时数据区
1)方法区。
2)堆。
3)虚拟机栈。
4)本地方法栈。
5)程序计数器。
C. 执行引擎
输入的是字节码文件,处理过程是字节码解析的过程,输出的是执行结果。
二、JVM生命周期:
JVM实例对应了一个独立运行的Java程序,它是进程级别。
JVM执行引擎实例则对应了属于用户运行程序的线程,它是线程级别。
A. 启动:
启动一个Java程序时,一个JVM实例就产生了,任何一个拥有 public static void main(String[] args) 函数的class都可以作为JVM实例的运行起点。
B. 运行:
main() 作为该程序初始线程的起点,任何其它线程均由该线程启动,JVM内部有两种线程(守护线程 和 非守护线程),main()属于非守护线程,守护线程通常由JVM自己使用,Java程序也可以标明自己创建的线程是守护线程。
C. 消亡:
当程序中的所有非守护线程终止时,JVM才退出,若安全管理器允许,程序也可以使用Runtime类或者System.exit()退出。
知识拓展
守护线程:
是指为其它线程服务的线程,在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
JVM退出时,不关心守护线程是否已结束。
创建守护线程,方法跟普通线程一样,只是在调用start()方法前,调用 setDaemon(true) 把该线程标记为守护线程。
非守护线程:
是指用户线程。
三、本地库接口:
被所有线程共享,虚拟机把描述类的数据从class文件通过本地库接口加载到内存,并对数据进行校验、装换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
A. 类加载器:
1. 启动类加载器
启动类加载器(Bootstrap ClassLoader):是用来加载Java的核心类,负责加载 $JAVA_HOME 中 jre/lib/rt.jar 里所有的class,用C++实现,不是ClassLoader的子类。
2. 扩展类加载器
扩展类加载器(extensions ClassLoader):负责加载jre的扩展目录,lib/ext或者由 java.ext.dirs 系统属性指定的目录中的JAR包的类,用Java语言实现,父类加载器为null。
3. 系统类加载器
系统类加载器(System ClassLoader):负责加载启动参数中指定的classpath中的jar包以及目录,在Sun JDK中对应的类名为Application ClassLoader。
4. 用户自定义类加载器
用户自定义类加载器(User-Defined ClassLoader):自定义的ClassLoader,负责加载非classpath中的jar以及目录,必须是ClassLoader类的子类。
4.1):继承java.lang.ClassLoader。
4.2):重写父类的findClass方法。
B. 类加载机制:
1. 双亲委派
双亲委派的工作原理:
1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。
3)如果父类加载器可以完成类加载任务,就成功返回;倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。
双亲委派机制的优势:
1)避免类的重复加载:当父类已经加载了该类时,就没有必要子ClassLoader再加载一次。
2)防止核心API库被随意篡改:假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已经被加载,便不会重新加载。
2. 全盘负责
所谓全盘负责,就是当一个类加载器加载某个Class时,该Class所依赖和引用其它Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
3. 缓存机制
所谓缓存机制,就是当一个类加载器加载某个Class时,类加载器先从缓存区中搜索该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。
这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
C. 类在内存中的生命周期:
加载 -> 链接(验证、准备、解析) -> 初始化 -> 使用 -> 卸载
1. 加载
加载过程就是负责找到二进制字节码并加载至 JVM 中。
1)通过一个类的全限定名(类名、类所在的包名)来获取定义此类的二进制字节流。
1)将这个字节流所代表的静态存储结构转化为方法区运行时的数据结构。
1)在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
1)加载 class 文件的方式:
a) 从本地系统中直接加载。
b) 通过网络下载 class 文件。
c) 从 zip,jar 等归档文件中加载 class 文件。
d) 从专有数据库中提取 class 文件。
e) 将 Java 源文件动态编译为 class 文件。
2. 链接
链接过程负责对二进制字节码的格式进行校验,初始化装载类中的静态变量以及解析类中调用的接口、类。
1)验证
是链接的第一步,确保 class 文件的字节流中包含的信息符合当前虚拟机的要求;
a) 文件格式验证:验证字节流是否符合 class 文件格式的规范。
b) 元数据验证:对字节码描述的信息进行语义分析,以保证其符合Java语言规范的要求。
c) 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
d) 符号引用验证:确保解析动作能正确执行。
2)准备
准备阶段是正式为类变量(static成员变量)分配内存并设置类变量默认值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。
只对static修饰的静态变量进行内存分配、赋默认值(零值)。
对final的静态字面值常量直接赋初始值。
3)解析
将常量池中的符号引用替换为直接引用(内存地址)的过程。
符号引用:用一组符号来描述目标,可以是任何形式的字面量。
直接引用:可以是直接指向目标的指针、相对偏移量 或是一个能间接定位到目标的句柄,如指向方法区某个类的一个指针。
什么是符号引用 ?
在Java中,一个Java类将会编译成一个class文件,在编译时Java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。
例:org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假如是这个)来表示Language类的地址。
3. 初始化
为类的静态变量 赋初值。
注意:只有对类的主动使用才会导致类的初始化。
D. 类加载时机:
1. 创建类的实例,也就是 new 一个对象。
2. 访问某个类或接口的静态变量