zoukankan      html  css  js  c++  java
  • 深入理解Java虚拟机

    目录

    第一部分:概述

    1 走进Java

        1.1 概述  

        1.2 Java技术体系

    第二部分:自动内存管理机制

    2 Java内存区域与内存溢出异常

        2.1 运行时数据区域

        2.2 HotSpot虚拟机对象探秘

    3  垃圾收集器与内存分配策略

     3.1 对象已死吗

     3.2 垃圾收集算法

     3.3 HotSpotd的算法实现

     3.4 垃圾收集器

     3.5 内存分配与回收策略

    第三部分: 虚拟机执行子系统

    6 类文件结构

     6.1 无关性的基石

     6.2 Class类文件的结构 

    7 虚拟机类加载机制

     7.1 类加载的时机

     7.2 类加载的过程

     7.3 类加载器

    8 虚拟机字节码执行引擎

    1 走进Java

    1.1 概述

      Java优点:它摆脱了硬件平台的東缚,实现了“一次编写,到处运行”的理想;它提供了一个相对安全的内存管理和访问机制,避免了绝大部分的内存泄露和指针越界问题;它实现了热点代码检测和运行时编译及优化,这使得Java应用能随着运行时间的增加而获得更高的性能;它有一套完善的应用程序接口,还有无数来自商业机构和开源社区的第三方类库来帮助它实现各种各样的功能等等。

    1.2 Java技术体系

    sun官方定义:

      1 Java程序设计语言

      2 各种硬件平台上的Java虚拟机

      3 Class文件格式

      4 Java API类库

      5 来自商业机构和开源社区的第三方Java类库

    把Java程序设计语言、Java虚拟机、 Java API类库这三部分统称为JDK(Java Development Kit),JDK是用于支持Java程序开发的最小环境。把 Java API类库中的Java SE API子集和Java虚拟机这两部分统称为JRE( Java Runtime Environment),JRE是支持Jav程序运行的标准环境。

     2 Java内存区域与内存溢出异常

     2.1 运行时数据区域

     

    (1)程序计数器

     程序计数器( Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

    特点:由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native方法,这个计数器值则为空( Undefined)。

    异常:此内存区域是唯一一个在Java虚拟机规范中没有规定任何 Outofmemory Error情况的区域

    (2)Java虚拟机栈

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

    栈帧(Stack Frame):每一次函数的调用,都会在调用栈(call stack)上维护一个独立的栈帧(stack frame).每个独立的栈帧一般包括:

    • 函数的返回地址和参数
    • 临时变量: 包括函数的非静态局部变量以及编译器自动生成的其他临时变量
    • 函数调用的上下文

    局部变量表存放了细期可知的各种基本数据类型、对象引用( reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress(指向了一条字节码指令的地址)

    特点:线程私有,生命周期与线程相同。

    异常:StackOverflowError、OutOfMemoryError。

    (3)本地方法栈

    本地方法栈与虚拟机栈所发挥的作用类似,虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈为虚拟机使用到的Native方法服务。有的虚拟机(Sun HotSpot)将二者合二为一。

    异常:StackOverflowError、OutOfMemoryError

    (4)Java堆

    Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”( GarbageCollected Heap)

    异常:OutOfMemoryError

    (5)方法区

     方法区是各个线程共享的内存区域,存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在Java规范中方法区为堆的一个逻辑部分但有一个别名Non-Heap(非堆)。

    特点:垃圾收集行为在这个区域较少出现,回收目标主要是针对常量池的回收和对类型的卸载。

    (6)运行时常量池

    运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容当类加载完成后进入常量池存放。

    特点:具备动态性,在运行期间也可将新的常量放入常量池中如String类的intern()方法

    异常:OutOfMemoryError

    (7)直接内存

     直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域。

     2.2 HotSpot虚拟机对象探秘

    (1)对象创建

    • 1 虚拟机遇到new指令时检查能否在常量池中定义到一个类符号的引用,并检查是否执行过类加载过程,并执行类加载过程。
    • 2 为对象分配内存。根据是堆否规整:指针碰撞、空闲列表
    • 3 在分配时考虑并发问题
    • 4 将对象分配到的内存空间初始化为0(不含对象头)
    • 5 对对象进行设置

    从虚拟机角度而言便结束了,从Java程序语言而言才刚开始,执行初始化方法,把对象按照代码的设置初始化。

    (2)对象的内存布局

    包含三部分:对象头、实例数据、对齐填充。

    • 对象头

    包含两部分信息:存储对象自身的运行时数据;类型指针,用来确定是哪个对象的实例。但是第二部分类型指针不一定所有的虚拟机上都有。当对象是数组时对象头还需要一块用于记录数组长度的数据。

    • 实例数据

    实例数据是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容包含:父类继承的,子类特有的。

    • 对齐填充

    并不是必然存在的,因为HOtSpot VM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍(1~2倍)。

    (3)对象的访问定位

    • 句柄访问

     

    •  通过直接指针访问对象

     

     3  垃圾收集器与内存分配策略

    主要关注:哪些内存需要回收?什么时候回收?如何回收?

     3.1 对象已死吗

    (1)判断对象是否已死:

      (1)引用计数法。添加引用计数器,但很难解决对象间相互循环引用问题,实际中基本不用。

      (2)可达性分析算法。以GC Roots对象为根进行搜索。

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

    (2)再谈引用

    在JDK1.2前引用的定义:如果 reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中,如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

    后来对引用概念做了扩充:将引用分为强引用( Strong Reference)、软引用( Soft Reference)、弱引用( Weak Reference)、虚引用( Phantom Reference)4种,这4种引用强度依次逐渐减弱。

    • 强引用就是指在程序代码之中普遍存在的,类似“ Object obj= new Object0”这类的引用,只要强引用还存在,垃圾收集器水远不会回收掉被引用的对象。
    • 软引用是用来描述一些还有用但并非必需的对象。
    • 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些。
    • 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。

    (3)生存还是死亡

    可达性分析算法中不可达的对象,处于缓刑阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。如果对象在进行可达性分析后发现没有与 GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。如果这个对象被判定为有必要执行 finalizer()方法,那么这个对象将会放置在一个叫做F-queue的队列之中,虚拟机会触发这个方法,但并不承诺会等待它运行结束,稍后GC将对 F-queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己一一只要重新与引用链上的任何一个对象建立关联即可,如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

    (4)回收方法区

    方法区(永久代)的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象非常类似。要判读无用的类则需要满足下面三个条件:

    • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
    • 加载该类的 ClassLoader已经被回收
    • 该类对应的java.lang.Clas对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    虚拟机可以对满足上述3个条件的无用类进行回收,但仅仅是可以而不是一定。

     3.2 垃圾收集算法

    1)标记-清除算法(Mark-Sweep)

    该算法分为两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程在对象标记判定时已经介绍过了。后续的收集算法都是基于这种思路并对其不足进行改进而得到的。

    它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高:另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

    (2)复制算法

    复制算法是为了解决效率问题。把内存分为大小相等的两个块,每次只使用其中一块,当一块用完了把其中还存活的对象复制到另一块中。这样也不用考虑内存分配时内存碎片问题了。

    不足:只是这种算法的代价是将内存缩小为了原来的一半。

    商用中采用将内存分为一块较大的Eden和两块较小的Survivor,每次使用Eden和一块Survivor,回收时将活着的对象复制到另一块Survivor中。HotSpot默认Eden和Survivor大小为8:1。但没有办法保证每次回收都只有不多于10%的对象存活,当 Survivor空间不够用时需要依赖其他内存(这里指老年代)进行分配担保( Handle Promotion)。即如果另外一块 Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

    (3)标记-整理算法(Mark-Compact)

     复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低,此外老年代对象一般是100%存活的因此不能直接选用这种算法。根据老年代的特点标记-一整理(Mark- Compact)算法,标记过程仍然与标记一清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

    (4)分代收集算法

    目前广泛使用的是分代收集算法,根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记一清理”或者“标记一整理”算法来进行回收。

     3.3 HotSpot的算法实现

    此部分为看,主要是分析代码的

    3.4 垃圾收集器

    收集算法是内存回收的方法论,垃圾收集器则是内存回收的具体实现。下图中连线表示可以相互配合:

     

     (1)Serial收集器

    单线程;在工作时必须暂停其他所有的工作线程(Stop The World);简单高效。

     (2)ParNew收集器

    Serial的多线程版本。

     注:并行( Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

    并发( Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另ー个CPU上

    (3)Parallel Scavenge收集器

    其他收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量( Throughput)

    吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

    Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的参数以及直接设置吞吐量大小的参数

    (4)Serial Old收集器

    单线程;给Client模式下的虚拟机使用;在Server模式下可作为CMS收集器的后备预案

    (5)Parallel Old收集器

    多线程。在注重香吐量以及CPU资源感的场合,都可以优先考虑 Parallel Scavenge加 ParallelOd收集器

    (6)CMS收集器

    (7)G1收集器

    3.5 内存分配与回收策略

    Java内存自动化管理最终目的是自动化解决:给对象分配内存以及回收分配给对象的内存。

     分配策略:

    • 对象优先在Eden分配,当没有足够空间时虚拟机发起一次Minor GC
    • 大对象直接进入老年代如:字符串、数组。
    • 长期存活的对象进入老年代。采用对象年龄计数器,默认15岁以上进入
    • 动态对象年龄判定。若Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可直接进入老年代。
    • 空间分配担保。一共有多少对象存活在实际完成回收前无法确定,因此取之前每次回收到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间比较,决定是否Full GC。

     注: Minor GC和 Full GC区别

    新生代GC( Minor GC)指发生在新生代的拉圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以 Minor GC非常频繁,一般回收速度也比较快

    老年代GC( Major GC/ Full GC):指发生在老年代的GC,出现了 Major GC,经常会伴随至少ー次的 Minor GC(但非绝对的,在 Parallel Scavenge收集器的收集策略里就有直接进行 Major GC的策略选择过程)。 Major GC的速度一般会比 Minor GC慢10倍以上。

    6 类文件结构

    代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

    6.1 无关性的基石 

    实现语言无关性的基础是虚拟机字节码存储格式。Java虚拟机不和包括Java在内的任何语言绑定,它只与Clas文件这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。Java虚拟机可以运行其他语言编译的字节码文件。

    6.2 Class类文件的结构

    Class文件是一组以8位字节为基础单位的二进制流,中间无分隔符。Class文件格式如下图所示:

     

     (1)魔数与Class文件的版本

    每个 Class文件的头4个字节称为魔数( Magic Number),它的唯一作用是确定这个文是否为一个能被虚拟机接受的Css文件。紧接着魔数的4个字节存储的是CS文件的版本号:第5和第6个字节是次版本号( Minor Version),第7和第8个字节是主版本号( Major Version)。

     (2)常量池

    主次版本之后便是常量池,从1开始而不是从0开始计数(只有常量池特例),为了表达某些指向常量池的索引值的数据在特定的情况下需要表达:不引用任何一个常量池项目的含义。

    常量池中主要存放两大类常量:字面量和符号引用。字面量接近于Java语言层面的常量概念,如文本字符串、声明为fnal的常量值等。符号引用则属于编译原理方面的概念,包括了下面三类常量:

    • 类和接口的全限定名( Fully Qualified Name)
    • 字段的名称和描述符( Descriptor)
    • 方法的名称和描述符

     (3)访问标志

    在常量池结束之后,紧接着的两个字节代表访问标志( access flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为 public类型;是否定为 abstract类型;如果是类的话,是否被声明为 final等。

     (4)类索引、父类索引、接口索引集合

    类索引( this class)父类索引( super class)都是一个u2类型的数据,而接口索引集合( interfaces)是一组u2类型的数据的集合,Cass文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按 implements语句(如果这个类本身是一个接口,则应当是 extends语句)后的接口顺序从左到右排列在接口索引集合中。

     (5)字段表集合

    字段表( (field info)用于描述接口或者类中声明的变量。字段(feld)包括类级变量以实例级变量,但不包括在方法内部声明的局部变量。

     (6)方法表集合

    表结构和字段表类似。

     (7)属性表集合

    在Class文件、字段表、方法表都可以携带自己的属性表集合,来描述某些场景专有的信息。属性表中的属性较多,这里只以Code属性为例其余的可参考书籍。Java程序方法体中的代码经过 Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,如接口或者抽象类中的方法就不存在Code属性。

    如果把一个Java程序中的信息分为代码(方法体里面的Java代码)和元数据( 包括类、字段、方法定义及其他信息)两部分,那么在整个 Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。

    7 虚拟机类加载机制

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

    7.1 类加载的时机

    Java类的生命周期如下图所示:

     

    加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

    对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

    • 遇到new、 getstatic、putstatic 或 invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
    • 使用 java. lang reflect t包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
    • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
    • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main0方法的那个类),虚拟机会先初始化这个主类
    • 当使用JDK1.7的动态语言支持时,如果一个java. lang invoke. Methodhandle实例最后的解析结果 REF_getstatic、 REF_putstatic、 REF_invokestatic I的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

    除此之外,所有引用类的方式都不会触发初始化,称为被动引用

    • 通过子类引用父类的静态字段,不会导致子类初始化
    • 通过数组定义来引用类不会触发此类的初始化
    • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

       接口的初始化与类的初始化基本类似,只有类的初始化场景中的第三种不同:一个接口初始化并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

     7.2 类加载的过程

    (1)加载

    “加载”是“类加载”( Class Loading)过程的一个阶段,在加载阶段,虚拟机需要完成以下3件事情:

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

     一个非数组类的加载阶段可以由用户自定义的类加载器去完成,而对数组类而言由于数组类本身不同过类加载器创建,是有Java虚拟机直接创建。

    加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义。注意,加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这两个阶段的开始时间仍然后固定先后顺序。

     (2)验证

    验证是连接阶段的第一步,这一阶段的目的是为了确保 Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段包含:文件格式验证、元数据验证、字节码验证、符号引用验证。

     (3)准备

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被saic修饰的变量),而不包括实例变量,实例交量将会在对象实例化时随着对象一起分配在Java雄中。其次,这里所说的初始值通常情况下是数据类型的零值。

     (4)解析

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

    • 符号引用( Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
    • 直接引用( Direct References):直接引用可以是直接指向目标的指针、相对偏移量是一个能间接定位到目标的句柄。

     (5)初始化

    类的初始化是类加载的最后一步,到了初始化阶段才真正开始执行类中的Java程序代码(字节码)。初始化阶段是执行类构造器<clinit>()方法的过程

     7.3 类加载器

    (1)类与类加载器

    对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

    (2)双亲委派模式

    从Java虚拟机角度只存在两种不同的类加载器:一种是启动类加载器( Bootstrap Classloader),这个类加载器使用C++语言实现,是虚拟机自身的一部分:另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

     

    上图为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加載器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承( Inheritance)的关系来实现,而是都使用组合( Composition)关系来复用父加载器的。

    双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器オ会尝试自己去加载。

    实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中。

    0

  • 相关阅读:
    (转)分布式系统原理
    Java常用排序
    19.Java5同步集合类的应用
    18.Java5阻塞队列的应用
    17.Java5的Exchanger同步工具
    16.Java5的CountDownLatch同步工具
    15.Java5的CyclicBarrier同步工具
    14.Java5的Semaphere同步工具
    13.Java5条件阻塞Condition的应用
    Docker部署JavaWeb项目实战
  • 原文地址:https://www.cnblogs.com/youngao/p/11570496.html
Copyright © 2011-2022 走看看