目录
一、类加载的阶段
当我们的Java代码编译完成后,会生成对应的 .class 文件。接着我们运行java Demo
命令的时候,我们其实是启动了JVM 虚拟机执行 class 字节码文件的内容。而 JVM 虚拟机执行 class 字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。重点介绍前五个。
1.1 加载
JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存的方法区中。内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
_java_mirror 即 java 的类镜像(instanceMirrorKlass)。
例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
_super 即父类
_fields 即成员变量
_methods 即方法
_constants 即常量池
_class_loader 即类加载器
_vtable 虚方法表
_itable 接口方法表
-如果这个类还有父类没有加载,先加载父类。
流程:
- 通过类的全限定名获取存储该类的class文件(字节码文件)
这里获取的class文件是二进制字节流- 解析成运行时数据,即InstanceKlass实例,存在方法区
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构- 在堆区生成该类的Class对象,即InstanceMirrorKlass实例
注意:
-instanceKlass 存储在方法区(元空间)
-类镜像(InstanceMirrorKlass)存在于内存的堆中。
补充:
- class是java类(java代码)
- klass是java类在JVM的存在形式(c++代码)
- Klass类分别由InstanceKlass和ArrayKlass继承。其中InstanceClass用来表示普通的java类,ArrayKlass是来表示Java数组的元信息。
- class对象和存放元信息的结构是分开的(元信息是在InstanceKlass,class对象是在InstanceMirrorKlass)
1.2 验证
该阶段验证类是否符合 JVM规范(版本是否规范),进行代码的逻辑校验(对代码组成的数据流和控制流进行校验,防止出现致命错误)。
1.3 准备
这个阶段为类变量分配内存空间。
Java 中的变量有「类变量」(static修饰)和「类成员变量」两种类型。在该阶段,只为【类变量】(static修饰)分配内存空间(赋默认值),若其被final修饰就赋实际值。其他变量等到初始化(new 对象)时才分配。
1.4 解析
这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。
1.5 初始化
到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化。(非final修饰的static变量、普通变量赋值)
- 概括地说,类初始化是【懒惰的】 ,如下情况会触发类初始化:
1.main 方法所在的类,总会被首先初始化
2.首次调用这个类的静态变量或静态方法时
3.子类初始化,如果父类还没初始化,会先触发父类的初始化
4.子类访问父类的静态变量,只会触发父类的初始化
5. 使用new关键字实例化对象的时候
6.使用反射对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
- 以下情况不会触发类的初始化:
1.访问类的 static final 静态常量(基本类型和字符串)(编译期已经初始化好了)
2.类对象.class 不会触发初始化
3.创建该类的数组不会触发初始化
二、类加载器
类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成java.lang.Class
类的一个实例。每个这样的实例用来表示一个Java 类。通过此实例的 newInstance()
方法就可以创建出该类的一个对象。
1)BootStrap:引导类加载器:加载都是最基础的文件,一般清况用户无法直接使用
2)ExtClassLoader:扩展类加载器:加载都是基础的文件
3)AppClassLoader:应用类加载器:三方jar包和自己编写java文件
2.1 双亲委派模式
所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则。
类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。
Java 虚拟机在判断两个JAVA类是否相同时不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。
类加载器在成功加载某个类之后,会把得到的java.lang.Class
类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即loadClass
方法不会被重复调用。
2.2 线程上下文类加载器
首先明确一点,Bootstrap ClassLoader、ExtClassLoader、AppClassLoader是真实存在的类,遵从”双亲委托“的机制。而ContextClassLoader其实只是一个概念,它是Thread类一个成员变量。
- 为什么要引入ContextClassLoader?
java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。例如JDBC。
这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。
问题在于Bootstrap ClassLoader无法找到 SPI 的实现类,因为它只加载 Java 的核心库。但根据“双亲委派机制”,它也没办法交给AppClassLoader来加载。所以必须打破这个规则。
- 使用
通过Thread类的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)获取和设置线程的上下文类加载器。
Thread默认集成父线程的 Context ClassLoader(注意,是父线程不是父类)。如果整个应用中没有对此作任何处理,那么所有的线程都会以System ClassLoader(AppClassLoader)作为线程的上下文类加载器。
2.3 自定义类加载器
使用场景:
1)想加载非 classpath 随意路径中的类文件
2)都是通过接口来使用实现,希望解耦时,常用在框架设计
3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
定义方式:
1. 继承 ClassLoader 父类
2. 要遵从双亲委派机制,重写 findClass 方法 注意不是重写 loadClass 方法,否则不会走双亲委派机制
3. 读取类文件的字节码
4. 调用(return)父类的 defineClass 方法来加载类
使用:
使用者调用该类加载器的 loadClass 方法
示例:
class MyClassLoader extends ClassLoader {
@Override // name 就是类名称
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = "e:\\myclasspath\\" + name + ".class";
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path), os);
// 得到字节数组
byte[] bytes = os.toByteArray();
// byte[] -> *.class
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类文件未找到", e);
}
}
}
//测试
MyClassLoader classLoader = new MyClassLoader();
Class<?> c1 = classLoader.loadClass("MapImpl1");
三、运行期优化
3.1 分层编译
假设我们使用While循环创建Object对象,会发现刚开始时对象创建速度较慢,到后来突然变快了,这就涉及到了即时编译的优化了。
- JVM 将执行状态分成了 5 个层次:
0 层,解释执行(Interpreter)
1 层,使用 C1 即时编译器编译执行(不带 profiling)
2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
4 层,使用 C2 即时编译器编译执行
(profiling 是指在运行过程中收集一些程序执行状态的数据)
- 即时编译器(JIT)与解释器的区别
-解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释 -JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
-解释器是将字节码解释为针对所有平台都通用的机器码
-JIT 会根据平台类型,生成平台特定的机器码
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。
执行效率上简单比较: Interpreter < C1 < C2,
3.2 方法内联
对于热点方法,也会进行优化:内联
所谓的内联就是把方法内代码拷贝、 粘贴到调用者的位置
例如:
//这是一个方法,被反复调用
private static int square(final int i) {
return i * i;
}
//原始代码
System.out.println(square(9));
//优化后的代码
System.out.println(9 * 9);
//甚至能直接优化为这样
System.out.println(81);
3.3 反射优化
假设我们现在使用反射来反复调用一个方法,可以发现刚开始调用的时间较长,在执行16次后调用速度突然变快了。查看源码可以得知:
1.前15次反射调用,是通过代理类DelegatingMethodAccessorImpl去调用NativeMethodAccessorImpl.invoke0这个方法,触发run方法执行,此invoke是native调用;
2.第15次反射调用的时候,该方法反射调用的次数,大于inflationThreshold默认值15,开始进行反射优化,将DelegatingMethodAccessorImpl的代理对象被替换为动态生成的GeneratedMethodAccessor;
3.第15次后的反射调用,调用GeneratedMethodAccessor.invoke方法,再调用run方法,此invoke方法是纯Java调用。
综上,我们可以得出上面的结论:
方法反射调用的过程中,会把耗时的native调用替换为纯Java调用,这样JVM可以对字节码进行优化,来提高反射执行的效率。 优化阈值inflationThreshold 是可以调整的。