jvm虚拟机总结

JVM学习总结

    当一个程序启动前,它的class会被类加载器加载到方法区,执行引擎读取方法区的字节自动解析,边解析边运行(JIT),然后PC寄存器指向main,程序执行的入口,jvm开始为main在java栈中预留一个栈帧,然后开始执行main,main方法内的代码被执行引擎映射为本地操作系统(windows/linx)里,然后调用本地方法接口,本地方法运行时,操作系统会为本地方法分配本地方法栈储存临时变量,然后运行本地方法,调用操作系统API等。

    以上面的流程为导向,我们首先说一下JVM的体系结构:

  1. 类加载器:

负责加载class文件,class文件结构体包含:

  1. magic魔数:确定class文件被JVM接受,固定值为0xCAFEBABE。
  2. 常量池和常量池计数器:class文件的资源仓库,用于存放字面量(文本字符串,final修饰的常量值)和符号引用(类/接口的完全限定名,字段、方法的名称和描述符),由于常量池的数量不固定,所以诞生了常量池计数器。
  3. 访问标志:表示类或接口的访问权限和基本属性。
  4. 类索引和父类索引:值必须是项目的一个有效索引值。
  5. 接口表:这是一个数组集合,里面元素的值必须是一对有效索引值,计数器记录长度。
  6. 字段表:描述接口或者类中声明的变量。字段计数器计算这个集合中的size()。
  7. 方法表:当前类或接口中某个方法的完整描述。只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。
  8. 属性表:前面的Class文件、字段表和方法表都可以携带自己的属性信息,这个信息用属性表进行描述,用于描述某些场景专有的信息。

Class文件从被加载到虚拟机内存中到使用(不进行类卸载),整个生命周期是:加载—>连接—>初始化—>使用,连接部分为验证、准备、解析,但是由于java支持动态绑定,所以解析有可能是动态的。但具体什么时候开始加载,都由虚拟机决定,并且jvm规定了有且只有五种情况必须对类进行“初始化”,场景如下:

  1. 遇到new(实例化对象)、getstatic(读取或设置一个类得静态字段,被final修饰或放在常量池得字段除外)、putstatic或invokestatic(调用一个类得静态方法),必须初始类(在未初始化的情况下)。
  2. 对类进行反射得时候,必须初始类(在未初始化的情况下)。
  3. 初始化子类时,父类未初始化必须先初始化父类。
  4. 包含main方法的类,jvm会先初始化。
  5. MethodHandle方法句柄(现代化反射)对应的类必须初始化。

类加载的过程:

  1. 加载:通过类的完全限定名获取定义它的二进制字节流,并将字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生产一个代表这个类的对象,作为方法去这个类的各种数据访问入口。
  2. 验证:主要就是确保class文件的字节流中包含的信息是否符合jvm的要求,并且不会危害jvm的安全。验证包括对文件格式、元数据、字节码、符合引用的验证。
  3. 准备:为类变量分配内存并设置初始值,变量所使用的内存会放入方法区进行分配。只包括类变量(被static修饰的变量),不包括实例变量,实例变量在对象实例化时会被分配到java堆。
  4. 解析:将常量池的符号引用替换为直接引用。
  5. 初始化:初始化阶段是执行类构造器<clinit>方法的过程。执行流程是父类静态字段和静态块—>子类静态字段和静态块,父类构造块和构造方法—>子类构造块和构造方法。
  6. 使用:绑定本地方法实现,实现navtive方法。

类加载器:

