目标概述
这篇文章主要认识jvm和懂得类加载机制,双亲委派模型,分为基础认识,和类加载两大模块,
我们知道,java可以做到平台无关性,一次编译四处运行,其实依靠的就是虚拟机,对.class字节码文件来说,不论是Liunx、windows
运行在一个软件上,就是jvm虚拟机,他提供了java编译运行的条件,而同一个jvm,在不同操作系统上,是根据不同操作系统编写的,
相当于做了一层过度,通过jvm,来让java具有跨平台的特性,是java语言的核心
User.java--->编译器----->User.class------>虚拟机------->Window系统
|
|----->虚拟机----->mac系统
|
|
类加载过程
|--->加载--->验证---->准备----->解析---->初始化
操作系统<虚拟机<JRE<JDK
JRE是java运行的环境,而JDK则是多了一些调试的工具,如编译器,调试器,和一些开发工具
生产环境部署的时候,一般选择JRE而不是JDK,
JRE安装包更小,打包镜像更省空间,但是缺少调试开发工具,如编译器与调试器,
特殊情况,需要编译调试的程序,就需要安装JDK
基于JDK8的HotSpot虚拟机,不同虚拟机不同版本会有不一样)
名称 | 作用 | 特点 |
---|---|---|
程序计数器 | 也叫PC寄存器,用于记录当前线程执行的字节码指令位置,以便线程在恢复执行时能够从正确的位置开始 | 线程私有 |
Java虚拟机栈 | 用于存储Java方法执行过程中的局部变量、方法参数和返回值,以及方法执行时的操作数栈 | 线程私有 |
本地方法栈 | 用于存储Java程序调用本地方法的参数和返回值等信息。 | 线程私有 |
堆 | 用于存储Java程序创建的对象,所有线程共享一个堆,堆中的对象可以被垃圾回收器回收,以便为新的对象分配空间 | 线程共享 |
元数据区 | 用于存储类的元数据信息,如类名、方法名、字段名等,以及动态生成的代理类、动态生成的字节码等 元空间是位于本地(直接)内存中的,而不是像JDK8之前方法区位于堆内存中的。 | 线程共享 |
运行时常量池就时类加载之后,把类中常量的信息移动到元数据区,
字符串常量池jdk1.7以后哦才能够元数据区移动到堆中
常量池时编译后的概念,属于字节码class文件中,而运行时常量池是类加载之后的概念,存活时间不一样
方法区---可以看成一个接口,永久代和元数据区看成实现,元数据区是代替了永久代
方法区/元空间存储类元数据信息的区域,包括类的结构、方法、字段信息等
User.java--->编译器----->User.class------>虚拟机------->Window系统
|
|----->虚拟机----->mac系统
|
|
类加载过程
|--->加载--->验证---->准备----->解析---->初始化
jvm面试必问内容,负责将类的字节码加载的jvm中,具体在下面的双亲委派机制细说
类加载器指的是类加载过程中的加载
双亲委派模型、
延迟加载(在需要某个类的时候再加载,减少启动时间)、
动态加载(在jvm运行的时候动态加载和卸载类(反射机制))
比如定义一个与jdk中重名的类,比如java.long.Object,与jdk同名同包
恶意注入的话,为了防止这种错乱,采用双亲委派模型
应用程序加载代码的时候,不止直接加载,而是先看父类加载器--拓展类加载器-,这里加载不了,再看启动类加载器,没有的话才会区加载应用程序类加载器,
上面Object的例子,再启动类的时候能加载出来,就不会报错了,在这里可以找到
随着jdk9模块化系统的引入,类加载器也发生了变化
(至于为何引入模块化系统,这里不做谈论,有人说没有意义,只是新的管理方式,每个模块有独立的开发测试部署环境)
大家可以看IDEA中External Libraies库中查看区别
主要就是loadClass方法
双亲委派就是在这里实现的
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先检查这个class是否已经加载过了
Class<?> c = findLoadedClass(name);
//c==null表示没有加载
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果有父类的加载器则让父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空 则递归到bootStrapClassloader,这里就是双亲委派模型的实现
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
//这里就是启动类加载不到的情况,会调用findClass法
long t1 = System.nanoTime();
//findClass默认是未实现的,找不到,就拿应用程序类加载器加载了
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
由此可见,findClass是留给我们程序员实现的,实现这个,就会去找新的其他的Class而不是应用程序中的Class,重写这个方法,就能不走Application,走我们指定生成的类,就能实现自定义类加载器
defineClass适用于定义类的方法,将字节数组转化为Class对象,将其添加到类加载器的命名空间中。这个是我们直接调用就可以了
resloveClass:解析类的方法,将类的引用转换为实际的类,进行链接和初始化
值得注意的是
private static class PlatformClassLoader extends BuiltinClassLoader {
static {
if (!ClassLoader.registerAsParallelCapable())
throw new InternalError();
}
private static class AppClassLoader extends BuiltinClassLoader {
static {
if (!ClassLoader.registerAsParallelCapable())
throw new InternalError();
}
说的是继承关系,其实这些类加载器,其实是组合关系,通过组合实现父子属性
比如需要网络传来的字节码文件,应用程序加载不到,就需要自定义类加载器,拓展加载源
还有就是可以自定义加密的类文件,通过加密,只有这个类加载器加载,保护机密类的安全
做重要的就是:实现类隔离(tomcat有大量的应用)
注意点:
通过上面ClassLoader的解析,可以知道,
我们实现类加载器,要先继承ClassLoader,然后重写findClass而不是loadClass(loadClass会破坏类的双亲委派模型)
重写的时候,获取加载源的流,调用defindClass方法,加载到自定义的命名空间就可以了
可能看到这里,还是不太理解为什么重写findClass方法能实现自定义类加载器了。
重写前
重写后
这样就实现了自定义类加载器了!
public class JosephClassLoader extends ClassLoader {
private String codePath;
public JosephLoader(String codePath) {
this.codePath = codePath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = codePath + name + ".class";
System.out.println(fileName);
//获取输入流
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName));
ByteArrayOutputStream bos = new ByteArrayOutputStream();) {
int len;
byte[] data = new byte[1024];
while ((len = bis.read(data)) != -1) {
bos.write(data, 0, len);
}
//获取内存中字节数组,因为要调用defineClass方法,获得Class类对象
byte[] byteCode = bos.toByteArray();
//执行 defineClass 将字节数组转成Class对象
Class<?> defineClass = defineClass(null, byteCode, 0, byteCode.length);
return defineClass;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
自定义类加载器的这个用途,很多人不能理解,
个面试题:
不同类加载器是否会重复加载同个class类
答案是:不同类加载器会加载同名的,同路径,的类,即使时同一份字节码文件,被不同类加载器加载
对于同一份字节码文件,自定义类加载器去加载为对象实例,这些对象时不一样的,也就是说class字节码一样,但是Class对象时不一样的
可以这么理解,字节码文件,是编译好的,.class文件,还没经过jvm,不同的类加载器生成不同的Class类对象,可以类比为类和对象的关系。
这是tomcat常用的
但是!!!对于不自定义类加载器的场景
重复加载一个类,比如类静态代码的重复执行,引发难以意料的问题
为了避免上述情况的发生,采用双亲委派机制来处理
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。