JVM相关知识总结整理

JVM启动流程

jvm_001

JVM基本结构

jvm_002

PC寄存器

  • 每个线程拥有一个PC寄存器
  • 在线程创建时创建
  • 指向下一条指令的地址
  • 执行本地方法时,PC的值为undefined

方法区

  • 保存装载的类信息

    • 类型的常量池(JDK6时,String等常量池置于方法,JDK7时,已经移动到了堆)
    • 字段、方法信息
    • 方法字节码
  • 通常和永久区(Perm)关联在一起

Java堆

  • 和程序开发密切相关
  • 应用系统对象都保存在Java堆中
  • 所有线程共享Java堆
  • 对分代GC来说,堆也是分代的
  • GC的主要工作区间

    jvm_003

Java栈

  • 线程私有
  • 栈由一系列帧组成(因此Java栈也叫做帧栈)
  • 帧保存一个方法的局部变量、操作数栈、常量池指针
  • 每一次方法调用创建一个帧,并压栈

Java栈——局部变量表 包含参数和局部变量

public static int runStatic(int i, long l, float f, Object o, byte b) {
    return 0;
}

静态方法的局部变量表如下图所示

jvm_004

public int runInstance(char c, short s, boolean b) {
    return 0;
}

实例方法的局部变量表如下图所示

jvm_005

Java栈——函数调用组成帧栈

public static int runStatic(int i, long l, float f, Object o, byte b) {
    return runStatic(i, l, f, o, b);
}

jvm_006

Java栈——操作数栈

  • java没有寄存器,所有参数传递使用操作数栈

    public static int add(int a, int b) {
        int c = 0;
        c = a + b;
        return c;
    }
    

    对应的操作为:

    0: iconst_0 // 0 压栈
    1: istore_2 // 弹出int,存放于局部变量2
    2: iload_0  // 把局部变量0压栈
    3: iload_1  // 局部变量1压栈
    4: iadd     // 弹出2个变量,求和,结果压栈
    5: istore_2 // 弹出结果,放于局部变量2
    6: iload_2  // 局部变量2压栈
    7: ireturn  // 返回
    

    jvm_007

Java栈——栈上分配

  • 小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上
  • 直接分配在栈上,函数调用完成自动清理空间,减轻GC压力
  • 大对象或者逃逸对象无法栈上分配

栈、堆、方法区交互

// 运行时,JVM把AppMain的信息都放入方法区
public class AppMain {
    // main方法本身放入方法区
    public static void main(String[] args){
        // test1 是引用,所以放到栈区里,Sample是自定义对象应该放到堆里面
        Sample test1 = new Sample("测试1");
        Sample test2 = new Sample("测试2");

        test1.printName();
        test2.printName();
    }
}

// 运行时,JVM把Sample的信息都放入方法区
public class Sample {
    private String name;

    // new Sample实例后,name引用放入栈区里,name对象放入堆里
    public Sample(String name){
        this.name = name;
    }

    // print方法本身放入方法区里
    public void printName(){
        System.out.println(name);
    }
}

jvm_008

内存模型

  • 每一个线程有一个工作内存和主内存
  • 工作内存存放主内存中变量的值的拷贝

当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作

jvm_009

每一个操作都是原子的,即执行期间不会被中断

对于普通变量,一个线程中更新的值,不能马上反应在其他线程中

jvm_010

如果需要在其他线程中立即可见,需要使用volatile关键字

volatile

public class VolatileStopThread extends Thread {
    private volatile boolean stop = false;
    public void stopMe(){
        stop = true;
    }

    @Override
    public void run(){
        int i = 0;
        while(!stop){
            i++;
        }
        System.out.println("Stop thread");
    }

    public static void main(String args[]) throws InterruptedException {
        VolatileStopThread t = new VolatileStopThread();
        t.start();
        Thread.sleep(1000);
        t.stopMe();
        Thread.sleep(1000);
    }
}

如果没有volatile关键字,server运行就无法停止

volatile不能代替锁,一般认为volatile比锁性能好(不绝对)

选择使用volatile的条件是:语义是否满足应用

