操作系统-用户接口与作业调度

1、程 序 的 启 动 和 结 束
1.1.程序的启动
– 程序开始执行时必须满足两个前提条件:
• 程序已装入内存
• 程序计数器PC中已置入该程序在内存的入口地址

1.2 五种启动程序执行的方式
1)命令方式
Image text

2)批处理方式
Image text

3)EXEC方式
– 在一个程序中运行另一个程序
– 返回原来的程序
Image text

4)由硬件装入程序和启动程序执行

5)自启程序
• 自己装入自己,并启动自己开始执行的程序
• 自启程序由两部分组成:引导程序和程序主体
Image text

2、程序的结束
正常结束:程序按自身的逻辑有效地完成预定功能
后结束
(a)返回父程序并回送结果信息。
(b)释放所用资源(空间、设备),记录使用情
况,记帐等
– 异常结束:发生了某些错误而导致程序在没有完成
预定功能时提前结束

3、用户与操作系统的接口
为用户提供两种接口:
(1)命令接口
用户通过这些命令来组织和控制作业的执行。
(2)程序接口
编程人员使用他们来请求操作系统服务。

作业控制的主要方式:
(1)联机命令接口
又称交互式命令接口,由一组键盘操作命令组
成。用户通过控制台或者终端键入操作命令,完
成对作业的控制。
(2)脱机命令接口
又称批处理命令接口,由一组作业控制语言组
成,由系统负责解释执行。(涉及到作业的相关
概念)

3、作 业 的 基 本 概 念
(1)作业
用户在一次计算过程中,或者一次事务处理过程中,
要求计算机系统所做工作的总称
(2)作业步
一个作业可划分成若干部分,称为一个作业步
(3)典型的作业控制过程:
“编译”、“连接装配”、“运行”

作业组织:
作业由三部分组成,即程序、数据、作业说明书。

作 业 控 制 块(JCB:Job Control Block)
– 作业控制块是作业存在的标志
– 保存现有系统对于作业进行管理所需要的全部信息
– 位于磁盘区域中

Image text

作 业 的 状 态 及 转 换:
Image text

4、作 业 调 度
调度的实质
• 资源的分配
– 调度算法定义
• 根据系统的资源分配策略所规定的资源分配算法

调 度 算 法 的 性 能 准 则:
– (平均)周转时间:作业从提交到完成的时间(用户角度)
周转时间:Ti=Tc-Ts
Tc作业完成时刻;
Ts作业进入系统时刻

平均周转时间:(T1+T2+…+Tn)/n

– (平均)带权周转时间:周转时间 / CPU运行时间(用户角度)
带权周转时间:
Image text

平均带权周转时间:
Image text

– 响应时间:用户输入一个请求(如击键)到系统给出首次响应(如屏幕
显示)的时间(处理机的角度)
– 公平性:不因作业或进程本身的特性而使上述指标过分恶化(算法本身的角度)
– 优先级:可以使关键任务达到更好的指标(算法本身的角度)

5、先来先服务算法FCFS (First Come First Served)
– 按作业的先后顺序进行调度
– 处理过程
1)按照作业提交先后次序,分配CPU执行;
2)当前作业占用CPU,直到执行完或阻塞(如申请I/O)让出CPU;
3)作业被唤醒后(如I/O完成),不立即恢复执行,等待当前作业
让出CPU后才可以恢复执行。
– 最简单的调度算法
– 对短作业不利(平均周转时间延长)

举例:假设在单道批处理环境下有四个作业,已知它们进入系统的时间、估计运行时间 ,求:
Image text

6、短 作 业 优 先 算 法 SJF (Shortest Job First )
按作业的长短顺序进行调度,短作业优先
• 对预计执行时间短的作业优先分配CPU
• 通常后来的短作业不抢占正在执行的作业
– 对FCFS算法的改进,目标是减少平均周转时间

– 优点:
• 相比于FCFS改善平均周转时间和平均带权周转时间;
• 缩短作业的等待时间;
• 提高系统的吞吐量;
– 缺点:
• 对长作业非常不利,可能长时间得不到执行;
• 难以准确估计作业(进程)的执行时间,从而影响调度性能。
• 未能依据作业的紧迫程度来划分执行的优先级;

举例:
假设在单道批处理环境下有四个作业,已知它们进入系统的时间、估计运行时间 ,求:
Image text

7、最 短 剩 余 时 间 优 先 算 法 SRT (Shortest Remaining Time)
– 短作业优先算法的变型,也称作抢占式的短作业优先算法
– 允许比当前进程剩余时间更短的进程来抢占
– 抢占时机:新作业加入队列时

8、最 高 响 应 比 优 先 算 法HRRN (Highest Response Ratio Next)
– 从就绪队列中选出响应比最高的作业投入执行
– 响应比R = (等待时间W +要求执行时间T) / 要求执行时间T
– FCFS和SJF的折衷

– 优点:既照顾了短作业,也考虑到先后顺序
– 缺点:每次调度时要调用响应比计算,增加了系统开销

举例:假设在单道批处理环境下有四个作业,已知它们进入系统的时间、估计运行时间 ,求:
Image text

9、基 于 优 先 数 调 度 算 法 HPF ( Highest Priority First )
(a)由用户规定优先数(外部优先数)
用户提交作业时,根据急迫程度规定适当的优先数
作业调度程序根据JCB优先数决定进入内存的次序
(b)由系统计算优先数(内部优先数)

举例:
在两道环境下有四个作业,已知它们进入系统的时间、估计运行时间; 作业调度采用短作业优先调度算法(作业被调度
运行后直到结束前不再退出内存); 进程调度采用最短剩余时间优先调度算法(当新作业投入运行后,可按照作业剩余运行时间长短调整次
序,可抢占CPU); 请给出这四个作业的执行时间序列,并计算出平均周转时间及平均带权周转时间;
Image text

10、系 统 调 用
系统调用,是用户在程序中调用操作系统所提供的一些子功能(程序接口)。
这个指令还将系统转入管态
系统调用是操作系统提供给编程人员的唯一接口

操作系统-文件管理

1、 文件与文件系统
(1)文件的概念
文件指的是一组带标识的在逻辑上有完整意义的信息项(构成文件内容的基本单元)的序列,或者是相关联纪录的集合。文件存放在磁盘或磁带等存
储介质上。
文件是一个抽象机制,它提供了一种把信息保存在存储介质上,而且便于以后存取的方法,用户不必关心实现细节

(2)文件系统
• 是操作系统中统一管理信息资源的一种软件,管理文件的存储、检索、更新,提供安全可靠的共享和保护手段,并且方便用户使用

1.1 文件系统的功能:
(1)统一管理文件的存储空间,实施存储空间的分配与回收;
(2)为用户提供可见的文件逻辑结构,实现文件的按名存取;名字空间 → → → 存储空间
(3)对文件及文件目录的管理,这是文件系统最基本的功能,包括文件(目录)的建立、删除、读写等;
(4)提供操作系统与用户的接口(提供对文件的操作命令:信息存取、加工等)。

1.2 文件的分类
(1)按文件性质和用途分类
• 系统文件:
有关OS及有关系统所组成文件,不能直接访问
• 用户文件:
用户委托系统保存的文件
• 库文件:
标准子程序及常用应用程序组成文件,允许用户使用但不能修改

2、文件的结构和存取方式
(1)流式文件(无结构文件):
• 构成文件的基本单位是字符,文件是有逻辑意义的、无结构的一串字符的集合。
• 管理简单,操作方便,但查找比较麻烦,对基本信息单位操作不多的文件比较适合用字符流的无结构方式,比如源程序文件。
(2)记录式文件(有结构文件):
• 文件是由若干个记录组成,每个记录有一个键,可按键进行查找,每条记录有其内部结构。
• 方便用户进行各种操作比如添加、删除、修改、查找等

2.1 文件的存取方法
常用存取方法:
①顺序存取。
顺序存取是按照文件的逻辑地址顺序存取。比如当前读取的记录为Ri,则下一条读取的记录被自动确定为Ri的下一个相邻的记录Ri+1。
②随机存取。
允许用户根据记录的编号来存取文件的任一记录。前两种方法用于一般OS,下面方法适用数据库系统。
③按键存取。

2.2 文件的物理结构
– 文件系统中,文件存储设备通常分块,每块1k或者512字节或其他大小,与此对应,文件信息也被划分为与物理块大小相等的逻辑块
(1)连续结构(顺序)
– 文件的信息存放在若干连续的物理块中。系统为每个文件都建立一个文件控制块FCB。对于顺序文件,只要从FCB中得到文件的第一个块的物理块
号和文件长度,便可确定位置。
Image text
– 优点: 简单
• 支持顺序存取和随机存取
• 顺序存取速度快
• 所需的磁盘寻道次数和寻道时间最少

– 缺点:
• 不利于文件动态增长重新分配和移动
• 不利于文件插入和删除(大量移动)
• 外部碎片问题

(2)链接结构
– 一个文件的信息存放在若干不连续的物理块中,各块之间通过指针连接,前一个物理块指向下一个物理块
Image text
– 优点:
• 提高了磁盘空间利用率,不存在外部碎片问题
• 有利于文件插入和删除
• 有利于文件动态扩充

