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

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

一、 问题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.有序性保障

待续。。

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理