启动类加载器(Bootstrap ClassLoader:

加载<JAVA_HOME>lib目录下的类库。

扩展类加载器(Extension ClassLoader):

       加载<JAVA_HOME>lib/ext路径下的类库。

应用程序类加载器(Application ClassLoader):

       加载ClassPath上指定的类库。默认的类加载器。

自定义类加载器(User ClassLoader):

所有自定义类加载器都继承于ClassLoader抽象类。

最重要的就是双亲委派机制,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

  1. 本地库接口:natice方法,作用是融合不同的编程语言为Java所用,具体作法是本地方法栈中登记native方法,执行引擎执行时加载本地库。
  2. 执行引擎:执行包在装载类的方法中的指令,也就是方法。
  3. 运行数据区:jvm内存,在计算机内存中开辟的一块物理内存,用来存储jvm需要用到的对象、变量等。

运行数据区分为:方法区、虚拟机栈、本地方法栈、java堆、程序计数器。其中方法区和java堆用做线程共享的数据区。虚拟机栈、本地方法栈、程序计数器是线程私有的数据区。(栈管运行,堆管存储)。

  1. 本地方法栈:登记native方法,在执行引擎执行时加载本地库。
  2. 程序计数器:每个线程都有一个程序计数器,就是一个指针,指向方法区的方法字节码(下一个要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间
  3. 方法区:被所有线程共享,所有定义的方法的信息都保存在该区域,此区域属于共享区间。静态变量+常量+类信息+运行时常量池存在方法区中,实例变量存在堆内存中。

常量池(Constant Pool)是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放。

  1. 虚拟机栈:主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。基本类型的变量和对象的引用变量都是在函数的栈内存中分配。存储本地变量(输入输出参数以及方法内的变量)、栈操作(记录出入栈的操作)、栈帧数据:(类文件、方法等)。遵循“先进后出”/“后进先出”原则。
  2. java堆:堆这块区域是JVM中最大的,应用的对象和数据都是存在这个区域,这块区域也是线程共享的,也是 GC 主要的回收区,一个 JVM 实例只存在一个堆类存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:
    1. 新生代:类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。分为两部分:伊甸园区和幸存者区,所有的类都是在伊甸园区被new出来的,幸存者区有两个,当伊甸园区空间用完,程序又需要创建对象,JVM的垃圾回收器将对伊甸园进行垃圾回收(Minor GC),将伊甸园中的剩余对象移动到幸存0区,若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1去也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生Major GC(FullGCC),进行养老区的内存清理。若养老区执行Full GC 之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。 如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:

Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。

代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

    1. 老年代:养老区用于保存从新生区筛选出来的 JAVA 对象,一般池对象都在这个区域活跃。
    2. 永久代:永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。

如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。原因有二:

程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。

大量动态反射生成的类不断被加载,最终导致Perm区被占满。

Java堆里面的垃圾回收机制:

  1. 判断对象是否死掉:

根搜索算法:以GC Roots作为起始点,如果一个对象的与GC roots没有任何引用,那它会被判定为可回收对象。

可作为GC Roots的对象包括下面几种:

1、虚拟机栈(栈帧中的本地变量表)中引用的对象。

2、方法区中类静态属性引用的对象。

3、方法区中常量引用的对象。

4、本地方法栈中JNI(即一般说的Native方法)引用的对象。

  1. 对象的引用
    1. 强引用:类似new object(),只要强引用还在,就不会被回收。
    2. 软引用:只有初始值的变量,系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。
    3. 弱引用:入参、返回值、局部变量等,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
    4. 虚引用:反射、代理模式、注解等,为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。
  2. 两次标记过程:
    1. 第一次标记过程:根搜索后发现   没有引用链,进行第一次标记筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
    2. 第二次标记过程:这个对象被判定为有必要执行finalize()方法,就会把它放在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行。对象要下在第二次标记后如果对象想要存活,必须重新在引用链上建立关联,此时会清楚掉两次标记,但是如果下次根搜索发现此对象没有引用,直接GC掉,第二次标记后,被标记过的对象就离死不远了。
  3. (方法区中)哪些内存需要回收:类需要同时满足下面3个条件才能算是“无用的类”:

第一个条件:该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。

第二个条件:加载该类的ClassLoader已经被回收。

第三个条件:该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

  1. 垃圾收集器算法:
    1. 标记-清除算法,效率不高,且会产生内存碎片,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。标记-清除算法的执行过程(需要较大内存时却不够了就要回收一次)。(伊甸园区)
    2. 复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。(幸存者区)。
    3. 标记-整理算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。(老年代)
  2. 大对象如何存放:如果新生代出现大对象,在GC后还存活,就会通过空间分配担保放入老年代。

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

Jvm优化策略:

  1. jvm内存的系统级调优:

目的:减少GC的频率和Full GC的次数,过多的GC和Full GC是会占用很多的系统资源(主要是CPU),影响系统的吞吐量。

导致Full GC的几种情况:

    老年代空间不足:尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象。

    永久代空间不足:增大永久代空间,避免太多静态对象,统计得到的GC后晋升到旧生代的平均大小大于旧生代剩余空间,控制好新生代和旧生代的比例。

    System.gc()被显示调用:垃圾回收不要手动触发,尽量依靠JVM自身的机制。调优手段主要是通过控制堆内存的各个部分的比例和GC策略来实现。

部分比例不良设置会导致什么后果:

1)新生代设置过小

一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入旧生代,占据了旧生代剩余空间,诱发Full GC

2)新生代设置过大