– 缺点:
• 存取速度慢,不适于随机存取
• 可靠性问题,如指针出错
• 更多的寻道次数和寻道时间
• 链接指针占用一定的空间

(3)索引结构
• 一个文件的信息存放在若干不连续物理块中,系统为每个文件建立一个专用数据结构——索引表,并将这些块的块号存放在一个索引表中。
• 一个索引表就是磁盘块地址数组,其中第i个条目指向文件的第i块
Image text

– 优点:
• 保持了链接结构的优点,又解决了其缺点:既能顺序存取,又能随机存取
• 满足了文件动态增长、插入删除的要求
• 能充分利用外存空间
– 缺点:
• 较多的寻道次数和寻道时间
• 索引表本身带来了系统开销
• 存取文件时至少访问存储器两次,一次是获得地址,一次是对物理块的访问。为了提高速度,将索引表放入内存,减少访问磁盘次数

文件的物理结构:
UNIX文件系统采用的是多级索引结构。每个文件的索引表为13个索引项,每项2个字节。最前面10项直接登记存放文件信息的物理块号(直接寻址)

如果文件大于10块,则利用第11项指向一个物理块,该块中最多可放256个文件物理块的块号(一次间接寻址)。对于更大的文件还可利用第12和第13项作为二次和三次间接寻址

– UNIX中采用了三级索引结构后,文件最大可达16兆个物理块
Image text

文件存储介质:
(1)物理块
在文件系统中,文件的存储设备常常划分为若干大小相等的物理块。同时也将文件信息划分成相同大小的逻辑块,所有块统一编号以块为单位进行信息的存储、传输、分配

(2)磁带
永久保存大容量数据
– 顺序存取设备:前面的物理块被存取访问之后,才能存取后续的物理块的内容
– 存取速度较慢,主要用于后备存储,或存储不经常用的信息

(3)磁盘
– 直接(随机)存取设备:
• 存取磁盘上任一物理块的时间不依赖于该物理块所处的位置

完成过程由三个动作组成:
• 寻道(时间):磁头移动定位到指定磁道
• 旋转延迟(时间):等待指定扇区从磁头下旋转经过
• 数据传输(时间):数据在磁盘与内存之间的实际传输

3、文件目录
3.1 基本概念
– 文件控制块(FCB):
• 文件控制块是操作系统为管理文件而设置的数据结构,存放了为管理文件所需的所有有关信息。
• 文件控制块是文件存在的标志

– 文件目录:
• 把所有的FCB组织在一起,就构成了文件目录,即文件控制块的有序集合。
– 目录项:
• 构成文件目录的项目(目录项就是FCB)。
– 目录文件:
• 为了实现对文件目录的管理,通常将文件目录以文件的形式保存在外存,这个文件就叫目录文件

目录结构:
(1)一级目录结构
– 为所有文件建立一个目录文件(组成一线性表)
– 优点:
• 简单,易实现

– 缺点:
• 限制了用户对文件的命名
• 文件平均检索时间长

(2)二级目录结构
– 为解决一级目录文件目录命名冲突,并提高对目录文件检索速度而改进。
– 目录分为两级:
• 一级称为主文件目录(MFD),给出用户名,用户子目录所在的物理位置;
• 二级称为用户文件目录(UFD),给出该用户所有文件的FCB
– 使用二级目录可以解决文件重名和文件共享问题,并可以获得较高的搜索速度。

– 优点:解决了文件的重名问题和共享问题
用户名|文件名
查找时间降低
– 缺点:增加了系统开销
Image text

(3)多级目录结构(树型目录)
– 优点:
• 层次结构清晰,便于管理和保护;有利于文件分类;解决重名问题;提高文件检索速度;能进行存取权限的控制 。
– 缺点:
• 查找一个文件按路径名逐层检查,由于每个文件都放在外存,多次访盘影响速度。

(4)文件目录改进
– 为加快目录检索可采用目录项分解法:把FCB分成两部分:
– 符号目录顶
文件名,文件号
– 基本目录项
除文件名外的所有项目

3.2 文 件 存 储 空 间 管 理:
– 辅存空间分配常采用以下两种办法。
– 连续分配:
• 文件被存放在辅存空间连续存储区中,在建立文件时,用户必须给出文件大小;
• 然后,查找到能满足的连续存储区供使用;否则文件不能建立。
– 连续分配的优点是文件查找速度快,管理较为简单,但为了获得足够大的连续存储区。需定时进行‘碎片’收集。因而,不适宜于文件频繁进行动态扩充和缩小的情况,用户事先不知道文件长度也无法进行分配。

非连续分配:
• 一种非连续分配方法是以块(或扇区)为单位,按文件动态要求分配给它若干扇区,这些扇区不一定要连续。
• 另一种非连续分配方法是以簇为单位,簇是由若干个连续扇区组成的分配单位;实质上是连续分配和非连续分配的结合。各个簇可以用链指针、索
引表,位示图来管理。非连续分配的优点是辅存空间管理效率高,访问文件执行速度快,特别是以簇为单位的分配方法已被广泛使用。

3.3 文件系统的使用
– 在文件系统中提供对文件的各种操作,形式分别为:系统调用或命令。

  1. 主要操作
    – 提供设置和修改对用户文件存取权限
    – 提供建立、修改、改变、删除目录的服务
    – 提供文件共享,设置访问路径的服务
    – 提供创建、打开、读、写、关闭、撤消文件等服务
    – 文件系统维护

  2. 操作介绍
    (1)建立文件
    实质是建立文件的FCB,并建立必要的存储空间,分配空FCB,根据提供的参数及需要填写有关内容,返回一个文件描述。
    目的:建立系统与文件的联系
    (2)打开文件
    使用文件的第一步,任何一个文件使用前都要先打开,即把FCB送到内存

3.4 文件系统的可靠性
– 可靠性:
• 系统抵抗和预防各种物理性破坏和人为性破坏的能力。
– 备份
• 通过转储操作,形成文件或文件系统的多个副本

3.5 磁盘冗余阵列 RAID
– RAID(Reundant Array of Independent Disks)
– 它是利用一台磁盘阵列控制器统一管理和控制一组磁盘驱动器。
– 其策略是:用一组较小容量的、独立的、可并行工作的磁盘驱动器组成阵列来代替单一的大容量磁盘,独立的I/O请求能被并行地从多个磁盘驱动器同时存取数据,从而,改进了I/O性能和系统可靠性

3.6 文 件 系 统 的 性 能
(1) 磁盘调度
当多个访盘请求在等待时,采用一定的策略,对这些请求的服务顺序调整安排,旨在降低平均磁盘服务时间,达到公平、高效
– 公平:一个I/O请求在有限时间内满足
– 高效:减少设备机械运动所带来的时间浪费

(2)磁盘调度考虑的问题:
一次访盘时间 = 寻道时间+旋转延迟时间+存取时间
– 减少寻道时间
– 减少延迟时间

(3)磁盘调度算法
1) 先来先服务:按访问请求到达的先后次序服务
例:假设磁盘访问序列:98,183,37,122,14,124,65,67,读写头起始位置:53,安排磁头服务序列,计算磁头移动总距离(道数)
Image text
– 优点:简单,公平;
– 缺点:效率不高,相临两次请求可能会造成最内到
最外的柱面寻道,使磁头反复移动,增加了服务时间,
对机械也不利。

2)最 短 寻 道 时 间 优 先
– 最短寻道时间优先:优先选择距当前磁头最近的访问请求进行服务,主要考虑寻道优先
– 优点:改善了磁盘平均服务时间;
– 缺点:造成某些访问请求长期等待得不到服务

例:假设磁盘访问序列:98,183,37,122,14,124,65,67,读写头起始位置:53,安排磁头服务序列,计算磁头移动总距离(道数)
Image text
– 采用最短寻道时间优先调度下的总移动道数:236
Image text

3)电梯算法
克服了最短寻道优先的缺点,既考虑了距离,同时又考虑了方向。
– 当设备无访问请求时,磁头不动;当有访问请求时,磁头按一个方向移动,在移动过程中对遇到的访问请求进行服务,然后判断该方向上是否还有访问请求,如果有则继续扫描;否则改变移动方向,并为经过的访问请求服务,如此反复
Image text

4)单 向 扫 描 调 度 算 法
– 总是从0号磁道开始向里扫描
– 按照各自所要访问的磁道位置的次序去选择访问者
– 移动臂到达最后个一个磁道后,立即带动读写磁头快速返回到0号磁道
– 返回时不为任何的等待访问者服务
– 返回后可再次进行扫描

操作系统-存储管理

1、存 储 概 述
由于任何程序、数据必须占用主存空间后才能执行,因此存储管理直接影响系统的性能。

– 主存储空间一般分为两部分:
• 一部分是系统区,存放操作系统常驻内存部分;
• 另一部分是用户区,存放用户的程序和数据等

存储管理主要是对用户区域进行管理,当然也包括对辅存的管理。目的是要尽可能地方便用户使用和提高主存储器的效率

