jvm 有 hotspot, JRockit, HotRockit, J9, Dalvik等
JVM 生命周期
虚拟机的启动
Java 虚拟机的启动是通过引导类加载器(Bootstrap ClassLoader)创建一个初始类来完成的,这个类是由虚拟机的具体实现决定。
虚拟机的退出
1.某线程调用Runtime类或者System类中的exit方法,或者Runtime类中的halt方法,并且Java安全管理器也允许这次操作
2.程序正常结束
3.程序遇到了异常或者错误而终止
4.操作系统出现错误导致虚拟机进程终止
HotSpot JVM 架构
]
字节码文件
JVM 可以理解的代码就叫做字节码
,,它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
class 文件结构
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口数量
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//字段数量
field_info fields[fields_count];//一个类可以有多个字段
u2 methods_count;//方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}
魔数 magic
class文件的标志。它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。Java 规范规定魔数为固定值:0xCAFEBABE。如果读取的文件不是以这个魔数开头,Java 虚拟机将拒绝加载它。
版本号 minor_version & major_version
每当 Java 发布大版本(比如 Java 8,Java9)的时候,主版本号都会加 1。你可以使用 javap -v
命令来快速查看 Class 文件的版本号信息。高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件。
常量池
constant_pool_count
常量池计数器,两字节,表示常量池中有多少常量,从1开始。constant_pool_count=1表示常量池中有0个常量项。第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项。
constant_pool[constant_pool_count-1]
常量池数据表
常量池主要存放两大常量:字面量和符号引用。字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
tips
符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要无歧义定位到目标即可,与虚拟机实现的内存布局无关,引用的目标也不一定加载到了内存中
直接引用:可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
访问标志 access flags
当前类、父类、接口索引集合
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 java.lang.Object
之外,所有的 Java 类都有父类,因此除了 java.lang.Object
外,所有 Java 类的父类索引都不为 0。
接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 implements
(如果这个类本身是接口的话则是extends
) 后的接口顺序从左到右排列在接口索引集合。
字段表集合
方法表集合
属性表集合
类的加载
类的生命周期
加载 —> 验证 —> 准备 —> 解析 —> 初始化 —> 使用 —> 卸载
类的加载过程
加载
就是将 Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型,主要就是完成三项工作:
- 通过全类名获取定义此类的二进制字节流。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口。
每个 Java 类都有一个引用指向加载它的 ClassLoader
。不过,数组类不是通过 ClassLoader
创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()
方法获取 ClassLoader
的时候和该数组的元素类型的 ClassLoader
是一致的。
链接
验证
就是保证加载的字节码文件是合法的符合规范的,保证这些信息被当作代码运行后不会危害虚拟机自身的安全
准备
准备阶段是正式为类变量(静态变量)分配内存并设置类变量初始值的阶段
tip: 不包含 static final 修饰的情况,因为 final 在编译时就会分配了
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行
初始化
初始化阶段主要是执行类的初试化方法[HTML_REMOVED]()方法。该方法仅能由 Java 编译器生成并由 JVM 调用,开发者无法定义一个同名的方法,更无法调用,它是由类静态成员的赋值语句以及static语句合并产生的。
虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化:
1、当遇到 new
、 getstatic
、putstatic
或 invokestatic
这 4 条字节码指令时,比如 new
一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
- 当 jvm 执行
new
指令时会初始化类。即当程序创建一个类的实例对象。 - 当 jvm 执行
getstatic
指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。 - 当 jvm 执行
putstatic
指令时会初始化类。即程序给类的静态变量赋值。 - 当 jvm 执行
invokestatic
指令时会初始化类。即程序调用类的静态方法。
2、使用 java.lang.reflect
包的方法对类进行反射调用时如 Class.forname("...")
, newInstance()
等等。如果类没初始化,需要触发其初始化。
3、初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
4、当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main
方法的那个类),虚拟机会先初始化这个类。
5、MethodHandle
和 VarHandle
可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,
就必须先使用 findStaticVarHandle
来初始化要调用的类。
6、当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
卸载
卸载类即该类的 Class 对象被 GC。
需要满足:
-
该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
-
该类没有在其他任何地方被引用
-
该类的类加载器的实例已被 GC
也就是说,由 JVM 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
类加载器
类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步,类加载器是 JVM 加载类的前提。除了加载类之外,类加载器还可以加载 Java 应用所需的资源如文本、图像、配置文件、视频等等文件资源。
JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。
对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。
何为类的唯一性?
任意一个类,都需要由加载它的类加载器何这个类本省一同确认其在 JVM 中的唯一性。每一个类加载器都拥有一个独立的命名空间,比较两个类是否相同只有在两个类是由同一个类加载器加载的前提才有意义。否则,即使两个类源于同一 Class 文件,被同一个 JVM 加载,这两个类也是不同的。
类加载器的分类
JVM 中内置了三个重要的 ClassLoader
1、BootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib
目录下的 rt.jar
、resources.jar
、charsets.jar
等 jar 包和类)以及被 -Xbootclasspath
参数指定的路径下的所有类。
2、ExtensionClassLoader(扩展类加载器):主要负责加载 %JRE_HOME%/lib/ext
目录下的 jar 包和类以及被 java.ext.dirs
系统变量所指定的路径下的所有类。
3、AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
自定义类加载器
除了 BootstrapClassLoader
,类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader
抽象类。
实现自定义类加载器,就是实现两个关键方法 loadClass()
和 findClass()
如果我们不想打破双亲委派模型,就重写 ClassLoader
类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass()
方法。
实现一个简单的自定义类加载器
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class MyClassLoader extends ClassLoader {
private String path;
public MyClassLoader(String path) {
this.path = path;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
String filepath = this.classToFilePath(name);
byte[] data = this.getByte(filepath);
return this.defineClass(name, data, 0, data.length);
}
private byte[] getByte(String filepath) {
FileInputStream fileInputStream = null;
ByteArrayOutputStream b = null;
try {
fileInputStream = new FileInputStream(filepath);
b = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while((len = fileInputStream.read(buffer)) != -1) {
b.write(buffer, 0, len);
}
byte[] var6 = b.toByteArray();
return var6;
} catch (IOException var20) {
var20.printStackTrace();
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
} catch (IOException var19) {
var19.printStackTrace();
}
try {
if (b != null) {
b.close();
}
} catch (IOException var18) {
var18.printStackTrace();
}
}
return null;
}
private String classToFilePath(String name) {
String var10000 = this.path;
return var10000 + "\\" + name.replace(".", "\\") + ".class";
}
}
测试代码
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader myClassLoader1 = new MyClassLoader("C:\\Users\\Xie\\Desktop\\MyTreadPool\\out\\production\\MyTreadPool");
Class myclass1 = myClassLoader1.findClass("Task");
System.out.println(myclass1);
MyClassLoader myClassLoader2 = new MyClassLoader("C:\\Users\\Xie\\Desktop\\MyTreadPool\\out\\production\\MyTreadPool");
Class myclass2 = myClassLoader2.findClass("Task");
System.out.println(myclass1 == myclass2);
}
双亲委派机制
什么是双亲委派机制
ClassLoader
类使用委托模型来搜索类和资源。每个 ClassLoader
实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader
实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
虚拟机中被称为 “bootstrap class loader”的内置类加载器本身没有父类加载器,但是可以作为 ClassLoader
实例的父类加载器。
loadClass 源码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
执行流程:
在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。
类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载,
如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。
双亲委派机制的好处
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载。
如何打破双亲委派机制
最简单的方法就是重写 loadClass 方法,改变传统双亲委派模型的执行流程。
然后是利用线程上下文类加载器来反向委托应用程序类加载器。
Thread.currentThread().getContextClassLoader();
什么场景会碰到呢?
在 SPI 中,SPI 的接口是由 Java 核心库提供的,是启动类加载器加载的,但是接口的实现是是由第三方供应商提供的,它们是由应用程序类加载器或者自定义类加载器来加载的。比如,java.sql.Driver
是由启动类加载器,但是像类似 com.mysql.cj.jdbc.Driver
是由应用程序类加载器或者自定义类加载器来加载的。一般来一个类以及它的依赖类是由同一个类加载器加载,加载接口类也会加载它的实现,但是在双亲委派模型中启动类加载器是无法找到 SPI 的实现类的,因为它无法委托给子类加载器去尝试加载。
Tomcat 的类加载机制
tips:
tomcat 8 和 tomcat 6 的区别:tomcat 8 可以通过配置 <Loader delegate="true"\>
来遵循双亲委派机制
CommonClassLoader
对应<Tomcat>/common/*
CatalinaClassLoader
对应<Tomcat >/server/*
SharedClassLoader
对应<Tomcat >/shared/*
WebAppClassloader
对应<Tomcat >/webapps/<app>/WEB-INF/*
Common 类加载器是为了实现公共类库的共享和隔离,整个tomcat容器和web应用都可用。
Catalina 类加载器加载的类是tomcat 容器私有的,是 web 应用无法访问的
Shared 类加载器加载的类是对web应用共享的
WebApp类加载器,每个 Web 应用都会创建一个单独的 WebAppClassLoader
,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 WebAppClassLoader
。各个 WebAppClassLoader
实例之间相互隔离,进而实现 Web 应用之间的类隔。
JSP 类加载器一个 JSP 文件对应一个
在顶层的类加载器完成之后,会直接到 WebApp 类加载器加载 /WEB_INF/class
下的字节码文件和/WEB_INF/lib
下的 .jar
包,在找不到之后再继续用上层加载器加载
大佬看的谁的课
尚硅谷,和各种blog