深入理解JVM笔记

2. Java内存区域

2.2 运行时数据区

Java虚拟机在执行Java程序时,会将其管理的内存划分为若干个区域。在JDK1.6时,内存模型如下。

图1.a JDK1.8前JVM内存区域(来自JavaGuide)
图1.b JDK1.8后JVM内存区域(来自JavaGuide)
  • 程序计数器:

用于记录当前线程执行的字节码的行号指示器。字节码解释器通过改变程序计数器的值确定下一条要执行的字节码。每条线程都有自己的程序计数器,保证线程切换后能恢复原来执行位置。

图2 程序计数器和栈(来自《Inside the Java Virtual Machine》)
  • Java虚拟机栈

线程私有的,每当一个方法被执行,Java虚拟机就会创建一个栈帧,用于存放局部变量表,方法出口,操作数栈,动态链接等信息。

  • 本地方法栈

线程私有的,与Java虚拟机栈类似,不过Java虚拟机栈使用的是Java方法,而本地方法栈使用的是本地(Native)方法。在Hot-Spot中本地方法栈和虚拟机栈是一体的。

  • Java堆

也叫作GC堆(Garbage Collected Heap), 所有线程共享,存放对象实例,几乎所有的对象都在这里被分配内存。不过从jdk 1.7开始已经默认开启逃逸分析,如果某些⽅法中的对象引⽤没有被返回或者未被外⾯使⽤(未逃逸),那么对象可以直接在栈上分配内存。

图3 堆和方法区(来自《Inside the Java Virtual Machine》)
  • 方法区

所有线程共享,⽤于存储已被虚拟机加载的所需的信息。在Hot-spot虚拟机中Jdk1.7前被称为永生代,后来被移动到元空间(下文的直接内存的一部分),当Java虚拟机加载类时,会从方法区提取类信息。这些信息包括类,方法,字段,接口等描述信息,还有运行时常量池运行时常量池类似于符号表,这里需要和.class文件中的常量池区别开,常量池是.class文件中用于存放字面量和符号引用(字段,方法,全限定名等)的区域。每个.class文件被加载时,其常量池就会被添加到JVM的运行时常量池中, 具体细节见12。除此之外,运行时常量池字符串常量池是两个东西。字符串常量池是JVM是所维护的一个字符串实例的引用表,位于堆中。3

  • 直接内存

并非运行时数据区的一部分,不过经常被使用,用于交换Java堆和本地堆的数据。

2.3 HotSpot虚拟机创建对象

2.3.1 对象的创建

new一个对象主要包括检查,分配内存,初始化零值,设置对象头和init方法执行5个步骤

  1. 检查,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的
    符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过
  2. 分配内存,为新⽣对象分配一块确定大小的内存。有指针碰撞空闲列表两种方法,指针碰撞中,内存被分为已分配和未分配的完全分割两个部分,用空闲指针将它们分割开,每次分配内存就移动空闲指针,移动一个对象大小的距离。简单高效。但当已分配和未分配的内存交织在一起的时候,就需要用到空闲列表,用列表来记录呐块内存被分配,哪块内存没有被分配。
图3 分配方式
  1. 将分配到的内存空间都初始化为零值(不包括对象头)
  2. 虚拟机要对对象进⾏必要的设置,例如分代年龄,hashCode,偏向锁等等信息。
图3 分配方式
  1. 此刻所有的成员变量均为0,这时再把对象按照程序员的意愿进⾏初始化。

2.3.3 对象的定位

用引用去定位堆中的对象可以分为两种,句柄访问直接指针访问

  • 句柄访问:引用指向的是句柄,通过句柄来指向堆中的对象的实例和方法区中对象对应的类。对象被移动时候,不用修改引用,修改句柄即可。
  • 直接指针访问:引用直接指向堆中的对象实例,对象实例自己包含方法区中的对象类。访问速度比较快。
图3 句柄访问
图3 句柄访问

3. 垃圾回收和内存分配

JVM的⾃动内存管理主要是针对对象内存的回收和分配。主要需要解决以下三大问题

  1. 哪些内存需要回收?
  2. 什么时候需要回收?
  3. 如何回收?

3.1 概述——哪些要回收