存储层次结构
Image text

存储管理应具有的四大功能:
1 ) 内存空间管理
– 记录每个内存单元的使用情况
– 内存分配
– 位示图:用一位(bit)表示一个空闲页面(0:空闲,1:占用)

2)地址变换(重定位,地址映射)
我们把用户编程时使用的地址称为逻辑地址,把程序在内存中的实际地址称为物理地址。为了保证程序的正确运行,必须把程序和数据的逻
辑地址转换为物理地址,这一工作称为地址转换或重定位。

– 静态地址转换
• 当用户程序被装入内存时,一次性实现逻辑地址到物理地址的转换,以后不再转换。
• 一般在装入内存时由重定位装入程序完成。

– 动态地址转换
• 在程序运行过程中要访问数据时再进行地址变换(即在逐条指令执行时完成地址映射。一般为了提高效率,此工作由硬件地址映射机制来
完成。硬件支持,软硬件结合完成)。
• 一对寄存器(VR,BR)

静态重定位:
– 优点:无须硬件支持;
– 缺点:
(1)不支持虚拟存储,原因是执行期间程序不能移动,因而不能实现重新分配内存,而虚拟存储则将部分程序装入内存。
(2)不能共享。因为每个程序必须占用连续的内存空间,因此很难做到。

动态重定位:
– 过程:
(1)设置基址寄存器BR,虚拟地址寄存器VR
(2)将程序首址送入BR
(3)程序执行时,将需要访问的虚址送入VR
(4)将BR和VR相加,得到实际访问的地址。

– 优点:
(1)可以对内存进行非连续分配,对于不同程序段设置不同的BR即可。
(2)提供了实现虚拟存储的基础,动态重定位可以部分地、动态地分配内存。
(3)有利于共享。
Image text

3)内 存 扩 充
内存的容量是受实际的存储单元限制的,而运行的程序又不受内存大小的限制,这就需要有效的存储管理技术来实现内存的逻辑扩充,这种扩充不是增加实际的存储单元,而是通过虚拟存储技术、覆盖技术、交换技术等技术来实现的。

4)内存共享和保护
– 为了更有效地使用内存空间,要求共享内存
– 为多个程序共享内存提供保障,使在内存中的各道程序,只能访问它自己的区域,避免各道程序间相互干扰,特别是当一道程序发生错误时,不致于影响其他程序的运行
– 保护过程—-防止地址越界
– 保护过程—-防止操作越权

2、存储管理的一些技术
2.1 覆盖(overlay)
– 引入:其目标是在较小的可用内存中运行较大的程序。常用于多道程序系统,与分区存储管理配合使用
– 引入:其目标是在较小的可用内存中运行较大的程序。常用于多道程序系统,与分区存储管理配合使用
• 不存在调用关系的模块不必同时装入到内存,从而可以相互覆盖。(即不同时用的模块可共用一个分区)
Image text

2.2 交换(swapping)
– 引入:多个程序并发执行,可以将暂时不能执行的程序送到外存中,从而获得空闲内存空间来装入新程序,或读入保存在外存中而目前到达就绪状态的进程。交换单位为整个进程的地址空间。
• 程序暂时不能执行的可能原因:处于阻塞状态,低优先级(确保高优先级程序执行);

– 原理:暂停执行内存中的进程,将整个进程的地址空间保存到外存的交换区中(换出swa pout)而将外存中由阻塞变为就绪的进程的地址空间读
入到内存中,并将该进程送到就绪队列(换入swap in)。

– 优点:增加并发运行的程序数目,并且给用户提供适当的响应时间;编写程序时不影响程序结构,对用户透明。
– 缺点:对换入和换出的控制增加处理机开销;程序整个地址空间都进行传送,没有考虑执行过程中地址访问的统计特性。
– 覆盖技术和交换技术的发展导致了虚拟存储技术的出现

2.3 虚拟存储技术
虚存:把内存与外存有机的结合起来使用,从而得到一个容量很大的“内存”,这就是虚存
– 实现思想:当进程运行时,先将一部分程序装入内存,另一部分暂时留在外存,当要执行的指令不在内存时,由系统自动完成将它们从外存调入内存工作
– 目的:提高内存利用率

虚存的物质基础:
系统要有足够大的外存;
– 要有一定容量的内存来存放运行作业的部分程序
– 要有动态地址转换机构,实现逻辑地址转换;

特征:
– 虚拟性:指逻辑上扩大了内存容量,使用户看到的内存空间大于实际空间;
– 离散性:指内存在分配时采用的是离散分配的方式,目的是为了避免内存空间的浪费;
– 多次性:指一个作业不是全部一次性装入内存,而是在需要时装入部分;
– 交换性:指在一个进程运行期间,将暂不使用的程序和数据从内存调至外存,被调出的程序和数据在需要时再调入内存中。
– 总容量不超过物理内存和外存交换区容量之和

3、分区存储管理
– 把内存分为一些大小相等或不等的分区(partition),每个应用进程占用一个分区。操作系统占用其中一个分区。
– 问题:可能存在内碎片和外碎片。
• 内碎片:占用分区之内未被利用的空间
• 外碎片:占用分区之间难以利用的空闲分区(通常是小空闲分区)。

3.1 固定分区(fixed partitioning)
– 把内存划分为若干个固定大小的连续分区。每个分区装一个且只能装一个作业。
• 分区大小相等:
• 分区大小不等:多个小分区、适量的中等分区、少量的大分区。根据程序的大小,分配当前空闲的、适当大小的分区。
Image text
优点:易于实现,开销小。
– 缺点:
• 内碎片造成浪费
• 分区总数固定,限制了并发执行的程序数目。

3.2 动态分区(dynamic partitioning)
基本思想:
• 作业装入时,根据作业的需求和内存空间的使用情况来决定是否分配
• 若有足够的空间,则按需要分割一部分分区给该进程;否则令其等待内存空间

Image text

– 分区分配算法:寻找某个空闲分区,其大小需大于或等于程序的要求。若是大于要求,则将该分区分割成两个分区,其中一个分区为要求的大小并标记为“占用”,而另一个分区为余下部分并标记为“空闲”。分区的先后次序通常是从内存低端到高端。
– 分区释放算法:需要将相邻的空闲分区合并成一个空闲分区。

1
2
3
4
1)最先适应法(first-fit):按分区的先后次序,从头查找,找到符合要求的第一个分区
2)下次适应法(next-fit):按分区的先后次序,从上次分配的分区起查找(到最后分区时再回到开头),找到符合要求的第一个分区
3)最佳适应法(best-fit):找到其大小与要求相差最小的空闲分区
4)最坏适应法(worst-fit):找到最大的空闲分区

3.3 碎 片 问 题
– 紧凑技术:通过在内存移动程序,将所有小的空闲区域合并为大的空闲区域
(内存紧凑(compaction):将各个占用分区向内存一端移动。使各个空闲分区聚集在另一端,然后将各个空闲分区合并成为一个空闲分区)

3.4 分区的回收及保护

1
2
3
4
1)该空闲区的上下相邻分区都是空闲区。将三个空闲区合并,合并后的起始地址为上空闲区的起始地址,修改可用表或自由链(取消下,修改上)
2)该空闲区的上相邻区是空闲区。与上相邻区合并,合并后的起始地址为上空闲区的起始地址,修改可用表或自由链(修改上的大小)。
3)该空闲区的下相邻区是空闲区。与下相邻区合并,合并后的起始地址为释放区的起始地址,修改可用表或自由链(修改下的始址和大小)。
4)该空闲区不与其他空闲区相邻,作为一个新空闲区插入可用表或自由链

4、页式存储管理
4.1 基本思想:
– 用户程序划分
• 把用户程序按逻辑页划分成大小相等的部分,称为页。从0开始编制页号,页内地址是相对于0编址
– 逻辑地址
Image text
内存空间
• 按页的大小划分为大小相等的区域,称为内存块(物理页面)
– 内存分配
• 以页为单位进行分配,并按作业的页数多少来分配。
• 逻辑上相邻的页,物理上不一定相邻

Image text

4.2 管理
– 页表:系统为每个进程建立一个页表,页表给出逻辑页号和具体内存块号相应的关系
Image text

地址映射机制:
Image text

地址映射机制(含快表):
Image text

4.3 静态页式管理
– 将程序的逻辑地址空间和物理内存划分为固定大小的页或页面(Page or Page frame),程序加载时,分配其所需的所有页,这些页不必连续
静态页式管理的地址变换:
– 指令所给出地址分为两部分:逻辑页号,页内偏移地址->查进程页表,得物理页号->物理地址

– 优点:
• 没有外碎片,每个内碎片不超过页大小(因为页面大小固定 要多少有多少)。
• 一个程序不必连续存放。
• 便于改变程序占用空间的大小。即随着程序运行
而动态生成的数据增多,地址空间可相应增长。
– 缺点:
• 程序全部装入内存,受到内存可用页面数的限制。