可见性

一个线程修改了变量,其他线程可以立即知道的方法:

  • volatile
  • synchronized(unlock之前,写变量值回主内存)
  • final(一旦初始化完成,其他线程就可见)

有序性

  • 在本线程内,操作都是有序的
  • 在线程外观察,操作都是无序的(指令重排或主内存同步延时)

指令重排

  • 线程内串行语义

    • 写后读 a = 1; b = a; 写一个变量之后,再读这个位置
    • 写后写 a = 1; a = 2; 写一个变量之后,再写这个变量
    • 读后写 a = b; b = 1; 读一个变量之后,再写这个变量
    • 以上语句不可重排
    • 编译器不考虑多线程间的语义
    • 可重排: a = 1; b = 2;
  • 指令重排——破坏线程间的有序性

    class OrderExample {
        int a = 0;
        boolean flag = false;
    
        public void writer(){
            a = 1;
            flag = true;
        }
    
        public void reader(){
            if(flag){
                int i = a + 1;
                ...
            }
        }
    }
    

    线程A首先执行writer()方法

    线程B接着执行reader()方法

    线程B在int i = a + 1是不一定能看到a已经被赋值为1,因为在writer中,两句话顺序可能打乱

    jvm_011

  • 指令重排——保证有序性的方法

    class OrderExample {
        int a = 0;
        boolean flag = false;
    
        public synchronized void writer(){
            a = 1;
            flag = true;
        }
    
        public synchronized void reader(){
            if(flag){
                int i = a + 1;
                ...
            }
        }
    }
    

    同步后,即使做了writer重排,因为互斥的缘故,reader线程看writer线程也是顺序执行的

    jvm_012

  • 指令重排的基本原则

    • 程序顺序原则:一个线程内保证语义的串行性
    • volatile规则:volatile变量的写,先发生于读
    • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
    • 传递性:A先于B,B先于C,那么A必然先于C
    • 线程的start方法先于它的每一个动作
    • 线程的所有操作先于线程的终结(Thread.join())
    • 线程的中断(interrupt())先于被中断线程的代码
    • 对象的构造函数执行结束先于finalize()方法

解释运行

  • 解释执行以解释方式运行字节码
  • 解释执行的意思是:读一句执行一句

编译运行(JIT)

  • 将字节码编译成机器码
  • 直接执行机器码
  • 运行时编译
  • 编译后性能有数量级的提升

常用JVM配置参数

Trace跟踪参数

  • -verbose:gc

    输出虚拟机中GC的详细情况

    使用后输出如下:

    [Full GC 168K->97K(1984K), 0.0253873 secs]
    

    168K和97K分别表示垃圾收集GC前后所有存活对象使用的内存容量,数据1984K为堆内存的总容量,收集所需要的时间是0.0253873秒

  • -XX:+PrintGC

    同-verbose:gc

  • -XX:+PrintGCDetails

    打印GC详细信息

    jvm_013

  • -XX:+PrintGCTimeStamps

    [GC[DefNew: 4416K->0K(4928K), 0.0001897 secs] 4790K->374K(15872K), 0.0002232 secs] 
    [Times: user=0.00 sys=0.00, real=0.00 secs] 
    
  • -Xloggc:log/gc.log

    • 指定GC log的位置,以文件输出
    • 帮助开发人员分析问题
  • -XX:+PrintHeapAtGC

    • 每次一次GC后,都打印堆信息

    jvm_014

  • -XX:+TraceClassLoading

    • 监控类的加载

    jvm_015

  • -XX:+PrintClassHistogram

    • 按下Ctrl+Break后,打印类的信息

    jvm_017

    • 分别显示:序号、实例数量、总大小、类型