一是新生代设置过大会导致旧生代过小(堆总量一定),从而诱发Full GC,二是新生代GC耗时大幅度增加。一般说来新生代占整个堆1/3比较合适。

3)Survivor设置过小

    导致对象从eden直接到达旧生代,降低了在新生代的存活时间

4)Survivor设置过大

导致eden过小,增加了GC频率。另外,通过-XX:MaxTenuringThreshold=n来控制新生代存活时间,尽量让对象在新生代被回收。

两种较为简单的GC策略的设置方式

1)吞吐量优先

    JVM以吞吐量为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,来达到吞吐量指标。这个值可由-XX:GCTimeRatio=n来设置

2)暂停时间优先

    JVM以暂停时间为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,尽量保证每次GC造成的应用停止时间都在指定的数值范围内完成。这个值可由-XX:MaxGCPauseRatio=n来设置。

  1. 编译期优化

javac编译过程大致可以分为3个过程:

    1、解析与填充符号表过程。

    2、插入式注解处理器的注解处理过程。

    3、分析与字节码生成过程。

整个编译最关键的处理由八个方法来完成。

准备过程:初始化插入式注解处理器。

过程2:执行注解处理。

  1. 词法、语法分析。词法分析是将源代码的字符流转变为标记(Token)集合,标记是编译过程的最小元素。关键字、变量名、字面量、运算符都可以成为标记。语法分析是根据 Token 序列构造抽象语法树的过程,语法树的每一个节点都代表着程序代码中的一个语法结构(Construct),例如包、类型、修饰符、运算符、接口、返回值甚至代码注释等都可以是一个语法结构。

经过这个步骤之后,编译器就基本不会再对源码文件进行操作了,后续的操作都建立在抽象语法树之上。

  1. 填充符号表。由一组符号地址和符号信息构成的表格,符号表中所登记的信息在编译的不同阶段都要用到。在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。

过程3:分析及字节码生成。(注解处理器)

  1. 标注检查。包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等。还有一个重要动作是常量折叠:将字面量进行运算折叠(如a=1+2,会被直接定义为a=3)。
  2. 数据及控制流分析。对程序上下文逻辑更进一步的验证,它可以检测出诸如程序局部变量是在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题。
  3. 解语法糖。对泛型、变长参数、自动装箱 / 拆箱等引用数据类型还原回简单的基础语法结构,这个过程称为解语法糖。
  4. 生成字节码。

把前面各个步骤所生成的信息(语法树、符号表)转化成字节码写到磁盘中,并进行了少量的代码添加和转换工作。

语法糖的一些小把戏:

  1. 泛型的擦除:Java 语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。List<String> 和 List<Integer> 擦除后是同一个类型。
  2. 自动装箱、拆箱与遍历循环:由于包装类的“==” 运算在不遇到算术运算的情况下不会自动拆箱,以及它们 equals() 方法不处理数据转型的关系,所以在实际编码中应尽量避免这样使用自动装箱与拆箱。
  3. 条件编译:条件为常量的 if 语句会自动编译条件成立的一方,另一边会直接舍弃掉。
  1. 运行期优化

在运行时,虚拟机会把运行特别频繁的代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just in Time Compiler JIT)。

  1. 解释器与编译器。
    1. 优点:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。当程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码后,可以获取更高的执行效率。
    2. HotSpot内置了两个即时编译器,简称为C1编译器–Client 和C2编译器– Server。

HotSpot虚拟机还会逐渐启用分层编译(Tiered Compilation)的策略:

第0层,程序解释执行,解释器不开启性能监控功能(Profiling),可触发第1层编译。

第1层,也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑。

第2层(或2层以上),也称为C2编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

  1. 编译对象与触发条件

“热点代码”有两类:被多次调用的方法;被多次执行的循环体。

热点探测判定方式有两种:基于采样的热点探测;基于计数器的热点探测。

HotSpot虚拟机中使用的是基于计数器的热点探测方法,因此它为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back EdgeCounter)。确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。

如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay),而这段时间就称为此方法统计的半衰周期(Counter Half Life Time)。

可以使用虚拟机参数-XX-UseCounterDecay来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。另外,可以使用-XXCounterHalfLifeTime参数设置半衰周期的时间,单位是秒。

回边计数器,它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge)。 显然,建立回边计数器统计的目的就是为了触发OSR编译。可以通过-XXOnStackReplacePercentage来间接调整回边计数器的阈值。 

回边计数器没有计数热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。 当计数器溢出的时候,它还会把方法计数器的值也调整到溢出状态,这样下次再进入该方法的时候就会执行标准编译过程。

  1. 编译过程
    1. Client Compiler是一个简单快速的三段式编译器,主要的关注点在于局部性的优化,而放弃了许多耗时较长的全局优化手段。
      1. 第一个阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示(High Level Intermediate Representaion,HIR)。 HIR使用静态单分配(Static Single Assignment,SSA)的形式来代表代码值,这可以使得一些在HIR的构造过程之中和之后进行的优化动作更容易实现。 在此之前编译器会在字节码上完成一部分基础优化,如方法内联、常量传播等优化将会在字节码被构造成HIR之前完成。
      2. 第二个阶段,一个平台相关的后端从HIR中产生低级中间代码表示(Low-Level Intermediate Representation,LIR),而在此之前会在HIR上完成另外一些优化,如空值检查消除、范围检查消除等,以便让HIR达到更高效的代码表示形式。
      3. 最后阶段是在平台相关的后端使用线性扫描算法(Linear Scan Register Allocation)在LIR 上分配寄存器,并在LIR上做窥孔(Peephole)优化,然后产生机器代码。
    2. Server Compiler则是专门面向服务端的典型应用并为服务端的性能配置特别调整过的编译器,也是一个充分优化过的高级编译器,几乎能达到GNU C++编译器使用-O2参数时的优化强度,它会执行所有经典的优化动作,
      1. 无用代码消除(Dead Code Elimination)、
      2. 循环展开(Loop Unrolling)、
      3. 循环表达式外提(Loop Expression Hoisting)、
      4. 消除公共子表达式(Common Subexpression Elimination)、
      5. 常量传播(Constant Propagation)、
      6. 基本块重排序(Basic Block Reordering),

还会实施与Java语言特性密切相关的优化技术,如范围检查消除(Range Check Elimination)、 空值检查消除(Null Check Elimination,不过并非所有的空值检查消除都是依赖编译器优化的,有一些是在代码运行过程中自动优化了)等。 另外,还可能根据解释器或Client Compiler提供的性能监控信息,进行一些不稳定的激进优化,如守护内联(Guarded Inlining)、分支频率预测(Branch Frequency Prediction)等。

Server Compiler编译速度依然远远超过传统的静态优化编译器,而且它相对于Client Compiler 编译输出的代码质量有所提高,可以减少本地代码的执行时间,从而抵消了额外的编译时间开销。

  1. 编译优化技术。

  1. 公共子表达式消除:如果一个表达式之前用过,且这次使用的变量都没有变化,那么这次使用的表达式就会成为公共子表达式。
  2. 数组边界检查消除:执行数组元素的时候系统将会自动进行上下界的范围检查,如果条件不满足界限,就会抛出java.lang.ArrayIndexOutOfBoundsException。
  3. 方法内联。把目标方法的代码“复制”到发起调用的方法之中,避免发生真实的方法调用而已。由于Java语言中默认的实例方法是虚方法。对于一个虚方法,编译期做内联的时候根本无法确定应该使用哪个方法版本。因此jvm引入了类型继承关系分析(CHA)技术,如果方法非虚,直接内联,如果是虚方法,检查当前程序下此方法有几个版本可选,只有一个的话,也可以进行内联,这种内联属于激进优化,会预留一个逃生门,称为守护内联。如果后续没有加载到这个方法的继承关系发生变化,那就一直用,如果加载导致继承关系产生新类,那就抛弃已编译的代码,回退到解释状态执行或者重新编译。
  4. 逃逸分析

被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。

赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。

如果能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些高效的优化。

  1. 栈上分配:如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存将会非常好 – 对象所占用的内存空间就可以随栈帧出栈而销毁。
  2. 同步消除:如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以消除掉。
  3. 标量替换:如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上分配和读写之外,还可以为后续进一步的优化手段创建条件。