4.4 动态(请求)页式管理
– 在进程开始运行之前,不是装入全部页面,而是装入部分页面,之后根据进程运行的需要,动态装入其它页面;当内存空间已满,而又需要装入新的页面时,则根据某种算法淘汰某个页面,以便装入新的页面。

页表表项:
– 页号、驻留位、内存块号、外存始址、访问位、修改位
– 驻留位(中断位):表示该页是在内存还是在外存
– 访问位:根据访问位来决定淘汰哪页(由不同的算法决定)
– 修改位:查看此页是否在内存中被修改过
Image text

缺页中断处理:
在地址映射过程中,在页表中发现所要访问的页不在内存,则产生缺页中断。操作系统接到此中断信号后,就调出缺页中断处理程序,根据页表中给出的外存地址,将该页调入内存,使作业继续运行下去
– 如果内存中有空闲块,则分配一页,将新调入页装入内存,并修改页表中相应表项
– 若此时内存中没有空闲块,则要淘汰某页,若该页在内存期间被修改过,则要将其写回外存
Image text

页面置换算法:

1
2
3
4
5
– 随机置换算法
– 先进先出算法(FIFO)
– 最近最久未使用算法(LRU, Least Recently Used)
– 时钟页面替换算法(Clock Policy)
– 最佳置换算法(OPT, optimal)

1)先进先出算法( F I F O )
– 选择建立最早的页面被置换,性能较差,较早调入的页往往是经常被访问的页,这些页在FIFO算法下被反复调入和调出。
– 并且有Belady现象。
– Belady现象:采用FIFO算法时,如果对一个进程未分配它所要求的全部页面,有时就会出现分配的页面数增多,缺页率反而提高的异常现象。
Image text

2)最近最久未使用算法( L R U )
– 该算法淘汰的页面是在最近一段时间里较久未被访问的那一页。
Image text

3)最佳算法(OPT, optimal)
– 选择“未来不再使用的”或“在离当前最远位置上出现的”页面被置换,这是一种理想情况。
Image text

影 响 缺 页 次 数 的 因 素:
(1) 分配给进程的物理页面数
(2) 页面本身的大小
(3) 程序的编制方法
(4) 页面淘汰算法

4.5 页式管理的优缺点
– 相对于分区管理而言,静态页式有效的解决了外部碎片的问题(当然有少量的内部碎片);
– 但是,静态页式要求全部装入,不支持虚拟存储,因而有了请求(动态)页式,允许部分装入;
– 显然地,请求页式更能有效利用有限的内存页面,不过,这种方式需要有效解决缺页率的问题,尤其是页面置换的问题;

5、段式存储管理
5.1 基本原理
用户程序划分
•按程序自身的逻辑关系划分为若干个程序段,每个程序段都有一个段名,且有一个段号。段号从0开始,每一段也从0开始编址,段内地址是连续的

– 逻辑地址(二维地址)
Image text

基本原理:
Image text

内存划分方式:
– 内存划分
内存空间被动态的划分为若干个长度不相同的区域,称为物理段,每个物理段由起始地址和长度确定
– 内存分配
以段为单位分配内存,每一个段在内存中占据连续空间(内存随机分割,需要多少分配多少),但各段之间可以不连续存放

段式管理:
(1) 段表:每进程一个
(2) 空闲表:系统一个(管理同动态分区)array of (addr,size)
Image text

内存分配:
(1)有足够空闲区(同动态分区)
最先适应
最佳适应
最坏适应
(2)没有足够空闲区(同请求页式)
FIFO,LRU,如果淘汰一段不能满足要求,就要进行多次淘汰

地址映射:
Image text

页式管理与段式管理的比较:
– 分页是出于系统管理的需要,分段是出于用户应用的需要
– 页大小是系统固定的,而段大小则通常不固定。
– 逻辑地址表示:
• 分页是一维的,各个模块在链接时必须组织成同一个地址空间;
• 分段是二维的,各个模块在链接时可以每个段组织成一个地址空间。
– 通常段比页大,因而段表比页表短,可以缩短查找时间,提高访问速度。

6、段页式存储管理
– 分段结构具有逻辑上清晰的优点,但它的一个致命弱点是每个段必须占据主存储器的连续区域,为了克服这个缺点,可兼用分段和分页的方法,构成段页式存储管理。
– 每个作业仍按逻辑分段,把每个段再分成若干个页面,每一段不必占据连续的主存空间,可把它按页存放在不连续的主存块中。

6.1 基 本 思 想
– 用户程序划分:按段式划分(对用户来讲,按段的逻辑关系进行划分;对系统讲,按页划分每一段)
内存划分:按页式存储管理方案
内存分配:以页为单位进行分配
Image text

段 表 和 页 表:
(1)在段页式系统中,每个分段又被分成若干个固定大小的页面,那么每个段又必须建立一张页表把段中的虚页变换成内存中的实际页面。显然,与页式管理时相同,页表中也要有相应的实现缺页中断处理和页面保护等功能表项。
(2)每个段有一个页表,段表中应有专项指出该段所对应页表的页表始址和页表长度

段 表 、 页 表 与 内 存 关 系:
Image text

段页式地址变换:
注:在段页式系统中,为了获取一条指令或数据,需三次访问内存。
• 第一次访问,是访问内存中的段表,从中取得页表始址
• 第二次访问,是访问内存中的页表,从中取得物理块号,并将该块号与页内地址一起形成指令或数据的物理地址
• 第三次访问,才是真正从第二次访问的地址中,取得指令和数据

抖 动:
– 虚存中,页面在内存与外存之间频繁调度,以至于调度页面所需时间比进程实际运行的时间还多,此时系统效率急剧下降,甚至导致系统崩溃。这种现象称为颠簸或抖动
– 原因:
• 页面淘汰算法不合理
• 分配给进程的物理页面数

一点总结:
Image text
Image text
Image text

操作系统-处理机管理

1、调度的性能准则:
– (平均)周转时间:作业从提交到完成的时间(用户角度)
周转时间:Ti=Tc-Ts
Tc作业完成时刻;
Ts作业进入系统时刻

平均周转时间:(T1+T2+…+Tn)/n

– (平均)带权周转时间:周转时间 / CPU运行时间(用户角度)
带权周转时间:
Image text

平均带权周转时间:
Image text

– 吞吐量:单位时间内所完成的作业数
– 处理机利用率:CPU运行时间 / 总时间
– 各种设备的均衡利用:如CPU繁忙的作业和I/O繁忙的作业搭配

调度的类型:
– 作业:又称为”宏观调度”、”高级调度”。从用户工作流程的角度,一次提交的若干个流程。
– 内外存交换:又称为”中级调度”。从存储器资源的角度。将进程的部分或全部换出到外存上,将当前所需部分换入到内存。指令和数据必须在内存里才能被CPU直接访问。
– 进程:又称为”微观调度”

“低级调度”。从CPU资源
的角度,执行的单位。时间上通常是毫秒。因为执行频
繁,要求在实现时达到高效率

2、处理机调度算法
2.1 先 来 先 服 务(非抢占方式)
按先后顺序进行调度

2.2 短 作 业 优 先(非抢占方式)
又称为“短进程优先”SPN(Shortest Process
Next);这是对FCFS算法的改进,其目标是减少平均周
转时间

– 优点:
• 比FCFS改善平均周转时间和平均带权周转时间,缩短作业
的等待时间;
• 提高系统的吞吐量;
– 缺点:
• 对长作业非常不利,可能长时间得不到执行;
• 未能依据作业的紧迫程度来划分执行的优先级;
• 难以准确估计作业(进程)的执行时间,从而影响调度性

2.3 最短剩余时间优先(抢占式)
允许比当前进程剩余时间更短的进程来抢占

2.4 最高响应比优先(非抢占方式)
• 响应比R = (等待时间 + 要求执行时间) / 要求执行时间
• 是FCFS和SJF的折衷

2.5 时间片轮转(Round Robin)算法
本算法主要用于微观调度,说明怎样并发运行,即切换的方式;设计目标是提高资源利用率。
其基本思路是通过时间片轮转,提高进程并发性和响应时间特性,从而提高资源利用率;

2.6 多级队列算法(Multiple-level Queue)
各队列的不同处理:不同队列可有不同的优先级、时间片长度、调度策略等

2.7 优先级算法(Priority Scheduling)(可分成抢先式和非抢先式)
1)静态优先级
– 创建进程时就确定,直到进程终止前都不改变。通常是一个整数

2)动态优先级
在就绪队列中,等待时间延长则优先级提高,从而使优先级较低的进程在等待足够的时间后,其优先级提高到可被调度执行;
– 进程每执行一个时间片,就降低其优先级,从而一个进程持续执行时,其优先级降低到出让CPU

注意:
I/O型进程:让其进入最高优先级队列,以及时响应I/O交互。通常执行一个小时间片,要求可处理完一次I/O请求的数据,然后转入到阻塞队列。
– 计算型进程:每次都执行完时间片,进入更低级队列。最终采用最大时间片来执行,减少调度次数。

java-map遍历

Map集合:即 接口Map<K,V>

