概述
虚拟机从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目间没有任何分隔符。当遇到8位字节以上空间的数据项时,则会按照高位在前的方式分隔成若干个8位字节进行存储。
类加载子系统工作步骤
- 类加载子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识;
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
- 加载的类信息存放于一块成为方法区的内存空间。除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
1.加载
1.1 什么时候会加载类
- 遇到new、getstatic、putstatic、invokestatic字节码指令时
- 使用java.lang.reflect的方法对类进行反射时
- 初始化一个类时,发现父类还没初始化时
- 虚拟机启动时,用户指定的主类时
- 使用JDK1.7的动态语言支持时
1.2 从哪加载
- 从zip包加载,如JAR,WAR等
- 从网络中获取,如applt
- 运行时计算生成,如java.lang.reflect.proxy中的"*$Proxy"就是类的二进制流
- 由其他文件生成的,如JSP
- 从数据库中读取,这种场景比较少见
1.3 怎么加载
- 通过一个类的全限定名获取定义此类的二进制字节流;
- 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据;
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
2.链接
2.1 验证
- 目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机的安全
- 主要包括四种验证,文件格式验证,源数据验证,字节码验证,符号引用验证。 (待补充)
2.2 准备
- 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段
- 这里进行分配的是类变量(被static修饰过)而不包括实例变量
- 类不会为实例变量分配初始化,类变量会分配在方法去中,而实例变量是会随着对象一起分配到java堆中
2.3 解析
- 解析阶段是虚拟机将常量池中的符号引用转换为直接引用
符号引用是:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。
直接引用是:有了直接引用,那引用的目标必定已经被加载入内存中了.
i.直接指向目标的指针(比如,指向Class对象、类变量、类方法的直接引用可能是指向方法区的指针)
ii.相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
iii.一个能间接定位到目标的句柄
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info/CONSTANT_Fieldref_info、CONSTAT_Methodref_info等。
- 解析步骤:
类或接口解析:当前类为A,假如符号引用为B,需要解析成类或接口C的至直接引用,三个步骤
i.如果C不是数组类型,则将代表类B的全限定名传递给A,由A的类加载器去加载C
ii.如是C是数组类型,并且数组内为对象,将会按第一点的规则去加载对象,接着有虚拟机生成一个对象,代表数组维度和元素的数据对象
iii.上述没异常,则已经生成对象,接着验证一下A是否有对C的访问权限,无权限则报java.lang.IIlegalAccessError异常
字段解析:要解析字段,首先将字段所属的类或接口的符号引用,如这步出现异常,则直接失败
i.如果C本身包含了引用字段,则查找结束
ii.如果C中实现了接口或父类,则由下向上递归查找各个接口和父类,如果包含了,则查找结束
iii.查找结束后,验证权限,无权限则报java.lang.IIlegalAccessError异常。如果查找失败,抛出java.lang.NoSuchFieldError异常
类方法解析:类方法和接口方法的符号引用是分开的,如果发现方法中表明C是接口的话,则抛出java.lang.IncompatibleClassChangeError异常.
i.如果C本身包含了引用方法,则查找结束
ii.如果C中实现了父类,则由下向上递归查找各个父类,如果包含了,则查找结束。 如果C是抽象类,则抛出java.lang.AbstractMethodError异常
iii.查找结束后,验证权限,无权限则报java.lang.IIlegalAccessError异常。如果查找失败,抛出java.lang.NoSuchFieldError异常
接口方法解析:如果发现方法中表明C是类的话,则抛出java.lang.IncompatibleClassChangeError异常.
i.如果C本身包含了引用方法,则查找结束
ii.如果C中实现了父接口,则由下向上递归查找各个父接口,如果包含了,则查找结束 iii.查找失败,抛出java.lang.NoSuchFieldError异常,接口种方法默认是public的,不存在访问权限的问题
3.初始化
- 初始化就是执行字节码中的()方法,clinit方法是由代码中的类变量的赋值动作和静态代码块(static{})合并产生的。没有类变量或静态代码块不会生成clinit方法
*构造器方法中指令按语句在源文件中出现的顺序执行,编译器收集的顺序是按照源文件中的顺序决定的,静态代码块能访问在它之前的类变量,之后的不能访问。
- Clinit不同于类构造器(init),不需要显式调用父类构造器,子类的clinit执行前,父类的clinit肯定已经执行完成,所以虚拟机启动的时候,第一个加载的执行的clinit方法肯定是java.lang.Object类的
- 虚拟机必须保证一个类的clinit()方法在多线程下被同步加锁。
- 接口有也会有clinit方法,(接口没有静态代码,但有初始化赋值),接口的clinit不需要先执行父接口的clinit方法。
类加载器
1.加载器介绍
- 通过一个类的全限定名来获取到描述此类的二进制字节流,这个动作放到
- 类加载器实现类的加载动作,同时用于确定一个类。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,这两个类就不相等。(比如通过自定义加载器)
- JVM支持两种类型的加载器,分别为引导类加载器(BootStrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
- 在程序中我们最常见的类加载器始终只有三个,如下所示:
2.自定义类与核心类库的加载器
- 对于用户自定义类来说:使用系统类加载器AppClassLoader进行加载
- java核心类库都是使用引导类加载器BootStrapClassLoader加载的
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//获取其上层:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
//获取其上层:获取不到引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
//对于用户自定义类来说:默认使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null
}
3.虚拟机自带的加载器
- 启动类加载器(引导类加载器,BootStrap ClassLoader)
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部
- 它用来加载java的核心库(JAVA_HOME/jre/lib/rt.jar/resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
- 并不继承自java.lang.ClassLoader,没有父加载器
- 加载拓展类和应用程序类加载器,并指定为他们的父加载器
- 处于安全考虑,BootStrap启动类加载器只加载包名为java、javax、sun等开头的类
- 拓展类加载器(Extension ClassLoader)
- java语言编写 ,由sun.misc.Launcher$ExtClassLoader实现
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会由拓展类加载器自动加载
- 应用程序类加载器(系统类加载器,AppClassLoader)
- java语言编写, 由sun.misc.Launcher$AppClassLoader实现。
- 派生于ClassLoader类
- 父类加载器为拓展类加载器
- 它负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库
- 该类加载器是程序中默认的类加载器,一般来说,java应用的类都是由它来完成加载
- 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
public static void main(String[] args) {
System.out.println("**********启动类加载器**************");
//获取BootstrapClassLoader能够加载的api的路径
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader);
System.out.println("***********扩展类加载器*************");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d
}
4.用户自定义类加载器
隔离加载类
拓展加载源
防止源码泄漏
多版本Jar包
操作步骤1.继承ClassLoader类 2. 重写findClass方法
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException();
} else {
return defineClass(name, result, 0, result.length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
private byte[] getClassFromCustomPath(String name) {
//从自定义路径中加载指定类:细节略
//如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
return null;
}
}
5.双亲委派机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将她的class文件加载到内存生成的class对象。而且加载某个类的class文件时,java虚拟机采用的是双亲微拍模式,即把请求交由父类处理,它是一种任务委派 模式
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先检查这个classsh是否已经加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// c==null表示没有加载,如果有父类的加载器则让父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空 则说明递归到bootStrapClassloader了
//bootStrapClassloader比较特殊无法通过get获取
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();
//如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
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;
}
}
5.1双亲委派机制的作用
- 防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
- 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
沙箱安全:防止恶意代码污染java源代码
比如我定义了一个类名为String所在包为java.lang,因为这个类本来是属于jdk的,如果没有沙箱安全机制的话,这个类将会污染到我所有的String,但是由于沙箱安全机制,所以就委托顶层的bootstrap加载器查找这个类,如果没有的话就委托extsion,extsion没有就到aapclassloader,但是由于String就是jdk的源代码,所以在bootstrap那里就加载到了,先找到先使用,所以就使用bootstrap里面的String,后面的一概不能使用,这就保证了不被恶意代码污染
类的主动使用和被动使用
- 创建类的实例
- 访问某各类或接口的静态变量,或者对静态变量赋值
- 调用类的静态方法
- 反射 比如Class.forName(com.dsh.jvm.xxx)
- 初始化一个类的子类
- java虚拟机启动时被标明为启动类的类
- JDK 7 开始提供的动态语言支持
- 同上方的1.1 什么时候会加载类
JVM完整目录
1. jvm概述
2.类加载机制
3.运行时数据区[PC寄存器、虚拟机栈、本地方法栈]
4.运行时数据区[堆]
5.运行时数据区[方法区]
6.暂缺
7. 运行时数据区[对象的实例化内存布局与访问定位、直接内存]
8.执行引擎(Execution Engine)
9.字符串常量池
10.垃圾回收[概述、相关算法]
11.垃圾回收[垃圾回收相关概念]
12.垃圾回收[垃圾回收器]
13.常见的OOM
14. JDK命令行工具