JVM-类加载和初始化
JVM-类加载和初始化
类加载-初始化
- loading 把class文件加载到内存
- linking
- Verification:校验class文件是否符合标准
- preparation:给静态变量赋默认值,如给static int i = 8赋值为i=0
- resolution:常量池中的用到的那些符号引用要准换成能访问到的内存地址
- initializing :这时候才会调用静态代码块给静态变量赋值
类加载器
loading
jvm中所有的class都是被classloader加载到内存
以上几个类加载器的关系不是继承,是父加载器与自加载器的关系。
双亲委派
- 父加载器
父加载器不是“类加载器的加载器” - 双亲委派是一个孩子向父亲方向,然后父亲向孩子方向的双亲委派过程
那么问题来了, 为什么要搞双亲委派
java.lang.String类由自定义加载器加载行不行?
回答这个问题, 首先要弄明白class的加载过程。
根据上图所示,一个class类首先要经过CustomClassloader(自定义类加载器),查询其缓存中是否已经将该class加载,如果有,则将其返回,没有,则向上检查,此时到了APP(AppClassLoader,同样检查其缓存是否已加载,没有,则继续向上,Extension加载器同样如此,一直检查到BootStrap加载器,当Bootstrap加载器同样没有加载该calss时,开始自顶向下进行实际查找和加载。首先判断该类是否该由Bootstrap加载,不是,则向下,一直到Custom加载器,如果没有找到,则抛异常(ClassNotFound)。
主要是为了安全
假设自定义了一个Java.lang.String,覆盖sun的String,同时自定义一个String的类加载器,将自定义的这个String加载到内存,接下来将整个自定义部分打包成一个类库,交给客户使用 ,此时客户输入密码将会变得非常不安全。
但是采用双亲委派就不会有这个问题,自低向上检查,一直到Bootstap,发现String类已经被Bootstrap加载,其他加载器便不能再次加载这个类,从而保证了安全。
类加载过程
类加载器范围
这些加载范围是由launcher的源码决定
查看每个目录下都有哪些jar包
public class T003_ClassLoaderScope {
public static void main(String[] args) {
String pathBoot = System.getProperty("sun.boot.class.path");
System.out.println(pathBoot.replaceAll(";", System.lineSeparator()));
System.out.println("--------------------");
String pathExt = System.getProperty("java.ext.dirs");
System.out.println(pathExt.replaceAll(";", System.lineSeparator()));
System.out.println("--------------------");
String pathApp = System.getProperty("java.class.path");
System.out.println(pathApp.replaceAll(";", System.lineSeparator()));
}
}
输出结果
C:\Program Files\Java\jdk1.8.0_51\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\rt.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\sunrsasign.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_51\jre\classes
--------------------
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext
C:\Windows\Sun\Java\lib\ext
--------------------
C:\Program Files\Java\jdk1.8.0_51\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\deploy.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\access-bridge-64.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\cldrdata.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\dnsns.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\jaccess.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\jfxrt.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\localedata.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\nashorn.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\sunec.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\sunjce_provider.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\sunmscapi.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\sunpkcs11.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\ext\zipfs.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\javaws.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\jfxswt.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\management-agent.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\plugin.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_51\jre\lib\rt.jar
G:\SoftWare\Java\IntelliJ IDEA 2019.1.3\lib\idea_rt.jar
ClassLoader类加载器
package com.cyc.jvm.c2_classloader;
/**
* classloader加载器
*/
public class T002_ClassLoaderLevel {
public static void main(String[] args) {
//String是由 bootStrapClassLoader加载的
System.out.println(String.class.getClassLoader());
System.out.println(sun.awt.HKSCS.class.getClassLoader());
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());
//自定义类的classLoader的类加载器为AppClassLoader
System.out.println(T002_ClassLoaderLevel.class.getClassLoader());
//extClassLoader的类加载器是BootStrapClassLoader加载的
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getClass().getClassLoader());
System.out.println(T002_ClassLoaderLevel.class.getClassLoader().getClass().getClassLoader());
//一个自定义classLoader的默认父classLoader是AppClassLoader
System.out.println(new T006_CYCClassLoader().getParent());
//ClassLoader的systemClassLoader也是AppClassLoader
System.out.println(ClassLoader.getSystemClassLoader());
}
}
自定义类加载器
准备阶段
- 一个需要解析的的class文件
package com.cyc.jvm;
public class Hello {
public void m() {
System.out.println("Hello JVM!");
}
}
- 去项目目录下找到Hello的class文件, 带上根目录复制到D盘的test文件夹下
- 自定义类加载器
public class T006_CYCClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File f = new File("D:/test/", name.replace(".", "/").concat(".class"));
try {
FileInputStream fis = new FileInputStream(f);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;
while ((b=fis.read()) !=0) {
baos.write(b);
}
byte[] bytes = baos.toByteArray();
baos.close();
fis.close();//可以写的更加严谨
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name); //throws ClassNotFoundException
}
public static void main(String[] args) throws Exception {
ClassLoader l = new T006_CYCClassLoader();
Class clazz = l.loadClass("com.cyc.jvm.Hello");
Class clazz1 = l.loadClass("com.cyc.jvm.Hello");
//由于使用的是同一个类加载器,加载出来的是同一个class对象, 所以这里会输出true
System.out.println(clazz == clazz1);
Hello h = (Hello)clazz.newInstance();
h.m();
//自定义类加载器的类加载器是AppClassLoader
System.out.println(l.getClass().getClassLoader());
//他的父加载器同样也是AppClassLoader,但是注意, 他们之间不是继承关系。这些类加载器继承的都是ClassLoader
System.out.println(l.getParent());
System.out.println(getSystemClassLoader());
}
}
lazyloading
public class T008_LazyLoading { //严格讲应该叫lazy initialzing,因为java虚拟机规范并没有严格规定什么时候必须loading,但严格规定了什么时候initialzing
public static void main(String[] args) throws Exception {
P p;
X x = new X();
System.out.println(P.i);
System.out.println(P.j);
Class.forName("com.cyc.jvm.c2_classloader.T008_LazyLoading$P");
}
public static class P {
final static int i = 8;
static int j = 9;
static {
System.out.println("P");
}
}
public static class X extends P {
static {
System.out.println("X");
}
}
}
混合模式
测试
package com.cyc.jvm.c2_classloader;
public class T009_WayToRun {
public static void main(String[] args) {
for(int i=0; i<10_0000; i++)
m();
long start = System.currentTimeMillis();
for(int i=0; i<10_0000; i++) {
m();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
public static void m() {
for(long i=0; i<10_0000L; i++) {
long j = i%3;
}
}
}
首先是混合模式,这也是默认的运行模式
混合模式运行时间
解释模式
查看运行时间(时间过于漫长)
编译模式
查看运行时间
初始化(initializing)
package com.cyc.jvm.c2_classloader;
public class T001_ClassLoadingProcedure {
public static void main(String[] args) {
System.out.println(T.count);
}
}
class T {
//这个顺序,由于new T(),会进行T的初始化, 给T赋值为null,T中的成员编程int型的count赋值为0,
//然后调用T的构造方法, 执行count++, 此时count为1,接来下开始执行 public static int count = 2;在这里给count赋值为2
public static T t = new T(); // null
public static int count = 2; //0
private T() {
count ++;
//System.out.println("--" + count);
}
}
class T {
//首先归对象T进行初始化, 此时对象为null, 对象内的变量count赋值为默认值0,然后在initial阶段给count赋指定值2
//接着调用T的构造方法, 执行count++, count变为3
public static int count = 2; //2->3
public static T t = new T(); // null->对象
private T() {
count ++;
//System.out.println("--" + count);
}
}
new对象的过程其实也是分为两步, new 出来T , 先给里面的成员变量赋默认值,new出来T,申请完内存之后,开始调用构造方法,才给成员变量赋初始值。
扩展
结合单例模式解析初始化过程
public class Singleton06 {
//volatile关键字禁止指令重排
private static volatile Singleton06 INSTANCE;
public void c() {
System.out.println("C");
}
/**
* 构造方法为私有,只能在当前类中new,外部类无法new出来
*/
private Singleton06() {
}
public static Singleton06 getInstance() {
if (INSTANCE == null) {
//双重检查
synchronized (Singleton06.class) {
if (INSTANCE == null) {
try {
//这里让进入此代码块的线程睡一毫秒
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton06();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() ->
System.out.println(Singleton06.getInstance().hashCode())
).start();
}
}
}
为什么要加volatile?
为了防止指令重排。这里涉及到类的加载过程。
首先,第一个线程进来了, 加上锁之后,进入到INSTANCE = new Singleton06();代码,在初始化进行到一半的时候,也就是在preparation阶段,已经给Singleton06申请完内存,里面的成员变量已经赋过默认值,比如0,此时INSTANCE 已经指向这个分配的内存, 已经不再是null,此时另外一个线程进来了,由于此时INSTANCE 已经进行了半初始化状态,所以在if (INSTANCE == null)为false,此时另一个线程会拿到这个INSTANCE中的成员变量进行操作, 这样显然是不满足要求的。
想要解析这个问题, 需要查看其字节码文件
例如下面这个测试类T, 使用idea插件查看其字节码文件
在0 new #2 <com/cyc/jvm/c0_basic/T>之后,已经申请过内存。
4 invokespecial #3 <com/cyc/jvm/c0_basic/T.> 这个给类中的静态变量赋初始值
在调用完4之后,才会把这块内存赋值给t,但是由于指令可能会重排的原因, 如果先执行的是7 astore_1, 相当于先把这个地址扔到内存中, 然后在进行的T初始化, 这种情况下,在双重检查懒汉式单例中,就会出现有别的线程读取到半初始化的单例。
相关问题
如何打破双亲委派机制
如果只是重写findClass方法, 是无法打破双亲委派机制的, 示例如下
package com.cyc.jvm.c2_classloader;
public class T011_ClassReloading1 {
public static void main(String[] args) throws Exception {
T006_CYCClassLoader cycClassLoader = new T006_CYCClassLoader();
Class clazz = cycClassLoader.loadClass("com.cyc.jvm.Hello");
cycClassLoader = null;
System.out.println(clazz.hashCode());
cycClassLoader = null;
cycClassLoader = new T006_CYCClassLoader();
Class clazz1 = cycClassLoader.loadClass("com.cyc.jvm.Hello");
System.out.println(clazz1.hashCode());
System.out.println(clazz == clazz1);
}
}
输出结果
可以看到两者class的hashcode值相同, 所以, 依然是同一个class对象。
显然这里需要重写loadclass方法才行
package com.cyc.jvm.c2_classloader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class T012_ClassReloading2 {
private static class MyLoader extends ClassLoader {
//重写loadClass方法, 每次都去加载新的class , 而不是去类加载器缓存池中去找该类是否已加载
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
File f = new File("D:/test/", name.replace(".", "/").concat(".class"));
if(!f.exists()) return super.loadClass(name);
try {
InputStream is = new FileInputStream(f);
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
}
return super.loadClass(name);
}
}
public static void main(String[] args) throws Exception {
MyLoader m = new MyLoader();
Class clazz = m.loadClass("com.cyc.jvm.Hello");
m = new MyLoader();
Class clazzNew = m.loadClass("com.cyc.jvm.Hello");
System.out.println(clazz == clazzNew);
}
}
查看输出结果
第一次被加载的类, 在类空间里,当它的classLoader被干掉之后, 由于没有任何引用指向它了, 所以会被gc回收。
-
bootstrap加载器为什么返回的是null?
因为它是由c++编写的,java中并没有与之对应的class
-
class的加载过程用到了哪些设计模式?
classloader的load过程用到了设计模式中的模板方法模式,因为所有方法都已经写好了,如果要自定义classloader,自己只需要重写findclass方法就可以了。
Tomcat为什么要重写类加载器?
无法实现隔离性:如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。一个web容器可能要部署两个或者多个应用程序,不同的应用程序,可能会依赖同一个第三方类库的不同版本,因此要保证每一个应用程序的类库都是独立、相互隔离的。部署在同一个web容器中的相同类库的相同版本可以共享,否则,会有重复的类库被加载进JVM, web容器也有自己的类库,不能和应用程序的类库混淆,需要相互隔离
无法实现热替换:jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。
打破双亲委派机制(参照JVM中的内容)OSGI是基于Java语言的动态模块化规范,类加载器之间是网状结构,更加灵活,但是也更复杂,JNDI服务,使用线程上线文类加载器,父类加载器去使用子类加载器
-
tomcat自己定义的类加载器:
CommonClassLoader:tomcat最基本的类加载器,加载路径中的class可以被tomcat和各个webapp访问
CatalinaClassLoader:tomcat私有的类加载器,webapp不能访问其加载路径下的class,即对webapp不可见
SharedClassLoader:各个webapp共享的类加载器,对tomcat不可见
WebappClassLoader:webapp私有的类加载器,只对当前webapp可见
-
每一个web应用程序对应一个WebappClassLoader,每一个jsp文件对应一个JspClassLoader,所以这两个类加载器有多个实例
-
工作原理:
a. CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用
b. CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离
c. WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离,多个WebAppClassLoader是同级关系
d. 而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能
-
tomcat目录结构,与上面的类加载器对应
/common/*
/server/*
/shared/*
/WEB-INF/*
-
默认情况下,conf目录下的catalina.properties文件,没有指定server.loader以及shared.loader,所以tomcat没有建立CatalinaClassLoader和SharedClassLoader的实例,这两个都会使用CommonClassLoader来代替。Tomcat6之后,把common、shared、server目录合成了一个lib目录。所以在我们的服务器里看不到common、shared、server目录。
总结
- 加载过程
-
Loading
-
双亲委派,主要出于安全来考虑
-
LazyLoading 五种情况
-
–new getstatic putstatic invokestatic指令,访问final变量除外
–java.lang.reflect对类进行反射调用时
–初始化子类的时候,父类首先初始化
–虚拟机启动时,被执行的主类必须初始化
–动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化
-
-
ClassLoader的源码
- findInCache -> parent.loadClass -> findClass()
-
自定义类加载器
- extends ClassLoader
- overwrite findClass() -> defineClass(byte[] -> Class clazz)
- 加密
- 如何打破双亲委派
- 用super(parent)指定
- 双亲委派的打破
- 如何打破:重写loadClass()
- 何时打破过?
-
JDK1.2之前,自定义ClassLoader都必须重写loadClass()
-
ThreadContextClassLoader可以实现基础类调用实现类代码,通过thread.setContextClassLoader指定
-
热启动,热部署(会把整个classLoader都干掉,把class重新load一遍)
-
osgi tomcat 都有自己的模块指定classloader(可以加载同一类库的不同版本)
每一个webApplication,都有自己的一个classLoader, 每个classLoader中可以有自己的类。
-
-
-
混合执行 编译执行 解释执行
- 检测热点代码:-XX:CompileThreshold = 10000
-
-
Linking
- Verification
- 验证文件是否符合JVM规定
- Preparation
- 静态成员变量赋默认值
- Resolution
- 将类、方法、属性等符号引用解析为直接引用
常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
- 将类、方法、属性等符号引用解析为直接引用
- Verification
-
Initializing
- 调用类初始化代码 ,给静态成员变量赋初始值
-