第二课-Java虚拟机(JVM)(上篇)
参考文章
- Java JVM类加载顺序详解
- jvm之java类加载机制和类加载器(ClassLoader)的详解
- Java虚拟机(JVM)你只要看这一篇就够了!
介绍
- 自动垃圾回收
- 跨平台
构成
- 类加载器
- 运行时内存空间
- 垃圾回收器(GC)
类加载器
类加载过程
准备、解析和初始化合并称为链接过程。
- 加载:使用到类时才会触发加载,例如创建对象。加载阶段会生成代表该类的java.lang.Class对象,作为方法区各个类访问该类的入口。
- 验证:校验字节码的正确性。
- 准备:给类的静态变量分配内存,并赋予默认值。注意,当被final修饰即常量时,会直接赋值为该常量值。
- 解析:将符号引用替换为直接引用(静态链接过程)。
- 初始化:
1) 初始化阶段是执行类构造器
()方法的过程。类构造器 ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。 2) 当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先触发其父类的初始化。 3) 虚拟机会保证一个类的 ()方法在多线程环境中被正确加锁和同步。 - 使用:
- 卸载:
类是否会被加载
- 类的主动引用(一定会发生类的初始化)
- new一个类的对象
- 调用类的静态成员(除了final常量)和静态方法
- 使用java.lang.reflect包的方法对类进行反射调用
- 当虚拟机启动,java HelloWorld,则一定会初始化HelloWorld类,说白了就是先启动main方法所在的类
- 当初始化一个类,如果其父类没有被初始化,则先初始化它父类
- 类的被动引用(不会发生类的初始化)
- 当访问一个静态域时,只有真正声名这个域的类才会被初始化
- 通过子类引用父类的静态变量,不会导致子类初始化
- 通过数组定义类的引用,不会触发此类初始化
- 引用常量不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)
- 当访问一个静态域时,只有真正声名这个域的类才会被初始化
类加载器
- 引导类加载器:它用来加载 Java 的核心类,是用原生代码C++来实现的,不是ClassLoader子类。负责加载JRE下
lib
里的核心类库,比如lib/rt.jar
、charsets.jar
等。 - 扩展类加载器:加载JRE下
lib/ext
或者由java.ext.dirs
系统属性指定的目录中的JAR包的类 - 系统类加载器(应用类加载器):加载在JVM启动时加载来自Java命令的
-classpath
选项、java.class.path
系统属性,或者CLASSPATH变量
所指定的JAR包和类路径 - 自定义加载器:不明确指定,默认父加载器为系统类加载器。实现只需继承
java.lang.ClassLoader
。
点击查看测试代码
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
// 引导类加载器
System.out.println(String.class.getClassLoader());
// 扩展类加载器
System.out.println(javafx.application.Application.class.getClassLoader());
// 系统类加载器
System.out.println(ClassLoaderTest.class.getClassLoader());
// 自定义加载器,实际会去classpath的my.org.archer.jvm.World目录查找class文件
Class<?> clz = Class.forName("org.archer.jvm.World", false, new MyClassLoader());
Object world = clz.getDeclaredConstructor(String.class).newInstance("Java");
Method hello = clz.getMethod("hello");
hello.invoke(world);
System.out.println(clz.getClassLoader());
/*
输出结果:
null
sun.misc.Launcher$ExtClassLoader@7106e68e
sun.misc.Launcher$AppClassLoader@18b4aac2
Hello World, Java!
sun.misc.Launcher$AppClassLoader@18b4aac2
*/
}
}
public class World {
private final String name;
public World(String name) {
this.name = name;
}
public void hello() {
System.out.println("Hello World, "+ name +"!");
}
}
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String myPath = "/my/" + name.replaceAll("\\.", "/") + ".class";
try (InputStream is = MyClassLoader.class.getResourceAsStream(myPath)) {
int len = is.available();
byte[] full = new byte[len];
IOUtils.read(is, full);
return defineClass(name, full, 0, len);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
类加载机制
- 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
- 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
- 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因
为什么设计双亲委派机制
- 沙箱安全机制:自己写的Ineger、String等Java核心类不会被加载,保证核心API的安全
- 避免类的重复加载:保证被加载类的唯一性(具备一种带有优先级的层次关系)
Tomcat自定义类加载器
-
Tomcat需要解决的问题
- Tomcat是WEB容器,可能需要部署两个到多个WEB应用,不同应用可能会依赖同一类库JAR包的不同版本
- 部署在同一个web容器中的所有应用相同的类库相同的版本可以共享,防止类的重复加载
- WEB容器也有自己依赖的类库,不能与应用程序的类库混淆,从而保证安全
- WEB容器要支持JSP的修改,而不重启容器
-
如何实现
- CommonClassLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问
- CatalinaClassLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见
- SharedClassLoader:各个Webapp共享的类加载器,加载路径中的class对于所有 Webapp可见,但是对于Tomcat容器不可见
- WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前 Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本, 这样实现就能加载各自的spring版本
- JasperLoader:每个jsp文件对应一个唯一的类加载器,加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件。它出现的目的 就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例, 并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能
-
JAR包加载顺序
- JRE下的类库
- webapp下应用的类库:
WEB-INF/classes
和WEB-INF/lib
- Tomcat的
lib
下的类库
-
线程上下文加载器
请看:
下篇请看: