类的初始化
clinit
- 初始化阶段就是执行类构造器方法clinit的过程。
- 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 构造器方法中指令按语句在源文件中出现的顺序执行。
- clinit不同于类的构造器。(关联:构造器是虚拟机视角下的init
- 若该类具有父类,JVM会保证子类的clinit执行前,父类的clinit已经执行完毕。
- 虚拟机必须保证一个类的clinit方法在多线程下被同步加锁。
只有给类中的static的变量显示赋值或者静态代码块中赋值了,才会生成此方法.
哪些类不会生成clinit方法
- 一个类中没有静态变量和静态代码块
- 有静态变量,但是没有静态变量的显示赋值以及静态代码来执行初始化操作
- 直接采用的 static final修饰的基本数据类型字段,这些字段在链接阶段的准备环节就已经初始化了
- 不是显示常量赋值的话 而是调用方法去赋值还是会生成clinit方法的(在初始化阶段给他赋值)
clinit方法会死锁吗
虚拟机会保证一个类的clinit方法在多线程环境下被正确的加锁,同步(所以他多线程下是线程安全的),这同样会导致如果clinit方法中有耗时过场的操作,就可能导致线程阻塞而死锁,并且非常难以排查.
类加载的时机(什么情况会触发类的加载)
- 当创建一个类的实例时,比如new关键字,或者是反射,克隆,反序列化等
- 当调用类的静态方法时,即当使用了字节码invokestatic指令
- 当使用类,接口的静态字段时(final修饰特殊考虑),比如使用getstatic或者putstatic指令
- 当使用java.lang.reflect包中的方法反射类的方法时.比如Class.forname(“aa.bb.Test”)
- 当初始化子类时,如果发现其父类还没有初始化,则需要先去触发父类的初始化
- 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类初始化,该接口要在其之前被初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类
- 当初次调用MethodHandle实例时,初始化该MethodHandle指向方法所在的类
当Java虚拟机初始化一个类时,要求他所有的父类都已经被初始化,这条规则不适用于接口 \
- 在初始化一个类时,并不会先初始化他所实现的接口
- 在初始化一个接口时,并不会初始化他的父接口
因此一个父接口并不会因为他的子接口或者实现类初始化而初始化,只有当程序首次使用特定接口的静态字段时,才会导致该接口初始化
被动使用不会触发类的初始化]
- 通过子类引用父类的静态变量,不会导致子类初始化
- 通过数组定义类引用,不会触发此类的初始化
- 引用常量不会触发此类或接口的初始化,因为常量已经在链接准备阶段被显示赋值了
- 调用ClassLoader类的loadClass()加载一个类,并不是对类的主动使用,不会导致类的初始化
被动的使用,意味着不需要执行初始化环节,意味着没有clinit的调用.
类的卸载
除非是你自定义的类加载器直接把这个类加载器也卸载掉(包括他下面的类),否则其他的已有的类加载器是不允许被卸载的,因为他们与类是双向绑定的关系.
类加载代码demo
public class T {
public static int k = 0;
public static T t1 = new T("t1");
public static T t2 = new T("t2");
public static int i = print("i");
public static int n = 99;
static {
print("静态块");
}
public int j = print("j");
{
print("构造块");
}
public T(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++n;
++i;
}
public static int print(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++n;
return ++i;
}
public static void main(String[] args) {
}
}
运行结果
1:j i=0 n=0
2:构造块 i=1 n=1
3:t1 i=2 n=2
4:j i=3 n=3
5:构造块 i=4 n=4
6:t2 i=5 n=5
7:i i=6 n=6
8:静态块 i=7 n=99
类的加载器
类的加载分类
- 显示加载:调用ClassLoader类中的方法去显示的加载某个类
- 隐式加载:是由jvm自动加载到内存中的,用到哪些加载哪些
类的唯一性
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间,比较这两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义
命名空间
- 每个类加载器都有自己的命名空间,由该加载器及其所有的父类加载器所加载的类组成
- 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
- 在不同的命名空间,有可能出现类的完整名字(包括包名)相同的情况
在大型应用中,可以借助这个特性,来运行一个类的不同版本
类加载的三个基本特征
- 双亲委派模型: 也有不用这个机制的 ,比如上下文加载器
- 可见性: 子类加载器可以访问父类加载器的类型,但是反过来不被允许.
- 单一性: 由于父类加载器对于子加载器是可见的,所以父类加载过的类,在子类中不会重复加载.
类加载的分类
JVM支持两种类型的类加载器,分别为引导类加载器(BootStrap ClassLoader)和自定义类加载器(User-Defined ClassLoader).
从概念上讲,自定义加载器应该是程序中由开发人员自定义的加载器,但是Java的虚拟机规范并没有如此定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义加载器,而引导类加载器是由C++源码是实现的,在Java环境中也根本获取不到
启动类加载器(BootStrap ClassLoader )
- 这个类加载使用C/C++语言实现,嵌套在JVM内部
- 他用来加载Java的核心库,用于提供JVM自身所需要的类
- 不继承自ClassLoader,没有父加载器
- 出于安全考虑,只加载包名为java,javax,sun等开头的类
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
扩展类加载器(Extension ClassLoader)
- Java语言编写,由ExtClassLoader实现
- 继承自ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre/lib/ext子目录下加载类库,如果用户创建的jar放在此目录下,也会由扩展类加载器加载
应用程序加载器(系统类加载器 AppClassLoader)
- Java语言编写,由AppClassLoader实现
- 继承自ClassLoader类
- 父类加载器为启动类加载器
- 他负责加载环境变量classPath或系统属性java.class.path指定路径下的类库
- 应用程序中的类加载器默认是系统类加载器
- 他是用户自定义类加载器的默认父加载器
- 通过ClassLoader的getSystemClassLoader()可以获取到该类加载器
用户自定义类加载器
通过类加载器可以实现非常绝妙的插件机制.自定义的类加载器能够实现应用隔离,tomcat和spring都在内部实现了自定义类加载器,这样的机制比c/c++要优秀太多,自定义的类加载器通常都需要继承自抽象类ClassLoader
ClassLoader源码分析
什么是双亲委派机制
在加载器加载类的时候,如果加载器有父类(一般是组合定义为父类),先让父类去加载,一层层往上找,如果父类都没有再由自己加载,否则由某一个父类加载进来.
关键方法源码分析
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 {
//在这里 Java源码判断了如果父类不为null,就去调用父类的加载方法,一直递归到最高的父类
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;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
- loadClass属于一个模板方法,不需要去重写他 否则有可能会破坏双亲委派机制
- 子类可以去重写findClass方法,到这里是需要子类加载器自己去加载类方法了(有些像spring的感觉 新建和获取都是通过getSingletonBean去获取单例bean的)
- defineClass是获取类的字节码流信息,并组装成class对象(这个方法也都会用到)
- preDefineClass 中判断必须以java.开头,避免双亲委派机制遭到破坏时系统受到威胁(又加了一层判断确保安全性)
自定义类加载器
public class UserDefineClassLoader extends ClassLoader {
private final String rootPath;
public UserDefineClassLoader(String rootPath) {
this.rootPath = rootPath;
}
@Override
protected Class<?> findClass(String name) {
//转换为以文件路径表示的文件
String filePath = classToFilePath(name);
//获取指定路径的class文件对应的二进制流数据
byte[] data = getBytesFromPath(filePath);
//自定义ClassLoader 内部需要调用defineClass() 把二进制流还原为Class实例
return defineClass(name, data, 0, data.length);
}
private byte[] getBytesFromPath(String filePath) {
FileInputStream fis = null;
ByteArrayOutputStream baos = null;
try {
fis = new FileInputStream(filePath);
baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (baos != null) {
try {
baos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
private String classToFilePath(String name) {
return rootPath + "\\" + name.replace(".", "\\") + ".class";
}
public static void main(String[] args) throws ClassNotFoundException {
UserDefineClassLoader loader = new UserDefineClassLoader("D:\\code\\test");
Class<?> aClass = loader.findClass("com.test.User");
System.out.println(aClass);
System.out.println(aClass.getClassLoader());
}
}
双亲委派类加载机制
优势与好处
- 保护程序安全,防止核心的API类库被随意篡改
- 避免类的重复加载,确保一个类的全局唯一性(当父类已经加载完成后,子类不会再去加载)
弊端
- 下层的加载器可以访问上层父类的加载器都有什么类,但是上层是没有办法得知下层类加载器都有什么类,他是单向的
破坏双亲委派机制
- 线程上下文加载器:(Thread.currentThread().getContextClassLoader()) jdbc等场景就是这么采用的
- 热部署 (每一个程序模块都有一个自己的类加载器,当需要替换一个bundle时,就把bundle连同类加载器一起换掉以实现代码的热替换,这时候类加载处于一种更加复杂的网状结构)
tomcat的类加载机制
- tomcat8可以配置
<Loader delegate="true"/>
表示遵循双亲委派机制
类的结构图
加载流程
当tomcat启动时,会创建几种类加载器
{fwtab}
{fwh}
{fwthead target=“1”} Bootstrap引导类加载器 {/fwthead}
{fwthead target=“2”} system系统类加载器 {/fwthead}
{fwthead target=“3”} tomcat自定义类加载器 {/fwthead}
{fwthead target=“4”} 加载顺序 {/fwthead}
{/fwh}
{fwb}
{fwtbody target=“1”}
加载jvm启动所需的类,以及标准扩展类(位于jre/lib/ext下)
{/fwtbody}
{fwtbody target=“2”}
加载tomcat启动的类,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定,位于CATALINA_HOME/bin下
{/fwtbody}
{fwtbody target=“3”}
Common/Catalina/Shared/WebappClassLoader
这些是tomcat自己定义的类加载器,它们分别加载’/common/,/server/,/shared/*'(在tomcat6以后已经合并到了根目录的lib目录下)和/WebApp/WEB-INF/*中的Java类库,其中WebApp类加载器和jsp类加载器通常会存在多个实例,每一个web应用程序对应一个WebApp类加载器,每一个jsp文件对应一个jsp类加载器
- CommonClassLoader,Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个WebApp访问(加载CATALINA_HOME/lib下的结构,比如servlet-api.jar)
- CatalinaClassLoader,Tomcat容器私有的类加载器,加载路径中的class对于WebApp不可见
- SharedClassLoader,各个WebApp共享的类加载器,加载路径中的class对于所有WebApp可见,但是对于Tomcat容器不可见
{/fwtbody}
{fwtbody target=“4”}
当应用需要某个类时,则会按照下面的顺序进行类加载 - 使用bootstrap引导类加载器加载
- 使用system系统类加载器加载
- 使用应用类加载器在WEB-INF/classes中加载
- 使用应用类加载器在WEB-INF/lib中加载
- 使用common类加载器在CATALINA_HOME/lib中加载
{/fwtbody}
{/fwb}
{/fwtab}
从图中的委派关系可以看出:
CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而
CatalinaClassLoader和SharedClassLoader自己能加载到类则与对方相互隔离.
WebAPPClassLoader可以使用SharedClassLoader加载到的类,但是每个WebAPPClassLoader实例之间互相隔离.
而JsperLoader的加载范围仅仅是这个jsp文件所编译出来的一个.class文件,他出现的目的就是为了被丢弃,当web容器检测到jsp文件被修改时,会替换掉目前的JsperLoader实例再去重建一个新的,以实现jsp文件的热修改功能
tomcat不遵循双亲委派机制会有风险吗
tomcat不遵循双亲委派机制,只是自定义的ClassLoader加载顺序不同,没有去严格的遵循双亲委派机制(我个人觉得这个设计是为了给每个WebApp更好的设置隔离性,以避免互相干扰),但是核心的jdk的api也是遵循双亲委派去由顶层的加载器加载的(他自己的核心api也是有自己的加载器去专门加载,也没有恶意篡改的风险)
tomcat作为web容器,要解决的问题是什么
- 一个web容器可能需要部署多个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,因此要保证每个应用程序的类库是独立的,保证相互的隔离性
- 部署在用一个web容器中相同的类库相同的版本可以共享,否则就会出现多个相同的类加载进入虚拟机,显然这是要去尽力避免的
- web容器自身也需要有一个自己的类库支持,他不能与应用的类库所混淆,基于安全上的考虑,应该让容器的类库和程序的类库相互隔离开
- 支持jsp的热修改(那时候的jsp属于主流技术,还是非常流行的 现在都是动静分离,前后分离了)
如果Tomcat的CommonClassLoader想加载WebAppClassLoader中的类怎么办
用线程上下文加载器
为什么Java文件放在eclipse/idea中的src文件夹下会优先于jar包中的class
tomcat破坏双亲委派机制,自己指定的加载顺序 先使用应用类加载器在WEB-INF/classes中加载,再使用应用类加载器在WEB-INF/lib中加载