我们都知道Java的最大的特点之一就是跨平台性,Java在诞生之时有一个很牛的口号:“Write Once, Run Anywhere!”.
那么为什么可以做到如此呢?关键点就在于JVM。存有我们业务逻辑的源文件,在通过编译器编译成字节码文件后,可以在不同平台(Windows、Linux等)上运行的 Java 虚拟机(JVM)——负责载入和执行 Java 编译后的字节码——运行。跨平台性的原理就是在JVM层面上屏蔽了不同操作系统上的差异。
操作系统中,是根据文件的类型后缀去判断文件类型的。那么JVM是如何判断一个文件是字节码文件的呢?
所有的字节码文件,都是以0xcafebabe开头的,这被称为“魔数”(Magic Number),是JVM识别字节码文件的标识。
字节码由10部分组成,依次是魔数、版本号、常量池、访问权限、类索引、父类索引、接口索引、字段表索引、方法、Attribute。
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个
java.lang.Class
对象,用来封装类在方法区内的数据结构。
通俗点来说,类加载就是把编译后的字节码文件装在JVM里的过程。
类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过 加载,连接,初始化 三步来实现对这个类进行初始化。
一个字节码文件,它肯定是存在于磁盘中或者某个区域,JVM肯定要找到它并导入它,那么是通过什么样的方式去找到它呢?——这个类的全限定路径名,那么加载在干什么事我们就知道了——通过一个类的全限定名来获取定义此类的二进制的字节流。这个二进制字节流所代表的静态结构,转化为内存中的一个代表这个类的java.lang.Class
对象,作为方法区里这个类的各种数据的访问入口。
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。简而言之是确保被加载类的正确性、合法性、安全性。验证阶段大致会完成4个阶段的检验动作:
文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE
开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object
之外。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
负责为类的静态成员分配内存并设置默认初始化值
将类中的符号引用替换为直接引用 (直接引用:直接指向目标的指针,指的是地址
给静态成员变量赋初值,执行类的初始化(静态代码块)内容。
初始化的详细过程:
刚刚所说的加载、连接、初始化最终加载到JVM是怎么做到的呢,其实就是通过类加载器。可以说,类加载器是类加载流程的实现者。
对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 equals)。
JDK自带的Class Loader:
Java语言编写的类加载器 sun.misc.Launcher$ExtClassLoader
指定Bootstrap Classloader为Parent加载器 --> getParent()可获取Bootstrap Classloader
负责加载Java平台中扩展功能的一些jar包,包括 jre/lib/ext 包下面的 jar 文件 或者 -Djava.ext.dirs指定目录下的jar包 。
如果我们自定义的类需要交给Ext来加载可以放置到ext的目录下。
还有需要注意的是,使用-Djava.ext.dirs指令会覆盖Java本身的ext设置。
解决方法:
1、把相关的lib复制到新的ext directory下;
2、用冒号拼接多个directory,e.g. -Djava.ext.dirs=./plugin**:$JAVA_HOME/jre/lib/ext**
sun.misc.Launcher$AppClassLoader
通过一个简单例子了解:
public class Test {
public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}
每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 类名.class.getClassLoader()
可以获取到此引用;然后通过 loader.getParent()
可以获取类加载器的上层类加载器。
其输出结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d
第一行输出为 Test 的类加载器,即应用类加载器,它是sun.misc.Launcher$AppClassLoader
类的实例;第二行输出为扩展类加载器,是 sun.misc.Launcher$ExtClassLoader
类的实例。那启动类加载器呢?
按理说,扩展类加载器的上层类加载器是启动类加载器,但启动类加载器由C/C++语言实现,在Java中为null。
先来看看ClassLoader的loadClassd()的源码:
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;
}
}
这段代码其实就是双亲委派机制的根本,如果parent加载器不为null,则继续调用parent的loadClass().
这种层次关系被称作为双亲委派模型:如果一个类加载器收到了加载类的请求,它会先把请求委托给上层加载器去完成,上层加载器又会委托上上层加载器,一直到最顶层的类加载器;如果上层加载器无法完成类的加载工作时,当前类加载器才会尝试自己去加载这个类。
Java类随着它的类加载器一起具备一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类的时候,就没有必要子类加载器再加载一次,这对于保证 Java 程序的稳定运作很重要。
如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等——双亲委派模型能够保证同一个类最终会被特定的类加载器加载。
考虑到安全因素,Java核心API中定义类型不会被随意替换。
举个栗子,在classpath路径下自定义一个名为java.lang.S的类,其包名为java.lang:
package java.lang;
public class S {
public S() {
System.out.println("111");
}
}
运行下面Demo的main方法:
public class Demo {
public static void main(String[] args) {
S s = new S();
}
}
输出台会报错:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
这个报错是说S类的自定义包名java.lang是非法的。java.lang.S类的类加载过程是:经过双亲委派机制,传递到启动类加载器中,由于父类加载器路径下并没有该类,也就是核心包中的java.lang包下没有S这个类,所以不会加载,将反向委托给子类加载器,最终会通过系统类加载器加载该类,但是这样做是不允许的,因为java.lang是核心的API包,核心包都应该交给BootStrap Classloader来加载,需要访问权限,强制加载将会报此异常。
三个重要函数:loadClass(),findClass(),defineClass()
loadClass():调用父类加载器的loadClass,加载失败则调用自己的findClass方法
findClass():根据名称读取文件存入字节数组
defineClass():把一个字节数组转为Class对象
自定义一个类加载器,只需继承ClassLoader类;也可以继承URLClassLoader类。后者的方法更加具象。
然后重写findClass()方法即可,其默认实现是会抛出一个异常。
我们知道Tomcat的源码是用Java语言编写出来的,那么在一个JVM的层面上为什么能够并行地运行多个Web应用呢?来看看《Tomcat类加载机制》这张图:在上方三个JDK自带的类加载器之下,有一个Common ClassLoader,这个类加载器是用来加载Tomcat中公共通用的类;如果是默认用Catalina启动,则用的是Catalina ClassLoader,加载的是Catalina.sh中指定的启动类;JasperLoader是主要加载jsp文件;WebApp ClassLoader 是基于WEB目录或者war工程来进行类加载;Shared ClassLoader 是整个Tomcat层面每一个WEB工程都可以共享使用的。所以说通过这样的一种环境隔离的方式,Tomcat可以做到分层加载,或者说按需加载。
类加载无非找到字节码文件的二进制流,然后读取到JVM内存中,有可能是从数据库、网络、Redis等将这个字节码加载过来。所以说,当我们需要适配各种.class数据源时可以去自定义ClassLoader。
Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。
重写loadClass()方法即可:
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
c = findClass(name);
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
把loadClass()中使用双亲委派的代码删掉,这样MyClassLoader不用再向上去找类加载器,只会在本类中处理,这样就打破了双亲委派模型。
参考地址:
1、java字节码的魔数是0xCAFEBABE,为什么是4字节,而不是8字节
2、我竟然不再抗拒 Java 的类加载机制了
3、JVM类加载机制
4、Java 类加载机制(阿里)-何时初始化类
5、自定义类加载器以及打破双亲委派模型
6、详解Java类的装载过程及类加载机制
7、Java类加载机制
8、自定义类加载器