Java中非逃逸对象的标量替换优化可以看做是一种高度优化后的栈上分配,但它相当于把对象拆散成局部变量再进行的栈上分配。

  1. 线程安全与锁优化。将 Java 语言中各个操作共享的数据分为以下 5 类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
    1. 不可变:其外部的可见状态永远也不会改变,永远也不会看到它在多个线程之中处于不一致的状态。“不可变” 带来的安全性是最简单和最纯粹的。
    2. 绝对线程安全:在正常操作中,使用一些同步的方法会达到绝对安全,但是在非正常操作情况下也不见得多安全。
    3. 相对线程安全:对于一些特定顺序的连续调用,调用端使用额外的同步手段来保证调用的正确性。逻辑安全。
    4. 线程兼容:对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。
    5. 线程对立:通常都是有害的,应当尽量避免。

线程安全的实现方法:

  1. 互斥同步:就是加一个同步锁,只能我自身去同步数据,不让其他线程操作此数据。无论共享数据是否真的会出现竞争,它都要进行加锁(属于悲观锁)
  2. 非阻塞同步:先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施。(乐观锁)
  3. 无同步方案:如果不进行数据共享,就不存在线程安全的情况了。
  4. 锁优化:适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。
    1. 自旋锁与自适应自旋:自旋锁就是没拿到锁的情况下,默认自旋次数10次,当等待时间超过次数还没得到锁,就挂起线程。(不让线程进入阻塞状态)。自适应自旋是不设置自旋次数,由虚拟机自动判断等待时长而进行调整(预测)。
    2. 锁消除:程序认为或者清楚的知道不存在数据争用的情况会自动为你清除锁。
    3. 锁粗化:当多条锁连续执行时,程序会自动将整段代码加锁,消除内部多个锁。
    4. 轻量级锁:在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。如果没出现多线程竞争,就正常执行,如果发现有竞争,自动切换为重要级锁。
    5. 偏向锁:这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
  1. Java内存模型与线程
    1. Java内存模型:

硬件效率与一致性,多条线程公用一个主内存时,会通过缓存一致性协议达到数据一致。

在硬件上由于内存和处理器之间的速度有所差异,所有会在之间加入告诉缓存,又为了保持数据的一致性,所以加入了缓存一致性协议。

而java内存模型的存在就是为了使java达到跨平台的效果。

Java内存模型的主要目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处所说的变量不包括局部变量和方法参数,因为他们是线程私有的不存在共享。

 

内存之间的交互如下图:

 

lock(锁定):作用于主内存的变量,它把一个变量标志为一条线程独占的状态。

unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。

load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎。

assign(赋值):作用于工作内存的变量,它把一个从执行引擎接受到的值赋给工作内存的变量。

store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。

write(写入):作用于主内存中的变量,它把store操作从主内存中得到的变量值放入主内存的变量中。

Volatile关键字主要增加了主内存中对象的可见性,当一条线程修改了这个变量的值,新值对于其它线程来说是可以立即得知的。

 

 

Java内存模型有以下几个特点:

原子性(Atomicity):由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、和write。

可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。volatile变量与普通变量的区别是,volatile的特殊规则保证新值能立即同步到主内存,每次使用前立即从主内存刷新。

有序性(Ordering):如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存和主内存同步延迟”现象。

    1. Java与线程

线程的实现分为以下三种:

使用内核线程实现,内核就是支持操作系统基本功能的源代码,内核线程指的是内核支持的线程。程序一般不会直接去使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),这种轻量级进程和内核线程之间1:1的关系称为一对一线程模型,使用内核线程效率较高。

使用用户线程实现:不是内核线程的其他线程都称为用户线程,用户线程不需要切换到内核态效率较高,但是又由于没有系统内核的支持需要自己进行线程的创建、切换等操作。现在很少使用。

混合使用:一起使用,既继承了用户线程的优点,也继承了内核线程的优点。

是多对多的线程模型。

 

Java线程的实现

Java线程最初是采用内心线程进行实现,而后又加入了混合线程。主要是将JAVA线程进行一对一映射到一条轻量级进程中进行运行。

 

Java线程的状态

新建(New):创建后尚未启动的线程处于这种状态。

运行(Runable):Runable包括了操作系统线程状态中的Running和Ready,也就是说处于此种状态的线程可能正在执行,也可能正在等待CPU为它分配执行时间。

无限期等待(Waiting):处于这种状态下的线程不会被分配CPU执行时间,他们要等待被其他线程显示唤醒。

限期等待(Timed Waiting):处于这种状态下的线程也不会被分配CPU执行时间,不过无须等待被其他线程显示唤醒,在一定时间之后它们由系统自动唤醒。

阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生。

结束(Terminate):已经终止的线程的线程状态,线程已经结束执行。

猜你喜欢

转载自blog.csdn.net/weixin_41565123/article/details/82532373