目录
12.loadClass()、findClass()、defineClass()区别
一.JVM简介
1.JVM的作用
java虚拟机就是二进制字节码的运行环境,负责装载字节码,解释或者编译为对应平台上的机器指令执行。
2.JVM的特点
一次编译,到处运行
自动内存管理
自动垃圾回收
3.JVM的位置
4.JVM的架构模型
5.JVM的生命周期
二.类加载子系统
1.jvm的内存结构
包含三部分
a.类加载子系统
类加载的过程 : 加载 链接 初始化
双亲委派模型 沙箱安全机制
常用的类加载器 bootstrap 类加载器(引导类加载器)、拓展类加载器、应用类加载器、用户自定义类加载器
引导类加载器不是由java实现的。其他类加载器是由java实现的。
关系为非继承关系,是父子关系 getParent();
b.运行时数据区
java方法栈、本地方法栈、堆、方法区(1.8以后叫元空间)、寄存器(或者叫程序计数器)
c.执行引擎
解释器、即时编译器(JIT)、GC
2.类加载子系统的作用
(1) 类加载子系统负责从文件系统或者网络中加载Class文件,Class文件开头有特定的文件标识。
(2)ClassLoader只负责class文件的加载,至于是否可以运行,有执行引擎决定。
(3)加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池的信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
3.类的加载过程
类的加载过程又分为 加载、链接、初始化三个部分。
(1)加载过程
(2)链接过程
链接过程 又细分为 验证、准备、解析三个阶段。
(3)类的初始化过程
初始化阶段就是执行类构造方法<clinit>()的过程
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
构造器方法中指令语句在源文件中出现的顺序执行。
<clinit>()不同于类的构造器。构造器是虚拟机视角下的<init>()
若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕
虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁。
4.类的加载器
(1) 引导类加载器 Bootstrap ClassLoader
(2) 拓展类加载器 Extension ClassLoader
(3) 应用程序类加载器 AppClassLoader
(4)用户自定义类加载器
(5)获取ClassLoader的途径
5.双亲委派机制
java虚拟机对class文件的加载采用按需加载的方式,需要时才会加载。
加载时采用双亲委派机制,它是一种任务委派模式。
a.双亲委派机制是什么:
(1)如果一个类加载器收到了类加载的请求,并不会自己先去加载,而是把这个请求委托给父类加载器去执行。
(2)如果父类加载器还存在父类加载器,则进一步向上委托,直到顶层的启动类加载器。
(3)如果父类加载器可以完成类加载任务,就成功返回,如果无法完成此加载任务,则由子加载器尝试自己去加载,这就是双亲委派机制。
b.优势是什么
(1) 避免类的重复加载
(2)保护程序安全,防止核心API被随意篡改
自定义类 java.lang.String
6.沙箱安全机制
7.如何判断两个对象是否是同一个类
JVM必须知道一个类型是由启动类加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用时,JVM需要保证这两个类型的类加载器是相同的。
8.类的主动使用和被动使用:主动使用才会进行类的初始化
案例一
先定义如下两个类:
public class SuperClazz {
static {
System.out.println("SuperClass init!");
}
public static int value=123;
public static final String HELLOWORLD="hello world";
public static final int WHAT = value;
}
public class SubClaszz extends SuperClazz {
static{
System.out.println("SubClass init!");
}
}
然后进行下面的调用:
public class Initialization {
public static void main(String[]args){
Initialization initialization = new Initialization();
initialization.M1();
}
public void M1(){
System.out.println(SubClaszz.value);
}
}
第一个案例是通过子类去引用父类中的静态变量,两个类都会加载和初始化么?打印结果看看:
SuperClass init!
123
可以看到只有父类初始化了,那么父类必然是加载了的,问题就在于子类有没有被加载呢?可以加上参数:-XX:+TraceClassLoading再执行(该参数的作用就是打印被加载了的类),可以看到子类是被加载了的。所以通过子类引用父类静态变量,父子类都会被加载,但只有父类会进行初始化。
为什么呢?反编译后可以看到生成了如下指令:
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic #6 // Field ex7/init/SubClaszz.value:I
6: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
9: return
关键就是getstatic指令就会触发类的初始化,但是为什么子类不会初始化呢?因为这个变量是来自于父类的,为了提高效率,所以虚拟机进行了优化,这种情况只需要初始化父类就行了。
案例二
调用下面的方法:
public void M2(){
SubClaszz[]sca = new SubClaszz[10];
}
执行后可以发现,使用数组,不会触发初始化,但父子类都会被加载。
案例三
public void M3(){
System.out.println(SuperClazz.HELLOWORLD);
}
引用常量不会触发类的加载和初始化,因为常量在编译后就已经存在当前class的常量池。
案例四
public void M4(){
System.out.println(SubClaszz.WHAT);
}
通过常量去引用其它的静态变量会发生什么呢?这个和案例一结果是一样的。
9.破坏双亲委派模型
刚刚我举了工作中的一个例子来说明双亲委派机制,但现实中我们不需要事事都去请示领导,同样类加载器也不是完全遵循双亲委派机制,在必要的时候是可以打破这个规则的。下面列举四个破坏的情况,在此之前我们需要先了解下双亲 委派的代码实现原理,在java.lang.ClassLoader类中有一个loadClass以及findClass方法:
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 {
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);
}
从上面可以看到首先是调用parent去加载类,没有加载到才调用自身的findClass方法去加载。也就是说用户在实现自定义类加载器的时候需要覆盖的是fiindClass而不是loadClass,这样才能满足双亲委派模型。
下面具体来看看破坏双亲委派的几个场景。
第一次
第一次破坏是在双亲委派模型出现之前, 因为该模型是在JDK1.2之后才引入的,那么在此之前,抽象类java.lang.ClassLoader就已经存在了,用户自定义的类加载器都会去覆盖该类中的loadClass方法,所以双亲委派模型出现后,就无法避免用户覆盖该方法,因此新增了findClass引导用户去覆盖该方法实现自己的类加载逻辑。
SPI
第二次破坏是由于这个模型本身缺陷导致的,因为该模型保证了类的加载优先级,但是有些接口是Java定义在核心类库中,但具体的服务实现是由用户提供的,这时候就不得不破坏该模型才能实现,典型的就是Java中的SPI机制(对SPI不了解的读者可以翻阅我之前的文章或是其它资料,这里不进行阐述)。J
DBC的驱动加载就是SPI实现的,所以直接看到java.sql.DriverManager类,该类中有一个静态初始化块:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
主要看ServiceLoader.load方法,这个就是通过SPI去加载我们引入java.sql.Driver实现类(比如引入mysql的驱动包就是com.mysql.cj.jdbc.Driver):
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
这个方法主要是从当前线程中获取类加载器,然后通过这个类加载器去加载驱动实现类(这个叫线程上下文类加载器,我们也可以使用这个技巧去打破双亲委派),那这里会获取到哪一个类加载器呢?具体的设置是在sun.misc.Launcher类的构造器中:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
可以看到设置的就是AppClassLoader。你可能会有点疑惑,这个类加载器加载类的时候不也是先调用父类加载器加载么,怎么就打破双亲委派了呢?其实打破双亲委派指的就是类的层次结构,延伸意思就是类的加载优先级,这里本应该是在加载核心类库的时候却提前将我们应用程序中的类库给加载到虚拟机中来了。
Tomcat
上图是Tomcat类加载的类图,前面三个不用说,CommonClassLoader、CatalinaClassLoader、SharedClassLoader、WebAppClassLoader、JspClassLoader则是Tomcat自己实现的类加载器,分别加载common包、server包、shared包、WebApp/WEB-INF/lib包以及JSP文件,前面三个在tomcat 6之后已经合并到根目录下的lib目录下。而WebAppClassLoader则是每一个应用程序对应一个,JspClassLoader是每一个JSP文件都会对应一个,并且这两个类加载器都没有父类加载器,这也就违背了双亲委派模型。
为什么每个应用程序需要单独的WebAppClassLoader实例?因为每个应用程序需要彼此隔离,假如在两个应用中定义了一样的类(完全限定名),如果遵循双亲委派那就只会存在一份了,另外不同的应用还有可能依赖同一个类库的不同版本,这也需要隔离,所以每一个应用程序都会对应一个WebAppClassLoader,它们共享的类库可以让SharedClassLoader加载,另外这些类加载加载的类对Tomcat本身来说也是隔离的(CatalinaClassLoader加载的)。
为什么每个JSP文件需要对应单独的一个JspClassLoader实例?这是由于JSP是支持运行时修改的,修改后会丢弃掉之前编译生成的class,并重新生成一个JspClassLoader实例去加载新的class。
以上就是Tomcat为什么要打破双亲委派模型的原因。
OSGI
OSGI是用于实现模块热部署,像Eclipse的插件系统就是利用OSGI实现的,这个技术非常复杂同时使用的也越来越少了,感兴趣的读者可自行查阅资料学习,这里不再进行阐述。
10.双亲委派是怎么实现的?
双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现并不复杂。
实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中:
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 {
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;
}
}
代码不难理解,主要就是以下几个步骤:
1、先检查类是否已经被加载过
2、若没有加载则调用父加载器的loadClass()方法进行加载
3、若父加载器为空则默认使用启动类加载器作为父加载器。
4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
11.如何主动破坏双亲委派机制?
知道了双亲委派模型的实现,那么想要破坏双亲委派机制就很简单了。
因为他的双亲委派过程都是在loadClass方法中实现的,那么想要破坏这种机制,那么就自定义一个类加载器,重写其中的loadClass方法,使其不进行双亲委派即可。
12.loadClass()、findClass()、defineClass()区别
ClassLoader中和类加载有关的方法有很多,前面提到了loadClass,除此之外,还有findClass和defineClass等,那么这几个方法有什么区别呢?
-
loadClass() 就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中。
-
findClass() 根据名称或位置加载.class字节码
-
definclass() 把字节码转化为Class
这里面需要展开讲一下loadClass和findClass,我们前面说过,当我们想要自定义一个类加载器的时候,并且像破坏双亲委派原则时,我们会重写loadClass方法。
那么,如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢?
这时候,就可以继承ClassLoader,并且重写findClass方法。findClass()方法是JDK1.2之后的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 {
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;
}
}
JDK1.2之后已不再提倡用户直接覆盖loadClass()方法,而是建议把自己的类加载逻辑实现到findClass()方法中。
因为在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载。
所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass中实现你自己的加载逻辑即可。
参考资料