堆分配参数

  • -Xmx -Xms

    • 指定最大堆和最小堆
    • -Xmx20m -Xms5m

      System.out.print("Xmx = ");
      System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
      
      System.out.print("free mem = ");
      System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");
      
      System.out.print("total mem = ");
      System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
      
      // 结果为
      Xmx = 19.375M
      free mem = 4.342750549316406M
      total mem = 4.875M
      
    • Java会尽可能维持在最小堆
  • -Xmn

    • 设置新生代大小(官方推荐新生代占堆的3/8)
  • -XX:NewRatio

    • 新生代(eden + 2 * s)和老年代(不包含永久区)的比值
    • 4表示新生代:老年代=1:4,即新生代占堆的1/5
  • -XX:SurvivorRatio

    • 设置两个Survivor区和eden的比
    • 8表示两个Survivor:eden=2:8,即一个Survivor占新生代的1/10(官方推荐)

    如下例

    public static void main(String[] args){
        byte[] b = null;
        for(int i = 0; i < 10; i++){
            b = new byte[1 * 1024 * 1024];
        }
    }
    

    如果设置

    -Xmx20m -Xms20m -Xmn1m -XX:+PrintGCDetails
    

    jvm_018

    则没有触发GC,数据全部分配在老年代

    如果设置

    -Xmx20m -Xms20m -Xmn15m -XX:+PrintGCDetails
    

    jvm_019

    则没有触发GC,数据全部分配在eden,老年代没有使用

    如果设置

    -Xmx20m -Xms20m -Xmn7m -XX:+PrintGCDetails
    

    jvm_020

    则进行了2次新生代GC,s0 s1太小需要老年代担保

    如果设置

    -Xmx20m -Xms20m -Xmn7m -XX:SurvivorRatio=2 -XX:+PrintGCDetails
    

    jvm_021

    则进行了3次新生代GC,s0 s1增大

    如果设置

    -Xmx20m -Xms20m -XX:NewRatio=1 -XX:SurvivorRatio=2 -XX:+PrintGCDetails
    

    jvm_022

    则进行了2次新生代GC,新生代空间增大

    如果设置

    -Xmx20m -Xms20m -XX:NewRatio=1 -XX:SurvivorRatio=3 -XX:+PrintGCDetails
    

    jvm_023

    则进行了1次新生代GC,新生代空间增大,s0 s1增大

  • -XX:+HeapDumpOnOutOfMemoryError

    • OOM时导出堆到文件
  • -XX:+HeapDumpPath

    • 导出OOM的路径

    示例:

    -Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:+HeapDumpPath
    
    Vector v = new Vector();
    for(int i = 0; i < 25; i++){
        v.add(new byte[1 * 1024 * 1024]);
    }
    

    jvm_024

  • -XX:OnOutOfMemoryError

    • 在OOM时,执行一个脚本
    • “-XX:OnOutOfMemoryError=D:/tools/printstack.bat %p”,printstack.bat的内容为

      D:/tools/jdk1.7_40/bin/jstack -F %1 > D:/a.txt
      
    • 当程序OOM时,在D:/a.txt中会生成线程的dump
    • 可以在OOM时,发送邮件,甚至是重启程序

永久区分配参数

  • -XX:PermSize -XX:MaxPermSize

    • 设置永久区的初始空间和最大空间
    • 他们表示,一个系统可以容纳多少个类型

使用CGLIB等库的时候,可能会产生大量的类,这些类,有可能撑爆永久区导致OOM

for(int i = 0; i < 100000; i++){
    CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean" + i, new HashMap()); // 不断地产生新的类
}

jvm_025

栈大小分配

  • -Xss

    • 通常只有几百K
    • 决定了函数调用的深度
    • 每个线程都有独立的栈空间
    • 局部变量、参数分配在栈上

如下例

public class TestStackDeep {
    private static int count = 0;

    public static void recursion(long a, long b, long c) {
        long e = 1, f = 2, g = 3, h = 4, i = 5, k = 6, q = 7, x = 8, y = 9, z = 10;
        count++;
        recursion(a, b, c);
    }

    public static void main(String args[]) {
        try {
            recursion(0L, 0L, 0L);
        } catch (Throwable e) {
            System.out.println("deep of calling = " + count);
            e.printStackTrace();
        }
    }
}

设置-Xss128K,抛出java.lang.StackOverflowError时,deep of calling = 292

设置-Xss256K,抛出java.lang.StackOverflowError时,deep of calling = 1080