JVM垃圾管理的主要模板是。由于程序计数器,本地方法栈,虚拟机栈都和线程的寿命周期相同,所需要的内存基本上在编译期间就可确定。当线程结束时,内存就自然回收了。

而堆和方法区比较不确定。只有在运行期间才能确定要创建哪些对象,加载哪些对象。而方法区的回收条件比较苛刻,这里回收的主要目标指的是

3.2 对象死亡的判定——什么时候回收

这里主要介绍两种方法,引用计数法可达性分析

  • 引用计数法:给对象中添加⼀个引⽤计数器,每当有⼀个地⽅引⽤它,计数器就加1;当引⽤失效,计数器就减1;若计数器为0,说明没有引用指向该对象,便可以回收。然而无法解决相互引用问题。
  • 可达性分析:通过⼀系列的称为 GC Roots 的对象作为起点,从这些节点开始向下搜索,节点所⾛过的路径称为引⽤链,当⼀个对象到GC Roots 没有任何引⽤链相连的话,则证明此对象是不可⽤的。GC Roots主要包括以下几种:
    • 虚拟机栈和本地方法栈中的引用
    • 方法区中的类变量和常量引用,例如字符串常量池
    • Class对象,常驻的异常对象,类加载器
    • 被同步锁持有的对象
图3 分配方式

由于对象的存活与否与引用息息相关,为了便于控制垃圾收集,JDK1.2后堆引用进行了扩展。主要包括强引用,弱引用,软引用,幻影引用。4

  • 强引用:形如Object obj = new Object()的引用关系,只要Obj不被回收,并且不指向别的对象或者赋值为空,那么它原本指向的对象就永远不会被回收。
  • 软引用:用来描述一些还有用,但非必须的对象。在内存要溢出时,才会回收它们。可⽤来实现内存敏感的⾼速缓存。
  • 弱引用:被弱引用指向的对象只会存活到下次收集。用于防止内存泄漏。
  • 幻影引用:是否有幻影引用完全不会影响到对象的收集,仅用于替代不可靠并且浪费资源finalize()方法,当对象被回收时发送系统通知。5

3.3 垃圾收集算法——如何回收

  • 标记-清除算法:⾸先标记出所有不需要回收的对象,在标记完成后统⼀回收掉所有没有被标记的对象。然而会导致大量的内存碎片,而且一旦有大量的对象需要回收,则标记回收的效率都太低。
  • 标记-复制算法:它可以将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。虽然比较高效,但是直接将可用内存减小了一般。未免太过浪费。假如只有少数对象存活,那么复制的开销就会比较小。
  • 标记-整理算法:标记过程与标记-清除算法一致,然而后序步骤是将存活的对象向内存一端移动,然后清理掉边界以外的内存。
  • 分代收集算法:根据对象存活周期的不同将内存分为⼏块。将 java 堆分为新⽣代和⽼年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。⽐如在新⽣代中,每次收集都会有⼤量对象死去,所以可以选择标记-复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。⽽⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集。

3.5 常见的垃圾回收器

  • Serial 收集器:一个单线程的垃圾收集器,在进行垃圾收集的时候必须中断所有用户线程。但是简单高效,省去了线程切换的开销。新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
  • ParNew 收集器: Serial 收集器的多线程版本。新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
  • Parallel Scavenge 收集器:和前者不同在于关注点是吞吐量( 代 码 执 行 时 间 代 码 执 行 时 间 + 垃 圾 收 集 时 间 \frac {代码执行时间} {代码执行时间+垃圾收集时间} +),新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
  • CMS 收集器:并发收集、低停顿。它第⼀次实现了让垃圾收集线程与⽤户线程(基本上)同时⼯作。并且以最短回收停顿时间为目标
  • G1 收集器:G1 收集器在后台维护了⼀个优先列表,每次根据允许的收集时间,优先选择回收价值最⼤的Region(这也就是它的名字 Garbage-First 的由来)

3.8 对象的分配

对象的分代收集理论时建立在以下两个理论的基础上的。

  • 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  • 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

因此收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。分配的策略如下。

  1. 对象优先在新生代分配,大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
  2. 大对象直接进入老年代, 为了防止来回复制的开销,对象的大小大于JVM所规定的阈值PretenureSizeThreshold时会直接被分配到老年代。
  3. 长期存活的对象进入老年代,对象如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(根据MaxTenuringThreshold所规定,默认为15),就会被晋升到老年代中。

