(学习总结)JDK源码解析

目录

一、Jdk源码解析过程

二、java虚拟机运行时数据区

1、Java虚拟机的五大分区

三、OutOfMemory异常实践(OOM)

 1、Java堆溢出

2、虚拟机栈和本地方法栈溢出

3、方法区和运行时常量池溢出

4、本机直接内存溢出

四、垃圾回收

(1)堆的回收

(2)方法区的回收

(3)垃圾回收算法

(4)如何回收 

(5)垃圾收集器(七种)

(6)内存分配回收策略

五、类(class)文件结构

1、Class文件的结构

六、java虚拟机类加载机制

1、虚拟机的启动

2、类加载的时机(类加载的过程)

3、类加载的过程

4、类加载器

七、早期优化(编译器的优化)

1、解析与填充符号表

2、注解处理器:对注解处理,JDK1.5之后。

3、语义分析和字节码生成

4、语法糖的应用

八、晚期优化(运行期的优化)

1、Hotspot的即时编译器

2、编译对象和触发条件

3、编译过程

九、Java内存模型

1、主内存和工作内存

2、Java与线程

3、Java线程调度

4、Java线程状态转化

十、线程安全与锁优化

1、Java语言中的线程安全

2、线程安全的实现方式

3、锁优化


一、Jdk源码解析过程

  1. Jdk(java开发工具包)-》jre(java运行时环境)-》jvm(java虚拟机)

二、java虚拟机运行时数据区

1、Java虚拟机的五大分区

首先来看一个图:

(学习总结)JDK源码解析_第1张图片

Java虚拟机的五大分区为:

  1. 方法区
  2. 虚拟机栈
  3. 本地方法栈
  4. 程序计数器

下面来一一介绍

程序计数器(PC寄存器)

可以理解为当前线程所执行的字节码的指示器。

是线程私有的,每条jvm线程都有自己的程序计数器,各条线程互不影响,独立存储,是“线程私有”内存。

Java虚拟机栈

线程私有的,每个jvm线程都有自己的java虚拟机栈,与线程同时创建,生命周期和线程相同。

虚拟机描述的是java方法执行的内存模型

每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

本地方法栈

和虚拟机栈作用相似,虚拟机栈为虚拟机执行的java方法服务,本地方法栈为虚拟机用到的Native方法服务。

线程私有的

HotSpot直接把java虚拟机栈和本地方法栈合二为一。

Java堆

是虚拟机内存中最大的一部分

用来存储对象实例,所有对象技术组都要在这里分配内存

线程共享

虚拟机创建的时候创建

由于这块区域是线程共享的,里面存的数据不能随线程消亡而删除,所以这里的存储的对象实例以及数组要在这里被自动管理,也就是垃圾回收(GC)(Garbage Collector)。

方法区

方法区也是一块被各个线程共享的区域。

存储被虚拟机加载的类信息,常量,静态变量。

在虚拟机启动的时候被创建

运行时常量池

是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。

类似于一个缓存区,避免了在Java堆和Native堆中来回复制数据。

三、OutOfMemory异常实践(OOM)

 1、Java堆溢出

当出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。

内存泄露:是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

2、虚拟机栈和本地方法栈溢出

(1)单线程:

①如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常 

②如果虚拟机在扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

(2)如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程了。如果没有这方面的处理经验,这种通过“减少内存”的手段来解决内存溢出的方式会比较难以想到。

3、方法区和运行时常量池溢出

运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是“PermGen space”

4、本机直接内存溢出

由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现OOM之后文件很小,而程序中有直接或简介使用了NIO,那就可以考虑一下是不是这方面的原因。

四、垃圾回收

由于程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭,栈帧随方法进行入栈出栈操作。所以的这些区域的内存分配和回收具有确定性,不考虑垃圾回收问题。内存会随线程的结束而自动回收。

堆和方法区不一样,我们只有在程序运行期间才知道会创建哪些对象,这部分的内存分配和回收是动态的,垃圾回收关注的是这部分内存。

(1)堆的回收

  1. 死掉的对象所占内存需要回收
  2. 判断对象是否死掉

1)引用计数法

  1. 每当有一个地方需要使用这个对象时,计数器加一,当引用失效的时候,计数器减一,任何时刻,计数器为0的对象都不会再被使用。
  2. Java中没有使用引用计数法,因为很难解决对象之间循环引用的问题。