map集合的两种取出方式:
1.Set keyset: 将map中所有的键存入到set集合(即将所有的key值存入到set中), 因为Set具备迭代器,可以进行迭代遍历。 所有可以迭代方式取出所有的链,再根据get方法。获取每一个键对应的值。

    Map 集合的取出原理: 将map集合转成set集合。 再通过迭代器取出
2. set<Map.Entry<k,v>>  entrySet: 将map集合中的映射关系(即键值对的方式存入到set中)存入到set集合中,而这个关系的数据类型就是:map.entry

jvm类加载3

Java类加载过程(详细)

1、加载:查找并加载类的二进制数据(读入虚拟机)
2、连接:
(1)验证:确保被加载的类的正确性
(2)准备:Java虚拟机为类的静态变量分配内存,并将其初始化为默认值(例如,静态的整型值初始化为默认值为0,但是第三个阶段初始化阶段由程序员主观显示的赋值才是真正的初始化,这里Java虚拟机事先设置默认初值只是为了防止空异常)
(3)解析:在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用。
3、初始化:为类的静态变量赋正确的初始值(程序员主观初始化赋值,例static int a=1)
4、类实例化:
(1)为新的对象分配内存
(2)为实例变量赋默认值
(3)为实例变量赋正确的初始值
(4)Java编译器为它编译的每一个类文件都至少生成一个实例初始化方法,在Java的class文件中,这个实例初始化方法被称为“”。针对源代码中的每一个类的构造方法,Java编译器都产生一个“”方法。
5、卸载:从内存中销毁类

类的加载

1、类的加载的最终产品是位于内存中的class对象
2、class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区的数据结构的接口

3、有两种类型的类加载器
1)Java虚拟机自带的加载器
(1)根类加载器(BootStrap 启动类加载器),没有父类加载器,由C++实现,不是ClassLoader的子类,内建于Jvm中,是jvm的一部分,会加载java.lang.ClassLoad一起其他的Java平台类,当jvm启动时,启动类加载器会加载扩展类加载器和系统类加载器。(除启动类加载器之外,其他类加载器都是由Java实现。启动类加载器是特定于平台的及其指令,负责开启震哥哥加载过程),启动类加载器还负责加载提供jre正常运行所需要的基本组件,包括java.util和java.lang中的包等等。

(2)扩展类加载器(Extension),父类加载器是根类加载器(需要将class文件打包成jar包,才能加载)
(3)系统(应用)类加载器(System),父类加载器是扩展类加载器

注:除根类加载器没有父类加载器外,其他加载器都有且仅有一个父类加载器。当Java程序请求加载器时,首先会去请求其父类加载器,若父类加载器能完成加载,则由父类加载器完成,若不能,再由子类加载器完成,这种模式称为双亲加载机制。

2)用户自定义的类加载器
(1)java.lang.ClassLoader的子类
(2)用户可以定制类的加载方式

注:所有的用户自定义加载器都继承自CLassLoad类
注:类加载器并不需要等到某个类被“首次主动使用”时再加载它

4、jvm规范允许类加载器再预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)

5、如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

类的验证

1、类被加载后,进入连接阶段。连接就是将已经读入到内存中的类的二进制数据合并到虚拟机的运行时环境中去。

2、类的验证的主要内容
(1)类文件的结构检查
(2)语义检查
(3)字节码验证
(4)二进制兼容性的验证

类的初始化

步骤:
1)假如类还没有加载和连接,那么就先加载和连接
2)假如类存在父类,并且父类还没有初始化,那就先初始化直接父类(不适用于接口)
3)假如类中存在初始化语句,那就依次执行这些初始化并语句

类加载器的双亲委托机制

1、在双亲委托机制中,各个加载器按照父子关系形成了逻辑上的树形结构(物理上没有关系),除了根类加载器之外,其余的类加载器都有且仅有一个父加载器
2、当Java程序请求加载器时,首先会去请求其父类加载器,若父类加载器能完成加载,则由父类加载器完成,若不能,再由子类加载器完成,这种模式称为双亲委托机制。(若父类再有父类,就继续委托给父类,层层往上委托一直到根类加载器)
Image text
3、定义类加载器:能够成功加载你所要加载类的加载器
4、初始化加载器:所有能成功返回Class对象引用的类加载器(包括定义类加载器)

例子:

1
2
3
4
5
6
7
8
9
10
11
package jvm;

public class ClassLoadTest {

public static void main(String[] args) throws ClassNotFoundException {
//得到java.lang.String类
Class<?> clazz=Class.forName("java.lang.String");
//得到String类的类加载器
System.out.println(clazz.getClassLoader());
}
}

输出:

1
null

若一个类的类加载器是根加载器,则用null来表示,因为根加载器是由C++实现的(不过不同版本jvm有不同的表示)。
由输出知:String类的加载器是根加载器

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package jvm;

public class ClassLoadTest {

public static void main(String[] args) throws ClassNotFoundException {
//得到LittleTest对类
Class<?> clazz=Class.forName("jvm.LittleTest");
//得到LittleTest类的类加载器
System.out.println(clazz.getClassLoader());
}
}
//自定义内部类LittleTest
class LittleTest{

}

输出:

1
2
sun.misc.Launcher$AppClassLoader@2a139a55
//AppClassLoader应用类加载器

由输出知:内部类LittleTest的类加载器为应用(系统)类加载器

注:对于数组类型,它的类加载器是由Java虚拟机在运行期动态创建的,Java虚拟机返回的类加载器类型与数组中元素的类加载器类型一样(例如一个string类型的数组,String的类加载器是根类加载器,那么Java虚拟机为数组类型创建的类加载器也是根类加载器,),如果元素类型为原生类型,则没有类加载器(例如int类型)。

5、好处:
1)可以确保Java核心类库的安全:所有的Java应用都至少会引用java.lang.Object,也就是说说运行期java.lang.Object这个类会被加载到Java虚拟机中,如果这个加载过程是由Java自定义类加载器所完成的,那么很可能在jvm中存在多个不同版本的java.lang.Object类,而这些类是不兼容的,相互不可见(命名空间)
2)借助于双亲委托机制,Java核心库中的类加载工具都是由启动类加载器来统一完成的,从而确保了Java应用所使用的都是同一个版本的Java核心库,他们之间相互兼容。
3)可以确保Java核心库所提供的类不会被自定义的类所替代
3)不同的类加载器可以为相同名称的类创建额外的命名空间。相同名称的类可以并存在jvm中,只需要用不同的命名空间加载他们即可。不同类加载器所加载的类之间不兼容,相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间。

命名空间

1、每个类加载器都有自己的命名空间,命名空间由该加载器及其所有的父类加载器所加载的类组成
2、在同一个命名空间中不会出现类的完整名字(包括类的包名)相同的两个类,所以一个类只会被加载一次(再次加载价将返回同第一次已经加载的结果)
3、在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类,完整名字(包括类的包名)相同的两个类可以分别的不同命名空间被加载
4、同一个命名空间内的类是相互可见的
5、子加载器的命名空间包括所有父类的命名空间,因此子加载器加载的类能看见父加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。
6、由父加载器加载的类不能看见子加载器加载的类
7、如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类互不可见
8、在运行期,一个Java是由该类的全类名和用于加载该类的定义类加载器所共同决定的。如果同样名字(全类名)的类由两个不同的加载器所加载,那么这些类就是不同的,即便.class文件的字节码完全一样,并且从相同位置加载亦如此。

注:子加载器所加载的类能够访问父加载器所加载的类,父加载器所加载的类无法访问子加载器所加载的类

类的卸载

1、当一个类被加载、连接、初始化之后,它的生命周期就开始了。当这个类的class对象不再被引用,即不可触及时,class对象就会结束生命周期,该类在方法区内的数据也会被卸载,从而结束该类的生命周期。
2、一个类何时结束生命周期,取决于代表它的class对象何时结束生命周期。
3、由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。
4、Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。Java虚拟机会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的class对象,因此这些class对象始终是可触及的。
5、有用户自定义的类加载器所加载的类是可以被卸载的。

forName()方法分析

JVM类加载2

JVM接口初始化规则

1、当一个接口再初始化时,并不要求其父接口都完成初始化,只有当真正使用到父接口时(如引用接口中定义的常量时)才会初始化。(对于类来说,子类初始化之前,它的父类要先全部初始化)
2、当一个类初始化时,并不要求先初始化其所实现的接口
注:1)只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
2)只有当程序访问的静态变量或静态方法确实在当前类或接口中定义时,才可以认为是对类或接口的主动使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package jvm;

public class InterFaceClass {

public static void main(String[] args) {
System.out.println(Childs.b);
}
}

interface Parents{
//接口中的属性就是static和final,这里显示定义为了更加清楚
public static int a=5;
}

interface Childs extends Parents{
public static final int b=6;
}

输出:

1
6

在上面这个例子中,
Childs.b调用子接口的属性b,属性b的定义为public static final int b=6,这是一个常量,在编译期间会被放入调用它的类的常量池中,这时,不会初始化Childs类,虽然子接口继承自父接口,但没有使用到父接口,所以没有主动使用父接口,不会初始化父接口,将子接口的class文件删除也不影响运行。(子接口初始化,父接口不一定初始化)

