1. JVM 类装载机制
1.1 JVM 内存模型
ClassLoader:依据特定格式,加载 class 文件到内存
Execution Engine:对命令进行解析
Native Interface:java 会通过这个模块融合不同开发语言的原生库—native 实现
Runtime Data Area: 运行时数据区域,即我们经常说的 JVM 内存空间模块
1.2 JVM 类装载的阶段
JVM 类加载机制分为加载》连接(验证\准备\解析)》初始化》使用》卸载五个部分
- 加载
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法
区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从
jar 包和 war 包中读取),也可以在运行时计算生成(动态代理), 也可以由其它文件生成(比如将 JSP 文件转换成对
应的 Class 类)。
- 验证
这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害
虚拟机自身的安全。
- 准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空
间。注意这里所说的初始值概念,比如一个类变量定义为: *public static int v = 8080;*实际上变量 v 在准备阶段过
后的初始值为 0 而不是 8080,将 v 赋值为 8080 的 put static 指令是程序被编译后,存放于类构造器
方法之中。但是注意如果声明为:public static final int v = 8080;在编译阶段会为 v 生成 ConstantValue 属性,在
准备阶段虚拟机会根据 ConstantValue 属性将 v 赋值为 8080。
- 解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的:
-
CONSTANT_Class_info
-
CONSTANT_Field_info
-
CONSTANT_Method_info 等类型的常量。
- 初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它
操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。初始化阶段是执行类构造器
方法的过程。
1.3 类加载详解
虚拟机设计团队把加载动作放到 JVM 外部实现,ClassLoader 主要作用是从系统外部获得 class 二进制数据流。
它是 Java 的核心组件,所有的 Class 都是有 ClassLoader 进行加载的,ClassLoader 负责通过将 Class 文件里的
热禁止数据流加载进系统,然后交给 Java 虚拟机进行连接、初始化等操作,JVM 提供了 3 种类加载器。
1.3.1 启动类加载器(Bootstrap ClassLoader)
c 编写,加载核心库 java.*,负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且
被虚拟机认可(按文件名识别,如 rt.jar)的类。
1.3.2 扩展类加载器(Extension ClassLoader)
Java 编写,加载扩展库 javax.*,负责加载 JAVA_HOME\jre\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路
径中的类库。
1.3.3 应用程序类加载器(Application ClassLoader)
Java 编写,加载程序所在目录,负责加载用户路径(classpath)上的类库。
1.3.4 自定义加载器
对于一些定制化的需求,我们可以通过继承 java.lang.ClassLoader 实现自定义的类加载器,自定义类加载器有
如下两个关键函数:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
- 编写一个简单的类
在 d 盘Java目录下定义一个类,并编译成 class 文件
public class Dog{
static {
System.out.println("Hello wangcai");
}
}
- 自定义类加载器
public class MyClassLoder extends ClassLoader {
public static void main(String[] args) {
MyClassLoder myClassLoder = new MyClassLoder("d:/Java/", "fClassLoader");
Class dog = myClassLoder.findClass("Dog");
System.out.println(dog.isInterface());
}
// 加载类文件所在的路径
private String path;
// 自定义类加载器的名称
private String ClassLoadername;
public MyClassLoder(String path, String classLoadername) {
this.path = path;
ClassLoadername = classLoadername;
}
/**
* 加载类文件返回类文件的二进制流
* @param name
* @return
*/
public byte[] loadClassData(String name) {
String classFilePath = path+ name+".class";
InputStream in = null ;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(new File(classFilePath));
out = new ByteArrayOutputStream();
int i =0;
while ((i=in.read())!=-1){
out.write(i);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
out.close();
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return out.toByteArray();
}
/**
* 查找类文件
* @param name
* @return
*/
@Override
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name,b,0,b.length);
}
}
1.4 双亲委派
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层
次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这
个请求的时候(在它的加载路径下没有找到所需加载的 Class),子类加载器才会尝试自己去加载。
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终
都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对
象。
跟踪 ClassLoader.loadClass()方法理解双亲委派机制
- 查看每种加载器的加载路径
Launcher 中定义了 ExtClassLoader 和 AppClassLoader
System.getProperty("java.ext.dirs");
System.getProperty("java.class.path");
- 类加载顺序
public class ClassLoaderChecker {
public static void main(String[] args) throws Exception{
MyClassLoder m = new MyClassLoder("d:/Java/", "myClassLoader");
Class c = m.loadClass("Dog");
System.out.println(c.getClassLoader()); // 自定义
System.out.println(c.getClassLoader().getParent()); // app
System.out.println(c.getClassLoader().getParent().getParent()); // ext
System.out.println(c.getClassLoader().getParent().getParent().getParent()); // boot(即null)
c.newInstance();
}
}
/*
输出结果:
com.netft.jvm.MyClassLoder@74a14482
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@677327b6
null
Hello wangcai
*/
1.5 反射
Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;这种动态获取信息以
及动态调整对象方法的功能称为 java 语言的反射机制
1.5.1 反射举例
public class ReflectDemo {
public static void main(String[] args) throws Exception {
// 反射获得动态的Class
Class<?> catClass = Class.forName("com.netft.reflect.TomCat");
// 1.动态的从class中获得对象
TomCat o = (TomCat) catClass.newInstance();
/**
* 2.动态的从class中的方法
* 能够获取所有的方法,私有的也可以。但是不能获得继承来的和实现接口来的方法
*/
Method sayHello = catClass.getDeclaredMethod("sayHello",String.class);
// 执行方法,如果方法是私有的,需要设置Accessible
sayHello.setAccessible(true);
Object zhangsan = sayHello.invoke(o, "zhangsan");
System.out.println("sayHello return::"+zhangsan);
/**
* 3.动态的从class中的方法
* 能够获得public中的方法,包括继承来的和实现接口来的方法
*/
Method sayHi = catClass.getMethod("sayHi", String.class);
sayHi.invoke(o,"李四");
/**
* 4.动态的从class中的得属性
*
*/
Field name = catClass.getDeclaredField("name");
name.setAccessible(true);
System.out.println("name 属性赋值前::"+name.get(o));
name.set(o,"张三");
System.out.println("name 属性赋值后::"+name.get(o));
}
}
class TomCat{
private String name;
public void sayHi(String person){
System.out.println("hi,"+person);
}
private String sayHello(String person){
System.out.println("Hello,"+person);
return "Hello";
}
}
1.5.2 类加载的方式总结
隐式加载:new
显示加载:loadClass 和 forName
1.5.2.1 ClassLoader.loadClass 和 Class.forName 的区别
Class.forName 得到的 class 时已经初始化完成的
ClassLoader.loadClass 得到的 class 是还没有连接的,Spring 的延迟加载使用 classloder.loadClass