2)可达性分析算法或者根搜索算法

  1. 即判定对象是否存活,即从“GC Roots”对象作为起始点,从这些节点向下搜索,搜索走过的路径叫做“引用链”,当一个对象到GC Roots 没有任何引用链,证明此对象是不可用的。
  2. (学习总结)JDK源码解析_第2张图片
  3. 在Java语言中,可作为GC Roots的对象包括下面几种:
  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

3)对象的引用

  1. JDK1.2之前,认为如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用,这种太过狭隘。
  2. 在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。
    1. 强引用:类似于“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
    2. 软引用:软引用用来描述一些还有用,但并非必需的对象。表示在java堆里面没有数据,但是在栈和方法区中有数据,比如:Object object= null;
    3. 弱引用:弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。所以弱引用的对象无论内存是否足够,都会被回收。例如:局部变量,返回值,参数。
    4. 虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。比如:反射获取的对象,注解。

(2)方法区的回收

  • 永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
  • 很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。
  • 无用的类是指:
    1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
    2. 加载该类的ClassLoader已经被回收。
    3. 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

(3)垃圾回收算法

  • 标记-清除算法(Mark-Sweep)
  1. 首先标记出所有需要清除的对象,标记完了统一回收被标记的对象。
  2. 主要缺点:
    1. 效率问题:要遍历两次
    2. 空间问题:产生大量不连续的内存碎片,使得程序在需要分配比较大的对象的时候无法找到连续的内存而不得不提前触发一次垃圾回收。

(学习总结)JDK源码解析_第3张图片

  • 复制算法
  1. 为了解决效率问题
  2. 将内存分为大小相同的两块,每次只使用其中的的一块内存,当这一块内存使用完了,将这块内存上有用的对象复制到另一块上,然后把这块内存所有的数据都清空。
  3. 优点:实现简单,运行高效
  4. 缺点:内存缩小为原来的一半,代价高。
  5. (学习总结)JDK源码解析_第4张图片
  6. 用来回收新生代,伊甸园区和两个幸存者区是8:1:1的关系。

  • 标记-整理算法
  1. 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
  2. 老年代使用标记整理算法。
  • 分代收集算法
  1. 一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
  2. 新生代--复制算法。老年代--标记-整理算法。

(4)如何回收 

枚举根节点:一次枚举根节点,数量庞大,逐个检查,会消耗很多时间。

安全点:由于GC必须停止进程,安全点就是,可以进行GC的点

安全区域:可以随时进行GC的区域

(5)垃圾收集器(七种)

  • Serial收集器(在GC日志中新生代的名称是DefNew)
  • ParNew收集器(在GC日志中新生代的名称是ParNew)
  • Parallel Scavenge收集器(在GC日志中新生代的名称是PSYongGen)
  • Serial Old(MSC)收集器
  • Parallel Old收集器
  • CMS收集器
  • G1收集器

(6)内存分配回收策略

  • Java内存回收机制吧java堆分为新生代和老年代。
  • 新生代分为伊甸园区(Eden),两个幸存者区(server),大小为8:1:1,对象优先分配在新生代的伊甸园区,当内存空间不够的时候,将在新生代发生一次GC(Minior GC),此时将伊甸园区的对象移动至幸存者一区,然后在两个幸存者去进行复制算法,将幸存者一区存活下来的对象赋值到幸存者二区中,再将幸存者一区清空,完成一次minior GC,当幸存者二区满了后,再将幸存者二区中存活的对象复制到幸存者一区中,将幸存者二区清空。如此往复循环,不断进行GC。
  • 进入老年带的对象
    1. 大对象直接进入老年代
    2. 长期存活的对象将进入老年代(默认年龄是15)
    3. 动态对象年龄判定:为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
    4. 空间分配担保:在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。

五、类(class)文件结构

1、Class文件的结构

  1. Class文件是一个由二进制字节组成的文件。
  2. 他的文件结构是:
  3. (学习总结)JDK源码解析_第5张图片
  4. 下面详细介绍结构体各部分详细介绍
  • Magic
    • 固定值,确定这个文件是否为一个能被虚拟机所接受的 Class 文件。
  • minor_version、major_version
    • 不同版本的java虚拟机支持的版本不一样。
    • 高版本jvm向下兼容低版本的class文件。
  • 常量池计数器
    • 有几个常量池,就是几。
    • constant_pool 表的索引值只有在大于 0 且小于 constant_pool_count 时才会被认为是有效的。
    • 使用索引 0 来表示“不引用任何一个常量池项”的意思。
  • constant_pool[ ]
    • 常量池:它包含 Class 文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其它常量。
  • access_flags
    • 用于表示某个类或者接口的访问权限及基础属性
  •  this_class
    • 表示这个 Class 文件所定义的类或接口
  • super_class
    • 表示这个class文件的父类或父接口
  • interfaces_count
    • 表示当前类或接口的直接父接口数量。
  •  interfaces[]
    • 表示这个类实现的接口(顺序:从左到右)。
  • fields_count
    • 类或接口的成员的个数
  • fields[]
    • 字段表,fields[]数组描述当前类或接口声明的所有字段,但不包括从父类或父接口继承的部分。
  • methods_count
    • 方法计数器,方法的个数。
  • methods[]
    • 方法表,methods[]数组,只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。
  • attributes_count
    • 属性计数器,attributes_count 的值表示当前 Class 文件 attributes 表的成员个数。
  • attributes[]
    • 属性表

六、java虚拟机类加载机制

虚拟机的类加载机制,虚拟机吧描述类的文件从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被java虚拟机直接使用的java类。

说白了,就是把编译好的class文件加载到方法区中的各个部分,并且进行校验。

1、虚拟机的启动

Java虚拟机的启动是有一个引导类加载器创建一个初始类来完成。

2、类加载的时机(类加载的过程)

(1)类从被加载到虚拟机内存中到卸载出内存,整个生命周期:加载、连接、初始化、使用、卸载。其中连接分为验证、准备、解析。解析的顺序不一定,有可能按照上述顺序,也有可能在初始化阶段之后才开始,这是为了支持Java的运行时绑定(动态绑定)。如下图:

(学习总结)JDK源码解析_第6张图片

(2)对类进行初始化的时机:

  • 使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化。则需要先触发其初始化

3、类加载的过程

(1)创建和加载

①Java 虚拟机支持两种类加载器:Java 虚拟机提供的引导类加载器(Bootstrap Class Loader)和用户自定义类加载器(User-Defined Class Loader)。

②加载需要完成:

1、通过类的完全限定名获取类的二进制字节流文件

也就是根据类名,获取class文件

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

也就是将class文件转化为方法区中jvm可以识别的数据结构

3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

也就是说,生成了一个类的访问接口,class对象,记录了类成员,接口等信息,那么可以推测,是不是可以通过反射机制获取类信息了。

(2)连接和验证

①验证保证类或接口的二进制表示结构上是正确的。

②验证过程可能会导致某些额外的类和接口被加载进来(§5.3),但不应该会导致它们也需要验证或准备。

③验证阶段大体会进行如下阶段的检查:

  • 文件格式验证:class文件是否符合规范
  • 元数据验证:是否存在不符合java语言规范的元数据信息
  • 字节码验证:保证验证类不会做出危害虚拟机安全的事件
  • 符号引用验证:确保后续解析阶段符号引用正确

(3)连接和准备

  • 正式为类分配内存和变量初始化阶段,这里进行分配内存的仅仅包括类变量

(4)连接和解析

  • 将符号引用解析为直接引用。

符号引用是指用符号来描述引用目标,符号可以是任何字面量。

直接引用,可以理解为引用他的内存地址

  1. 类与接口解析:jvm解析一个类
  2. 字段解析:jvm解析一个类的字段
  3. 类方法解析:jvm解析一个类方法:抛出异常:java.lang.IncompatibleClassChangeError,java.lang.AbstractMethodError,java.lang.NoSuchMethodError.的时机。
  4. 接口方法解析:

(5)初始化:初始一个类。

  • 在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划其余初始化变量和其他资源,或者从另一个角度来表达,初始化阶段是执行类构造器()方法的过程:

(6)使用

(7)退出

  • 某些线程调用 Runtime 类或 System 类的 exit 方法,或是 Runtime 类的 halt 方法,并且 Java 安全管理器也允许这些 exit 或 halt 操作。

4、类加载器

  1. 双亲委派机制,要加载一个类,优先于扔给他的父类的类加载器来加载,如果父类无法完成,才会使用子加载器来自己加载。
  2. 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载器请求最终都是应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要的类)时,子加载器才会尝试自己去加载。
  3. “双亲委派机制”是一种设计模式(代理模式)

七、早期优化(编译器的优化)

编译器的优化是在生成class文件的编译过程中的优化,发生在编译器上,生成class文件的时候。

Javac编译器的编译过程,基本可以按照下图来表示。

(学习总结)JDK源码解析_第7张图片

1、解析与填充符号表

(1)词法,语法分析

  • 语法分析是根据 Token 序列构造抽象语法树的过程,抽象语法树(Abstract Syntax Tree,AST)是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构(Construct),例如包、类型、修饰符、运算符、接口、返回值甚至代码注释等都可以是一个语法结构。

(2)填充符号表

  • 符号表中所登记的信息在编译的不同阶段都要用到。在语义分析中,符号表所登记的内容将用于语义检查(如检查一个名字的使用和原先的说明是否一致)和产生中间代码。在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。

2、注解处理器:对注解处理,JDK1.5之后。

3、语义分析和字节码生成

  1. 标注检查:检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等。常量折叠。
  2. 数据及控制流分析:对程序上下文逻辑更进一步的验证,它可以检测出诸如程序局部变量是在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题。
  3. 解语法糖:
    • 指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说,使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
    • 其实自动拆箱,装箱,泛型,都是语法糖。
  4. 字节码生成:
    • 在 Javac 源码里面由com.sun.tools.javac.jvm.Gen 类来完成
    • 生成class文件

4、语法糖的应用

  1. 泛型和泛型擦除,
  2. 自动装箱,自动拆箱,循环遍历
  3. 条件编译

八、晚期优化(运行期的优化)

Java程序最初是解释执行的,后来,发现某个方法使用的非常频繁,就会认为这段代码是热点代码,运行时,会把这些代码编译成平台相关的代码,并进行各种层次的优化,完成这个任务的编译器为即时编译器(JIT)。

1、Hotspot的即时编译器

(1)hotspot同时存在解释器和编译器,如下图所示。

(学习总结)JDK源码解析_第8张图片

(2)HotSpot虚拟机中内置了两个即时编译器,分别称为Client Compiler和Server Compiler,或者简称为C1编译器–Client 和C2编译器– Server(也叫Opto编译器)

2、编译对象和触发条件

(1)编译对象是“热点代码”

  • 被多次调用的方法
  • 被多次执行的循环体

(2)判断一段代码是不是“热点代码”的方法:

  • 基于采样的热点探测:就是每隔一段时间查看栈顶的方法(处于栈顶的方法就是当前正在执行的方法),如果一个方法经常出现在栈顶,那么,就认为这个方法是热点方法。不过这个方法容易受到线程阻塞或其他的外界因素影响。
  • 基于计数器的热点检测:即为每一个方法,或方法快,建立一个计数器,方法执行一次,计数器+1,超过一定的阈值就认为这个方法是热点方法。Hotspot是基于计数器的热点检测,为每个方法准备了两个计数器:
    1. 方法调用计数器
      1. 要执行一个方法,首先需要判断他有没有已编译的版本,如果有,直接执行已编译的代码,如果没有,方法计数器+1,如果两个计数器之和超过了阈值,那么,向编译器提交编译请求,并以解释的方式完成本次的代码执行。
      2. 方法调用计数器可以设置半衰周期,如果在一定的时间内,没有达到阈值,就将方法调用计数器的值减半,这样就保证了,只有在一定之间之内,调用够一定次数的方法才能进行编译执行。
    2. 回边计数器
      1. 统计一个方法体重代码的执行次数,遇到跳转代码,例如:continue,叫做回边。
      2. 遇到回边指令,首先需要判断他有没有已编译的版本,如果有,直接执行已编译的代码,如果没有,回边计数器+1,如果两个计数器之和超过了阈值,那么,向编译器提交编译请求,并以解释的方式完成本次的代码执行。
      3. 回边计数器没有半衰周期。

 

 

 

3、编译过程

  1. 默认设置下,无论是方法调用产生的即时编译请求,还是OSR编译请求,虚拟机在代码编译器还未完成之前,都仍然将按照解释方式继续执行,而编译动作则在后台的编译线程中进行。
  2. 编译过程中有各种编译优化技术,这里只说几种:
    • 公共子表达式消除
      1. 如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式。
      2. 优化仅限于程序的基本块内, 局部公共子表达式消除
      3. 优化的范围涵盖了多个基本块, 全局公共子表达式消除
    • 数组边界检查消除
      1. 如果编译器只要通过数据流分析就可以判定循环变量的取值范围永远在区间[0,foo.length)之内,那在整个循环中就可以把数组的上下界检查消除,这可以节省很多次的条件判断操作。
    • 方法内联
      1. 方法内联的优化行为只是把目标方法的代码“复制”到发起调用的方法之中,避免发生真实的方法调用而已。 (避免方法频繁的出栈和入栈)
      2. 为了解决虚方法的内联问题,(虚方法:有方法体,但是里面没有东西;抽象方法:没有方法体)
    • 逃逸分析
      1. 逃逸分析与类型继承关系分析一样,并不是直接优化代码的手段,而是为其他优化手段提供依据的分析技术。分为:
        1. 方法逃逸:在一个线程内,方法的变量被外部引用了,发生了传参。相当于参数的作用域大了,叫方法逃逸。
        2. 线程逃逸:一个线程调用另一个线程的一个带参方法,那么这个对象发生了线程逃逸。
      2. 基于逃逸分析的一些优化
        1. 栈上分配:不逃逸的对象,完全可以将他放在栈中,使之随线程消失,减少GC压力
        2. 同步消除:不逃逸的对象,在一个线程之内,不会出现不同步的问题,所以不逃逸的对象可以吧他涉及到的同步措施消除掉。
        3. 标量替换: 把一个聚合量(例如:对象),拆分为不可拆分的标量(例如:int等),程序创建对象变为创建一系列标量,可以让对象在栈上分配空间,还可以为进一步优化创建条件。

九、Java内存模型

Java虚拟机规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存差异,来实现Java程序在各种平台下都能达到一致的内存访问效果。即:一套完整的java内存使用规则,内存的规则。

保证了:环境一致 内存一致 访问方式一致

1、主内存和工作内存

(学习总结)JDK源码解析_第9张图片

(1)这里所讲的主内存、工作内存与Java内存区域中的Java堆、栈、方法区等并不是同一个层次的内存划分。

主内存主要对应java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分数据。从更低层次上说,主内存就直接对应于物理硬件的内存。
为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。

(2)内存之间的交互操作

①即,将一个数据在jvm中和硬盘中存入,取出的过程。见下图:

(学习总结)JDK源码解析_第10张图片

②分析

  1. lock(锁定):作用于主内存的变量,它把一个变量标志为一条线程独占的状态。
  2. unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  3. read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  5. use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎。
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接受到的值赋给工作内存的变量。
  7. store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  8. write(写入):作用于主内存中的变量,它把store操作从主内存中得到的变量值放入主内存的变量中。

③volatile(关键字)型变量的特殊规则

  1. 当一个变量定义为volatile之后,它将具备两种属性,
  2. 第一种是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其它线程来说是可以立即得知的。
  3. 禁止指令重排序优化:
    1. 指令重排序优化:吧一块代码,切分为几个部分,分别在多个线程中执行,那么,如果一个线程中的指令需要另一个线程中的值,那么,需要等待那个线程先执行,然后再调用,最后在根据一定规则吧代码块从新组合。叫做指令重排序
    2. valotile关键字是如何实现禁止指令重排序呢?
      1. 有volatile关键字修饰的变量,赋值后会多执行一个操作,这个操作会把修改同步到内存,意味着所有之前的操作都执行完毕了,这个操作相当于内存屏障(Memory Barrier),这样一来指令重排序的时候就不能把后面的指令重排序到内存屏障之前的位置。只有一个CPU访问内存时,不需要内存屏障。

2、Java与线程

(1)内核线程

(学习总结)JDK源码解析_第11张图片

  • 内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作系统调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。
  • 程序一般不直接使用内核线程,而是用他的高级接口,轻量级线程。
  • 这种轻量级进程(LMP)和内核线程(KLT)之间1:1的关系称为一对一线程模型。
  • 耗费资源,需要经常进行用户态和内核态的转化。

(2)用户线程

  • 从广义上来讲,一个线程只要不是内核线程,就可以认为是用户线程,因此,从这个定义上来讲,轻量级进程也属于用户线程,但轻量级进程的实现始终是建立在内核之上的,许多操作都进行系统调用,效率会受到限制。
  • 这种进程与用户线程之间1:N的关系称为一对多的线程模型。
  • (学习总结)JDK源码解析_第12张图片
  • 不耗费资源,但是使用方便,线程的创建、切换和调度都是需要考虑的问题。

(3)用户线程和轻量级线程混合使用

(学习总结)JDK源码解析_第13张图片

(4)Java线程的实现

