文章目录
三、类加载和初始化
面试题:
-
描述一下类加载器的层次?
-
双亲委派
-
为什么要双亲委派
Class文件 如何加载到内存中的 并且是如何执行的
3.1 Class Cycle
Class文件在我们硬盘中,那么它是如何加载到内存中 总共需要三个大的步骤
- Loading步骤
Loading 是将本地的classfile的二进制的内容加载到内存中
-
Linking 步骤
- Verification 校验
Verification 的主要过程就是用来校验,如何加载的文件的字节码头不是CAFEBABE,该过程就会被拒绝。
-
Preparation
Preparation过程 主要作用就是将静态变量赋默认值 假设定义public static int i=8;在这个过程中并不是把i的值赋值成8,而是要对静态变量i进行默认值的赋值 也就是0;
-
Resolution
该过程 将class文件中常量池用到的一些符号引用转换为内存地址
-
Initializing 步骤
静态变量在该步骤下进行赋值为初始值,才会调用静态代码块。
3.2 ClassLoader
JVM本身有个类加载器的层次 这个类加载器就是普通的Class,这个加载器的层次就是用来加载不同的class
注意: 任何一个classfile被加载到内存中的都会存在两个部分,
第一个部分 二进制的classfile确实被load内存中
第二个部分 生成的class类的对象,class中还会存在其他对象,引用到class对象,而class类对象指向classfile的内存加载
扩展为 Class对象究竟存储在哪里?
Class对象存储在metaspace里面
Metaspace 是JDK1.8版本出现的,Metaspace 就是方法区methodarea 1.8版本移出了永久代,原本在1.8版本之前 PermGenerationspace部分变更成了metaspace 而这两个地方指代的都是方法区。
怎么才能够知道哪些类是由哪些加载器进行加载的呢?
最顶层
BootstrapClassLoader
加载lib/rt.jar charset.jar等核心类 C++实现。
主要负责加载jdk中最核心的jar ,例如runtime.jar 或者是我们平时锁说的String.class,Object.class 都是位于lib/rt.jar
会出现null值 调用的是最顶层加载器,在java的类中没有这样的对象去应对他。
第二层
ExtClassLoader
加载扩展的jar包,jre/lib/ext/*.jar
或由-Djava.ext.dirs指定
第三层
AppClassLoader
加载classpath指定的内容
第四层
自定义加载器
加载自定义的类的内容。
package edu.yau;
import sun.net.spi.nameservice.dns.DNSNameService;
public class ClassLoaderTest01 {
public static void main(String[] args) {
//由最顶层的加载器加载的,核心代码库,由c++编写,java中没有一个可以对应的对象
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());
//当前本类对象获取的类的加载器
System.out.println(ClassLoaderTest01.class.getClassLoader());
//由最顶层的加载器加载的
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getClass().getClassLoader());
System.out.println(ClassLoaderTest01.class.getClassLoader().getClass().getClassLoader());
}
}
类加载器的加载过程叫双亲委派。
在双亲委派中存在一个概念 叫父加载器 这里的父加载器不是继承关系
该图描述的是语法上一种继承关系,而继承关系和父加载器没关系。
父加载器其实指代的是 ClassLoader源码中 有一个变量 这个变量叫Classloader类型 名称叫parent
3.3 双亲委派
Class文件通过自定义的classloader进行加载,如果他没有加载,那么则委托它的父加载器appclassloader 加载, appclassloader 判断是否为本地加载 如果有则直接加载,如果没有则继续向上委托,直到顶层的加载器bootstrapClassLoader,但是当顶层的加载器,也没有加载,就会向下委托,当所有的下级加载器都没有加载那么则抛出异常 classNotFound 异常,如果下级加载器能够加载,那么就由下级加载器进行加载。
双亲:指的有一个从子到父的过程 又有一从父到子的过程
委派:自己不想做的事情 委托别人去完成
向上委派的时候 父加载器都是到 Cache中取寻找
可以把这个缓存理解成是一个list或者是一个数组。
面试题 为什么要去使用双亲委派?
-
防止加载同一个class文件,保证数据的安全
-
保证核心的class文件不被篡改,即使被篡改了也不会加载,即使被加载也不会是同一个class对象 为了保证class的执行安全。
这部分代码是被写死的。
3.4 父加载器
父加载器不是了的加载器的加载器,也不是加载器的父类的加载器
父加载器其实指代的是 ClassLoader源码中 有一个变量 这个变量叫Classloader类型 名称叫parent
package edu.yau;
public class ClassLoaderTest02 {
public static void main(String[] args) {
//获取本类的加载器
System.out.println(ClassLoaderTest02.class.getClassLoader());
//获取本类的加载器的class对象的加载器--顶级加载器加载的
System.out.println(ClassLoaderTest02.class.getClassLoader().getClass().getClassLoader());
//获取本类加载器的父类加载器
System.out.println(ClassLoaderTest02.class.getClassLoader().getParent());
System.out.println(ClassLoaderTest02.class.getClassLoader().getParent().getParent());
//System.out.println(ClassLoaderTest02.class.getClassLoader().getParent().getParent().getParent());
}
}
3.5 类加载器范围
从上个案例的执行结果中,我们可以看出appclassloader和extclassloader 都是Launcher的内部类。 Launcher是classloader的包装类启动类
在Launcher源码中
private static String bootClassPath = System.getProperty("sun.boot.class.path");
final String var1 = System.getProperty("java.class.path");
String var0 = System.getProperty("java.ext.dirs");
sun.boot.class.path 是BootstrapClassloader的加载路径
java.class.path 是AppClassloader的加载路径
java.ext.dirs 是ExtClassLoader的加载路径
import sun.misc.Launcher;
public class ClassLoaderTest03 {
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())); } }
3.6 自定义加载器
小demo
public class ClassLoaderTest04 {
public static void main(String[] args) throws ClassNotFoundException {
Class clazz = ClassLoaderTest04.class.getClassLoader().loadClass("com.openlab.Person");
System.out.println(clazz.getName());
// 类加载器也可以用来加载资源
// ClassLoaderTest04.class.getClassLoader().getResourceAsStream();
}
}
Tomcat 加载的Servlet
Spring框架中加载ApplicationContext
比如在写一些类库的时候或者修改底层框架时。想加载哪个类就可以加载谁。
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 {
//继续使用parent的classloader 递归调用loadClass方法
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
}
// 调用findClass方法去找class
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;
}
}
-
继承ClassLoader
-
重写模板方法 findClass
----调用defineClass方法
从目录中读取class文件,将class文件通过自定义加载器进行加载
利用IO流
Java语言是比较容易被反编译
-防止反编译
-防止篡改
可以给class文件进行加密 解密
作业:1.自定义加载器的实现 视频到群里
2.classfile解析的内容 需要整理 博客的形式 Xmind的形式
3.预习JVM的基础知识点
import java.io.*;
public class MacluClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File file = new File(
"c:/test",
name.replaceAll(".","/").concat(".class"));
try {
FileInputStream fis = new FileInputStream(file);
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 (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);// throw ClassNotFoundException
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader cl = new MacluClassLoader();
Class clazz = cl.loadClass("com.openlab.Person");
Person person = (Person) clazz.newInstance();
person.m();
System.out.println(cl.getClass().getClassLoader());
System.out.println(cl.getParent());
}
}
我们可以定义自己格式的classloader,一般情况下class文件就是一个二进制文件流,可以采用一种比较简单的方式对class文件进行加密和解密
加密: 通过^ 异或 可以定义一个数字 在读取每一个字节的后的写入操作时,可以用流里面获取到的数据和这个数字进行异或的算法, 那么这种情况就可以进行加密的操作
解密: 字节数字这个数字这个数字 那么就完成了解密的操作。
import java.io.*;
public class MacluClassLoaderWithEncription extends ClassLoader{
public static int seed = 0B10110110; // 进行参加加密算法的数字
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File file = new File(
"c:/test",
name.replaceAll(".","/").concat(".class"));
try {
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;
while ((b = fis.read())!=0){
baos.write(b^seed);
}
byte[] bytes = baos.toByteArray();
baos.close();
fis.close();
return defineClass(name,bytes,0,bytes.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);// throw ClassNotFoundException
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, IOException {
encFile("com.openlab.Person");
ClassLoader cl = new MacluClassLoaderWithEncription();
Class clazz = cl.loadClass("com.openlab.Person");
Person person = (Person) clazz.newInstance();
person.m();
System.out.println(cl.getClass().getClassLoader());
System.out.println(cl.getParent());
}
private static void encFile(String name) throws IOException {
File file = new File(
"c:/test/",
name.replace(".","/").concat(".class"));
FileInputStream fis = new FileInputStream(file);
FileOutputStream fos = new FileOutputStream(
new File("c:/test",name.replaceAll(".","/").concat(".macluclass")));
int b = 0;
while ((b = fis.read())!=-1){
fos.write(b^seed);
}
fis.close();
fos.close();
}
}
生成加密好的文件
验证加密文件 打开后是乱码。
3.7 编辑器
解释器: bytecode-interpreter
JIT 即时编辑器 Just In Time compiler
Java语言究竟是一个解释型语言还是编译式的语言
想解释器的可以用解释器,想编译也可以用编译器 看需求是怎么写的 可以通过JVM的一些参数进行设置。
默认的情况是一种混合模式
混合模式:使用解释器+热点编辑器 hotspot
起始阶段采用解释来执行
热点代码的检测 默认值为10000
多次被调用的方法(方法计数器:检测方法的执行频率)
多次被调用的循环(循环的计数器:检测循环的执行频率)
当这样的一个循环或者是一个方法,或者是一段代码,一直都会被多次调用的时候,也就是这段代码执行频率特别高的情况下,那么干脆直接将这段代码编译成本地的代码,在下次直接访问的时候,直接访问本地的代码就可以。就不需要解释器对其进行解释执行。从而达到效率的提升。这种执行代码的方式被称为混合模式。
那么为什么不直接编译成本地代码,编译的执行速度更快?能够提高效率?
-
现在的解释器的执行效率已经是非常高的了,在一些简单的代码执行上,它并不属于编译器。
-
如果要执行的程序 依赖的类库特别多的情况下,在虚拟机中编译一遍,那么启动的过程会非常的缓慢。
-Xmixed 为混合模式:
开始解释执行,启动速度比较快,对热点代码进行检测和编译。
-Xint 解释模式
启动速度很快,执行较慢
-Xcomp 纯编译模式,
启动较慢,执行较快
测试这三个jvm参数
public class WayToRunTest01 { public static void main(String[] args){
*//**这段代码被短时间执行很多次,请JVM虚拟机对其进行优化 *
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*(); S
ystem.*out*.println(end-start);
}
*//* *该方法本身没有意义,就是耗时间用的。 *
public static void m(){
for (int i = 0;i<10_0000L;i++){
long j = i%3;
}
} }
默认的混合模式
在JVM的执行参数中 -Xint 解释模式
很慢 回去洗洗睡吧
纯编译的模式-Xcomp
3.8 懒加载
严格来讲应该叫lazyInitializing
JVM规范中并没有规定什么时候加载
严格的规定了初始化的规则 扩展
- New对象 getstatic 访问静态变量时 putstatic 访问静态实例时,invokestatic指令
以上指令是必须要初始化这个类 访问final变量除外。
-
当反射调用的时候
-
初始化子类的时候 首先父类初始化
-
虚拟机启动时 被执行的主类必须要初始化
-
动态语言支持java.lang.invoke.MethodHandler解析结果为REF-getstatic REF-putstatic REF-invokestatic的方法句柄时 该类必须要初始化。
这个案例 主要看什么时候打印P和X
public class LazyLoadingTest {
public static void main(String[] args) throws ClassNotFoundException {
// P p;
// X x = new X();
// System.out.println(P.i);
// System.out.println(P.j);
Class.forName("com.openlab.LazyLoadingTest$P");
}
public static class P{
final static int i=8;// 打印final的值是不需要加载整个类的
static int j = 9;
static{
System.out.println("P");
}
}
public static class X extends P{
static{
System.out.println("X");
}
}
}
面试题:
如何打破classloader的双亲委派模式?
去重写Classloader中的loadClass方法 而不是findClass方法 这个时候就能够打破双亲委派的机制。
什么时候需要打破需要去打破双亲委派的机制:
-
在JDK1.2版本之前 要自定义classloader的话 必须要重写loadClass方法
-
在一个线程中设定自己的线程的上下文的加载器对象 ThreadContextClassLoader 可以实现基础调用实现类的代码, 通过thread.setContextClassLoader 来设定。
-
模块化的 热部署 热启动
像osgi和tomcat 都有自己的模块指定classloader,可以加载同一个类库的不同版本的对象,目前这个方式用的比较多。
Tomcat中 Webapplication 对象是可以存在多个的,有两个Webapplication 被加载,但是他们的版本不同,这种情况下可以打破双亲委派的。
注意:类的名字的是相同的 只是说版本不同 如果采用双亲委派的机制,那么这两个对象是不可能加载到同一个空间里面 因为加载的过程中,发现在同一空间有同名的类,那么他一定不会被加载。
所以tomcat的每一个Webapplication 都有一个classloader
双亲委派模式加载
package com.openlab;
public class ClassReloadingTest {
public static void main(String [] args) throws ClassNotFoundException {
MacluClassLoader classloader = new MacluClassLoader();
Class clazz = classloader.loadClass("com.openlab.Person");
classloader = null;
System.out.println(clazz.hashCode());
classloader = null;
classloader = new MacluClassLoader();
Class clazz1 = classloader.loadClass("com.openlab.Person");
System.out.println(clazz1.hashCode());
System.out.println(clazz == clazz1);
}
}
从上案例中可以看出,双亲委派,即便重新创建了classloader对象,那么曾经被加载的对象,再次加载的时候,加载的还是这个对象。
热部署应该如何实现
import java.io.*;
public class T012_ClassReloading2 {
private static class MyLoader extends ClassLoader {
@Override public Class<?> loadClass(String name) throws ClassNotFoundException {
File f = new File("C:/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 ( FileNotFoundException e) { e.printStackTrace();
} 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.openlab.Person");
m = new MyLoader();
Class clazzNew = m.loadClass("com.openlab.Person");
System.out.println(clazz == clazzNew); } }
上一篇 jvm 基础到入门 class文件结构
下一篇 jvm java内存模型