1. JVMJDK&JRE&JVM之间的关系

JDK包含JRE和JVM,JRE包含JVMjavac用于编译java代码到字节码文件- 使用
java命令启动JVM,字节码最终运行在JVM上
内存结构
内存结构图

JVM 内存共分为 5 个区:Java 虚拟机栈、本地方法栈、堆、程序计数器、方法区(元空间)
程序计数器
程序计数器是一块比较小的内存空间,可以看作当前线程字节码所执行的行号指示器。属于线程独占区。如果线程执行的是java方法,则计数器的值是正在执行的字节码指令的地址。如果线程执行的是native方法,则计数器的值为undefined。
本地方法栈
本地方法栈为虚拟机执行Native服务,结构和虚拟机栈完全相同。用于管理本地方法的调用,里面并没有我们写的代码逻辑,其由 native 修饰,由 C 语言实现。
虚拟机栈
虚拟机栈描述的是Java方法执行的动态内存模型
- 栈帧:每个方法执行都会创建一个栈帧,栈帧伴随着方法的创建到执行完成,用于存储局部变量表、操作数栈、动态链接、方法出口等
(栈里面存的是地址,实际指向的是堆里面的对象) - 局部变量表:用于保存方法的参数及局部变量,局部变量表内存空间大小在编译时期固定,在运行过程中不会改变局部变量表的大小
- 操作数栈:主要保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
Java堆 存放对象的实例、垃圾收集器管理的主要区域。 Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
方法区
属于线程共享区,存储了虚拟机加载的类信息【版本、字段、方法、接口】、运行时常量池【字面量和符号引用】、静态变量、即时编译器编译后的代码等数据。在HotSpot中方法区是使用永久代实现的,所以永久代等于方法区。这里很少进行垃圾回收。
从Java8开始HotSpots使用
元空间取代了永久代,永久代物理是是堆的一部分而元空间属于本地内存。元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。
直接内存【并非JVM规范定义的区域,不属于虚拟机运行时内存的一部分】
JDK1.4为了弥补IO缺陷引入NIO,运行直接在堆外分配内存,不受JVM制约,由操作系统分配。
线程私有、公有

线程全局共享的区域:
- 堆
- 方法区
线程私有:每个线程在开辟、运行的过程中会单独创建这样的一份内存,有多少个线程可能有多少个内存Java
- 虚拟机栈
- 本地方法栈
- 程序计数器是线程私有的
栈虽然方法运行完毕了之后被清空了,但是堆上面的还没有被清空,所以引出了 GC(垃圾回收),不能立马删除,因为不知道是否还有其它的也是引用了当前的地址来访问的
性能调优
2. 类加载机制

