JDK / JRE / JVM 的关系
JDK 开发工具包
(java development kit) 支持开发和运行 Java 程序。
JDK 包含 JRE 以及各种 Java 开发工具(如编译器 javac 、调试器 jdb 等)。
JRE 运行环境
(java runtime environment) 能够运行已编译的 Java 程序。
JRE 包含 JVM 以及运行时所需调用的基础类库(如 java.lang 包、 java.util 包等)。
JVM 虚拟机
(java virtual machine) 运行 Java 程序的工作环境。
Java 程序在虚拟机上运行而不是直接在操作系统上运行,从软件层面屏蔽了底层硬件指令的细节。虚拟机会根据操作系统自动将字节码文件转化成相应的机器码,使 Java 字节码文件能够在多种平台上不加修改地运行。
HotSpot 虚拟机 是 SunJDK 和 OpenJDK 中所带的虚拟机,也是目前使用范围最广的 Java 虚拟机。
Java 运行原理
基本概念
解释和编译
- 解释:源文件经过编译器编译成为脚本文件,由解释器逐行解释并执行。灵活性更好。
- 编译:源文件经过编译器编译成为可执行文件,由计算机直接去执行。性能更好。
静态编译和动态编译
- 静态编译:编译时确定类型,绑定对象。性能更好。
- 动态编译:运行时确定类型,绑定对象。能更好地支持多态,灵活性强。
Java 运行原理
Java 是编译和解释并行的语言,采取动态编译,支持反射机制。
- 源文件(.java) 经过编译器编译成为 字节码文件(.class) ,通过类加载器搬运到 JVM 中逐行解释并执行。
- 即时编译器(JIT) 在编译时会识别反复执行的热点代码(超过 10000 次)并保存机器码,复用时可直接由 JVM 执行。
优势:以虚拟机作为中介,字节码文件可以在所有操作平台上通用。即一次编译、到处运行。
劣势:但这也导致了 Java 语言性能不如 C/C 等编译语言。
类加载
在 Java 程序里如果使用某个尚未加载到内存中的类,JVM 会通过加载、链接、初始化 3 个步骤来对该类进行初始化。
创建类的实例,访问类的静态变量,或者调用类的静态方法都会导致类的初始化。但要注意对于 final 常量,如果在编译时就可以确定该变量的值,编译器会在编译时直接把这个变量替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。
加载
类的加载由类加载器完成。类加载器会读取类的字节码文件,并为之创建一个 java.lang.Class 对象。
类加载器使用双亲委派模型,类加载器具备一种带有优先级的层次关系:
- 根类加载器:负责加载 Java 的核心类,原生代码实现,并不继承自 java.lang.ClassLoader 类。
- 扩展类加载器:负责加载 JRE 扩展目录中的类。由 Java 语言实现,没有父类加载器。
- 系统类加载器:负责加载 CLASSPATH 路径中的类。由 Java 语言实现,父类加载器为扩展类加载器。
- 用户类加载器:开发者通过继承 ClassLoader 基类来创建的类加载器。由 Java 语言实现,默认父类加载器为系统类加载器。
任何类加载器在接到加载类的请求时,都会将加载任务委托给父类加载器,最终委派给处于模型最顶端的根类加载器进行加载。只有父类加载器无法完成此加载任务时,才自己去加载。如果均无法载入类,则抛出 ClassNotFountException 异常。
- 通过这种层级关系可以避免类的重复加载。
- 其次可以防止核心 API 库被随意篡改,用户即使编写了 java.lang.Object 的同名类,也永远无法被加载运行。
【在双亲委派模型中,由父加载类加载的类,下层加载器是不能加载的。用户在 classpath 路径下自定义的java.*包内的类会抛出异常:SecurityException: Prohibited package name】
链接
当类被加载并生成 Class 对象后,连接阶段负责把类的二进制数据合并到 JRE 中。
- 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。
- 准备:类准备阶段负责为类的静态变量分配内存,并赋予类型的默认值。
- 解析:将类的二进制数据中的符号引用替换成直接引用,指向具体的内存空间。
Java 是相对 C 语言是安全的语言,验证过程用于确保 Class 文件的字节流符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
初始化
为类的静态变量赋予程序设定的初始值。