JVM随笔

运行时数据区

1、程序计数器:一块较小的内存空间,唯一一个不会OOM的区域。字节码解释器工作时通过改变该计数器的值来选取下一条需要执行的字节码指令,属于线程私有,如果执行的是Native方法,则计数器数值为空。

2、Java虚拟机栈:线程私有,描述的是Java方法执行的内存模型。每个方法被执行的同时会创建一个栈帧,用于存储局部变量表、操作栈、动态链接和方法出口等信息。方法开始执行到结束对应栈帧入栈和出栈。

a、局部变量表存放了基本类型数据、对象引用和returnAddress类型。
基本数据类型:boolean byte short int long float double char。
对象引用:不等同与对象本身,可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象有关的位置。Sun HotSpot使用直接指针引用。
returnAddress:指向一条字节码指令的位置,目前已经很少见了,很古老的Java虚拟机用这些指令实现异常处理,现在已经被异常表代替。

b、该区域规定了两种异常情况:StackOverflowError和OutOfMemoryError。
StackOverflowError:线程请求的栈深度大于虚拟机允许的深度。
OutOfMemoryError:扩展时无法申请足够的内存。

3、本地方法栈:线程私有,和Java虚拟机栈作用类似,区别在于服务使用到的Native方法。

4、Java堆:是Java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建,被所有线程共享,其作用是存放对象实例

a、Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
b、大小可以扩展,通过-Xmx和-Xms分别设置最大最小内存。
c、内存不足会抛出OutOfMemoryError。

5、方法区:被所有线程共享,其作用是存储已被虚拟机加载的类信息、常量、静态变量、即时编辑后的代码等数据

a、GC行为在这个区域比较少见,这个区域的内存回收目标主要是针对常量池的回收和类型的卸载。
b、方法区无法满足内存分配需求时,抛出OutOfMemoryError。
c、运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后存放到常量池。运行时常量池具有动态性,运行期间也可以将新的常量放入池中(如String.intern())

6、直接内存:不是虚拟机运行时数据区的一部分,在NIO中引入了基于channel和缓冲区的I/O方式,可以使用Native函数直接分配堆外内存。直接内存的分配受机器总内存的限制,动态扩展时可能出现OutOfMemoryError。

垃圾收集与内存分配

判断对象是否存活

1、根搜索算法:通过一系列名为"GC Roots"对象作为起始点,从这些节点向下搜索,搜索走过的路径叫做引用链。如果一个对象到GC Roots没有引用链相连,则对象是不可用的,需要被GC。

GC Roots对象包括:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法JNI中引用的对象

2、引用类型分为:强引用、软引用、弱引用和虚引用。

强引用:普遍存在的类似Object o = new Object()的引用,只要引用还存在就不会被GC。
软引用:堆内存不足时会回收。
弱引用:下一次GC时回收。
虚引用:唯一目的是能在这个对象被回收时收到一个系统通知,一个对象是否有虚引用的存在不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。

3、一个对象GC Root不可达时,将会进行一次标记并进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。

1、没有覆盖过该方法的对象,或者已经执行过finalize()的对象会被回收,finalize()只会被执行一次。
2、需要执行finalize()的对象会被放在名为F-QUEUE的队列中,稍后GC将对F-QUEUE中的对象进行第二次小规模标记。如果对象重新建立了引用链,将不会被回收。

垃圾收集算法

  • 标记清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收。其缺点是效率不高,同时会产生大量不连续的空间碎片。

  • 复制算法:将内存分为两部分,GC时将存活的对象复制到另一部分,然后将已使用的内存空间一次清理掉。不会产生空间碎片,效率高,代价是可用内存变小。一般采用这种算法回收新生代

  • 标记整理算法:让所有的对象都向一端移动,然后直接清理掉端边界以外的部分,一般采用这种算法回收老年代,因为老年代对象存活率高。

  • 分代收集算法:根据对象存活周期的不同将内存分为几块(新生代和老年代),根据各个年代特点采用最适当的收集算法。

垃圾收集器