JIT及其相关参数

  • 字节码执行性能较差,所以可以对于热点代码编译成机器码再执行,在运行时的编译,叫做JIT Just-In-Time
  • JIT的基本思路是,将热点代码,就是执行比较频繁的代码,编译成机器码

jvm_038

  • 相关参数

    • Xint

      • 解释执行
    • Xcomp

      • 全部编译执行
    • Xmixed

      • 默认,混合

GC算法与种类

引用计数法

  • 老牌垃圾回收算法
  • 通过引用计算来回收垃圾
  • 使用者

    • COM
    • ActionScript
    • Python

引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能被再被使用

jvm_026

引用计数法的问题

  • 引用和去引用伴随着加法和减法,影响性能
  • 很难处理循环引用

jvm_027

标记-清除

标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法是将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根结点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象,然后,在清除阶段,清除所有未被标记的对象

jvm_028

标记-压缩

标记-压缩算法适合用于存活对象较多的场合,如老年代,它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端,之后,清理边界外所有的空间

jvm_029

复制算法

  • 与标记-清除算法相比,复制算法是一种相对高效的回收方法
  • 不适用于存活对象较多的场合,如老年代
  • 将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收

jvm_030

分代思想

  • 依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代
  • 根据不同代的特点,选取合适的收集算法

    • 少量对象存活,适合复制算法
    • 大量对象存活,适合标记清理或者标记压缩

GC算法总结

  • 引用计数

    • 没有被Java采用
  • 标记-清除

  • 标记-压缩
  • 复制算法

    • 新生代

所有的算法,需要能够识别一个垃圾对象,因此需要给出一个可触及性的定义

可触及性

  • 可触及的

    • 从根节点可以触及到这个对象

      • 根节点包括

        • 栈中引用的对象
        • 方法区中静态成员或者常量引用的对象(全局对象)
        • JNI方法栈中引用对象
  • 可复活的

    • 一旦所有引用被释放,就是可复活状态
    • 因为在finalize()中可能复活该对象
  • 不可触及的

    • 在finalize()后,可能会进入不可触及状态
    • 不可触及的对象不可能复活
    • 可以回收

      public class CanReliveObj {
      
          public static CanReliveObj obj;
      
          @Override
          protected void finalize() throws Throwable {
              super.finalize();
              System.out.println("CanReliveObj finalize called");
              obj = this;
          }
      
          @Override
          public String toString() {
              return "I am CanReliveObj";
          }
      
          public static void main(String[] args) throws
                  InterruptedException {
              obj = new CanReliveObj();
              obj = null;   //可复活
              System.gc();
              Thread.sleep(1000);
              if (obj == null) {
                  System.out.println("obj 是 null");
              } else {
                  System.out.println("obj 可用");
              }
              System.out.println("第二次gc");
              obj = null;    //不可复活
              System.gc();
              Thread.sleep(1000);
              if (obj == null) {
                  System.out.println("obj 是 null");
              } else {
                  System.out.println("obj 可用");
              }
          }
      }
      
      // 输出结果为:
      CanReliveObj finalize called
      obj 可用
      第二次gc
      obj 是 null
      

应该尽量避免使用finalize(),操作不慎可能导致错误,因为它的优先级低,何时被调用不确定,何时发生GC也不确定,可以使用try-catch-finally来替代它

Stop-The-World

  • Stop-The-World

    • java中一种全局暂停的现象
    • 全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互
    • 多半由于GC引起

      • Dump线程
      • 死锁检查
      • 堆Dump
  • GC时为什么会有全局停顿

    • 类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净
  • 危害

    • 长时间服务停止,没有响应
    • 遇到HA系统,可能引起主备切换,严重危害生产环境

GC参数

串行收集器

  • 最古老,最稳定
  • 效率高
  • 可能会产生较长的停顿
  • -XX:+UseSerialGC

    • 新生代、老年代使用串行回收
    • 新生代复制算法
    • 老年代标记-压缩

jvm_031

