話說 類加載過程 第二篇

上一篇說了類加載器、雙親委派機制、自定義類加載器java

1、 問題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加載不一樣版本的同一個類

2、Linking

  1. verification
    對文件格式進行校驗小程序

  2. preparation
    給靜態變量賦默認值緩存

  3. resolutiontomcat

    1. 將類、方法 、屬性等符號引用解析爲直接引用app

      常量池中的各類符號引用解析爲指針、偏移量等內存地址的直接引用。dom

      好比java.lang.Object 他是個符號引用ide

      若是想找他真是的內存數據 須要根據java.lang.Object先去常量池找見這個符號,而後再根據符號找對應的類型,這個就太繞了 ,直接把符號引用解析爲直接引用的話 java.lang.Object 就變爲0x00012 內存地址 ,直接根據這個地址找類型就能夠了工具

3、Initializing

調用初始化代碼 給靜態成員賦初始值 oop

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、L一、L2中的任何一個地方 這是cpu獨享的
線程2 使用cpu2 把數據x 也讀到了 cpu2的 L0 、L一、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、2九、2三、2六、2七、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;
}
執行屢次結果:
    1四、1六、1六、1四、1七、1四、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.有序性保證

待續。。

歡迎關注公-衆-號:

相關文章
相關標籤/搜索