再看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package jvm;

import java.util.Random;

public class InterFaceClass {

public static void main(String[] args) {
System.out.println(Childs.b);
}
}

interface Parents{
public int a=5;
}

interface Childs extends Parents{
public static final int b=new Random().nextInt();
}

输出:

1
2024458112

在上面的例子中,Childs.b调用子接口的属性b,但b的定义为public static final int b=new Random().nextInt(),是一个随机数,要在运行期间才能确定,所以这个值不会被放到调用它的类的常量池中,这时会主动使用父接口,使父接口初始化,若把父接口的class文件删除,会报找不到对象异常。

上面的例子不是很准确的说明接口的初始化的规则,但是我太懒了,并且上面的例子对于Final关键字有更深的理解,所以我懒得去改了,请看下面这个比较确切的例子:

验证接口初始化规则第二点:

当一个实现接口的类在初始化时,并不要求其接口初始化
1、接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package jvm;

public class InterfaceInit {

public static void main(String[] args) {
//调用MyChilds的静态属性b
System.out.println(MyChilds.b);

}
}

interface MyGrandpa{
public static Thread thread=new Thread(){
//一个实例化代码块
{
System.out.println("MyGrandpa...........");
}
};
}

interface MyParents extends MyGrandpa{
public static Thread thread=new Thread(){
//一个实例化代码块
{
System.out.println("MyParents...........");
}
};
}

class MyChilds implements MyParents{
public static int b=5;
}

输出:

1
5

由输出可知:当初始化一个接口的实现类时,并不一定会初始化这个接口(接口初始化规则第二条)
1)MyChilds.b,在InterfaceInit类中调用MyChilds的静态属性b,主动使用MyChilds类并初始化它。
2)但是并没有主动使用它的接口MyParents,所以不会初始化MyParents类,也不会初始化MyParents的父类MyGrandpa

2、把接口改成类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package jvm;

public class InterfaceInit {

public static void main(String[] args) {
System.out.println(MyChilds.b);

}
}

class MyGrandpa{
public static Thread thread=new Thread(){
//一个实例化代码块
{
System.out.println("MyGrandpa...........");
}
};
}

class MyParents extends MyGrandpa{
public static Thread thread=new Thread(){
//一个实例化代码块
{
System.out.println("MyParents...........");
}
};
}

class MyChilds extends MyParents{
public static int b=5;
}

输出:

1
2
3
MyGrandpa...........
MyParents...........
5

由输出知:当初始化一个子类时,一定会先初始化它的父类
1)MyChilds.b,在InterfaceInit类中调用MyChilds的静态属性b,主动使用MyChilds类并初始化它。
2)由于MyChilds继承自MyParents类,所以会主动使用MyParents类,并初始化MyParents类
3)初始化MyParents类之前,由MyParents类继承自MyParents的父类MyGrandpa类,所以会主动使用MyGrandpa类,并且会初始化MyGrandpa类
4)在MyGrandpa类初始化阶段,它的静态属性初始化代码块中会打印”MyGrandpa………..”
5)MyGrandpa类初始化完之后,是MyParents类初始化,在MyParents类初始化阶段,它的静态属性初始化代码块中会打印”MyParents………..”

3、把定义的静态变量改成final static变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package jvm;

public class InterfaceInit {

public static void main(String[] args) {
System.out.println(MyChilds.b);

}
}

class MyGrandpa{
public static Thread thread=new Thread(){
//一个实例化代码块
{
System.out.println("MyGrandpa...........");
}
};
}

class MyParents extends MyGrandpa{
public static Thread thread=new Thread(){
//一个实例化代码块
{
System.out.println("MyParents...........");
}
};
}

class MyChilds extends MyParents{
//静态常量属性
public final static int b=5;
}

输出:

1
5

由输出可知:
1)MyChilds.b,在InterfaceInit类中调用MyChilds的静态常量属性b,主动使用MyChilds类
2)但是b是静态常量,在编译阶段将b放入它的调用类InterfaceInit的常量池中,之后属性b的定义类MyChilds与它的调用类InterfaceInit就没有关系了,不会初始化MyChilds,就不会主动使用MyChilds的父类MyParents且不会初始化。

验证接口初始化规则第一点:

当一个接口在初始化时,并不要求其父接口都完成初始化
例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package jvm;

public class InterfaceInit {

public static void main(String[] args) {
System.out.println(MyParents.thread);

}
}

interface MyGrandpa{
public static Thread thread=new Thread(){
//一个实例化代码块
{
System.out.println("MyGrandpa...........");
}
};
}

interface MyParents extends MyGrandpa{
public static Thread thread=new Thread(){
//一个实例化代码块
{
System.out.println("MyParents...........");
}
};
}

输出:

1
2
MyParents...........
Thread[Thread-0,5,main]//Thread的toString()方法

由输出可知:
1)MyParents.thread,在InterfaceInit类中调用接口MyParents的静态属性thread,主动使用MyParents接口并初始化它的静态属性thread,在静态属性thread的初始化代码块中,打印”MyParents………..”
2)虽然接口MyParents继承自接口MyGrandpa,但是并没有初始化父接口MyGrandpa

类加载的准备阶段与初始化阶段

类加载过程:

1
2
3
4
5
6
7
8
1)加载:查找并加载类的二进制数据(将类从磁盘文件写入内存)
2)连接:
1)验证:确保被加载的类的正确性
2)准备:为类的静态变量分配内存,并将其初始化为默认值(例如,静态的整型值初始化为默认值为0,由Java虚拟机来完成)
3)解析:把类中的符号引用转换为直接引用
3)初始化:为类的静态变量赋正确的初始值(程序员主观初始化赋值,例int a=1
4)使用
5)卸载:从内存中销毁类

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 package jvm;

public class InitClass {

public static void main(String[] args) {
//调用Singleton的静态方法来创建实例singleton
Singleton singleton=Singleton.getInstance();
System.out.println(singleton.count1);
System.out.println(singleton.count2);
}
}

class Singleton{
public static int count1;
public static int count2=0;

private static Singleton singleton=new Singleton();

private Singleton(){
count1++;
count2++;
}

public static Singleton getInstance() {
return singleton;
}
}

输出:

1
2
1
1

由输出知:
1)Singleton.getInstance()调用Singleton的静态方法来创建实例singleton,所以会主动使用类Singleton,则先加载类,然后连接
2)在连接的第二阶段准备这个过程中,Java虚拟机为类Singleton的静态变量count1,count2,singleton分配内存,并将其初始化为默认值:count1=0,count2=0,singleton=null
3)连接完了以后是初始化过程,执行代码中显示赋初值的部分,public static int count2=0,private static Singleton singleton=new Singleton()
4)new Singleton()会执行构造函数,在构造函数中执行 count1++,count2++。
5)所以最后输出count1的值为1,count2的值为1。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 package jvm;

public class InitClass {


public static void main(String[] args) {
Singleton singleton=Singleton.getInstance();
System.out.println("main函数中的值");
System.out.println(singleton.count1);
System.out.println(singleton.count2);
}
}

class Singleton{
public static int count1;

private static Singleton singleton=new Singleton();

private Singleton(){
count1++;
count2++;

System.out.println("构造函数内的值");
System.out.println(singleton.count1);
System.out.println(singleton.count2);
}

public static int count2=0;

public static Singleton getInstance() {
return singleton;
}
}

输出:

1
2
3
4
5
6
构造函数内的值
1
1
main函数中的值
1
0

由输出知:
1)Singleton.getInstance()调用Singleton的静态方法来创建实例singleton,所以会主动使用类Singleton,则先加载类,然后连接
2)在连接的第二阶段准备这个过程中,Java虚拟机为类Singleton的静态变量count1,count2,singleton分配内存,并将其初始化为默认值:count1=0,count2=0,singleton=null
3)连接完了以后是初始化过程,执行代码中显示赋初值的部分,private static Singleton singleton=new Singleton()
4)new Singleton()会执行构造函数,在构造函数中执行 count1++,count2++,构造函数中执行完两个语句后,count1的值为1,count2的值为1。
5)构造函数为属性singleton初始化之后,下一条初始化语句是public static int count2=0,这时将count2的值初始化为0
6)所以最后输出count1=1,count2=0

初始化进一步理解


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 package jvm;

public class FinalClass {
static {
System.out.println("FinalClass-------");
}
public static void main(String[] args) {
System.out.println(ChildTest.b);

}

}
class ParentTest{
static int a=3;

static {
System.out.println("ParentTest--------");
}
}
class ChildTest extends ParentTest{
static int b=4;

static {
System.out.println("ChildTest--------");
}
}

输出:

1
2
3
4
FinalClass------
ParentTest--------
ChildTest--------
4

