上一篇说了类加载器、双亲委派机制、自定义类加载器
一、 问题ask
1. 自定义类加载器的上一层也就是父类加载器是谁
System.out.println(new MyClassLoader().getParent());
输出结果:sun.misc.Launcher$AppClassLoader@18b4aac2
2. 我没有指定parent呀 为什么不是null呢
我们自定义类加载器继承了ClassLoader,new MyClassLoader()的时候会先走类加载器的构造
// 无参构造 调用了2个参数的构造
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
// 这里指定了parent parent从哪儿来 看getSystemClassLoader()
private ClassLoader(Void unused, ClassLoader parent) {
// 指定parent
this.parent = parent;
// 其他操作
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
@CallerSensitive
public static ClassLoader getSystemClassLoader() {
// 返回的scl 看scl怎么初始化的
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
// 获取classLoader
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
// 直接返回了loader loader 是怎么来的
public ClassLoader getClassLoader() {
return this.loader;
}
// Launcher类初始化的时候 构造方法里初始化了load 默认是appclassloader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
3. 直接获取系统类加载器
ClassLoader.getSystemClassLoader(); -> appClassLoader
4. 可以自己指定parent
// 写一个有参的构造 传入一个你想认的爹 然后调用super 把parent传进去就行了
public MyClassLoader(ClassLoader parent) {
super(parent);
}
5. 打破双亲委派? 看一眼 不理解没关系 我也不理解 从别的地方抄过来的
- 重写LoadClass方法
因为双亲委派是在loadClass里边的逻辑指定的 - 什么时候打破 ?
- JDK1.2之前 没有findClass 必须重写loadClass
- ThreadCotextClassLoader 可以实现基础类调用实现类代码,通过thread.setContentClassLoader 指定
- 热启动 热部署
osgi 、tomcat 都有自己的模块指定classloader (可以加载同一类库不同版本)
比如两个WebApplication加载不同版本的同一个类
二、Linking
-
verification
对文件格式进行校验 -
preparation
给静态变量赋默认值 -
resolution
-
将类、方法 、属性等符号引用解析为直接引用
常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用。
比如java.lang.Object 他是个符号引用
如果想找他真是的内存数据 需要根据java.lang.Object先去常量池找见这个符号,然后再根据符号找对应的类型,这个就太绕了 ,直接把符号引用解析为直接引用的话 java.lang.Object 就变为0x00012 内存地址 ,直接根据这个地址找类型就可以了
-
三、Initializing
调用初始化代码
1. 面试题 输出结果是多少
/**
* @author 木子的昼夜
*/
public class Mr {
public static void main(String[] args) {
System.out.println(T.count);
}
}
class T{
// 成员变量
public static int count = 10;
public static T t = new T();
// 构造
private T(){
count++;
}
}
结果:11
如果赋值和new 对象 换一下位置呢
/**
* @author 木子的昼夜
*/
public class Mr {
public static void main(String[] args) {
System.out.println(T.count);
}
}
class T{
// 成员变量
public static T t = new T();
public static int count = 10;
// 构造
private T(){
count++;
}
}
结果: 10
自己想下这个过程 想不通可以公众号留言 我再进行解答 应该都可以想的通 。。
2. 也就是
- 静态属性 : load->默认值->初始值
- 成员属性: new -> 申请内存->默认值->初始值
3. 这里有个面试题 单例 双重校验
/**
* @author 木子的昼夜
*/
public class Sig {
private static T03 t03;
public static T03 getInstance(){
// 先校验是否是null
if (t03 == null) {
// 等锁
synchronized (T03.class){
// 接着校验是否是null 因为可能多个人等锁
if (t03 == null){
t03 = new T03();
}
}
}
return t03;
}
}
class T03{
}
这个单例模式有什么问题吗 ?
面试官会疯狂的暗示你 加volatile .
接着会问volatile的作用 : 禁止指令重排 保证可见性
这里就是因为 我们说的 new T03() 的时候 先分配内存 再赋初始值 再赋默认值
如果内存分配好了 另一个线程 if(t03 == null) 就是false了
然后就返回了 如果用t03.count 那他还是0呢
当然 概率很低 但是这是会出现的
让我们看一下T03 t03 = new T03();的过程
public class T03 {
public int count =8;
}
public class Test {
public static void main(String[] args) {
T03 t03 = new T03();
}
}
注意:这里需要使用 idea的一个工具->jclasslib ByteCode Viewer 直接搜索安装即可
-
先运行一下main方法 生成class文件
-
选中Test文件
-
view 视图 找 Show ByteCode By jclasslib
-
看生成过程
0 new #2 <T03> // (1)这句话就是在内存开辟一块空间 count = 0
3 dup
4 invokespecial #3 <T03.<init>> // (2)这句话就是初始化count值 count = 8
7 astore_1 //(3) 这句话 就是把内存空间 地址引用 赋值给t03变量
8 retur
正常情况下 按照(1) (2) (3)的顺序执行 是没有任何问题的 但是指令可能重排
可能会出现 (1) (3)(2) 这种情况 就是我们上边说的出现问题的情况 所以要禁止指令重排 volatile
4. JMM 不是接妹妹 是 Java Memory Model
1. 先来一个存储器的层次结构图 来开开胃
2. 为什么会出现数据不一致 ?
假设线程1使用cpu1 把数据 x 读到了L0、L1、L2中的任何一个地方 这是cpu独享的
线程2 使用cpu2 把数据x 也读到了 cpu2的 L0 、L1、L2的任何一个地方
这时候就是一个数据 在内存中存储着2份了 其中一份修改了 那另一份没改 是不是就有问题了
3.硬件层面怎么来解决这个问题 -- 总线锁
在cpu 读取数据 L3-->L2 都要过总线
在cpu1读取x的时候 给总线上一把锁 这时候cpu2不允许读
缺点: 总线锁是锁总线,也就是我cpu2不访问x 我cpu2去访问y 也不能访问 这样不是很合理吧
大家去洗脚了,你找了小丽,然后在门口上了一把锁,凭什么不让我去找小兰。。。
4.硬件层面怎么来解决这个问题 -- 一致性协议(各种各样)MESI 、MSI MOSI 、Synaose 、Firefly 、Dragon 等
一般大家聊的时候 是MESI -- intel CPU 实现协议
what is MESI ? is this !
-
数据存储在缓存行上 缓存行用额外两位two bit 来标记状态 ,这里需要注意,如果数据夸缓存行了,那就很难用这种方式标记了,就需要使用总线锁了,呀呼嘿嘿
-
这个很难表达 我试着说一下子
1.我是cpu1, 我从主从读取了x ,这时候只有我读没有其他cpu读,我会标记位Exclusive
- 如果我读的时候,还有别的cpu在读,那我就标记位Shared
- 如果我读回来,我做了修改,那我就标记位Modified ,这个时候别人就会变成Invalid
- 如果我读回来,别的cpu不要脸的进行了修改(为啥我修改就不是不要懒 哈哈),那我就标记为Invalid ,这时候如果我要用这个数计算的时候,我会重新从内存读取一下
至于这些状态都是在什么时候变化的,这个学问就大了去了,主板上各种逻辑单元,我也不知道是什么高科技实现的。
5. 再叙--缓存行
上边说了 缓存行的2bit标记状态 那什么是缓存行呢?
cpu这个家伙呀,在读取数据的时候,是以缓存行为最小单位读取的
比如int x =666; cpu在读取x的时候不会只读取这四个字节,他会读取x及x以后的N个字节
这些个字节总的就叫缓存行,一般缓存行是64字节
缓存行问题:
我是cpu1, 我读取x的时候,会把整个缓存行读取了
我修改了x ,我把缓存行状态改为invalid,其实我没有
修改y z w j 但是如果别的cpu在使用y z w j的话
就需要重新加载一遍
这个问题叫:伪共享 : 位于同一缓存行的两个不同数据被两个CPU锁定,产生互相影响。
这里有一个缓存行对齐的例子:
public class CacheLineTest01 {
static T[] arr = new T[2];
static{
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws InterruptedException {
final CountDownLatch cdl = new CountDownLatch(2);
final long count = 1_0000_0000L;
long start = System.currentTimeMillis();
// 起两个线程 分别修改arr[0] arr[1] 对应对象T的属性
// 这个arr很大概率上会在一个缓存行 因为就2个T对象 每个对象就一个Long类型属性 总共不够64字节
new Thread(()->{
for (long i = 0; i <count; i++) {
arr[0].x = i;
}
cdl.countDown();
}).start();
new Thread(()->{
for (int i = 0; i < count; i++) {
arr[1].x = i;
}
cdl.countDown();
}).start();
cdl.await();
long end = System.currentTimeMillis();
System.out.println((end-start)/100);
}
}
class T{
public volatile long x=0L;
}
执行多次输出结果:
30、29、23、26、27、30
public class CacheLineTest02 {
static T006[] arr = new T006[2];
static{
arr[0] = new T006();
arr[1] = new T006();
}
public static void main(String[] args) throws InterruptedException {
final CountDownLatch cdl = new CountDownLatch(2);
long start = System.currentTimeMillis();
final long count = 1_0000_0000L;
new Thread(()->{
for (long i = 0; i < count; i++) {
arr[0].x = i;
}
cdl.countDown();
}).start();
new Thread(()->{
for (int i = 0; i <count; i++) {
arr[1].x = i;
}
cdl.countDown();
}).start();
cdl.await();
long end = System.currentTimeMillis();
System.out.println((end-start)/100);
}
}
// 加了一个对齐 也就是Padding 这样new2个T006之后 绝对不在一个缓存行
// 所以两个cpu修改属性 不会相互影响
class T006 extends Padding{
public volatile long x=0L;
}
class Padding{
long a,b,c,d,e,f,g;
}
执行多次结果:
14、16、16、14、17、14、15
很明显,第二段代码的执行时间更快 这就是缓存行对齐对程序效率提升的作用
可以看图:第一段代码 会走invalid 每次都会去内存拿数据 再进行修改 ,而第二段代码会走Modified不需要去内存再一次拿数据
6. 乱序执行 01
用一句话总结:cpu为了提高执行效率,会在一条指令准备数据过程中,执行另一条不依赖于前一条指令的指令
可以看一个例子:cpu在执行指令1的时候,指令1 需要去内存拿数据 ,大家知道内存读取数据耗时至少是cpu的100倍起步,这个时间cpu等着吗? 不能呀! 那你电脑不卡成狗了吗。
这个时间cpu会接着去判断下一条指令2,看指令2是否依赖指令1的执行结果,如果依赖,接着看指令3,如果不依赖就执行,依次往下执行,直到指令1拿回来数据为止
举个例子:
小强做饭,第一道菜是土豆炖牛腩,第二道菜是拍黄瓜
如果是你,你会怎么做?
最容易些想到的是这样:
准备土豆->准备牛腩->放锅里->看着它炖熟了->盛出来->准备黄瓜->拍黄瓜->倒酱汁->拍黄瓜做好了
但是我们一般不会这么做,我们跟cpu一样聪明:
我们会这样做:
准备土豆->准备牛腩->放锅里->判断拍黄瓜这道菜要不要等土豆牛腩好了才能做?->不是->准备黄瓜->拍黄瓜->倒酱汁->拍黄瓜做好了->在做拍黄瓜的过程中你肯定会看着土豆牛腩,防止干锅,如果拍黄瓜过程中土豆牛腩好了,你会先停止拍黄瓜,先去把牛腩捞出来(不然土豆块成土豆汤了),然后再去拍黄瓜
7.乱序执行 02
合并写的概念:
拿生活中的例子就是,小强的土豆炖牛肉好了,可以放上桌让别人吃了,但是他觉得,这顿饭拍黄瓜跟土豆炖牛肉一起吃才能称之为“一顿饭”,注意这里一顿饭在cpu中可以对应一个数据。然后他就俩都做好了,拿一个大托盘,把2道菜合成了“一顿饭” 放上桌,大家吃的不亦乐乎。
学术上的概念大概意思就是: 多个程序对同一个数据x进行操作,cpu执行x=x+1; 准备把结果写回L3内存,但是他“自作聪明”的发现,后边好像还有一句 x = x+10;所以他就等着x=x+10;这句执行完之后 再把一个最终结果写回L3内存 ,而不是写2次。
合并写的缓冲区WCbuffer 很小很小 只有4个字节
8.乱序执行 证明小程序
import java.util.concurrent.CountDownLatch;
public class TestOrder {
private static int a=0,b=0,x=0,y=0;
public static void main(String[] args) throws InterruptedException {
long count = 0;
for (;;){
count++;
CountDownLatch cdl = new CountDownLatch(1);
CountDownLatch cdlres = new CountDownLatch(2);
// 默认值
a=0;b=0;x=0;y=0;
new Thread(()->{
try {
cdl.await();
a = 1;
x = b;
} catch (InterruptedException e) { }finally {
cdlres.countDown();
}
}).start();
new Thread(()->{
try {
cdl.await();
b = 1;
y = a;
} catch (InterruptedException e) { }finally {
cdlres.countDown();
}
}).start();
cdl.countDown();
cdlres.await();
if (x==0&&y==0){
System.out.println("存在乱序"+",一共执行:"+count+ " 次");
break;
}
}
}
}
如果不重排出现的结果应该是:
如果出现x==0 && y == 0 的情况 说明指令重拍了
想要证明,你就拿着这个程序,跑吧, 跑一会儿 ,要有耐心
看看我执行的次数:40多万次
9.有序性保证
待续。。
欢迎关注公-众-号: