前言:
本文主要介绍java的类加载过程和类加载器的种类、双亲委派机制、自定义类加载器和打破双亲委派的写法,以及缓存行、伪共享和cpu指令重排的一些验证(为下一篇做引子)。
1、java类加载
类加载的过程
类加载的时候把class(二进制字节码文件)加载到内存,做两件事,生成一个区域放字节码,生成一个class的对象,以后使用的的时候都是用过class对象代理去执行对应的字节码指令。
类加载器
publicstaticvoidmain(String[] args) {
//核心类库的加载bootstrap
System.out.println(String.class.getClassLoader());
//核心类库的加载bootstrap
System.out.println(HKSCS.class.getClassLoader());
//扩展库的类加载extension
System.out.println(DNSNameService.class.getClassLoader());
//应用本身的类加载application
System.out.println(ClassTest.class.getClassLoader());}
双亲委派(类加载的过程)
自底向上检查该类是否已加载,子到父的方向
class加载的时候先去调用customer加载器查看缓存是否有加载,有的话返回结果,没有的话调用application的类加载器查看缓存是否有加载,有的话返回结果,没有的话继续向上需查看extension的类加载器的缓存是否有加载,有的话返回结果,没有的话继续查看bootstrap是否加载有加载,有的话返回结果
自顶向下进行实际的查找和加载,父到子的方向
从下到上都没有加载到的话,开始向下通知extension去加载,可以加载的话返回结果,不能加载的话继续向下找application,能加载的话返回结果,不能加载的话继续向下找customer,能加载的话返回结果,不能加载的话,报错classNotFound;整个加载的过程需要循环走两次
为什么使用双亲委派:
安全问题(主要) 资源浪费问题(次要)
类加载器加载的具体路径(Launcher)
bootstrap:
exsention:
application:
代码验证:
StringbootPath=System.getProperty("sun.boot.class.path");
System.out.println(bootPath.replaceAll(";",System.lineSeparator()));
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
StringextPath=System.getProperty("java.ext.dirs");
System.out.println(extPath.replaceAll(";",System.lineSeparator()));
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
StringappPath=System.getProperty("java.class.path");
System.out.println(appPath.replaceAll(";",System.lineSeparator()));
自定义类加载器
实现方式:继承classLoader,并且重写他的findClass()。
意义:自己写类库实现;为了加密使用
相关代码块
packagecom.java.czing.jvm.classfile;
importjava.io.ByteArrayOutputStream;
importjava.io.File;
importjava.io.FileInputStream;
/**
* @Description 实现自定义类加载器
* @Author wangchengzhi
* @Date 2023/2/18 21:59
*/
publicclassCustomerClassLoaderextendsClassLoader {
@Override
protectedClassfindClass(Stringname) throwsClassNotFoundException {
Filef=newFile("D:\\workspace\\jvm\\target\\classes\\com\\java\\czing\\jvm\\classfile", name.replace(".", "/").concat(".class"));
try {
FileInputStreamfis=newFileInputStream(f);
ByteArrayOutputStreambaos=newByteArrayOutputStream();
intb=0;
while ((b=fis.read()) !=0) {
baos.write(b);
}
byte[] bytes=baos.toByteArray();
baos.close();
fis.close();//可以写的更加严谨
//二进制转换为class类对象
returndefineClass(name, bytes, 0, bytes.length);
} catch (Exceptione) {
e.printStackTrace();
}
returnsuper.findClass(name); //throws ClassNotFoundException
}
publicstaticvoidmain(String[] args) throwsException {
//获取自定义的类
ClassLoaderclassLoader=newCustomerClassLoader();
//加载自定义类下面的具体对象
Classclazz=classLoader.loadClass("com.java.czing.jvm.classfile.ClassTest");
Classclazz1=classLoader.loadClass("com.java.czing.jvm.classfile.ClassTest");
//只加载一次,加载到的是同一个对象
System.out.println(clazz==clazz1);
//通过反射创建实例
ClassTesto= (ClassTest)clazz.newInstance();
//调用方法
o.test();
//自定义类加载器
System.out.println(classLoader.getClass().getClassLoader());
//类加载器的父级加载器
System.out.println(classLoader.getParent());
System.out.println(getSystemClassLoader());
}
}
java是解释型和编译型语言
指定类型:-Xmixed默认混合模式 -Xint 纯解释模式 -Xcomp 纯编译模式
默认为混合模式,开始解释执行,启动速度较快,对热点代码实行检测和编译。
混合模式:
1.混合使用解释器+热点代码编译
2.起始阶段采用解释执行
3.热点代码检测
-Xint: 使用解释模式,启动很快,执行稍慢
-Xcomp: 使用纯编译模式,执行很快,启动很慢。
jvm是懒加载的
java不是一次性加载所有类库 按需加载
lazyLoading的5种情况:
new getstatic putstaticinvokestatic指令, 访问final变量除外
java.lang.reflect对类进行反射调用时
初始化子类的时候,父类首先初始化
虚拟机启动时,被执行的主类必须初始化
动态语言支持java.lang.invoke.MethodHandle解析的结果为
REF getstatic REF putstatic REF invokestatic的方法句柄时,该类必须初始化
打破双亲委派机制
集成classloader重写Loadclass方法,打破双亲委派机制可以实现热部署。
对应的源码:
protectedClassloadClass(Stringname, booleanresolve)
throwsClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Classc=findLoadedClass(name);
if (c==null) {
longt0=System.nanoTime();
try {
if (parent!=null) {
c=parent.loadClass(name, false);
} else {
c=findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundExceptione) {
// 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.
longt1=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);
}
returnc;
}
}
2、类的链接过程(Linking)
verification(验证)
验证文件是否符合JVM规定
preparation(准备)
静态成员变量赋默认值
resolution(解析)
将类、方法、属性等符号引用解析为直接引用
常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
load - 默认值 - 初始值
new object - 申请内存 - 默认值 - 初始值
4、CPU读取文件
3、缓存行 伪共享
线程a 在CPU1上读写变量X, 同时线程b上读写变量Y,但是巧合的是变量 X , Y 在同一个缓存行上,那边线程a,b就要互相竞争获取该缓存行的读写权限,才可以进行读写。假如 线程a在内核1上 获取了缓存行的读写权限,进行了操作,就会导致其他内核中的x变量和y变量同时失效,那么线程b 就必须刷新它的缓存后才能在内核2上获取缓存行的读写权限。 这就导致了这个缓存行在不同的线程之间多次通过 L3缓存进行交换最新复制的数据,极大的影响了CPU性能。
不同的cpu读取同一个缓存行操作不同数据的时候,会因为分别操作数据,导致缓存行的不断更新,重新拉取同步内存,会有数据伪共享的问题,导致效率降低。
解决方法:
1、可以采用padding缓存行填充的方式解决,每个缓存行64字节,在该变量的前后分别加7个long类型的属性,共56字节,这样就能保证让数据自己独立存在于一个缓存行。
2、disruptor框架实现缓存行填充,前后都创建7个对象。空间换取时间。
4、cpu的乱序执行问题
cpu为了提高效率,不会完全按照指令顺序去执行,会同时执行一些不相关的指令操作。
写操作也可以进行合并写的操作,合并写的技术分词合并提交,比一次提交效率要高
证明指令重排的编码:
packagecom.java.czing.jvm.jmm;
/**
* @Description 如果x,y出现0,0
* 说明出现了指令重排
* @Author wangchengzhi
* @Date 2023/2/19 21:42
*/
publicclassDisorder {
privatestaticintx=0, y=0;
privatestaticinta=0, b=0;
publicstaticvoidmain(String[] args) throwsInterruptedException {
inti=0;
for(;;) {
i++;
x=0; y=0;
a=0; b=0;
Threadone=newThread(newRunnable() {
publicvoidrun() {
//由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
//shortWait(100000);
a=1;
x=b;
}
});
Threadother=newThread(newRunnable() {
publicvoidrun() {
b=1;
y=a;
}
});
one.start();other.start();
one.join();other.join();
Stringresult="第"+i+"次 ("+x+","+y+")";
if(x==0&&y==0) {
System.err.println(result);
break;
} else {
//System.out.println(result);
}
}
}
publicstaticvoidshortWait(longinterval){
longstart=System.nanoTime();
longend;
do{
end=System.nanoTime();
}while(start+interval>=end);
}
}
如何保证有序性:
1、volitile保证了有序
2、加锁但是效率低
内存屏障(cpu级别)
结语
本文主要内容是针对类加载的说明和分享,双亲委派机制的讲解,最后带出缓存行等硬件层面的一些技术原理,有需要代码验证的随时留言交流。