关于java:话说-类加载过程-第二篇

45次阅读

共计 7893 个字符,预计需要花费 20 分钟才能阅读完成。

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

一、问题 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

调用初始化代码 <clinit> 给动态成员赋初始值

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. 有序性保障

待续。。

有问题欢送 留言,可也在公众号留言(想赢快):

正文完
 0