新生代收集器

  • Serial收集器:最基本、最悠久的单线程收集器,使用复制算法的收集器。会造成“stop the world”,即垃圾收集时所有的工作线程都会暂停等它收集结束。优点是与其他收集器相比,有更高单线程手机效率,是运行在Client模式下的默认新生代收集器。

  • ParNew收集器:Serial收集器的多线程版本。

  • Parallel Scavenge收集器:使用复制算法的并行多线程收集器收集器,其目标是达到一个可控制的吞吐量(CPU运行用户代码的时间和CPU总消耗时间的比值),适用于运算多的场景。

老年代收集器

  • Serial Old收集器:Serial收集器的老年代版本,单线程收集器,使用标记整理算法

  • Parallel Old收集器:Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法

  • CMS收集器使用标记清除算法,以获取最短回收停顿时间为目的的收集器,其优点是并发收集和低停顿,适用于对响应速度要求高的场景。缺点是对CPU资源比较敏感、无法处理浮动垃圾以及标记清除算法会产生空间碎片。

  • G1收集器:当今收集器技术较为前沿的成果,基于标记整理算法,可以精确控制停顿,实现在不牺牲吞吐量的前提下完成地停顿的内存回收。

GC相关参数

参数 描述
UseSerialGC client模式下默认值,使用Serial + Serial Old组合
UseParNewGC 使用ParNew + Serial Old组合
UseConcMarkSweepGC 使用ParNew + CMS + Serial Old组合,Serial Old作为CMS出现Concurrent Mode Failure的后备收集器
UseParallelGC Server模式下的默认值,使用Parallel Scavenge + Serial Old组合
UseParallelOldGC Server模式下的默认值,使用Parallel Scavenge + Parallel Old组合
SuvivorRatio 新生代中Eden区域和Survivor区域的容量比值,默认为8,代表Eden:Survivor=8:1
PretenureSizeThreshold 直接晋升到老年代的对象大小
MaxTenuringThreshold 直接晋升到老年代的对象年龄,每个对象坚持过一次Minor GC后年龄加1
UseAdaptiveSizePolicy 动态调整堆中各个区域大小以及进入老年代的年龄
HandlePromotionFailure 是否允许分配担保失败,即老年代的剩余空间不足以应对新生代整个Eden和Survivor区所有对象存活的情况
ParallelGCThread 设置并行GC时进行回收的线程数
GCTimeRatio GC时间占总时间的比率(1/(1+值)),默认为99,即运行1%的GC时间
MaxGCPauseMillis 设置GC的最大停顿时间
CMSInitiatingOccupancyFraction 设置CMS在老年代空间被使用多少后触发垃圾收集,默认值为68%
UseCMSCompactAtFullCollection 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理
CMSFullGCsBeforeCompaction 设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理

内存分配策略

  • 对象优先在Eden分配。

  • 大对象直接进入老年代。大对象是指需要大量连续空间的Java对象,如很长的字符串及数组。

  • 长期存活的对象进入老年代。

  • 动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

  • 空间分配担保:发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间,如果大于则直接进行一次Full GC;如果小于,则查看HandlePromotionFailure是否运行担保失败,允许则进行MinorGC,
    不允许也要进行Full GC。

虚拟机类加载机制

类的生命周期

类从加载到虚拟机内存开始到卸载出内存为止,它的生命周期包括:加载、连接、初始化、使用、卸载,其中连接阶段包括验证、准备和解析。

加载

  • 通过一个类的全限定名获取定义此类的二进制字节流。
  • 将字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区数据的访问入口。

验证

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

准备

准备阶段是正式为类变量(static修饰)分配内存并设置初始值的阶段,这些内存都将在方法区中进行分配。

例:public static int value = 123
准备阶段会将value初始化为0,将value赋值为123是在类初始化的时候进行,参照类初始化的第一种情形。

如果是static final修饰,则在准备阶段就被初始化为123。

解析

虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用就是字符串,这个字符串包含足够的信息,以供实际使用时可以找到相应的位置。你比如说某个方法的符号引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有类的信息,方法名,方法参数等信息。

当第一次运行时,要根据字符串的内容,到该类的方法表中搜索这个方法。运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。

