深入浅出JVM之调优(一)

339 阅读6分钟

类的加载过程

Loading->Linking(verification->preparation->resolution)->Initializing

Loading

将.class文件load到内存中

Linking

  1. verification(验证):验证文件是否符合JVM规定
  2. Preparation(准备):静态成员变量赋默认值
  3. Resolution(解析,解释):将类、方法、属性等符号引用解析为直接引用,常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用

Initializing

调用类初始化代码 ,给静态成员变量赋初始值

类加载器(ClassLoader)

说明:一般情况下,是从底层往上按顺序处理,先查找是否已经被load过,如果CustomClassLoad没有加载过,继续往上从AppClassLoad中找,如果加载过直接返回,依次类推到最上层BootStrap,然后往下去findClass并load,如果不是自己加载的就往下寻找直到类被ClassLoad加载,如果到最没有没有加载的话抛出异常notfoundClass.

Loading

  1. 双亲委派,主要出于安全来考虑
  2. LazyLoading (懒加载,需要时加载)五种情况
    1. new getstatic putstatic invokestatic指令,访问final变量除外
    2. java.lang.reflect对类进行反射调用时
    3. 初始化子类的时候,父类首先初始化
    4. 虚拟机启动时,被执行的主类必须初始化
    5. 动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化
  3. ClassLoader的源码 :findInCache -> parent.loadClass -> findClass()
  4. 自定义类加载器
  5. 混合执行 编译执行 解释执行

Linking

下面我会用一段小程序来体现一个类的加载过程

public class ClassLoadingDemo{
    public static void main(String[] args) {
        System.out.println("count:"+T.count);
    }
}

class T {
    public static int count = 2; //0
    public static T t = new T(); // null


    private T() {
        count ++;
           }
}

// count:3

这里为什么count会等于3呢?因为一般实例化有一个开辟空间准备的过程,然后在赋值,首先int count =0,T t =null,然后在初始化 int cout =2,t实例化会调用构造函数执行了count++,所以打印出来count:3,下来这段代码的话,我们自己来分析下打印结果是什么

public class ClassLoadingDemo{
    public static void main(String[] args) {
        System.out.println("count:"+T.count);
    }
}

class T {
    public static T t = new T(); // null
    public static int count = 2; //0
    

    private T() {
        count ++;
           }
}

JMM java内存模型

硬件级别缓存一致性(缓存锁MESI)

缓存行,伪共享,一般我们cpu执行的时候拿取数据都是一块一块的拿,不用的数据也会拿过来,可能多个cpu使用一个缓存行的时候会互相争抢,cache失效等操作影响性能。 but,有一些无法被缓存或者MESI解决不了的问题依然必须使用锁总线的方式

MESI Cache一致性协议

一致性协议(MESI):www.cnblogs.com/z00377750/p…

MESI协议中的状态

CPU中每个缓存行(cache line)使用4种状态进行标记(使用额外的两位(bit)表示)

M:被修改(Modified)

该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。 当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。

E:独享的(Exclusive)

该缓存行只被缓存在该CPU的缓存中,它是未被修改过的,与主内存的数据一致,该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。 同样的,它是可以被修改的,状态变为(Modified)被修改的。

S:共享的(Shared)

该状态意味着该缓存行可能被多个CPU缓存,并且缓存行数据与主存一致,如果其中一个缓存行被修改,那么其他的CPU缓存状态变为(Invalid)无效的

I:无效的(Invalid)

该缓存是无效的(可能有其它CPU修改了该缓存行)。

缓存行

缓存行越大,局部性空间效率越高,但是读取效率越低 缓存行越小,局部性空间效率越低,但是读取效率越高 工业实验取舍后,目前来说,多用64字节 使用缓存行的对齐能够提高效率。

乱序问题

CPU为了提高指令执行效率,会在一条指令执行过程中(比如去内存读数据(慢100倍)),去同时执行另一条指令,前提是,两条指令没有依赖关系 www.cnblogs.com/liushaodong… 写操作有一个合并写的概念,WCBuffer,把写操作合并有可能也会出现乱序,原因是有的写操作快有的写操作慢。 一般只有4个位置(特别宝贵,稀有物品) 在批处理的场景里,我们合理的运用WCbuffer可以提高程序的效率。

如何证明乱序问题

public class T04_Disorder {
    private static int x = 0, y = 0;
    private static int a = 0, b =0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for(;;) {
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread one = new Thread(new Runnable() {
                public void run() {
                    a = 1;
                    x = b;
                }
            });

            Thread other = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    y = a;
                }
            });
            one.start();other.start();
            one.join();other.join();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                //System.out.println(result);
            }
        }
    }


    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }
}

第218385次 (0,0)

以上代码执行一段时间后,会出现0,0的情况,这就充分说明了乱序执行。

如何保证有序性

硬件内存屏障 X86
  1. sfence(savefence):在sfence指令前的写操作必须在sfence指令后的写操作前完成。
  2. lfence(loadfence):在lfence指令前的读操作必须在lfence指令后的读操作前完成。
  3. mfence(mixfence):在mfence指令前的读写操作必须在mfence指令后的读写操作前完成。

原子指令,如x86上的”lock …” 指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序 如 lock addl

JVM级别如何规范(JSR133)
  1. LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2, 在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  2. StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  3. LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  4. StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

volatile实现细节

字节码层面

ACC_VOLATILE

JVM层面

LoadLoad屏障、StoreStore屏障、LoadStore屏障、StoreLoad屏障

OS和硬件层面

blog.csdn.net/qq_26222859… hsdis - HotSpot Dis Assembler windows lock 指令实现 | MESI实现

synchronized实现细节

字节码层面

ACC_SYNCHRONIZED monitorenter ---> monitorexit

JVM层面

C C++ 调用了操作系统提供的同步机制

OS和硬件层面

X86 : lock cmpxchg / xxx https://blog.csdn.net/21aspnet/article/details/88571740