6.类文件结构

我们知道,一个Java程序hello.java要被执行,需要经过Java编译器编译成hello.class也就是所谓的字节码,交给虚拟机去执行。这样针对不同的操作系统设计不同的虚拟机,让虚拟机去完成平台无关性的任务,实现Java语言的一次编译,到处运行。而程序员只需要负责编写Java代码就可以了。而不用像C语言一样考虑不同平台上一个int所占的空间大小。这篇文章6用动画展示了编译语言,解释语言和介于两者之间的Java语言执行过程。

图3 分配方式

6.3 Class文件的结构

这里用结构体的方式展示一下.class文件的结构。7

struct Class_File_Format {
	//魔数,固定值为为0xCAFEBABE,标记这是个class文件
   u4 magic_number;
	// 标记class文件的次版本号和主版本号
   u2 minor_version;   
   u2 major_version;
	//常量池的大小,注意是从1开始,其他大小均是从0开始
   u2 constant_pool_count;   
  	//常量池存储的具体信息,字面量和符号引用
   cp_info constant_pool[constant_pool_count - 1];
	// 访问标志,有9个标记位用来标记接口,类,枚举,public,final等
   u2 access_flags;
	//当前类的类索引,类索引指向该类的全限定名
   u2 this_class;
  //父类的类索引
   u2 super_class;
	//实现接口的数量
   u2 interfaces_count;   
   // 实现接口的索引集合
   u2 interfaces[interfaces_count];
	// 字段的个数
   u2 fields_count;
   //字段的描述信息,类型,名称,public等,不会记录父类的字段。
   //多维数组的全限定名,每一维度将使用一个前置的“[”字符来描述   
   field_info fields[fields_count];
	// 方法的个数
   u2 methods_count;
   // 方法的描述信息,类似于上述的字段。
   //如果父类一个方法不被重写,这里也是不会被记录的
   method_info methods[methods_count];
	//一些额外的属性的个数和描述内容,例如类常量的值,类文件的名称等
   u2 attributes_count;   
   attribute_info attributes[attributes_count];
}

这里复制粘贴一下《深入理解JVM》中的一段话。

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤(博主注:指把几个编译后的.o文件连接为一个可执行文件。6 8),而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

7. 虚拟机类加载机制

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。下图是Java类的生命周期。

图3 一个Java类的生命周期

7.3 类加载的过程

Java虚拟机中类加载的全过程,即加载、验证、准备、解析和初始化这五个阶段所执行的具体动作。

  1. 加载,作为类加载的一个阶段,完成以下三件事
    • 通过一个类的全限定名来获取定义此类的二进制字节流。
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  2. 验证,主要包括文件格式验证(是否符合.class文件规范),元数据验证(字段和父类冲突,抽象类却实现了抽象方法),字节码验证,验证能否将符号引用转化为直接引用。
  3. 准备,将所有的类变量初始化为零值。例如基本变量int,long初始值为0,布尔值默认为false,引用变量则为Null.
  4. 解析,将常量池内的符号引用替换为直接引用的过程

7.4 类加载器

加载阶段需要通过类的全限定名来获取定义了此类的二进制字节流。而完成这一任务的是类加载器。不同的加载器即便是加载同一个类,得到的也会是不同的类。在JVM中,加载器有三层的划分,也被称为双亲委派模型(Parents Delegation Model):

  • 启动类加载器 Bootstrap ClassLoader,虚拟机自身一部分,用C++实现的,主要负责加载<JAVA_HOME>\lib目录中或被-Xbootclasspath指定的路径中的并且文件名是被虚拟机识别的文件。

  • 扩展类加载器 Extension ClassLoader,Java实现的,独立于虚拟机,主要负责加载<JAVA_HOME>\lib\ext目录中或被java.ext.dirs系统变量所指定的路径的类库。

  • 应用程序类加载器 Application ClassLoader,Java实现的,独立于虚拟机。主要负责加载用户类路径(classPath)上的类库。如果不自定义类加载器,那么这就是我们默认的类加载器。

图3 双亲加载模型

双亲委派模型加载顺序如下: 如果一个类收到加载类的请求,它会把这项任务交给它父类加载器去加载,直到最顶层的启动类加载器,当父类无法进行加载,那么才会给子类加载器去加载。

类似于出了事情,下属会甩锅说这都是主人的任务,然后主人就会说, 这是主人的主人的任务。直到找到最终的主人。如果他愿意负责,那么锅就甩给了他。当他决定不愿意负责,就会一层层把锅往下甩,直到有个冤大头愿意承担责任为止。

双亲模型的优点:能够保证加载类的唯一性。上文说过,类的唯一性由它的字节码和加载它的类共同确定,不同加载器即便加载同一个.class文件,在虚拟机中得到的也会是两个类。假如有个捣蛋鬼自建了一个java.lang.Object类,有了双亲加载模型,就能保证它最后是由启动类加载器加载<JAVA_HOME>\lib中rt.jar中的Object类。反之,如果不存在,那么系统就会出现两个Object对象,就会乱了套。

然而有时候我们不得不破坏双亲委派模型,例如jdbc,不同的厂商对jdbc由不同的实现。因此必须由子类加载器去加载它们。然而jdbc作为Java的标准服务,又必须由启动类加载器去加载。这就导致启动类加载器必须依赖子类加载器去加载类。也就违反了双亲模型。9

8.虚拟机字节码执行引擎

Java虚拟机以方法作为最基本的执行单元。在一个线程中,JVM每调用一个方法,就会在该线程的虚拟机栈上开辟一个栈帧,当方法调用结束,该栈帧就会出栈。每个栈帧包括局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。下面让我们来看看。

8.2 运行时栈帧结构

  • 局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量
  • 操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出栈
  • 动态连接每个栈帧都包含一个指向运行时常量池中该栈帧所属方法引用
  • 方法返回地址附加信息不表

8.3 方法调用

一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址。有些方法调用的符号引用在编译期间就可以确定,这种方法调用称为解析(Resolution)。有些方法调用的符号要在运行期间才能确定,这种方法调用称为分派

解析

解析调用的方法要求“编译期可知,运行期不可变”,符合这类要求的有静态方法、私有方法、实例构造器、父类方法以及被final修饰的方法。

分派

  1. 静态分派:下面这段代码中,sayHello(man)最后会输出Hi, human。因为man这个引用指向的对象的实际类型Man,然而却被声明为Human类的引用,也就是说它的外观类型Human。静态类型在编译期间即可确定,而当发生重载,有多个同名方法是,方法调用会根据变量的外观类型来选则对应的方法版本。所以会选择sayHello(Human human)。这种分派其实在编译期间就确定了,因此有的资料会认为这属于解析。
	Human man = new Man();
	sayHello(man);
	void sayHello(Man guy){System.out.println("Hi, guy")}
	void sayHello(Human human){System.out.println("Hi, human")}
  1. 动态分派:下面这段代码很明显会根据运行期间的实际类型来完成方法调用。最后会调用manwomansayHello方法而不是Human的。这是如何实现的呢?通常,虚拟机会先确定该引用指向的对象的实际类型,如果有对应的方法,就直接调用,否则会按照继承关系自下而上地进行查找其各个父类。

  2. 单分派和多分派:方法的接收者与方法的参数统称为方法的宗量。Java语言是一门静态多分派、动态单分派的语言。不太明白。mark一下。

	Human man = new Man();
	Human woman = new Woman();
	man.sayHello();
	woman.sayHello();

10.前端编译与优化

这里的前端编译指的是用Java编译器把.java文件编译为.class文件的过程。

10.3 语法糖的味道

泛型

Java语言中的泛型只在程序源码中存在,在编译后的字节码文件中,泛型会被擦除,例如ArrayList<Integer>ArrayList<String>会被转化为ArrayList,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList<int>ArrayList<String>其实是同一个类型。

自动装箱和自动拆箱

这里坑蛮多的。举个例子10

Integer i = 10; //自动装箱,通过Integer.valueOf()实现
int val = i;    //自动拆箱,通过Integer.intValue()实现
	Integer i1 = 100;
    Integer i2 = 100;
    Integer i3 = 200;
    Integer i4 = 200;
        
    System.out.println(i1==i2); //输出true
    System.out.println(i3==i4); //输出false

这是因为自动装箱时Integer.valueOf(),如果值在 [ − 128 , h ] [-128, h] [128,h]之间,则会返回一个缓存的Integer数组cache中对应的Integer对象,其中 h h h由虚拟机自己给定11。一般是 127 127 127。如果不在,则直接生成新的Integer对象返回,而Double类型和Float类型的valueOf()函数则是直接返回新的对象。BooleanvalueOf()则是直接返回True或者False两个对象之一。所以只要布尔值相等一定相等。

12. Java内存模型与线程

在Java以前的C语言和C++它们都是直接使用硬件设备的内存模型,由于不同硬件的内存模型不同,因此有时需要根据不同的平台编写不同的代码。这是一件非常痛苦的工作,比如有时候需要考虑一个int到底需要占用几个字节。为了避免这种尴尬的情况,Java虚拟机定义了自己独立于平台的内存模型。

12.3 Java内存模型

内存模型

在Java内存模型中,划分了两种内存,一种是主存,也就是硬件设备的储存,所有的变量都被存储在里面。另一种是工作内存,每天线程都会被分配一个,它存储着工作内存中变量的拷贝,Java线程不能隔山打牛,直接修改和读取主内存的变量,只能通过读写工作内存,再由工作内存写入主内存。也不能修改其他线程的工作内存。
在这里插入图片描述
volatile型变量

一个volatile型变量具有如下两个特性:

  • 该变量对其他线程具有可见性,意思是一旦这个变量被修改,其他线程会立刻得知这一修改。而不是像其他变量一样,需要经过工作内存。
  • 禁止重排序:重排序是指当相邻的两行代码没有关联的时候,CPU可能会进行指令重排序,对程序进行优化执行。但是也会产生问题。例如单例模式中的双重校验锁,如果不设置instance为Volatile,那可能导致线程取到的实例并未完全初始化完毕。12volatile关键字能够实现内存屏障利用内存屏障,保证代码安装规定的顺序执行,不会重排序。13

当线程在new instance的时候,并非原子操作,包括多个步骤,例如给成员变量赋值、将引用指向堆内存的对象,假如这两个步骤发生了指令重排,正好有另一个线程执行的时候,判断instance是否为空,发现不为空,则直接返回该instance,用instance中的未初始化的成员变量去做业务逻辑,那么就可能出现问题。所以Volatile关键字在这里的作用是为了禁止指令重排序,防止线程获取instance的时候,取到的是未赋值完毕的中间态的对象。

Java内存模型的三大特性

  • 原子性:基本数据类型的访问、读写都是具备原子性的(long和double在32位机器上不保证)
  • 可见性:指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。finalsynchronize以及volatile都能保证可见性。
  • 有序性:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的,volatile和synchronized可以保证有序性。

12.4 Java与线程

线程的实现

Java的Thread类的所有方法都被声明为native,这意味着Java语言对于线程的具体实现没有明确规定,不同的虚拟机对线程有着不同类型的实现。那么让我们先看看常见的线程实现方式。

这里先规定一些定义,内核线程,也就是操作系统内核支持的线程。内核调度各个线程对处理器的调用。轻量级线程,建立在内核之上并由内核支持的供用户调用的线程。用户线程,指的是完全建立在用户空间的线程库,用户线程的建立,同步,销毁,调度完全在用户空间完成,不需要内核的帮助。14

  • 内核线程实现,也被称为1对1实现,每个轻量级进程都由一个内核线程支持。比较简单。然而,由于是基于内核实现的,所以线程的新建,销毁都是需要进行系统调用,在用户态和核心态进行切换。开销很大。而且每个内核支持的轻量级线程都是有限的。
    在这里插入图片描述
  • 用户线程实现, 被称为1:N实现, 不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也能够支持规模更大的线程数量。不过由于缺少内核的支持,线程的调度,阻塞的解决方法都会相当困难。
图3 用户线程实现
  • 混合实现:前两者的混合。这里不表。

  • Java线程的主流实现,主要是采用1:1实现。

13. 线程安全和锁优化

这里省略了线程安全的定义,让我们直奔主题,看看Java中实现线程安全的办法。

13.2 线程安全的实现方法

  1. 互斥同步。同步代表多个线程并发访问一个数据时,在同一时间只有一个线程或者某些线程能够操作该对象。通过临界区,互斥量和信号量来实现互斥。在Java中,常见的实现互斥的方法为synchronized关键字和JUC包中的Lock接口。
    • synchronized关键字能够在形成一个同步块,多个线程要想执行这个块内的代码,就需要获得该对象的锁,进入代码块,就把锁的计数器加1,执行完代码块,就把计数器减一。当获得锁失败时,就会进入阻塞状态,直到锁被释放。然而由于Java的线程是一对一的实现,导致线程的调用需要借助用户态和内核态进行切换。导致开销很大。
    • JUC包中有一系列的对于Lock接口的实现,如WriteLockReadLockReentrantLock等。这里只介绍ReentrantLock,它可以实现不公平锁,而且可以根据多个条件关联来判定是否加锁。
  2. 非阻塞同步,上述的互斥同步方法是一种悲观锁,也就是说如果不加锁,那么多线程并发时必定会产生冲突。而与其相反的是乐观锁,即先不加锁,直接对数据进行操作,然后再判定是否冲突,如果产生冲突,再解决。乐观锁可以基于CAS(比较和交换)来实现。CAS是一个原子操作,它大致是这样,读取到一个值为 A ,在要将这个值更新为B 之前,检查是否等于 A (比较),如果是则将 A 更新为 B(交换) ,否则什么都不做。不过问题在于假如是 A → B → A A\rightarrow B\rightarrow A ABA。这种篡改是无法被检查到的。15
  3. 无同步方案,略。

13.3 锁优化

锁优化有如下几种常见的措施。

  • 自旋锁。在多核处理器中,多个线程可以在同一时刻执行。假如线程A和线程B在同时执行任务,而线程A需要获得线程B的锁,假如是互斥锁,A就会直接放弃处理器的使用权进入阻塞。而自旋锁则不同,它会让A执行一个空循环,暂时先不放弃处理器。观察B是否能在自旋期间释放锁。16
  • 锁消除。将某些不可能共享数据加的锁进行消除。
  • 锁粗化。将加锁的范围扩大。
  • 轻量级锁。线程在执行同步块之前,JVM会现在当前线程的栈帧中创建锁记录(LockRecord),并将对象头的Mark Word信息复制到锁记录中。然后线程尝试使用CAS将对象头的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,并且对象的锁标志位转变为“00”,如果失败,那么要膨胀为重量级锁,锁标志的状态变为“10”,后面等待的线程也要进入阻塞状态。17解锁时同样是利用CAS把锁记录中复制的内容还原到对象头中。
    在这里插入图片描述
  • 偏向锁:偏向锁用于消除在没有数据竞争的情况下的多余同步。其核心思想就是锁偏向获得它的第一个线程,如果没有其他线程尝试获取该锁,那么就不需要同步。当第一个线程获取对象的锁时,会通过CAS的方式在对象的Mark Word设定偏向位为1,设置标志位10,设定偏向的线程ID。如果操作成功时,以后该线程访问该对象,都无需同步。而一旦有其他线程访问对象时,则偏向模式结束。则进入轻量级锁或者重量级锁。

参考文献


  1. An Introduction to the Constant Pool in the JVM ↩︎

  2. What is Run-Time Constant Pool and Method-Area in java ↩︎

  3. 这一次,彻底弄懂java中的常量池 ↩︎

  4. Types of References in Java ↩︎

  5. Understanding Types of References in Java ↩︎

  6. ICS111 Home ↩︎ ↩︎

  7. Java class file in Wikipedia ↩︎

  8. Linker ↩︎

  9. 面试官:说说双亲委派模型? ↩︎

  10. 深入剖析Java中的装箱和拆箱 ↩︎

  11. Integer缓存池(IntegerCache)及整型缓存池 ↩︎

  12. 代码证明CPU指令重排序 ↩︎

  13. 指令重排序、内存屏障很难?看完这篇你就懂了 ↩︎

  14. 关于进程、线程和轻量级进程的一些笔记 ↩︎

  15. 一文带你整明白Java的N种锁 ↩︎

  16. 面试必备之深入理解自旋锁 ↩︎

  17. Synchronized与三种锁态 ↩︎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值