初始化

初始化阶段是执行类构造器()方法的过程。()方法是编译器自动收集类中所有static变量的赋值动作和静态语句块中的语句合并产生,收集顺序由语句在源文件出现的顺序决定。静态语句块只能访问到定义在块之前的变量,定义在之后的变量,块中可以赋值但是不能访问。

有且只有四种情况会触发类的初始化:
1、使用new关键字实例化对象、读取或者设置一个类的静态字段(被final字段修饰、已在编译期把结果放入常量池的静态字段除外)以及调用一个类的静态方法时。
2、使用反射包的方法对类进行反射调用时。
3、当初始化一个类时,如果父类没有初始化则触发父类初始化。
4、包含main方法的类。

下列情形属于被动引用并不会触发类的初始化:
1、通过子类引用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义该字段的类才会被初始化,因此当我们通过子类来引用父类中定义的静态字段时,只会触发父类的初始化,而不会触发子类的初始化。
2、通过数组定义来引用类,不会触发此类的初始化。
3、常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
  • 父类的()比子类的优先执行。
  • ()方法对于类或者接口来说并不是必须的,如果一个类中没有静态代码块或者类变量的赋值操作,那么可以不为类生成该方法。
  • 实现类或者子接口的初始化并不会导致父接口的初始化,只有当程序首次主动使用特定接口的静态变量(运行期常量)时,才会导致接口的初始化。这里需要注意的是接口中定义的变量都是常量,而常量又分为编译期常量和运行期常量,编译期常量的值在编译期间就可以确定,直接存储在了调用类的常量池中,所以访问接口中的编译期常量并不会导致接口的初始化,只有访问接口中的运行期常量才会引起接口的初始化。

类加载器

双亲委派模型

加载器
  • 启动类加载器(Bootstrap Classloader):负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。
  • 扩展类加载器(Extension Classloader):由sun.misc.Launcher$ExtClassLoader实现,负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):由sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径(ClassPath)上所指定的类库,一般情况下是程序中的默认类加载器。
加载过程

如果一个类加载器收到了类加载的请求,它首先会委派给父类加载器加载,每一层加载器都是如此,只有当父加载器反馈无法自己完成这个加载请求,子加载器才会尝试自己去加载。

字节码执行引擎

运行时栈帧结构

  • 栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈的栈元素
  • 每一个栈帧都包含局部变量表、操作数栈、动态连接和方法返回地址等信息。
  • 在活动线程中只有栈顶的栈帧是有效的称为当前栈帧,当前栈帧所关联的方法称为当前方法。

局部变量表

  • 局部变量表以容量槽slot为最小单位,虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的slot数量。
  • 编译(javap -v *.class)后方法Code属性locals数据项确定了该方法所需要分配的最大局部变量表容量。
  • 如果是实例方法,局部变量表的第0位索引的Slot默认是对象实例的引用,即this。
  • 局部表中的slot是可重用的,在某些情况下slot的复用会直接影响系统的垃圾收集行为。
如果方法中后面的代码有一个非常耗时的操作,而前面又定义了大量占用内存的变量,那么在方法结束之前,由于slot没有被释放,大变量不会被gc,这时候可以手动设置变量为null。
  • 局部变量必须赋值才能使用。

操作数栈

  • 操作数栈的每一个元素可以是任意的Java数据类型包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。
  • 编译(javap -v *.class)后方法Code属性stack数据项确定了该方法的最大栈深度。

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化成为直接引用。另外一部分会在每次的运行期间转化为直接引用,这部分称为动态连接。

方法返回地址

用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理表来确定的,栈帧中一般不会保存这部分信息。

方法调用

Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件中存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。

解析

在编译期间确定下来的一类方法调用称为解析。
Java虚拟机提供了5条方法调用字节码指令:

- invokestatic:调用静态方法。
- invokespecial:调用实例构造器方法、私有方法和父类方法。
- invokevirtual:调用所有的虚方法。
- invokeinterface:调用接口方法,会在运行期再确定一个实现此接口的对象。
- invokdynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,该指令的分派逻辑由用户所设定的引导方法决定。