注:static代码块也被看做是要初始化的变量,同static变量一样
有输出知:
1)由主动使用的条件第六条(Java虚拟机启动时被标明为启动类的类(Java Test,包含main()方法的类))知,含有main方法的类为启动类FinalClass,会主动使用并初始化,初始化阶段会执行static代码块打印”FinalClass——-“
2)初始化FinalClass类之后,执行main方法中的ChildTest.b代码
3)ChildTest.b调用ChildTest类的静态变量b,所以会主动使用ChildTest并初始化
4)在初始化ChildTest类之前会先初始化它的父类ParentTest,在父类ParentTest初始化时打印”ParentTest——–”
5)父类ParentTest初始化完之后,初始化ChildTest打印”ChildTest——–”
6)初始化ChildTest类完成之后,返回ChildTest类属性b的值到它的调用处main方法中并打印。

首次使用与再次使用类

类仅会被初始化一次,在首次主动使用它时初始化。
例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
 package jvm;

public class FinalClass {
static {
System.out.println("FinalClass-------");
}
public static void main(String[] args) {
System.out.println("----------");
System.out.println();

ParentTest p;
System.out.println("----------");
System.out.println();

ParentTest pa=new ParentTest();
System.out.println("----------");
System.out.println();

System.out.println(pa.a);
System.out.println("----------");
System.out.println();

System.out.println(ChildTest.b);
System.out.println("----------");
System.out.println();
}
}
class ParentTest{
static int a=3;

static {
System.out.println("ParentTest--------");
}
}
class ChildTest extends ParentTest{
static int b=4;

static {
System.out.println("ChildTest--------");
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FinalClass-------
----------

----------

ParentTest--------
----------

3
----------

ChildTest--------
4
----------

由输出:
1)由主动使用的条件第六条(Java虚拟机启动时被标明为启动类的类(Java Test,包含main()方法的类))知,含有main方法的类为启动类FinalClass,会主动使用并初始化,初始化阶段会执行static代码块打印”FinalClass——-“
2)对于”ParentTest p;“,并没有实例化ParentTest,所以没有主动使用ParentTest,也不会初始化,所以不会打印
3)对于”ParentTest pa=new ParentTest()“,是对ParentTest类的首次实例化,所以会初始化并打印其静态代码块中的内容”ParentTest——–“(ParentTest没有父类,所以直接初始化本身)
4)对于”pa.a“,是对”ParentTest pa=new ParentTest()“的首次实例化的结果,直接打印ParentTest的属性a。
5)对于”ChildTest.b“,调用ChildTest的静态属性b,会主动首次使用ChildTest类并对它初始化,在他初始化之前,应先初始化它的父类ParentTest,但在这里,它的父类已经初始化完成,不必再次初始化,所以不会再次打印父类静态代码块的内容,直接初始化ChildTest自身并打印”ChildTest——–”,然后打印”ChildTest.b“的结果。

静态变量(静态代码块也可看做静态变量)定义在哪个类,才会对哪个类主动使用并初始化

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package jvm;

public class FirstClassLoad {

public static void main(String[] args) {
System.out.println(Child1.a);
System.out.println();

Child1.doSomething();
}
}
class Parents1{
static int a=3;

static {
System.out.println("Parents1!!!");
}

static void doSomething() {
System.out.println("doSomething!!!");
}
}

class Child1 extends Parents1{
static {
System.out.println("Child1!!!");
}
}

输出:

1
2
3
4
Parents1!!!
3

doSomething!!!

由输出知:
1)在main方法中有Child1.a,通过子类Child1调用父类的静态属性a,这时,静态变量定义在哪个类才会对哪个类初始化,所以不会初始化子类Child1,而是初始化父类Parents1,并打印“Parents1!!!”,再打印Child1.a的结果3
2)对于 Child1.doSomething(),doSomething()方法同样定义在父类,会主动使用父类并初始化,但是父类已经初始化过了,所以直接打印 Child1.doSomething()的结果。

反射导致一个类的初始化

加载类不是对类的主动使用,不会导致类的初始化,反射是对类的主动使用,会导致类的初始化。
例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package jvm;

public class ClassForName {

public static void main(String[] args) throws ClassNotFoundException {
//获取一个类加载器
ClassLoader loader=ClassLoader.getSystemClassLoader();
//加载jvm.ClassLoadTest1
Class<?> clazz=loader.loadClass("jvm.ClassLoadTest1");
System.out.println(clazz);
System.out.println();

clazz=Class.forName("jvm.ClassLoadTest1");//反射
System.out.println(clazz);
}
}

class ClassLoadTest1{
static {
System.out.println("ClassLoadTest!!!");
}
}

输出:

1
2
3
4
class jvm.ClassLoadTest1

ClassLoadTest!!!
class jvm.ClassLoadTest1

由输出可知:
1)调用ClassLoader类的loadClass()方法加载一个类,不是对类的主动使用,并不会导致类的初始化,所以不会初始化ClassLoadTest1类,不打印这个类中的静态代码块。
2)对于clazz=Class.forName(“jvm.ClassLoadTest1”)是一种反射,是对类ClassLoadTest1的主动使用,会导致类ClassLoadTest1的初始化,所以会打印静态代码块。

JVM类加载

JVM类加载

1、Java中,类型的加载、连接与初始化过程都是在程序运行期间完成的。
1)加载:查找并加载类的二进制数据(将类从磁盘文件写入内存)
2)连接:
(1)验证:确保被加载的类的正确性
(2)准备:为类的静态变量分配内存,并将其初始化为默认值(例如,静态的整型值初始化为默认值为0,由Java虚拟机来完成)
(3)解析:把类中的符号引用转换为直接引用
3)初始化:为类的静态变量赋正确的初始值(程序员主观初始化赋值,例int a=1)
4)使用
5)卸载:从内存中销毁类

2、类加载器:加载类,将类写入内存

3、jvm结束生命周期的几种方式:
1)执行System.exit()方法
2)程序正常执行结束
3)程序执行过程中遇到异常或错误而异常终止
4)操作系统的错误

4、Java程序对类的使用方式:
1)主动使用
(1)创建类的实例
(2)访问某个类或接口的静态变量(取值getStatic()),或对该静态变量赋值(putStatic())
(3)调用类的静态方法(invokeStatic())
(4)反射(如Class.forName(“com.Test”))
(5)初始化一个类的子类(初始化子类时,会先去初始化父类,这是一个对父类的主动使用)
(6)Java虚拟机启动时被标明为启动类的类(Java Test,包含main()方法的类)
(7)JDK1.7开始提供的动态语言支持:
java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化。

2)被动使用
除以上七种情况外,其他使用Java类的方法都被看做是对类的被动使用,都不会导致类的初始化

注:所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们
举个很有用栗子:一个父类Parent,一个子类Child

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package jvm;
//-XX:TraceClassLoading:用于追踪类的信息并打印出来
public class ClassLoad {

public static void main(String[] args) {

//用子类名调用父类的静态属性str
System.out.println(Child.str);
}
}

class Parent{
//静态属性str
public static String str="Hello world";

//静态代码块
static {
System.out.println("Parent!!!");
}
}

class Child extends Parent{
//静态代码块
static {
System.out.println("Child!!!");
}
}

输出:

1
2
3
4
Parent!!!
Hello world
//用子类名调用父类的静态属性str,输出的是父类的静态代码块和父类的静态属性值
//因为调用父类的静态属性str,符合Java程序对类的主动使用第二点(访问某个类或接口的静态变量(取值getStatic()),或对该静态变量赋值(putStatic())),则Child.str的过程就是对父类的主动使用,但是没有主动使用子类,所以Java虚拟机不会初始化子类,则不会执行,虽然子类没有初始化,但是依然加载了,可用TraceClassLoading助记符来追踪的知。

再看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package jvm;
//-XX:TraceClassLoading:用于追踪类的信息并打印出来
public class ClassLoad {

public static void main(String[] args) {

//用子类名调用子类的静态属性str2
System.out.println(Child.str2);
}
}

class Parent{
//对于静态字段来说,只有直接定义了该字段的类才会被初始化
//静态属性str
public static String str="Hello world";

//静态代码块
static {
System.out.println("Parent!!!");
}
}

class Child extends Parent{
//对于静态字段来说,只有直接定义了该字段的类才会被初始化
//对一个子类初始化时,要求其父类已经全部初始化
//子类静态属性str2
public static String str2="Hello world";
//静态代码块
static {
System.out.println("Child!!!");
}
}

输出:

1
2
3
4
5
Parent!!!
Child!!!
Hello world
//用子类名调用子类的静态属性str2,输出的是父类的静态代码块、子类的静态代码块和子类的静态属性值
//因为调用子类的静态属性str,符合Java程序对类的主动使用第二点(访问某个类或接口的静态变量(取值getStatic()),或对该静态变量赋值(putStatic())),则Child.str22的过程就是对子类的主动使用,又因为子类继承与父类,这也是一种对父类的主动使用,所以父类与子类都会初始化并执行。

类的加载

是指将类的.class文件中的二进制数据读入内存,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象,用来封装类在方法区内的数据结构

1、加载.class文件的方式
1)从本地系统中直接加载
2)通过网络下载.class文件
3)从zip,jar等归档文件中加载
4)从专有数据库加载
5)将Java源文件动态编译为.class文件

常量

