话说 类加载过程 第二篇

上一篇说了类加载器、双亲委派机制、自定义类加载器

一、 问题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. 打破双亲委派? 看一眼 不理解没关系 我也不理解 从别的地方抄过来的
  1. 重写LoadClass方法
    因为双亲委派是在loadClass里边的逻辑指定的
  2. 什么时候打破 ?
    1. JDK1.2之前 没有findClass 必须重写loadClass
    2. ThreadCotextClassLoader 可以实现基础类调用实现类代码,通过thread.setContentClassLoader 指定
    3. 热启动 热部署
      osgi 、tomcat 都有自己的模块指定classloader (可以加载同一类库不同版本)
      比如两个WebApplication加载不同版本的同一个类

二、Linking

  1. verification
    对文件格式进行校验

  2. preparation
    给静态变量赋默认值

  3. resolution

    1. 将类、方法 、属性等符号引用解析为直接引用

      常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用。

      比如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. 也就是
  1. 静态属性 : load->默认值->初始值
  2. 成员属性: 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 直接搜索安装即可


  1. 先运行一下main方法 生成class文件

  2. 选中Test文件

  3. view 视图 找 Show ByteCode By jclasslib

  4. 看生成过程

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 !

  1. 数据存储在缓存行上 缓存行用额外两位two bit 来标记状态 ,这里需要注意,如果数据夸缓存行了,那就很难用这种方式标记了,就需要使用总线锁了,呀呼嘿嘿

  2. 这个很难表达 我试着说一下子

    1.我是cpu1, 我从主从读取了x ,这时候只有我读没有其他cpu读,我会标记位Exclusive

    1. 如果我读的时候,还有别的cpu在读,那我就标记位Shared
    2. 如果我读回来,我做了修改,那我就标记位Modified ,这个时候别人就会变成Invalid
    3. 如果我读回来,别的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.有序性保证

待续。。

欢迎关注公-众-号: