当前位置: 首页 > news >正文

哈尔滨微网站建设珠海网站制作设计

哈尔滨微网站建设,珠海网站制作设计,国际新闻最新消息今天中国,移动应用软件开发第 2 章 Java 内存区域与内存溢出异常 2.2 运行时数据区域 程序计数器-线程私有:是一块较小的内存空间#xff0c;它可以看作是当前线程所执行的字节码的行号指示器。 程序计数器是唯一一个没有规定任何OutOfMemoryError 情况的区域。 Java 虚拟机栈-线程私有:用于执行Java …第 2 章 Java 内存区域与内存溢出异常 2.2 运行时数据区域 程序计数器-线程私有:是一块较小的内存空间它可以看作是当前线程所执行的字节码的行号指示器。 程序计数器是唯一一个没有规定任何OutOfMemoryError 情况的区域。 Java 虚拟机栈-线程私有:用于执行Java 方法,存储方法的局部变量、操作数栈、动态链接、方法出口等信息。 每一个方法被调用直至执行完毕的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 虚拟机栈规定了两类异常状况 如果线程请求的栈深度大于虚拟机所允许的深度将抛出 StackOverflowError 异常如果 Java 虚拟机栈容量可以动态扩展当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。 本地方法栈-线程私有用于执行本地Native方法。 与虚拟机栈一样本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出 StackOverflowError 和OutOfMemoryError 异常。 Java 堆-线程共享存放对象实例 如果在 Java 堆中没有内存完成实例分配并且堆也无法再扩展时Java 虚拟机将会抛出 OutOfMemoryError 异常。 方法区-线程共享用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。 如果方法区无法满足新的内存分配需求时将抛出 OutOfMemoryError 异常。 运行时常量池-线程共享是方法区的一部分用于存放编译期生成的各种字面量与符号引用 当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常 从垃圾回收的角度划分java堆和方法区 新生代Young Generation新生代主要用于存放新创建的对象。它通常被划分为三个部分 Eden区伊甸园区这是新对象最初被分配的地方。大多数新对象都被分配到Eden区。Survivor区幸存区Survivor区包括两个区域通常称为From区和To区。当垃圾回收发生时存活下来的对象会被移到一个Survivor区然后在不同区域之间来回移动最终进入老年代。 老年代Tenured Generation老年代用于存放生命周期较长的对象通常由Eden区和Survivor区中存活时间较长的对象填充而成。垃圾回收发生在这个区域因为老年代中的对象更难以回收。 永久代Permanent Generation在Java 7及之前的版本中存在一个称为永久代的特殊区域用于存放类的元数据、常量池等信息。但在Java 8及以后的版本中永久代被元空间Metaspace所取代。元空间不再属于Java堆的一部分它位于本地内存中。 元空间Metaspace在Java 8及以后的版本中元空间取代了永久代。它用于存储类的元数据包括类的结构信息、方法信息等。元空间的大小可以动态扩展而不受Java堆的限制。 垃圾回收算法和行为也会因不同区域而异例如新生代通常使用复制算法而老年代使用标记-清理算法或标记-整理算法 永久代和元空间不属于java堆它们存在于方法区中 第 3 章 垃圾收集器与内存分配策略 3.2 对象已死 引用计数算法 在对象中添加一个引用计数器每当有一个地方引用它时计数器值就加一当引用失效时计数器值就减一任何时刻计数器为零的对象就是不可能再被使用的。 Java 虚拟机并不是通过引用计数算法来判断对象是否存活的 可达性分析算法 通过“GC Roots”根对象作为起始节点集从这些节点开始根据引用关系向下搜索搜索过程所走过的路径称为“引用链”如果某个对象到 GC Roots 间没有任何引用链相连或者用图论的话来说就是从 GC Roots 到这个对象不可达时则证明此对象是不可能再被使用的。 在 Java 技术体系里面固定可作为 GC Roots 的对象包括以下几种 在虚拟机栈栈帧中的本地变量表中引用的对象譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。在方法区中类静态属性引用的对象譬如 Java 类的引用类型静态变量。在方法区中常量引用的对象譬如字符串常量池String Table里的引用。在本地方法栈中 JNI即通常所说的 Native 方法引用的对象。Java 虚拟机内部的引用如基本数据类型对应的 Class 对象一些常驻的异常对象比如 NullPointExcepiton、OutOfMemoryError等还有系统类加载器。所有被同步锁synchronized 关键字持有的对象。反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。 引用类型 强引用Strongly Re-ference、软引用Soft Reference、弱引用Weak Reference和虚引用Phantom Reference。 强引用是最传统的“引用”的定义是指在程序代码之中普遍存在的引用赋值即类似“Object objnew Object()”这种引用关系。无论任何情况下只要强引用关系还存在垃圾收集器就永远不会回收掉被引用的对象。软引用是用来描述一些还有用但非必须的对象。只被软引用关联着的对象在系统将要发生内存溢出异常前会把这些对象列进回收范围之中进行第二次回收如果这次回收还没有足够的内存才会抛出内存溢出异常。在 JDK 1.2 版之后提供了SoftReference 类来实现软引用。弱引用也是用来描述那些非必须对象但是它的强度比软引用更弱一些被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作无论当前内存是否足够都会回收掉只被弱引用关联的对象。在 JDK 1.2 版之后提供了WeakReference 类来实现弱引用。虚引用也称为“幽灵引用”或者“幻影引用”它是最弱的一种引用关系。一个对象是否有虚引用的存在完全不会对其生存时间构成影响也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在 JDK 1.2 版之后提供了 PhantomReference 类来实现虚引用。 判断对象是否死亡 1.使用可达性分析算法判断A对象不可达 2.如果A对象覆盖finalize()方法或未者已经执行过finalize则进入3否则进入5; 3.将A对象放到F-Queue队列中有虚拟机创建的Finalizer线程去执行 4.执行A对象的finalize对象如果在执行过程中对象又建立了引用关联则从F-Queue队列中去除否则进入5 5.A对象执行GC 判断是否有必要执行finalize()方法 假如对象没有覆盖 finalize()方法或者 finalize()方法已经被虚拟机调用过那么虚拟机将这两种情况都视为“没有必要执行”。 任何一个对象的 finalize()方法都只会被系统自动调用一次。 回收方法区 方法区的垃圾收集主要回收两部分内容废弃的常量和不再使用的类型。 废弃常量 当常量池中一个常量没有被任何地方引用如果这时候发生内存回收而且必要的话这个常量就会被系统清理出常量池。 无用的类 该类的所有实例都已经被回收。 加载该类的ClassLoader已经被回收。 该类对应的java.lang.Class对象没有在任何地方被引用。 3.3 垃圾收集算法 3.3.1 分代收集理论 分代假说 弱分代假说Weak Generational Hypothesis绝大多数对象都是朝生夕灭的。强分代假说Strong Generational Hypothesis熬过越多次垃圾收集过程的对象就越难以消亡。跨代引用假说Intergenerational Reference Hypothesis跨代引用相对于同代引用来说仅占极少数。 3.3.2 标记-清除算法 算法分为“标记”和“清除”两个阶段首先标记出所有需要回收的对象在标记完成后统一回收掉所有被标记的对象也可以反过来标记存活的对象统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。 它的主要缺点有两个 第一个是执行效率不稳定。如果 Java 堆中包含大量对象而且其中大部分是需要被回收的这时必须进行大量标记和清除的动作导致标记和清除两个过程的执行效率都随对象数量增长而降低第二个是内存空间的碎片化。 标记、清除之后会产生大量不连续的内存碎片空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。 3.3.3 标记-复制算法一般用于回收新生代 为了解决标记-清除算法面对大量可回收对象时执行效率低的问题标记-复制算法 将可用内存按容量划分为大小相等的两块每次只使用其中的一块。当这一块的内存用完了就将还存活着的对象复制到另外一块上面然后再把已使用过的内存空间一次清理掉。 优点实现简单运行高效缺点浪费空间 现在的商用 Java 虚拟机大多都优先采用了这种收集算法去回收新生代新生代中的对象有98%熬不过第一轮收集。因此并不需要按照 1∶1 的比例来划分新生代的内存空间。 Appel 式回收的具体做法是把新生代分为一块较大的 Eden 空间和两块较小的 Survivor 空间每次分配内存只使用Eden 和其中一块 Survivor。发生垃圾搜集时将 Eden 和 Survivor 中仍然存活的对象一次性复制到另外一块 Survivor 空间上然后直接清理掉 Eden 和已用过的那块 Survivor空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8∶1也即每次新生代中可用内存空间为整个新生代容量的 90%Eden 的 80%加上一个 Survivor 的 10%只有一个 Survivor 空间即 10%的新生代是会被“浪费”的。 当然98%的对象可被回收仅仅是“普通场景”下测得的数据任何人都没有办法百分百保证每次回收都只有不多于 10%的对象存活因此 Appel 式回收还有一个充当罕见情况的“逃生门”的安全设计当Survivor 空间不足以容纳一次 Minor GC 之后存活的对象时就需要依赖其他内存区域实际上大多就是老年代进行分配担保Handle Promotion。 内存的分配担保: 如果另外一块 Survivor 空间没有足够空间存放上一次新生代收集下来的存活对象这些对象便将通过分配担保机制直接进入老年代这对虚拟机来说就是安全的。 3.3.4 标记-整理算法 针对老年代对象的存亡特征提出了另外一种有针对性的“标记-整理”Mark-Compact算法 其中的标记过程仍然与“标记-清除”算法一样但后续步骤不是直接对可回收对象进行清理而是让所有存活的对象都向内存空间一端移动然后直接清理掉边界以外的内存。 标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法而后者是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策 如果移动存活对象尤其是在老年代这种每次回收都有大量对象存活区域移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作而且这种对象移动操作 必须全程暂停用户应用程序 才能进行这就更加让使用者不得不小心翼翼地权衡其弊端了像这样的停顿被最初的虚拟机设计者形象地描述为“Stop The World”。 3.5 经典垃圾收集器 3.5.1 Serial 收集器 /ˈsɪəriəl/ Serial 收集器是最基础、历史最悠久的收集器。 这个收集器是一个单线程工作的收集器但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作更重要的是强调在它进行垃圾收集时必须暂停其他所有工作线程直到它收集结束。 3.5.2 ParNew 收集器 ParNew 收集器实质上是 Serial 收集器的多线程并行版本 除了同时使用多条线程进行垃圾收集之外其余的行为包括 Serial 收集器可用的所有控制参数例如-XXSurvivorRatio、-XX PretenureSizeThreshold、-XXHandlePromotionFailure 等、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一致在实现上这两种收集器也共用了相当多的代码。 ParNew 收集器是 JDK 7 之前的遗留系统中首选的新生代收集器其中有一个与功能、性能无关但其实很重要的原因是 除了 Serial 收集器外目前只有它能与 CMS 收集器配合工作。 3.5.3 Parallel Scavenge 收集器 / ˈ P Å r ə Lel/ /ˈskévɪndʒ/ Parallel Scavenge 收集器也是一款新生代收集器它同样是基于标记-复制算法实现的收集器也是能够并行收集的多线程收集器……Parallel Scavenge 的诸多特性从表面上看和 ParNew 非常相似。 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量Throughput。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值即 如果虚拟机完成某个任务用户代码加上垃圾收集总共耗费了 100 分钟其中垃圾收集花掉 1 分钟那吞吐量就是 99%。停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序良好的响应速度能提升用户体验而高吞吐量则可以最高效率地利用处理器资源尽快完成程序的运算任务主要适合在后台运算而不需要太多交互的分析任务。 3.5.4 Serial Old 收集器 Serial Old 是 Serial 收集器的老年代版本它同样是一个单线程收集器使用标记-整理算法。 这个收集器的主要意义也是供客户端模式下的 HotSpot 虚拟机使用。如果在服务端模式下它也可能有两种用途一种是在 JDK 5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用[1]另外一种就是作为 CMS 收集器发生失败时的后备预案在并发收集发生 Concurrent Mode Failure 时使用。 3.5.5 Parallel Old 收集器 “标记-整理”算法 Parallel Old 是 Parallel Scavenge 收集器的老年代版本支持多线程并发收集基于标记-整理算法实现。 Parallel Old 收集器出现后“吞吐量优先”收集器终于有了比较名副其实的搭配组合在注重吞吐量或者处理器资源较为稀缺的场合都可以优先考虑 Parallel cavenge 加 Parallel Old 收集器这个组合。 3.5.6 CMS 收集器 “标记-清除”算法 CMSConcurrent Mark Sweep收集器是一种以获取最短回收停顿时间为目标的收集器。 CMS 收集器是基于标记-清除算法实现的它的运作过程分为四个步骤包括 初始标记CMS initial mark并发标记CMS concurrent mark重新标记CMS remark并发清除CMS concurrent sweep 其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下 GC Roots 能直接关联到的对象速度很快并发标记阶段就是从 GC Roots 的直接关联对象开始遍历整个对象图的过程这个过程耗时较长但是不需要停顿用户线程可以与垃圾收集线程一起并发运行而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录这个阶段的停顿时间通常会比初始标记阶段稍长一些但也远比并发标记阶段的时间短最后是并发清除阶段清理删除掉标记阶段判断的已经死亡的对象由于不需要移动存活对象所以这个阶段也是可以与用户线程同时并发的。 由于在整个过程中耗时最长的并发标记和并发清除阶段中垃圾收集器线程都可以与用户线程一起工作所以从总体上来说CMS 收集器的内存回收过程是与用户线程一起并发执行的。通过图 3-11 可以比较清楚地看到 CMS 收集器的运作步骤中并发和需要停顿的阶段。 CMS 是一款优秀的收集器它最主要的优点并发收集、低停顿。CMS 收集器是 至少有以下三个明显的缺点 首先CMS 收集器对处理器资源非常敏感。事实上面向并发设计的程序都对处理器资源比较敏感。在并发阶段它虽然不会导致用户线程停顿但却会因为占用了一部分线程或者说处理器的计算能力而导致应用程序变慢降低总吞吐量。CMS 默认启动的回收线程数是处理器核心数量3/4也就是说如果处理器核心数在四个或以上并发回收时垃圾收集线程只占用不超过 25%的处理器运算资源并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时 CMS 对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高还要分出一半的运算能力去执行收集器线程就可能导致用户程序的执行速度忽然大幅降低。 然后由于 CMS 收集器无法处理“浮动垃圾”Floating Garbage有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的 Full GC 的产生。 浮动垃圾 : 在 CMS 的并发标记和并发清理阶段用户线程是还在继续运行的程序在运行自然就还会伴随有新的垃圾对象不断产生但这一部分垃圾对象是出现在标记过程结束以后CMS 无法在当次收集中处理掉它们只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。 同样也是由于在垃圾收集阶段用户线程还需要持续运行那就还需要预留足够内存空间提供给用户线程使用因此 CMS 收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集必须预留一部分空间供并发收集时的程序运作使用。在 JDK5 的默认设置下CMS 收集器当老年代使用了 68%的空间后就会被激活这是一个偏保守的设置如果在实际应用中老年代增长并不是太快可以适当调高参数-XXCMSInitiatingOccu-pancyFraction 的值来提高 CMS 的触发百分比降低内存回收频率获取更好的性能。到了 JDK 6 时CMS 收集器的启动阈值就已经默认提升至 92%。但这又会更容易面临另一种风险 要是 CMS 运行期间预留的内存无法满足程序分配新对象的需要就会出现一次“并发失败”Concurrent Mode Failure这时候虚拟机将不得不启动后备预案冻结用户线程的执行临时启用 Serial Old 收集器来重新进行老年代的垃圾收集但这样停顿时间就很长了。所以参数-XXCMSInitiatingOccupancyFraction 设置得太高将会很容易导致大量的并发失败产生性能反而降低用户应在生产环境中根据实际应用情况来权衡设置。 还有最后一个缺点在本节的开头曾提到CMS 是一款基于“标记-清除”算法实现的收集器这意味着收集结束时会有大量空间碎片产生。 空间碎片过多时将会给大对象分配带来很大麻烦往往会出现老年代还有很多剩余空间但就是无法找到足够大的连续空间来分配当前对象而不得不提前触发一次 Full GC 的情况。为了解决这个问题 CMS 收集器提供了一个-XXUseCMS-CompactAtFullCollection 开关参数默认是开启的此参数从 JDK 9 开始废弃用于在 CMS 收集器不得不进行 Full GC 时开启内存碎片的合并整理过程由于这个内存整理必须移动存活对象在 Shenandoah 和 ZGC 出现前是无法并发的。这样空间碎片问题是解决了但停顿时间又会变长因此虚拟机设计者们还提供了另外一个参数-XXCMSFullGCsBeforeCompaction此参数从 JDK 9 开始废弃这个参数的作用是要求 CMS 收集器在执行过若干次数量由参数值决定不整理空间的 Full GC 之后下一次进入 Full GC 前会先进行碎片整理默认值为 0表示每次进入 Full GC 时都进行碎片整理。 3.5.7 Garbage First 收集器简称 G1- 标记-整理 算法 G1 开创了基于 Region(区域) 的堆内存布局,可以面向堆内存任何部分来组成回收集进行回收衡量标准不再是它属于哪个分代而是哪块内存中存放的垃圾数量最多回收收益最大。 G1不再坚持固定大小以及固定数量的分代区域划分而是把连续的 Java 堆划分为多个大小相等的独立区域Region每一个 Region(区域) 都可以根据需要扮演新生代的 Eden 空间、Survivor 空间或者老年代空间。收集器能够对扮演不同角色的 Region 采用不同的策略去处理这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。 Region 中还有一类特殊的 Humongous([hjuːˈmʌŋɡəs]) 区域专门用来存储大对象。G1 认为只要大小超过了一个 Region 容量一半的对象即可判定为大对象。每个 Region 的大小可以通过参数-XXG1HeapRegionSize 设定取值范围为 1MB32MB且应为 2 的 N 次幂。而对于那些超过了整个 Region 容量的超级大对象将会被存放在 N 个连续的 Humongous Region 之中G1 的大多数行为都把 Humongous Region 作为老年代的一部分来进行看待如图 3-12 所示。 虽然 G1 仍然保留新生代和老年代的概念但新生代和老年代不再是固定的了它们都是一系列区域不需要连续的动态集合。 如果我们不去计算用户线程运行过程中的动作如使用写屏障维护记忆集的操作G1 收集器的运作过程大致可划分为以下四个步骤 初始标记Initial Marking仅仅只是标记一下 GC Roots 能直接关联到的对象并且修改 TAMS 指针的值让下一阶段用户线程并发运行时能正确地在可用的Region 中分配新对象。这个阶段需要停顿线程但耗时很短而且是借用进行 Minor GC 的时候同步完成的所以 G1 收集器在这个阶段实际并没有额外的停顿。并发标记Concurrent Marking从 GC Root 开始对堆中对象进行可达性分析递归扫描整个堆里的对象图找出要回收的对象这阶段耗时较长但可与用户程序并发执行。当对象图扫描完成以后还要重新处理 SATB 记录下的在并发时有引用变动的对象。最终标记Final Marking对用户线程做另一个短暂的暂停用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录。筛选回收Live Data Counting and Evacuation负责更新 Region 的统计数据对各个 Region 的回收价值和成本进行排序根据用户所期望的停顿时间来制定回收计划可以自由选择任意多个 Region 构成回收集然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动是必须暂停用户线程由多条收集器线程并行完成的。 从上述阶段的描述可以看出G1 收集器除了并发标记外其余阶段也是要完全暂停用户线程的换言之它并非纯粹地追求低延迟官方给它设定的标是在延迟可控的情况下获得尽可能高的吞吐量所以才能担当起“全功能收集器”的重任与期望。从Oracle 官方透露出来的信息可获知回收阶段Evacuation其实本也有想过设计成与用户程序一起并发执行但这件事情做起来比较复杂考虑到 G1 只是回收一部分Region停顿时间是用户可控制的所以并不迫切去实现而选择把这个特性放到了G1 之后出现的低延迟垃圾收集器即 ZGC中。另外还考虑到 G1 不是仅仅面向低延迟停顿用户线程能够最大幅度提高垃圾收集效率为了保证吞吐量所以才选择了完全暂停用户线程的实现方案。通过图 3-13 可以比较清楚地看到 G1 收集器的运作步骤中并发和需要停顿的阶段。 相比 CMSG1 的优点有很多暂且不论可以指定最大停顿时间、分 Region 的内存布局、按收益动态确定回收集这些创新性设计带来的红利单从最传统的算法理论上看G1 也更有发展潜力。与 CMS 的“标记-清除”算法不同G1 从整体来看是基于“标记-整理”算法实现的收集器但从局部两个 Region 之间上看又是基于“标记-复制”算法实现无论如何这两种算法都意味着 G1 运作期间不会产生内存空间碎片垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集。 在用户程序运行过程中G1 无论是为了垃圾收集产生的内存占用Footprint还是程序运行时的额外执行负载Overload都要比 CMS 要高。 3.8 实战内存分配与回收策略 在经典分代的设计下新生对象通常会分配在新生代中少数情况下例如对象大小超过一定阈值也可能会直接分配在老年代。 3.8.1 对象优先在 Eden 分配 大多数情况下对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时虚拟机将发起一次 Minor GC。 Minor GC触发条件 当 Eden 区没有足够空间进行分配时虚拟机将发起一次 Minor GC。 3.8.2 大对象直接进入老年代 大对象就是指需要大量连续内存空间的 Java 对象最典型的大对象便是那种很长的字符串或者元素数量很庞大的数组本节例子中的 byte[]数组就是典型的大对象。 3.8.3 长期存活的对象将进入老年代 虚拟机给每个对象定义了一个对象年龄Age计数器存储在对象头中。对象通常在 Eden 区里诞生如果经过第一次 Minor GC 后仍然存活并且能被 Survivor 容纳的话该对象会被移动到 Survivor 空间中并且将其对象年龄设为 1 岁。对象在Survivor 区中每熬过一次 Minor GC年龄就增加 1 岁当它的年龄增加到一定程度默认为 15就会被晋升到老年代中。对象晋升老年代的年龄阈值可以通过参数-XX MaxTenuringThreshold 设置。 3.8.4 动态对象年龄判定 为了能更好地适应不同程序的内存状况HotSpot 虚拟机并不是永远要求对象的年龄必须达到 XXMaxTenuringThreshold 才能晋升老年代 如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半年龄大于或等于该年龄的对象就可以直接进入老年代无须等到-XX MaxTenuringThreshold 中要求的年龄。 3.8.5 空间分配担保 新生代使用复制收集算法但为了内存利用率只使用其中一个 Survivor 空间来作为轮换备份因此当出现大量对象在 Minor GC 后仍然存活的情况 ——最极端的情况就是内存回收后新生代中所有对象都存活需要老年代进行分配担保把 Survivor 无法容纳的对象直接送入老年代。 在发生 Minor GC 之前只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC否则将进行Full GC。 第 7 章 虚拟机类加载机制 7.1 概述 Java 虚拟机把描述类的数据从 Class 文件加载到内存并对数据进行校验、转换解析和初始化最终形成可以被虚拟机直接使用的 Java 类型这个过程被称作虚拟机的类加载机制。 在 Java 语言里面类型的加载、连接和初始化过程都是在程序运行期间完成的。 7.2 类加载的时机 一个类型从被加载到虚拟机内存中开始到卸载出内存为止它的整个生命周期将会经历加载Loading、验证Verification、准备Preparation、解析Resolution、初始化Initialization、使用Using和卸载Unloading七个阶段其中验证、准备、解析三个部分统称为连接Linking。 《Java 虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行“初始化”而加载、验证、准备自然需要在此之前开始 遇到 new、getstatic、putstatic 或 invokestatic 这四条字节码指令时如果类型没有进行过初始化则需要先触发其初始化阶段。 能够生成这四条指令的典型Java 代码场景有 使用 new 关键字实例化对象的时候。读取或设置一个类型的静态字段被 final 修饰、已在编译期把结果放入常量池的静态字段除外的时候。调用一个类型的静态方法的时候。 使用 java.lang.reflect 包的方法对类型进行反射调用的时候如果类型没有进行过初始化则需要先触发其初始化。当初始化类的时候如果发现其父类还没有进行过初始化则需要先触发其父类的初始化。当虚拟机启动时用户需要指定一个要执行的主类包含 main()方法的那个类虚拟机会先初始化这个主类。当使用 JDK 7 新加入的动态语言支持时如果一个java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄并且这个方法句柄对应的类没有进行过初始化则需要先触发其初始化。当一个接口中定义了 JDK 8 新加入的默认方法被 default 关键字修饰的接口方法时如果有这个接口的实现类发生了初始化那该接口要在其之前被初始化。 这六种场景中的行为称为对一个类型进行主动引用。除此之外所有引用类型的方式都不会触发初始化称为被动引用。 7.3 类加载的过程 7.3.1 加载 “加载”Loading阶段是整个“类加载”Class Loading过程中的一个阶段希望读者没有混淆这两个看起来很相似的名词。在加载阶段Java 虚拟机需要完成以下三件事情 通过一个类的全限定名来获取定义此类的二进制字节流。将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存中生成一个代表这个类的 java.lang.Class 对象作为方法区这个类的各种数据的访问入口。 7.3.2 验证 验证是连接阶段的第一步这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求保证这些信息被当作代码运行后不会危害虚拟机自身的安全。 验证阶段大致上会完成下面四个阶段的检验动作文件格式验证、元数据验证、字节码验证和符号引用验证。 7.3.3 准备 准备阶段是正式为类中定义的变量即静态变量被 static 修饰的变量分配内存并设置类变量初始值的阶段从概念上讲这些变量所使用的内存都应当在方法区中进行分配但必须注意到方法区本身是一个逻辑上的区域在 JDK 7 及之前HotSpot 使用永久代来实现方法区时实现是完全符合这种逻辑概念的而在 JDK 8 及之后类变量则会随着 Class 对象一起存放在 Java 堆中这时候“类变量在方法区”就完全是一种对逻辑概念的表述了关于这部分内容笔者已在 4.3.1 节介绍并且验证过(jdk1.7把永久代里的 静态变量、字符常量 移动到了java堆)。 关于准备阶段还有两个容易产生混淆的概念笔者需要着重强调首先是这时候进行内存分配的仅包括类变量而不包括实例变量实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值赋值的动作要到类的初始化阶段才会被执行。 表 7-1 列出了 Java 中所有基本数据类型的零值。 7.3.4 解析 解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程 符号引用Symbolic References符号引用以一组符号来描述所引用的目标符号可以是任何形式的字面量只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关引用的目标并不一定是已经加载到虚拟机内存当中的内容。直接引用Direct References直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用那引用的目标必定已经在虚拟机的内存中存在。 7.3.5 初始化 类的初始化阶段是类加载过程的最后一个步骤。 进行准备阶段时变量已经赋过一次系统要求的初始零值而在初始化阶段则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。 7.4 类加载器 7.4.1 类与类加载器 比较两个类是否“相等”只有在这两个类是由同一个类加载器加载的前提下才有意义否则即使这两个类来源于同一个 Class 文件被同一个 Java 虚拟机加载只要加载它们的类加载器不同那这两个类就必定不相等。 7.4.2 双亲委派模型 类加载器 启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器 双亲委派模型的工作过程 如果一个类加载器收到了类加载的请求它首先不会自己去尝试加载这个类而是把这个请求委派给父类加载器去完成每一个层次的类加载器都是如此因此所有的加载请求最终都应该传送到最顶层的启动类加载器中只有当父加载器反馈自己无法完成这个加载请求它的搜索范围中没有找到所需的类时子加载器才会尝试自己去完成加载。 启动类加载器Bootstrap Class Loader负责加载存放在 JAVA_HOME\lib 目录下的核心类库以及被 -Xbootclasspath参数指定的路径下的所有类。扩展类加载器Extension Class Loader这个类加载器是在类sun.misc.Launcher$ExtClassLoader 中以 Java 代码的形式实现的。它负责加载JAVA_HOME\lib\ext 目录中或者被 java.ext.dirs 系统变量所指定的路径中所有的类库。应用程序类加载器Application Class Loader这个类加载器由sun.misc.Launcher$AppClassLoader 来实现。负责加载当前应用 classpath 下的所有 jar 包和类。 * 第 8 章 虚拟机字节码执行引擎 8.2 运行时栈帧结构 Java 虚拟机以方法作为最基本的执行单元“栈帧”则是用于支持虚拟机进行方法调用和方法执行背后的数据结构它也是虚拟机运行时数据区中的虚拟机栈的栈元素。 栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。 每一个方法从调用开始至执行结束的过程都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。 每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。 8.2.1 局部变量表 局部变量表Local Variables Table是一组变量值的存储空间用于存放方法参数和方法内部定义的局部变量。 8.2.2 操作数栈 操作数栈也常被称为操作栈它是一个后入先出栈。 8.2.3 动态连接 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用持有这个引用是为了支持方法调用过程中的动态连接Dynamic Linking。 Class 文件的常量池中存有大量的符号引用字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用这部分就称为动态连接。 8.2.4 方法返回地址 当一个方法开始执行后只有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令这时候可能会有返回值传递给上层的方法调用者方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定这种退出方法的方式称为“正常调用完成”。 另外一种退出方式是在方法执行的过程中遇到了异常并且这个异常没有在方法体内得到妥善处理。无论是 Java 虚拟机内部产生的异常还是代码中使用 athrow 字节码指令产生的异常只要在本方法的异常表中没有搜索到匹配的异常处理器就会导致方法退出这种退出方法的方式称为“异常调用完成”。一个方法使用异常完成出口的方式退出是不会给它的上层调用者提供任何返回值的。 无论采用何种退出方式在方法退出之后都必须返回到最初方法被调用时的位置程序才能继续执行方法返回时可能需要在栈帧中保存一些信息用来帮助恢复它的上层主调方法的执行状态。一般来说方法正常退出时主调方法的 PC 计数器的值就可以作为返回地址栈帧中很可能会保存这个计数器值。而方法异常退出时返回地址是要通过异常处理器表来确定的栈帧中就一般不会保存这部分信息。 方法退出的过程实际上等同于把当前栈帧出栈因此退出时可能执行的操作有恢复上层方法的局部变量表和操作数栈把返回值如果有的话压入调用者栈帧的操作数栈中调整 PC 计数器的值以指向方法调用指令后面的一条指令等。 8.2.5 附加信息 《Java 虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中。 在讨论概念时一般会把动态连接、方法返回地址与其他附加信息全部归为一类称为栈帧信息。 8.3 方法调用 方法调用并不等同于方法中的代码被执行方法调用阶段唯一的任务就是确定被调用方法的版本即调用哪一个方法暂时还未涉及方法内部的具体运行过程。 一切方法调用在 Class 文件里面存储的都只是符号引用而不是方法在实际运行时内存布局中的入口地址也就是之前说的直接引用。这个特性给 Java 带来了更强大的动态扩展能力但也使得 Java 方法调用过程变得相对复杂某些调用需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。 8.3.1 解析 所有方法调用的目标方法在 Class 文件里面都是一个常量池中的符号引用在类加载的解析阶段会将其中的一部分符号引用转化为直接引用这种解析能够成立的前提是方法在程序真正运行之前就有一个可确定的调用版本并且这个方法的调用版本在运行期是不可改变的。换句话说调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法的调用被称为解析Resolution。 静态方法、私有方法、实例构造器、父类方法、被 final 修饰的方法这 5 种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。这些方法统称为“非虚方法”与之相反其他方法就被称为“虚方法”。 解析调用一定是个静态的过程在编译期间就完全确定在类加载的解析阶段就会把涉及的符号引用全部转变为明确的直接引用不必延迟到运行期再去完成。 8.3.2 分派 Java 具备面向对象的 3 个基本特征继承、封装和多态 1.静态分派 Human man new Man();我们把上面代码中的“Human”称为变量的“静态类型”Static Type或者叫“外观类型”Apparent Type后面的“Man”则被称为变量的“实际类型”Actual Type或者叫“运行时类型”Runtime Type。 代码清单 8-6 方法静态分派演示 所有依赖静态类型来决定方法执行版本的分派动作都称为静态分派。静态分派的最典型应用表现就是方法重载。静态分派发生在编译阶段因此确定静态分派的动作实际上不是由虚拟机来执行的这点也是为何一些资料选择把它归入“解析”而不是“分派”的原因。 1.动态分派 第 12 章 Java 内存模型与线程 12.3 Java 内存模型 Java 内存模型”用来屏蔽各种硬件和操作系统的内存访问差异以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。 12.3.1 主内存与工作内存 Java 内存模型的主要目的是定义程序中各种变量的访问规则即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。 Java 内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存线程的工作内存中保存了被该线程使用的变量的主内存副本线程对变量的所有操作读取、赋值等都必须在工作内存中进行而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量线程间变量值的传递均需要通过主内存来完成。 12.3.3 对于 volatile 型变量的特殊规则 [ˈvɒlətaɪl] volatile 是 Java 虚拟机 最轻量级的同步机制具有 可见性和有序性(禁止指令重排)没有原子性 synchronized 具有 原子性、可见性与有序性 12.3.5 原子性、可见性与有序性 Java 内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的。 1.原子性Atomicity 由 Java 内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store和 write 这六个基本数据类型的访问、读写都是具备原子性的。 如果应用场景需要一个更大范围的原子性保证Java 内存模型还提供了 lock 和 unlock 操作来满足这种需求。synchronized 块之间的操作也具备原子性。 2.可见性Visibility 可见性就是指当一个线程修改了共享变量的值时其他线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的无论是普通变量还是 volatile 变量都是如此。普通变量与 volatile 变量的区别是volatile 的特殊规则保证了新值能立即同步到主内存以及每次使用前立即从主内存刷新。因此我们可以说 volatile 保证了多线程操作时变量的可见性而普通变量则不能保证这一点。除了 volatile 之外Java 还有两个关键字能实现可见性它们是 synchronized 和 final。同步块的可见性是由“对一个变量执行 unlock 操作之前必须先把此变量同步回主内存中执行 store、write 操作”这条规则获得的。而 final 关键字的可见性是指被final 修饰的字段在构造器中一旦被初始化完成并且构造器没有把“this”的引用传递出去那么在其他线程中就能看见 final 字段的值。如代码清单 12-7 所示变量 i 与 j 都具备可见性它们无须同步就能被其他线程正确访问。 代码清单 12-7 final 与可见性 public static final int i; public final int j; static {i 0;// 省略后续动作 } {// 也可以选择在构造函数中初始化j 0;// 省略后续动作 }3.有序性Ordering 如果在本线程内观察所有的操作都是有序的如果在一个线程中观察另一个线程所有的操作都是无序的。 前半句是指“线程内似表现为串行的语义”后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。 Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性volatile 关键字本身就包含了禁止指令重排序的语义而 synchronized 则是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的这个规则决定了持有同一个锁的两个同步块只能串行地进入。 12.4 Java 与线程 12.4.1 线程的实现 我们知道线程是比进程更轻量级的调度执行单位线程的引入可以把一个进程的资源分配和执行调度分开各个线程既可以共享进程资源内存地址、文件 I/O 等又可以独立调度。目前线程是 Java 里面进行处理器资源调度的最基本单位。 主流的操作系统都提供了线程实现Java 语言则提供了在不同硬件和操作系统平台下对线程操作的统一处理每个已经调用过 start()方法且还未结束的 java.lang.Thread 类的实例就代表着一个线程。我们注意到 Thread 类与大部分的 Java 类库 API 有着显著差别它的所有关键方法都被声明为 Native。在 Java 类库 API 中一个 Native 方法往往就意味着这个方法没有使用或无法使用平台无关的手段来实现当然也可能是为了执行效率而使用 Native 方法不过通常最高效率的手段也就是平台相关的手段。正因为这个原因本节的标题被定为“线程的实现”而不是“Java 线程的实现”在稍后介绍的实现方式中我们也先把 Java 的技术背景放下以一个通用的应用程序的角度来看看线程是如何实现的。 实现线程主要有三种方式使用内核线程实现11 实现使用用户线程实现1N 实现使用用户线程加轻量级进程混合实现NM 实现。 Java 线程的实现 12.4.2 Java 线程调度 线程调度是指系统为线程分配处理器使用权的过程调度主要方式有两种分别是 协同式 线程调度和 抢占式 线程调度。 如果使用抢占式调度的多线程系统那么每个线程将由系统来分配执行时间线程的切换不由线程本身来决定。Java 使用的线程调度方式就是抢占式调度。 12.4.3 状态转换 Java 语言定义了 6 种线程状态在任意一个时间点中一个线程只能有且只有其中的一种状态并且可以通过特定的方法在不同状态之间转换。这 6 种状态分别是 新建New创建后尚未启动的线程处于这种状态。运行Runnable包括操作系统线程状态中的 Running 和 Ready也就是处于此状态的线程有可能正在执行也有可能正在等待着操作系统为它分配执行时间。无限期等待Waiting处于这种状态的线程不会被分配处理器执行时间它们要等待被其他线程显式唤醒。 以下方法会让线程陷入无限期的等待状态 没有设置 Timeout 参数的 Object::wait()方法没有设置 Timeout 参数的 Thread::join()方法LockSupport::park()方法。 限期等待Timed Waiting处于这种状态的线程也不会被分配处理器执行时间不过无须等待被其他线程显式唤醒在一定时间之后它们会由系统自动唤醒。 以下方法会让线程进入限期等待状态 Thread::sleep()方法设置了 Timeout 参数的 Object::wait()方法设置了 Timeout 参数的 Thread::join()方法LockSupport::parkNanos()方法LockSupport::parkUntil()方法。 阻塞Blocked线程被阻塞了“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排它锁这个事件将在另外一个线程放弃这个锁的时候发生而“等待状态”则是在等待一段时间或者唤醒动作的发生。在程序等待进入同步区域的时候线程将进入这种状态。结束Terminated已终止线程的线程状态线程已经结束执行。 上述 6 种状态在遇到特定事件发生的时候将会互相转换它们的转换关系如图 12-6所示。 第 13 章 线程安全与锁优化 13.2 线程安全 当多个线程同时访问一个对象时如果不用考虑这些线程在运行时环境下的调度和交替执行也不需要进行额外的同步或者在调用方进行任何其他的协调操作调用这个对象的行为都可以获得正确的结果那就称这个对象是线程安全的。 13.2.1 Java 语言中的线程安全 我们可以将Java 语言中各种操作共享的数据分为以下五类不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。 13.2.2 线程安全的实现方法 1.互斥同步 互斥同步Mutual Exclusion Synchronization是一种最常见也是最主要的并发正确性保障手段。同步是指在多个线程并发访问共享数据时保证共享数据在同一个时刻只被一条或者是一些当使用信号量的时候线程使用。而互斥是实现同步的一种手段临界区Critical Section、互斥量Mutex和信号量Semaphore都是常见的互斥实现方式。因此在“互斥同步”这四个字里面互斥是因同步是果互斥是方法同步是目的。 在 Java 里面最基本的互斥同步手段就是 synchronized 关键字这是一种块结构的同步语法。synchronized 关键字经过 Javac 编译之后会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令。 根据以上《Java 虚拟机规范》对 monitorenter 和 monitorexit 的行为描述我们可以得出两个关于 synchronized 的直接推论这是使用它时需特别注意的 被 synchronized 修饰的同步块对同一条线程来说是可重入的。这意味着同一线程反复进入同步块也不会出现自己把自己锁死的情况。被 synchronized 修饰的同步块在持有锁的线程执行完毕并释放锁之前会无条件地阻塞后面其他线程的进入。这意味着无法像处理某些数据库中的锁那样强制已获取锁的线程释放锁也无法强制正在等待锁的线程中断等待或超时退出。 从上面的介绍中我们可以看到 synchronized 的局限性除了 synchronized 关键字以外自 JDK 5 起Java 类库中新提供了 java.util.concurrent 包下文称 J.U.C 包其中的 java.util.concurrent.locks.Lock 接口便成了 Java 的另一种全新的互斥同步手段。基于 Lock 接口用户能够以非块结构Non-Block Structured来实现互斥同步从而摆脱了语言特性的束缚改为在类库层面去实现同步。 重入锁ReentrantLock是 Lock 接口最常见的一种实现顾名思义它与synchronized 一样是可重入的。在基本用法上ReentrantLock 也与 synchronized 很相似只是代码写法上稍有区别而已。不过ReentrantLock 与 synchronized 相比增加了一些高级功能主要有以下三项等待可中断、可实现公平锁及锁可以绑定多个条件。 等待可中断是指当持有锁的线程长期不释放锁的时候正在等待的线程可以选择放弃等待改为处理其他事情。可中断特性对处理执行时间非常长的同步块很有帮助。公平锁是指多个线程在等待同一个锁时必须按照申请锁的时间顺序来依次获得锁而非公平锁则不保证这一点在锁被释放时任何一个等待锁的线程都有机会获得锁。synchronized 中的锁是非公平的ReentrantLock 在默认情况下也是非公平的但可以通过带布尔值的构造函数要求使用公平锁。不过一旦使用了公平锁将会导致ReentrantLock 的性能急剧下降会明显影响吞吐量。锁绑定多个条件是指一个 ReentrantLock 对象可以同时绑定多个 Condition 对象。 基于以下理由笔者仍然推荐在 synchronized 与 ReentrantLock 都可满足需要时优先使用 synchronized synchronized 是在 Java 语法层面的同步足够清晰也足够简单。每个 Java 程序员都熟悉 synchronized但 J.U.C 中的 Lock 接口则并非如此。因此在只需要基础的同步功能时更推荐 synchronized。Lock 应该确保在 finally 块中释放锁否则一旦受同步保护的代码块中抛出异常则有可能永远不会释放持有的锁。这一点必须由程序员自己来保证而使用synchronized 的话则可以由 Java 虚拟机来确保即使出现异常锁也能被自动释放。尽管在 JDK 5 时代 ReentrantLock 曾经在性能上领先过 synchronized但这已经是十多年之前的胜利了。从长远来看Java 虚拟机更容易针对 synchronized 来进行优化因为 Java 虚拟机可以在线程和对象的元数据中记录 synchronized 中锁的相关信息而使用 J.U.C 中的 Lock 的话Java 虚拟机是很难得知具体哪些锁对象是由特定线程锁持有的。 2.非阻塞同步 互斥同步面临的主要问题是进行线程阻塞和唤醒所带来的性能开销因此这种同步也被称为阻塞同步Blocking Synchronization。 非阻塞同步 基于冲突检测的乐观并发策略通俗地说就是不管风险先进行操作如果没有其他线程争用共享数据那操作就直接成功了如果共享的数据的确被争用产生了冲突那再进行其他的补偿措施最常用的补偿措施是不断地重试直到出现没有竞争的共享数据为止。 CAS 指令需要有三个操作数分别是内存位置用 V 表示、旧的预期值用 A 表示和准备设置的新值用 B 表示。 CAS 指令执行时当且仅当 V 符合 A 时处理器才会用 B 更新 V 的值否则它就不执行更新。但是不管是否更新了 V 的值都会返回 V 的旧值上述的处理过程是一个原子操作执行期间不会被其他线程中断。 CAS 操作的“ABA 问题” 如果一个变量 V 初次读取的时候是 A 值并且在准备赋值的时候检查到它仍然为 A 值可是在这段期间它的值曾经被改成 B后来又被改回为 A那 CAS 操作就会误认为它从来没有被改变过。 J.U.C 包为了解决这个问题提供了一个带有标记的原子引用类 AtomicStampedReference它可以通过控制变量值的版本来保证 CAS 的正确性。不过目前来说这个类处于相当鸡肋的位置大部分情况下 ABA 问题不会影响程序并发的正确性如果需要解决 ABA 问题改用传统的互斥同步可能会比原子类更为高效。
http://www.zqtcl.cn/news/124363/

相关文章:

  • 无锡地区做网站嵌入式软硬件开发
  • 网站建设框架怎么写企业网站本身应该就是企业( )的一部分
  • 如果做公司网站WordPress出现归档
  • 温州开发网站公司阿里云 拦截网站
  • 网站建设与管理实践实践报告南宁小程序建设
  • 网站后台功能技术要求网站建设 手机和pc
  • 嘉兴住房和城乡建设厅网站仿网站被封怎么办
  • 设计君seo查询怎么查
  • 购物网站ppt怎么做网站建设的申请理由
  • 美食网站要怎么做背景墙素材高清图片免费
  • 广东专业网站优化制作公司做编辑器的网站
  • 优惠券怎做网站自己注册网站
  • 网站建设中应该返回502还是301动画短视频制作教程
  • o2o网站设计公司韩都衣舍网站建设
  • 做网站用别人的源码可以吗在线视频制作
  • 响应式网站 有哪些弊端北京网站建设怎么样
  • 轮播网站碑林微网站建设
  • 韩国网站免费观看网站建设 博客
  • 网站网商wordpress图片生成插件下载
  • seo网站营销推广桂林网站建设内容
  • 乐达淄博网站建设制作html网站开发流程
  • 赤峰网站建设flash教程网站都有哪些
  • 网站建设哪里学成品短视频app源码搭建
  • 网站可以自己做温州制作手机网站
  • 根河企业网站建设房地产如何做网站推广
  • 东莞个人网站建设南宁网站制作公
  • 网站推广seo是什么上海市人力资源网官网
  • 玉溪做网站的公司delphi xe10网站开发
  • 使用vue做的网站有哪些企业门为什么要建设门户网站
  • 上海移动云网站建设在门户网站上爆光怎么做