非虚方法:解析阶段确定的方法,包括invokestatic和invokespecial指令调用的方法,即静态方法、私有方法、实例构造器和父类方法4类,还包括final修饰的方法。

虚方法:非虚方法之外的方法。

分派

静态分派:所有依赖静态类型来定位方法执行版本的分派动作,典型应用是方法重载。静态分派发生在编译期,因此确定静态分派的动实际上不是由虚拟机决定的。

例如:Man和Woman类继承Human类
Human man = new Man()
Human woman = new Woman()
称Human为变量的静态类型或外观类型(左边的类型),Man或者Woman为变量的实际类型(右边的类型)。
如果一个类中有三个重载方法sayHello(Human human),sayHello(Man man),sayHello(Woman woman),传入上述实际类型为Man的Human对象,那么实际执行的是sayHello(Human human)方法。

动态分派:在运行期根据实际类型确定方法执行版本的分派动作,典型应用是方法重写

invokevirtual指令的多态查找:
1、找到操作数栈顶的第一个元素所指向的对象实际类型,记做C;
2、如果在C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,通过则返回这个方法的直接引用,不通过则返回IllegalAccessError异常。
3、否则,按照继承关系从上往下依次对C的各个父类进行第二步的搜索和验证过程。
4、如果始终没有找到合适的方法,则抛出AbstractMethodError异常。

单分派和多分派:方法的接收者(执行方法的对象)和参数称为方法的宗量,单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。Java语言属于静态多分派和动态单分派的语言。

静态分派 编译器选择的依据方法接收者的静态类型和方法参数,因此属于多分派;
动态分派 编译器选择的依据只有方法的接收者,属于单分派。

基于栈的字节码解释执行引擎

Javac编译器工作:程序源码 -> 词法分析 -> 单词流 -> 语法分析 -> 抽象语法树 -> 字节码指令流。

Java编译器输出的指令流,基本上是一种基于栈的指令集架构,指令流的指令大部分都是零地址指令,依赖操作数栈进行工作。

虚拟机:解释执行,也可能通过即时编译器编译执行,由虚拟机决定。

Java内存模型(JMM)与线程

  • JMM的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
  • JMM规定所有的变量都存储在主内存中,每个线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,线程间变量值的传递需要通过主内存来完成。

内存间交互操作

下面的八个操作都具有原子性

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占的状态。
  • unlock(解锁):作用域主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load(载入):作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量值放入主内存的变量中。
把一个变量从主内存复制到工作内存,要按顺序执行read和load操作,相反要按顺序执行store和write操作。JMM要求上述两种操作必须按顺序执行,没有保证必须是连续执行。

volatile语义

1、保证修饰的变量对所有线程的可见性。当一条线程修改了这个变量的值,新值对于其它所有线程来说是立即得知的。

2、禁止指令重排序优化。普通变量仅仅会保证在该方法执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

JMM对volatile变量定义的特殊规则
1、在工作内存中,每次使用变量前都必须从主内存中刷新最新的值,用于保证看见其他线程对变量所做的修改后的值(变量的use动作必须与load和read动作连续出现)。
2、在工作内存中,每次修改变量后都必须立刻同步回主内存中,用于保证看见其他线程对变量所做的修改后的值(变量的assign动作必须与store和write动作连续出现)。
3、volatile变量不会被指令重排序。

原子性、可见性和有序性

  • 原子性:由JMM直接保证的原子性变量操作包括read、write、load、store、use和assign,我们大致可以认为基本数据类型的访问读写是原子性的。synchronized块之间的操作也具有原子性,其编译后的字节码指令对应为monitorentermonitorexit,这两个指令会隐式使用lock和unlock操作。

  • 可见性:当一条线程修改了这个变量的值,新值对于其它所有线程来说是立即得知的。除了volatile之外,synchronized和final也可以实现可见性。

synchronized的可见性是由“unlock操作前,必须把变量同步回主内存中”这条规则获得的。
final关键字的可见性是指:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去,那么其他线程中就能看见final字段的值。
  • 有序性:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存和主内存同步延迟”现象。