并行收集器

  • ParNew

    • -XX:+UseParNewGC

      • 新生代并行
      • 老年代串行
    • Serial收集器新生代的并行版本
    • 复制算法
    • 多线程,需要多核支持
    • -XX:ParallelGCThreads 限制线程数量

jvm_032

  • Parallel收集器

    • 类似ParNew
    • 新生代复制算法
    • 老年代标记-压缩
    • 更加关注吞吐量
    • -XX:+UseParallelGC

      • 使用Parallel收集器+老年代串行
    • -XX:+UseParallelOldGC

      • 使用Parallel收集器+老年代并行

jvm_033

  • -XX:MaxGCPauseMills

    • 最大停顿时间,单位毫秒
    • GC尽力保证回收时间不超过设定值
  • -XX:GCTimeRatio

    • 0-100的取值范围
    • 垃圾收集时间占总时间的比
    • 默认99,即最大允许1%时间做GC
  • XX:MaxGCPauseMills和XX:GCTimeRatio,这两个参数是矛盾的,因为停顿时间和吞吐量不可能同时调优

CMS收集器

  • CMS收集器

    • Concurrent Mark Sweep 并发(与用户线程一起执行)标记清除
    • 标记-清除算法
    • 并发阶段会降低吞吐量
    • 老年代收集器(新生代使用ParNew)
    • -XX:+UseConcMarkSweepGC

CMS运行过程比较复杂,着重实现了标记过程,可分为

  • 初始标记

    • 根可以直接关联到的对象
    • 速度快
  • 并发标记(和用户线程一起)

    • 主要标记过程,标记全部对象
  • 重新标记

    • 由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
  • 并发清除(和用户线程一起)

    • 基于标记结果,直接清理对象

jvm_034

  • 特点

    • 尽可能降低停顿
    • 会影响系统整体吞吐量和性能

      • 比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半
    • 清理不彻底

      • 因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理
    • 因为和用户线程一起运行,不能在空间快满时再清理

      • -XX:CMSInitiatingOccupancyFraction设置触发GC的阀值
      • 如果不幸内存预留空间不够,就会引起concurrent mode failure

jvm_035

  • -XX:+UseCMSCompactAtFullCollection Full GC后,进行一次整理

    • 整理过程是独占的,会引起停顿时间变长
  • -XX:+CMSFullGCsBeforeCompaction

    • 设置进行几次Full GC后,进行一次碎片整理
  • -XX:ParallelCMSThreads

    • 设定CMS的线程数量
  • -XX:CMSInitiatingPermOccupancyFraction

    • 当永久区占用率达到这一百分比时,启动CMS回收
  • XX:UseCMSInitiatingOccupancyOnly

    • 表示只在到达阀值的时候,才进行CMS回收

类装载器

class装载验证流程

  • 加载

    • 取得类的二进制流
    • 转为方法区数据结构
    • 在Java堆中生成对应的java.lang.Class对象
  • 链接

    • 验证

      • 目的:保证Class流的格式是正确的

        • 文件格式的验证

          • 是否以0xCAFEBABE开头
          • 版本号是否合理
        • 元数据验证

          • 是否有父类
          • 是否继承了final类
          • 非抽象类是否实现了所有的抽象方法
        • 字节码验证(很复杂)

          • 运行检查
          • 栈数据类型和操作码数据参数是否吻合
          • 跳转指令是否指定到合理的位置
    • 准备

      • 分配内存,并为类设置初始值(方法区中)

        • public static int v = 1;
        • 在准备阶段中,v会被设置为0
        • 在初始化的<clinit>中才会被设置为1
        • 对于static final类型,在准备阶段就会被赋上正确的值
    • 解析

      • 符合引用(字符串)替换为直接引用(指针或者地址偏移量)
  • 初始化

    • 执行类构造器<clinit>

      • static变量赋值语句
      • static{}语句
    • 子类的<clinit>调用前保证父类的<clinit>被调用

    • <clinit>是线程安全的

什么是类装载器ClassLoader

  • ClassLoader是一个抽象类
  • ClassLoader的实例将读入Java字节码将类装载到JVM中
  • ClassLoader可以定制,满足不同的字节码流获取方式
  • ClassLoader负责类装载过程中的加载阶段