Java线程在JDK 1.2之前是基于用户线程实现的,而JDK 1.2中,线程模型替换为基于操作系统原生线程来实现。后来变为混合实现。

3、Java线程调度

抢占式调度,但是可以设置优先级,因为Java线程是通过映射到原生线程上来实现的,所以线程调度最终还是取决于操作系统。

4、Java线程状态转化

(学习总结)JDK源码解析_第14张图片

  • 新建(New):创建后尚未启动的线程处于这种状态。
  • 运行(Runable):Runable包括了操作系统线程状态中的Running和Ready,也就是说处于此种状态的线程可能正在执行,也可能正在等待CPU为它分配执行时间。
  • 无限期等待(Waiting):处于这种状态下的线程不会被分配CPU执行时间,他们要等待被其他线程显示唤醒。
  • 限期等待(Timed Waiting):处于这种状态下的线程也不会被分配CPU执行时间,不过无须等待被其他线程显示唤醒,在一定时间之后它们由系统自动唤醒。
  • 阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生。
  • 结束(Terminate):已经终止的线程的线程状态,线程已经结束执行。

阻塞还在占用资源,等待不占用资源。

十、线程安全与锁优化

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。

1、Java语言中的线程安全

(1)不可变

  • 使用final修饰的常量是不可能发生线程不安全的情况的。

(2)绝对线程安全

  • 绝对的线程安全完全满足 Brian Goetz 给出的线程安全的定义,这个定义其实是很严格的,一个类要达到 “不管运行是环境如何,调用者都不需要任何额外的同步措施” 通常需要付出很大的,甚至有时候是不切实际的代价。

(3)相对线程安全

  • 它需要保证对这个对象单独的操作是线程安全,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
  • 例如: Vector、HashTable

(4)线程兼容

  • 线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况

(5)线程对立

  • 线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。
  • 例如:System.setIn()、System.setOut() 

*我们一般说的线程安全指的是:相对线程安全和线程兼容。

2、线程安全的实现方式

(1)互斥同步

  • 最基本的互斥同步手段就是 synchronized 关键字,他是悲观的认为任何时刻都会发生线程不安全的情况。所以每次都要进行锁操作。
  • 重入锁:高级功能:
    1. 等待可中断
    2. 公平锁
    3. 锁绑定多个条件
  • JDK1.6中对synchronized 做了优化,使他的效率和互斥锁基本持平,所以我们在不是必须使用重入锁的情况,优先使用synchronized 。

(2)非阻塞同步

  • 基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,知道成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。
  • 他是一个乐观锁

(3)无同步方案

  • 要保证线程安全,并不是一定就要进行同步,两者没有因果关系。
  • 不需要同步的代码有两类:
    1. 可重入代码:在代码任何时间中断,然后在重新调用这个代码,结果都不会出错。那他就是线程安全的。
    2. 线程本地存储:在一个线程中,无需同步

3、锁优化

(1)自旋锁和自适应自旋

  • 如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
  • JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如 100 个循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越 “聪明” 了。

(2)锁消除

  • 有些代码,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。

(3)锁粗化

  • 如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,只需要加一次锁。

(4)轻量级锁

  •  JDK 1.6 之中加入的新型锁机制,它名字中的 “轻量级” 是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为 “重量级” 锁。首先需要强调一点的是,轻量级锁并不是用来代替重要级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
  • 他是一个乐观锁
  • 先进行CAS操作,如果CAS失败,则说明这个锁被其他线程抢占了,进入重量级锁。如果CAS成功了,那么,不进行锁操作,直接进入代码块。
  • 如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了 CAS 操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

(5)偏向锁

  • 他也是一个乐观锁
  • 偏向锁也是 JDK 1.6 中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不做了。
  • 它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
  • 如果被其他线程获取了,其他线程像执行轻量级锁那样继续执行。轻量级锁CAS失败,执行重量锁,否则,直接进入代码块。
  • 偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数 -XX:-UseBiasedLocking 来禁止偏向锁优化反而可以提升性能。

总结:轻量级锁和偏向锁,并不是总是对程序运行有利的,当在经常出现冲突的程序中,反而增加了CAS操作和偏向操作,降低了运行效率。


参考资料:https://me.csdn.net/sinat_38259539(敬业的小码哥)


真正的平静,不是避开车马喧嚣,而是在心中修篱种菊。  ——白落梅 《你若安好 便是晴天》

(学习总结)JDK源码解析_第15张图片

 

 

 

你可能感兴趣的:(JDK)