你所知道的JVM前置知识
本系列是为面试,更深的可以参看周志明那本国内最好的JVM书(要出第3版了,不要买第二版)
更好的阅读体验【长期有效】
java代码是如何运行起来的?
假设你写好了一堆Java代码,正准备把它们部署到你的服务器上。一般来说是把但代码打包成war/jar
包然后部署到服务器上。当然这个部署有很多途径,最基本的方式就是通过Tomcat
这类容器,也可以是自己手动通过 java -jar
这种命令来运行 jar
包中的代码,但是实际上这有一个十分关键的步骤,就是 编译
。
也就是说,我们写好 java
代码打包的过程中一般就是把代码编译成 .class
字节码文件,而这个文件才是可以被运行起来的。
对于这些编译好的 .class
字节码文件,需要我们使用诸如 java -jar
的命令来运行。此时一旦你采用java
命令,实际上此时是启动会一个 JVM
进程。
接下来,JVM
要运行这些字节码文件,首先要把 .class
文件包含的各个类加载进来,而这些 .class
文件就是我们写好的一个个类。此时就会有一个 类加载器 的概念。此时会采用类加载器把编译好的这些字节码文件加载到 JVM
中,给后续代码运行来使用。
最后,JVM
会基于自己的执行引擎,来执行加载到内存里的我们写好的类。JVM
会从 main()
为入口开始执行,当它需要哪个类时就会加载对应的类,反正对应的类就在字节码文件里面。
类加载机制是什么?
下面就平时工作实用角度去讨论这个问题,主要是把握他的核心原理就行。
一个类从加载到使用,一般会经历这几个过程:
加载 验证 准备 解析 初始化 使用 卸载
首先我们要明白 JVM
在执行字节码文件时,一般在什么情况下我们需要加载一个类?
很简单,就是你的代码需要这个类的时候(好像是句废话诶)。简单来说:首先你的代码中包含 main()
的主类一定会在 JVM
进程启动之后被加载到内存,开始执行 main()
里面的代码,如果此时遇到别的类,就会从相应的字节码文件加载对应的类到内存。
接着来过一下 验证、准备、初始化 这3个概念:
1. 验证
简单来说就是根据 java
虚拟机规范,来校验加载进来的字节码文件的内容是否符合指定的规范。这个很好理解,比如你的字节码文件被别人恶意篡改而不符合规范,JVM
压根没法执行这个文件的。
2. 准备
准备工作其实就是给加载到内存的类分配一定的内存空间(不然它放哪,也符合OS的内存保护机制)。然后给里面的类变量(static修饰的)分配空间,给一个默认初始值。
3. 解析
这个阶段实际上是把 符号引用替换为直接引用 的过程,但是这个过程很复杂,涉及到JVM
底层。(不过从实用角度,大家在工作中实践JVM
技术其实用不到,知道有这么一个阶段就行。)
在这3个阶段中最重要的就是 准备阶段 。因为这个阶段是给加载 的类分配空间,类变量也分配好空间,并给默认初始值。这个概念,大家一定要有!
4. 初始化
在准备阶段已经为类分配好内存空间了,类变量也给了默认初始值,那么接下来就要正式执行类初始化代码了。那么 什么是类初始化代码呢?
public class Manager {
public static int interval = Configuration.getInt("interval");
}
可以看出,interval
这个类变量,我们是打算在执行完 Configuration.getInt("interval");
把这个结果赋值给它的。但是 在准备阶段是不会执行这句代码的 。而这句代码是在 初始化阶段 执行。
在这个阶段,会执行类的初始化代码,另外比如:
public class Manager {
public static int interval = Configuration.getInt("interval");
public static Map<String, String> repl;
static {
load();
}
public static void load() {
this.repl = new HashMap();
}
}
上述的 static静态代码块 ,可以理解为调用 load()
完成了静态变量 repl
初始化。
说完了什么是类初始化,接下来看看类初始化的规则,也就是 什么时候会初始化一个类?
一般来说有以下一些时机:
new Manager();
,实例化类的对象时会触发类的加载到初始化的全过程;包含
main()
方法的 主类 ,会立马初始化;如果初始化一个类时,发现他的父类没有初始化,那就 必须先初始化他的父类 ;
java public class Manager extends AbstractManager { public static int interval = Configuration.getInt("interval"); public static Map<String, String> repl; static { load(); } public static void load() { this.repl = new HashMap(); } }
在初始化Manager
之前会先初始化AbstractManager
。
明白了整个类从触发到初始化的过程,我们回到 类加载器 这个概念。因为完成以上必须依靠这个才能完成。那么 Java
里有哪些类加载器?简单来说说下面几种:
1. 启动类加载器
Bootstrap ClassLoader 主要负责加载我们在机器上安装的 Java
目录下的核心类。这个位置在 lib/
,目录下的都是Java
最核心的一些类库。JVM
一旦启动,首先就会依托类加载器去加载核心类库。
2. 扩展类加载器
Extension ClassLoader 与上面类似,在你安装 JDK
目录下的 lib/ext
下的类,需要使用这个类加载器来加载从而支撑你的系统运行。
3. 应用程序类加载器
Application ClassLoader 负责去加载 ClassPath
环境变量所指定的路径的类。其实你理解为去 加载你写好的 Java
代码就行(可以回想一下Spring
的配置文件,就是放在ClassPath
)。
4. 自定义类加载器
除上面几种外,可以自定义类加载器根据你的需求去加载你的类。
介绍完了有哪些类加载器,我们再来看看加载器的层级关系:
而基于这个亲子层级关系,就有一个 双亲委派机制。
通俗解释:现在你的应用程序需要加载一个类,JVM
会依照这么一个顺序去加载。然后一句话:先去让父亲去加载,不行再由儿子来加载(其中省去了一些举例,这是最通俗的解释)。这样做的可以避免多层级的加载器结构重复加载某些类。
对这个机制多说几句:
- 双桥委派模型要求:所有的类加载器有应有父类加载器(除了顶层的启动类加载器);
- 类加载器的父子关系是以 组合关系 来父加载器的代码,而不是以 继承关系 ;
- 这个模型是Java设计者推荐给开发者的一种类加载器实现方法,不是强制性的约束模型。
所以这里举个Tomcat
的类加载器的例子:
Tomcat
自定义了Common
,Catalina
,Shard
等类加载,其实就是原来加载Tomcat
自己的一些核心类库。然后Tomcat
为每个部署在里面的Web应用都有一个WebApp
类加载器,负责加载我们部署的Web应用的类。至于Jsp
类加载器,则是给每个JSP都准备了一个加载器。
大家一定要记住,Tomcat
是打破了双亲委派机制 。
每个WebApp
赋值加载自己对应的那个WebApp应用的class文件,也就是我们写好的某个系统打包好的war包中的所有class文件,不会传导给上层的类加载器去加载。
如果大家感兴趣,可以研究一下Tomcat
的类加载机制。
好吧,之前跳票的
Mysql
&&面试
系列也会更起来的
写的很好,从实用的角度出发。
找到个错别字~
双桥委派模型要求 -> 双
亲
委派模型要求hhh,好的。