ClassLoader的重要方法

  • public Class <?> loadClass(String name) throws ClassNotFoundException

    • 载入并返回一个Class
  • protected final Class<?> defineClass(byte[] b, int off, int len)

    • 定义一个类,不公开调用
  • protected Class<?> findClass(String name) throws ClassNotFoundException

    • loadClass回调该方法,自定义ClassLoader的推荐做法
  • protected final Class<?> findLoadedClass(String name)

    • 寻找已经加载的类

ClassLoader的分类

  • BootStrap ClassLoader(启动ClassLoader)
  • Extension ClassLoader(扩展ClassLoader)
  • App ClassLoader(应用ClassLoader/系统ClassLoader)
  • Custom ClassLoader(自定义ClassLoader)

ClassLoader的协同工作

jvm_036

对象头Mark

  • Mark Word,对象头的标记,32位
  • 描述对象的hash、锁信息,垃圾回收标记,年龄

    • 指向锁记录的指针
    • 指向monitor的指针
    • GC标记
    • 偏向锁线程ID

偏向锁

  • 大部分情况是没有竞争的,所以可以通过偏向来提高性能
  • 所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程
  • 将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark
  • 只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步
  • 当其他线程请求相同的锁时,偏向模式结束
  • -XX:+UseBiasedLocking

    • 默认启用
  • 在竞争激烈的场合,偏向锁会增加系统负担

轻量级锁

  • 普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法
  • 如果对象没有被锁定

    • 将对象头的Mark指针保存到锁对象中
    • 将对象头设置为指向锁的指针(在线程栈空间中)
    • 如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁)
    • 在没有锁竞争的前提下,减少传统锁使用OS互斥量产生的性能损耗
    • 在竞争激烈时,轻量级锁会多做很多额外操作,导致性能下降

自旋锁

  • 当竞争存在时,如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(自旋)
  • JDK1.6中-XX:+UseSpinning开启
  • JDK1.7中,去掉此参数,改为内置实现
  • 如果同步块很长,自旋失败,会降低系统性能
  • 如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能

JVM中获取锁的步骤

  • 偏向锁可用会尝试偏向锁
  • 轻量级锁可用会先尝试轻量级锁
  • 以上都失败,尝试自旋锁
  • 再失败,尝试普通锁,使用OS互斥量在操作系统层挂起

锁优化方法

  • 减少锁持有时间

    public synchronized void syncMethod(){
        othercode1();
        mutextMethod();
        othercode2();
    }
    
    =>
    
    public void syncMethod2(){
        othercode1()
        synchronized(this){
            mutextMethod();
        }
        othercode2();
    }
    

减小锁粒度

  • 将大对象,拆成小对象,大大增加并行度,降低锁竞争
  • 偏向锁,轻量级锁成功率提高
  • ConcurrentHashMap

    • 若干个Segment:Segment<K,V>[] segments
    • Segment中维护HashEntry<K,V>
    • put操作时,先定位到Segment,锁定一个Segment,执行input
    • 在减小锁粒度后,ConcurrentHashMap允许若干个线程同时进入

锁分离

  • 根据功能进行锁分离
  • ReadWriteLock
  • 读多写少的情况,可以提高性能

jvm_037

锁粗化

  • 如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化

    public void demoMethod(){
        synchronized(lock){
            // do sth
        }
    
        // 做其他不需要同步的工作,但能很快执行完毕
        synchronized(lock){
            // do sth
        }
    }
    
    => 
    
    public void demoMethod(){
        // 整合成一次锁请求
        synchronized(lock){
            // do sth
            // 做其他不需要同步的工作,但能很快执行完毕
            // do sth
        }
    }
    

锁消除

  • 在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作

无锁

  • 锁是悲观的操作
  • 无锁是乐观的操作
  • 无锁是一种实现方式

    • CAS(Compare And Swap)
    • 非阻塞的同步
    • CAS(V,E,N)
  • 在应用层面判断多线程的干扰,如果有干扰,则通知线程重试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值