先行发生关系

JMM中存在一些天生的先行发生关系,无需任何同步器协助。如果两个操作之间的关系不在此列,并且无法根据这些规则推导的话,虚拟机就可以对它们进行随意的重排序。

  • 程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。(实际上在一个线程内如果前后两个操作没有依赖关系,JVM也会对这两个操作重排序,但是单线程的执行结果不能被改变,所以这种重排我们是无感知的,并不违反程序次序规则)

  • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作,后面指的是时间上的先后顺序。

  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,后面指的是时间上的先后顺序。

  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。

  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测

  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生(理解是先中断了线程才能检测到中断事件)。

  • 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。

  • 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

线程的实现

线程是CPU调度的基本单位,线程的引入可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源,又可以独立调度。

1、使用内核线程实现

内核线程就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。程序一般使用内核线程的一种高级接口——轻量级进程(LWP),每个LWP进程都由一个内核线程支持,这种模型称为一对一线程模型

每个LWP都是一个独立的调度单元,即时有一个LWP阻塞了也不会影响整个进程继续工作。但是,由于基于内核线程实现,各种进程操作都需要进行系统调用,调用代价相对较高;另外,LWP要消耗一定内核资源,因此一个系统支持的轻量级线程是有限的。

2、使用用户线程实现

狭义的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到线程存在。这种线程不需要切换到内核态,因此操作可以是非常快且低消耗的,也可以支持更大规模的线程数量。这种模型称为一对多的线程模型

劣势在于所有的线程操作都需要用户自己处理,程序实现起来比较复杂。JDK1.2之前使用用户线程,后面被替换成基于操作系统原生线程模型来实现。

3、混合实现

将内核线程和用户线程一起使用的方式,用户线程还是完全建立在用户空间中,其调度通过LWP来完成,这种模型称为多对多的线程模型

线程安全

定义

按照线程安全程度由强到弱来排序,可以将Java语言中各种操作共享的数据分为五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

  • 不可变:使用final修饰基本数据类型,对象的行为(方法)不会改变其状态(变量)。
  • 绝对线程安全:不管运行时环境如何,调用者都不需要额外的同步措施。JavaAPI中标注自己是线程安全的类大多数都不是绝对安全。
  • 相对线程安全:我们通常所讲的线程安全。
  • 线程兼容:对象本身不是线程安全的,可以通过在调用端正确使用同步手段保证对象在并发环境下安全
  • 线程对立:不管是否采取了同步措施,都无法在多线程环境中并发使用,如Thread类的suspend()和resume()方法,已经被标记为过时。

实现方法

  • 互斥同步:保证共享数据同一时刻只被一条(使用信号量的时候则是一些,如Semaphore)线程使用,又被称为阻塞同步,性能瓶颈在于线程阻塞和唤醒带来的性能问题。
最基本的同步手段是synchronized关键字,编译后同步块的前后会形成monitorenter和monitorexit两个字节码指令,这两个指令需要一个引用类型参数来指明要锁定和解锁的对象。

synchronized同步块对同一条线程来说是可重入的,执行monitorenter计算器加1,执行monitorexit计数器减1,计数器为0释放锁。
同步块在执行完之前会阻塞后面其他线程的进入,由于Java的线程是映射到系统的原生线程上的,会涉及到用户态到核心态的切换,因此synchronized是一个重量级的操作。
  • 非阻塞同步:基于冲突检测的乐观并发策略,先进行操作,如果产生了冲突就进行其他的补偿措施,如CAS。
  • 无同步方案:如果一个方法本来就不涉及共享数据,就无需任何同步措施去保证正确性,这些代码天生线程安全。
可重入代码:也叫做纯代码,可以在代码执行的任何时候中断,而在控制权返回后原来的程序不会出现任何错误。可重入代码有一些共同的特征,如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数传入、不调用非可重入的方法等。

线程本地存储:将共享变量的范围限制在同一个线程之内,如ThreadLocal。

参考文献

  • 《深入理解JAVA虚拟机》 第2版

你可能感兴趣的:(JAVA)