编译阶段,常量会被存入调用这个常量的那个方法所在类的常量池中,本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。之后定义常量的类与调用常量的类就没有任何关系了,删除定义常量的class文件也不影响运行。
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package jvm;

public class ChangLiang {

public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println(Test.str);
}
}

class Test{
public static String str="hello world";

static {
System.out.println("Test类");
}
}

输出:

1
2
Test类
hello world

再看下列代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package jvm;

public class ChangLiang {

public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println(Test.str);
}
}

class Test{
//静态常量属性str
public static final String str="hello world";

static {
System.out.println("Test类");
}
}

输出:

1
2
hello world
//因为str是静态常量,在编译阶段,这个常量会被存入调用这个常量的那个方法所在类的常量池中,即str会被放在ChangLiang类的常量池中,本质上,调用类并没有直接引用到定义常量的类(Test类),因此并不会触发定义常量的类的初始化。之后定义常量的类(Test)与调用常量的类(ChangLiang)就没有任何关系了,删除定义常量(Test)的class文件也不影响运行。对调用常量的类ChangLiang反编译,会发现Test.str这一句代码直接被常量"hello world"替换。

编译期常量与运行期常量的区别

上面的例子是编译期常量,下面演示运行期常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package jvm;

import java.util.UUID;

public class MyTest {

public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println(MyParent.str);
}

}
class MyParent{
//UUID 是 通用唯一识别码(Universally Unique Identifier)的缩写,是一种软件建构的标准
//UUID.randomUUID().toString()是随机的,编译器无法确定
public static final String str=UUID.randomUUID().toString();

static {
System.out.println("UUID CODE");
}
}

输出:

1
2
UUID CODE
f624dd9f-5d47-42f4-8fb2-41812b231bda

注意:当一个常量并非编译器可以确定,那么其值就不会被放到调用类的常量池中,这时程序运行时,会会主动使用定义这个常量所在的类,则会导致该类被初始化(在这个例子中,对于public static final String str=UUID.randomUUID().toString(),是随机的,在编译器无法确定,则会加载它所在的类MyParent,从而输出UUID CODE)

数组创建本质

对于数组类型,它的类加载器是由Java虚拟机在运行期动态创建的,Java虚拟机返回的类加载器类型与数组中元素的类加载器类型一样(例如一个string类型的数组,String的类加载器是根类加载器,那么Java虚拟机为数组类型创建的类加载器也是根类加载器,),如果元素类型为原生类型,则没有类加载器(例如int类型)。
一个前言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package jvm;

public class ArrayClass {

public static void main(String[] args) {
// TODO Auto-generated method stub
TestArray a=new TestArray();//首次实例化
TestArray b=new TestArray();//再次实例化
}

}

class TestArray{
static {
System.out.println("TestArray.........");
}
}

输出

1
TestArray.........

结果分析:当对一个类首次实例化时,会主动调用并初始化,再次则不会主动使用了,所以实例化两次,只打印一次。

正文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package jvm;

public class ArrayClass {

public static void main(String[] args) {
TestArray[] a=new TestArray[1];
}

}

class TestArray{
static {
System.out.println("TestArray.........");
}
}

输出:
上面这个代码不会有任何输出,因为数组的类型实际上并不是TestArray。可以用getClass()方法查看数组的类型,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package jvm;

public class ArrayClass {

public static void main(String[] args) {
TestArray[] a=new TestArray[1];
//查看数组所属类型
System.out.println(a.getClass());
}
}

class TestArray{
static {
System.out.println("TestArray.........");
}
}

输出:

1
class [Ljvm.TestArray;

由输出可知,数组的类型是“[Ljvm.TestArray;”,这个类型由Java虚拟机在运行期动态创建。

下面再看看二维数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package jvm;

public class ArrayClass {

public static void main(String[] args) {
TestArray[][] a=new TestArray[1][1];
System.out.println(a.getClass());
}
}

class TestArray{
static {
System.out.println("TestArray.........");
}
}

输出:

1
class [[Ljvm.TestArray;

由输出知,二位数组的类型为“[[Ljvm.TestArray;”,比一维数组多一个方括号“[”。

下面调用getSuperClass()方法查看一下这个数组类型的父类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package jvm;

public class ArrayClass {

public static void main(String[] args) {
TestArray[][] a=new TestArray[1][1];
//查看数组类型
System.out.println(a.getClass());
//查看数组类型的父类型
System.out.println(a.getClass().getSuperclass());
}
}

class TestArray{
static {
System.out.println("TestArray.........");
}
}

输出:

1
2
class [[Ljvm.TestArray;
class java.lang.Object

由输出知,二位数组的类型为“[[Ljvm.TestArray;”,二位数组类型的父类型为“java.lang.Object”,经实验,一维数组的父类型也是“java.lang.Object”

引用类型与基本数组类型的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package jvm;

public class ArrayClass {

public static void main(String[] args) {
TestArray[] a=new TestArray[1];
//查看引用(数组)类型
System.out.println(a.getClass());
//查看引用(数组)类型的父类型
System.out.println(a.getClass().getSuperclass());
System.out.println();

int[] ints=new int[1];
//查看基本类型(int)的数组类型
System.out.println(ints.getClass());
//查看基本类型(int)的数组类型的父类型
System.out.println(ints.getClass().getSuperclass());
System.out.println();

char[] chars=new char[1];
//查看基本类型(int)的数组类型
System.out.println(chars.getClass());
//查看基本类型(int)的数组类型的父类型
System.out.println(chars.getClass().getSuperclass());
}
}

class TestArray{
static {
System.out.println("TestArray.........");
}
}

输出:

1
2
3
4
5
6
7
8
class [Ljvm.TestArray;
class java.lang.Object

class [I
class java.lang.Object

class [C
class java.lang.Object

由输出可知:对于数组实例来说,其类型是jVM再运行期间动态创建的,表示为“[Ljvm.TestArray;”这种格式,对于基本类型的数组来说,如int类型表示为“[I”。父类都是object。
对于数组来说:javaDOC经常将构成数组的元素为Component,实际上就是将数组降低一个维度。

助记符

-XX:+

TraceClassLoading:用于追踪类的信息并打印出来
ldc:表示将int、float或string类型二等常量从常量池中推送至栈顶
blpush:将单字节(-128至127)二等常量推送至栈顶
sipush:将短整型常量值(-32768至32767)二等常量推送至栈顶
iconst_1:将int型的1推送至栈顶(iconst_-1 - iconst_5共七个数)
anewarray:创建一个引用类型的(如类、接口、数组)数组,并将其引用值压入栈顶
newarray:创建一个指定额原始类型(如int、float、char)等,,并将其引用值压入栈顶

JVM内存模型

JVM内存模型

jvm启动流程:
Image text

1、JVM = 类加载器(classloader) + 执行引擎(execution engine) + 运行时数据区域(runtime data area)

Image text

2、运行时数据区域 

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。
Image text

3、程序计数器(Program Counter Register)

线程私有,它的生命周期与线程相同。
可以看做是当前线程所执行的字节码的行号指示器。
在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,如:分支、循环、跳转、异常处理、线程恢复(多线程切换)等基础功能。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(undefined)。
程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,所以此区域不会出现OutOfMemoryError的情况。

4、Java虚拟机栈(JVM Stacks)

线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

1)局部变量表
包含参数和局部变量,存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型),它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
该区域可能抛出以下异常:
当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。

2) 操作数栈
Java没有寄存器,所有的参数传递使用操作数栈

3)Java栈上分配
如果是小对象(一般几十个bytes),在没有逃逸的情况下,可直接分配在栈上,减轻堆的压力

5、本地方法栈(Native Method Stacks)

与虚拟机栈非常相似,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

6、Java堆(Heap)

被所有线程共享,在虚拟机启动时创建,用来存放对象实例,几乎所有的对象实例都在这里分配内存。
对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;新生代又有Eden空间、From Survivor空间、To Survivor空间三部分。
Java 堆不需要连续内存,并且可以通过动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
Image text

7、方法区(Method Area)

用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现,HotSpot 虚拟机把它当成永久代(Permanent Generation)来进行垃圾回收。
方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。

运行时常量池(Runtime Constant Pool)


运行时常量池是方法区的一部分。
Class 文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。这部分常量也会被放入运行时常量池。
Image text
注:
在 JDK1.7之前,HotSpot 使用永久代实现方法区;HotSpot 使用 GC 分代实现方法区带来了很大便利;
从 JDK1.7 开始HotSpot 开始移除永久代。其中符号引用(Symbols)被移动到 Native Heap中,字符串常量和类引用被移动到 Java Heap中。
在 JDK1.8 中,永久代已完全被元空间(Meatspace)所取代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
直接内存(Direct Memory)
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现。
在 JDK 1.4 中新加入了 NIO 类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和 Native 堆中来回复制数据。

栈、堆、方法区交互
Image text

可见性

一个线程修改了变量,其他线程可以立即知道

保证可见性的方法:
1)volatile
2)synchronized(unlock之前,写变量值回主存)
3)final(一旦初始化完成,其他线程就可见)

参考:http://baijiahao.baidu.com/s?id=1598140630731512683&wfr=spider&for=pc