一个Java对象的创建过程往往包括两个阶段:类初始化阶段 和 类实例化阶段
类的加载:代表jvm将java文件编译成class文件后,以二进制流的方式存放到运行时数据的方法区中,并在java的堆中创建一个java.lang.Class对象,用来指向存放在方法堆中的数据结构。且虚拟机加载Class文件是懒加载机制。
类的加载
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 文件方式【class文件、jar文件】
- 网络
- 程序生成【动态代理】
- 其它【jsp-会转换为servlet,数据库】
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的Class对象,作为这个类的各种数据的访问入口
类的验证
验证是连接的第一步,但是并非是必须的。这一阶段的目的是为了确保Class文件的字流中包含的狺息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全包含:
- 文件格式验证
- 是否以魔数0xCAFFEBABE开头
- 主次版本号是否在虚拟机处理范围内
- 常量池中的常量是否有不被支持的常量类型
- 指向常量的各种索引值是否有指向不存在的常量
- 元数据验证
- 这个类是否有父类
- 这个类的父类是否继承不被允许的类(final修饰的类)
- 如果这个类不是抽象类,是否实现了接口要求实现的方法
- 类中的字段,方法是否与父类矛盾(例如出现不符合规则的方法重载)
- 字节码验证
- 保证任何时刻操作数栈的数据类型与指令代码序列能配合工作
- 保证跳转指令不会跳转到方法体以外的字节码指令
- 保证方法体中类型转换有效,如避免出现将父类对象赋值到子类数据类型上
- 符号引用验证
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述符号
- 符号引用的类,字段,方法的访问性是否可以被当前类访问
类的准备
准备阶段正式为类的静态变量分配内存并设置变量的初始值。这些变量使用的内存都将在方法区中进行分配。设置的初始值并非我们指定的值而是这种变量的默认值,但是如果是被final修饰的常量则会被初始为我们指定的值
类的解析
**解析阶段是将常量池中的符号引用替换为直接引用的过程。**在进行解析之前需要对符号引用进行解析,不同虚拟机实现可以根据需要判断到底是在类被加载器加载的时候对常量池的符号引用进行解析(也就是初始化之前),还是等到一个符号引用被使用之前进行解析(也就是在初始化之后)。如果一个符号引用进行多次解析请求,虚拟机中除了invokedynamic指令外,虚拟机可以对第一次解析的结果进行缓存(在运行时常量池中记录引用,并把常量标识为一解析状态),这样就避免了一个符号引用的多次解析。
类或者接口解析
要把一个类或者接口的符号引用解析为直接引用,需要以下三个步骤
如果该符号引用不是一个数组类型,那么虚拟机将会把该符号代表的全限定名称传递给类加载器去加载这个类。这个过程由于涉及验证过程所以可能会触发其他相关类的加载
如果该符号引用是一个数组类型,并且该数组的元素类型是对象。我们知道符号引用是存在方法区的常量池中的,该符号引用的描述符会类似”[java/lang/Integer”的形式,将会按照上面的规则进行加载数组元素类型,如果描述符如前面假设的形式,需要加载的元素类型就是java.lang.Integer ,接着由虚拟机将会生成一个代表此数组对象的直接引用
如果上面的步骤都没有出现异常,那么该符号引用已经在虚拟机中产生了一个直接引用,但是在解析完成之前需要对符号引用进行验证,主要是确认当前调用这个符号引用的类是否具有访问权限,如果没有访问权限将抛出java.lang.IllegalAccess异常
字段解析
对字段的解析需要首先对其所属的类进行解析,因为字段是属于类的,只有在正确解析得到其类的正确的直接引用才能继续对字段的解析。对字段的解析主要包括以下几个步骤:
- 如果该字段符号引用就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,解析结束
- 否则,如果在该符号的类实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果在接口中包含了简单名称和字段描述符都与目标相匹配的字段,那么久直接返回这个字段的直接引用,解析结束
- 否则,如果该符号所在的类不是Object类的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都相匹配的字段,那么直接返回这个字段的直接引用,解析结束
- 否则,解析失败,抛出java.lang.NoSuchFieldError异常
如果最终返回了这个字段的直接引用,就进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常
类方法解析
进行类方法的解析仍然需要先解析此类方法的类,在正确解析之后需要进行如下的步骤:
- 类方法和接口方法的符号引用是分开的,所以如果在类方法表中发现class_index(类中方法的符号引用)的索引是一个接口,那么会抛出java.lang.IncompatibleClassChangeError的异常
- 如果class_index的索引确实是一个类,那么在该类中查找是否有简单名称和描述符都与目标字段相匹配的方法,如果有的话就返回这个方法的直接引用,查找结束
- 否则,在该类的父类中递归查找是否具有简单名称和描述符都与目标字段相匹配的字段,如果有,则直接返回这个字段的直接引用,查找结束
- 否则,在这个类的接口以及它的父接口中递归查找,如果找到的话就说明这个方法是一个抽象类,查找结束,返回java.lang.AbstractMethodError异常
- 否则,查找失败,抛出java.lang.NoSuchMethodError异常
如果最终返回了直接引用,还需要对该符号引用进行权限验证,如果没有访问权限,就抛出java.lang.IllegalAccessError异常
接口方法解析
同类方法解析一样,也需要先解析出该方法的类或者接口的符号引用,如果解析成功,就进行下面的解析工作:
如果在接口方法表中发现class_index的索引是一个类而不是一个接口,那么也会抛出java.lang.IncompatibleClassChangeError的异常
否则,在该接口方法的所属的接口中查找是否具有简单名称和描述符都与目标字段相匹配的方法,如果有的话就直接返回这个方法的直接引用。
否则,在该接口以及其父接口中查找,直到Object类,如果找到则直接返回这个方法的直接引用
否则,查找失败
类的初始化
初始化时机
对于初始化阶段,虚拟机规范严格规定了有且只有5种情况必须对类进行初始化(加载、验证、准备自然需在此之前开始):
①遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这四条指令的最常见的java代码场景是:
使用new关键字实例化对象的时候
读取或设置一个类的静态字段【被
final修饰己在编译器把结果放入常量池的静态字段除外】调用一个类的静态方法的时候
②使用java.lang.reflect包的方法对类进行反射调用的时,如果类没有进行过初始化则需要先触发其初始化
③当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
④当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
⑤当使用JDK7动态语言支持时,如果MethodHandle实例最后的解析结果REF_getstatic、REF_putstatic、REF_invokestatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化
不执行初始化的例子
- 调用类常量【public static final】
- 通过数组定义来引用类【Base[] car = new Base[5]】
- 通过子类引用父类的静态字段【Base.ParentVariable】时,
子类不会被初始化
初始化过程
类初始化时类加载过程的最后一步,前面的类加载过程中,除了在加载阶段(类加载过程的一个阶段)应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的java程序代码。初始化阶段是执行类构造器<clinit>()方法的过程。
<clinit>()方法执行过程中可能会影响程序运行行为:
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但不能访问,属于非法向前引用。1 2 3 4 5 6 7 8public class Test { static { v = 3; System.out.println(v); //编译报错,只能赋值不能访问 } static int v = 1; } // 顺序执行最后v的值为"1"<clinit>()方法与类的构造函数(即类的实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法一定是java.lang.Object<clinit>()方法对于类(抽象类)或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成
<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法虚拟机会保证一个类的
<clinit>()方法在多线程环境中被正确的加锁,同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。在实际应用中这种阻塞引起的问问往往是很隐蔽
类加载器的种类
启动(Bootstrap)类加载器
启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 /lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中。它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用。即Extension ClassLoader的代码中的parent为null
注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)
扩展(Extension)类加载器
扩展类加载器是指Sun公司实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
系统(System)类加载器
也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
自定义(custom)类加载器
自定义类加载器由第三方实现,建议满足双亲委派模式。
只有被同一个类加载器加载的类才会相等,相同的字节码被不同的类加载器加载的类不相同。
双亲委派模式
双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器【并非继承】采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:

3. 对象初始化机制
对象创建过程

对象内存分配方式
指针碰撞
假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为指针碰撞
空闲列表
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为空闲列表
具体是使用空闲列表还是指针碰撞需要根据内存状况决定
多线程内存分配管理
不管使用哪种对象内存分配方式,在多线程环境时,如果一个线程正在给A对象分配内存,指针还没有来的及修改,其它为B对象分配内存的线程,而且还是引用这之前的指针指向,这样会带来分配问题。于是有两种处理方式。
分配时加锁
堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的。
TLAB【线程本地分配缓存区】
为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配。
JVM在内存新生代Eden Space中开辟了一小块线程私有的区域称作TLAB。默认设定为占用Eden Space的1%。
对象的结构

对象头
- _mark:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,称之为“MarkWord”
- _klass:指向类元数据的指针
- _length:数组长度(只有数组对象有)
实例数据
存储在程序代码中所定义的各种类型的字段内容
对齐填充
帮助对象凑满8字节的倍数,不一定存在
对象访问方式
句柄访问
使用句柄访问。Java堆中将会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。

直接指针访问
使用直接指针访问。reference中存储的就是对象的地址。

HotSpot是用直接指针访问方式进行对象访问的。
4. 内存分配机制
分配策略
对象优先分配到Eden区
大对象直接分配到老年代
长期存活对象分配到老年代
空间分配担保
存入对象时发现年轻代空间不足,将判断老年代最大可用的连续空间是否大于当前年起代所有对象
- 满足,minor gc是安全的,可以进行minor gc
- 不满足,虚拟机查看HandlePromotionFailure参数
- true会继续检测[老年代最大可用的连续空间]是否大于[历次晋升到老年代对象的平均大小]。若大于,将尝试进行一次minor gc,若失败,则重新进行一次full gc
- false则不允许冒险,要进行full gc
栈上分配和逃逸分析
在JDK7(包括)之后完全支持栈上分配和逃逸分析。
逃逸分析
如果某个方法之内创建的实例只在方法内被使用,方法结束之后没有任何对象引用它【即可被GC回收】,这样的对象叫做未发生逃逸对象。如果某个方法之内创建的实例在方法结束之后有对象引用它【即不可被GC回收】,这样的对象叫做逃逸对象。
栈上分配
如果一个对象未发生逃逸则这个对象的生命周期只存在一个方法体内,这样的对象可以直接在栈上分配提高效率,也方便回收【方法结束,栈销毁,未发生逃逸也就随着销毁】
5. GC垃圾处理回收机制
鉴定垃圾对象

引用计数法
在对象中添加一个引用计数器,当有地方释放这个引用时,对象上存储的引用计数减一,但是当出现互相引用时【对象实例3和对象实例5】引用计数依旧不为0,无法对其进行垃圾回收
可达性分析
该算法的核心算法是从GC Roots对象作为起始点,如果对象不可达到GC Root则认为此对象是要回收的对象。可作为GC Roots的对象
- 虚拟机栈的局部变量所引用的对象
- 本地方法栈的JNI所引用的对象
- 方法区的静态变量和常量所引用的对象
回收策略
标记-清除算法

算法分为标记和清除两个阶段:先标记出所有需要回收的对象,完成后统一回收掉所有被标记的对象。
缺陷:①标记和清除过程的效率不高;②极易造成空间碎片问题
复制收集算法

复制收集算法将可用内存按容量划分为大小相等的两块,survivor区每次只使用其中的一块。当这一块的内存用完了,就将eden和survivor还存活着的对象复制到另外一块survivor上面,然后再把已使用过的内存空间一次清理掉。 这样内存分配时也就不用考虑内存碎片等复杂情况,实现简单运行高效。长期存活的对象移入oldGen区这里面的对象很少执行垃圾回收。
标记-整理算法

当预估能回收的对象并不多时(例如老年代)采用复制收集算法就要执行较多的复制操作,效率将会变低且还浪费了大量空间,所以此时进行复制收集算法不明智。采用标记-整理算法较为合适,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。且下次在此空间继续分配内存可以使用指针碰撞法提高速度。
分代收集算法
把Java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。新生代每次垃圾收集时都有大量对象需要回收那就选用复制算法。而老年代中因为对象存活率高就使用标记-清理或标记-整理算法进行回收。
GC分类
Minor GC:清理年轻代,不会影响到永久代
Major GC :清理老年代,Major GC大部分由Minor GC触发
Full GC :清理整个堆空间—包括年轻代、老年代和永久代
垃圾回收器
针对新生代的垃圾回收器
Serial收集器【复制收集算法】
- JDK1.3之前回收新生代内存的唯一选择
- 单线程的垃圾收集器,单CPU环境下效果较好

ParNew【复制收集算法】
- Serial收集器的多线程版本
- ParNew收集器是许多运行多CPU模式下的虚拟机中首选的新生代收集器
- 它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的

Parallel Scavenge【复制收集算法】
- 达到一个可控制的吞吐量有GC自适应调节策略

针对老年代的垃圾回收器
Serial Old 收集器【标记-整理算法】
- 单线程垃圾收集器

Parallel Old 收集器【标记-整理算法】
Parallel Scavenge的老年版本

cms 收集器【标记-清除算法】
- 一种以获取最短回收停顿时间为目标的收集器

[1]初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
[2]并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
[3]重新标记:为了修正并发标记期间因用户程序继续运行发生改变的对象的记录。仍然存在Stop The World。
[4]并发清除:对标记的对象进行清除回收。
特殊收集器
G1收集器

G1垃圾收集器并没有将内存按照连续内存地址分为新生代、老年代。而是分成了一个个的区域【Region】,采用了分代与分区算法。这些区域大小不固定,根据回收的情况进行评估,到达阀值视为老年代。在进行一次垃圾回收之后,会进行日志收集确定分代划分和是否进行混合清理(新生代和老年代一起清理)。
- 并行性:G1回收期间可以多线程同时工作
- 并发性:G1拥有与应用程序交替执行的能力,部分工作可与应用程序同时执行,在整个GC期间不会完全阻塞应用程序
- 分代GC:依旧分为新生代和老年代,新生代依然有eden,from和to
- 空间整理:G1在垃圾回收过程中,不会像CMS那样在若干次GC后需要进行碎片整理,G1采用了有效复制对象方式
- 可预见性:由于分区的原因,G1可以只选取部分区域进行回收,缩小了回收的范围,提高性能
G1的内存结构如下所示:

JDK11之后G1触发FULL GC时可并行处理,以前只能串行处理
各代HotSpot(Server模式下)默认垃圾收集器
- jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
- jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
- jdk1.9 默认垃圾收集器G1
附录
JIT编译(just-in-time compilation)
在部分商用虚拟机中(如HotSpot),Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compile)。
即时编译器并不是虚拟机必须的部分,Java虚拟机规范并没有规定Java虚拟机内必须要有即时编译器存在,更没有限定或指导即时编译器应该如何去实现。但是,即时编译器编译性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键的指标之一,它也是虚拟机中最核心且最能体现虚拟机技术水平的部分。
JIT编译狭义来说是当某段代码即将第一次被执行时进行编译,因而叫“即时编译”。JIT编译是动态编译的一种特例。JIT编译一词后来被泛化,时常与动态编译等价。
符号引用&直接引用
符号引用(Symbolic References)
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。
直接引用(Direct References)
(1)直接指向目标的指针【比如指向Class对象、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量【比如指向实例变量、实例方法的直接引用都是偏移量】
(3)一个能间接定位到目标的句柄
管程
管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。进程只能互斥得使用管程,即当一个进程使用管程时,另一个进程必须等待。当一个进程使用完管程后,它必须释放管程并唤醒等待管程的某一个进程。在管程入口处的等待队列称为入口等待队列,由于进程会执行唤醒操作,因此可能有多个等待使用管程的队列,这样的队列称为紧急队列,它的优先级高于等待队列。