学习笔记,仅供参考,禁止搬运,如有不正确的地方欢迎大家指正,谢谢!!!
一、IO控制方式:即用什么样的方式控制IO设备数据的读/写(四种)
Java 如何和外部设备通信
|
计算机的外部设备有鼠标、键盘、打印机、网卡等,通常我们将
外部设备
和和
主存
之间的信息传递称为
I/O 操作
, 按操作特性可以分为,
输出型设备,输入型设备,存储设备
。
现代设备都采用通道方式和主存进行交互,
其他处理 IO 的方式,例如
轮询、中断、DMA
,在性能上都不如
通道
。当然 Java 程序和外部设备通信也是通过系统调用完成。
| ||||
IO控制方式
|
描述
|
完成一次读/写操作的流程
|
cpu干预频率
|
数据传送单位和数据流向
|
优势和弊端
|
程序直接控制方式
|
1. 即轮询
就是cpu直接控制IO设备读/写
2. 需要切换上下文和内核空间到用户空间的数据拷贝
3. 此方式CPU需要一直被此进程占用,直到IO操作结束,即使在IO设备缓慢工作时,依然处于“忙等”状态(轮询)
|
![]() |
很频繁,IO操作开始之前,完成之后需要CPU介入,并且在等待IO完成的过程中需要CPU不断地轮询检查
|
单位:
每次读/写一个字
流向:
读操作(数据输入):IO设备 --> CPU数据寄存器 --> 内存
写操作(数据输出):内存 ---> CPU数据寄存器 --> IO设备
|
优势:
实现简单,在发出读/写指令之后,加上实现循环检查的一系列指令即可(因此称为“程序直接控制方式”)
弊端:
CPU和IO设备只能串行工作,CPU需要一直轮询检查,长期处于“忙等”状态。CPU利用率低。
|
中断驱动方式
|
1. 为了解决“程序直接控制方式”的CPU利用率低提出的。
2. IO中断,阻塞等待IO的进程,CPU先切换到别的进程执行。IO设备数据准备好以后,给CPU发一个中断信号,CPU保存当前正在执行的进程的运行环境,保护现场信息,然后去处理中断(中断处理程序),将IO设备输入的数据读到CPU寄存器,再写入主存,接着CPU将根据调度,恢复IO进程或其他进程。
注:IO进程指发送IO请求给CPU的进程
|
![]() |
每次IO操作开始之前、完成之后需要CPU介入。
等待IO完成的过程中CPU可以切换到别的进程执行
|
单位:
每次读/写一个字
流向:
读操作(数据输入):IO设备 --> CPU数据寄存器 --> 内存
写操作(数据输出):内存 ---> CPU数据寄存器 --> IO设备
|
优势:
与“程序直接控制方式”相比,IO控制器会通过中断信号主动报告IO已完成,CPU不再需要不停的轮询,CPU和IO设备可以并行工作,CPU利用率明显提升。
弊端:
每个字在IO设备与内存之间的传输,都需要经过CPU,而频繁的中断处理会消耗较多的CPU时间。(CPU需要保护进程现场、处理中断、恢复进程)
|
DMA方式
|
为了解决“每次读/写一个字,频繁切换CPU,消耗资源与时间”的问题,提出了DMA方式。
DMA是IO处理器,附属于CPU
|
![]() ![]() |
只在传送一个或多个数据块的开始和结束时,才需要CPU干预
|
单位:
每次读写一个或多个块(
注意:每次读写的只能是连续的多个块,且这些块读入内存后在内存中也必须是连续的)
流向:
读(输入数据):IO设备 --> 内存
写(输出数据):内存 --> IO设备
|
优势:
数据传输以“块”为单位,CPU介入频率进一步降低。数据的传输不再需要先经过CPU再写入内存,数据传输效率进一步增加。CPU和IO设备的并行性得到提升。
弊端:
CPU每发送一条IO指令,只能读写一个或多个连续的数据块。如果要读写多个离散存储的数据块,或者要将数据分别写到不同的内存区域时,CPU要分别发出多条IO指令,进行多次中断处理才能完成。
|
通道控制方式
|
现代设备都采用通道方式和主存进行交互,
通道是一个专门用来处理IO任务的设备
通道是为了解决DMA方式连续存储的问题
|
![]() CPU 在处理主程序时遇到I/O请求,启动指定通道上选址的设备,一旦启动成功,通道开始控制设备进行操作,而 CPU 可以继续执行其他任务,I/O 操作完成后,通道发出 I/O 操作结束的中断,处理器转而处理 IO 结束后的事件。 |
极低,通道会根据CPU的指示执行相应的通道程序,只有完成一组数据块的读/写后才需要发出中断信号,请求CPU干预。
|
单位:
每次读/写一组数据块
流向:在通道的控制下进行
读(输入数据):IO设备 --> 内存
写(输出数据):内存 --> IO设备
|
优势:
CPU、通道、IO设备可并行工作、资源利用率很高
弊端:
实现复杂、需要专门的通道硬件支持。
|
二、通道Channel
代码
|
package com.lihefei.nio.day01;
import org.junit.jupiter.api.Test;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Set;
import static java.nio.channels.FileChannel.*;
import static java.util.Map.*;
/**
* 一、通道 Channel
* 通道用于源节点与目标节点的连接。
* 在 Java NIO 中主要负责缓冲区中数据的传输。Channel 本身不存储数据,因此需要配合缓冲区进行传输。
* <p>
* 二、通道的主要实现类
* java.nio.channels.Channel 接口:
* |-- FileChannel
* |-- SocketChannel
* |-- ServerSocketChannel
* |-- DatagramChannel
* <p>
* 三、获取通道
* 1.Java 针对支持通道的类提供了 getChannel() 方法
* 1> 本地 IO:
* FileInputStream / FileOutputStream
* RandomAccessFile 随机存取文件流
* 2> 网络 IO:
* Socket
* ServerSocket
* DatagramSocket
* 2.在 jdk 1.7 中 NIO.2 针对各个通道提供了静态方法 open()
* 3.在 jdk 1.7 中 NIO.2 的Files工具类的 newByteChannel()
*
* 四、获取直接缓冲区的方式
* 方式一:ByteBuffer.allocateDirect()方法创建
* 方式二:通过FileChannel.map()返回MappedByteBuffer
* 直接缓冲区只有ByteBuffer支持
*
* 五、通道之间的数据传输 (使用的是直接缓冲区)
* transferFrom()
* transferTo()
*
* 六、分散(Scatter)读取与聚集(Gather)写入
* 分散读取(Scattering Reads):将通道里的数据分散到多个缓冲区中
* 聚集写入(Gathering Write):将多个缓冲区的数据都聚集到通道中。
*
* 七、字符集Charset
* 编码:字符串 --> 字节数组
* 解码:字节数组 --> 字符串
*
* @author lotus
* @create 2020-11-25 4:31 上午
*/
public class ChannelTest {
// 利用通道完成文件的复制 (非直接缓冲区)
@Test
public void test1() {
Instant start = Instant.now();
FileInputStream fis = null;
FileOutputStream fos = null;
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
// 1.准备通道连接的IO设备
fis = new FileInputStream("/Users/lotus/Desktop/juc.zip");
fos = new FileOutputStream("/Users/lotus/Desktop/juc_2.zip");
// 2.获取输入输出通道 (传输数据)
inChannel = fis.getChannel();
outChannel = fos.getChannel();
// 3.创建非直接缓冲区(存储数据)
ByteBuffer buf = ByteBuffer.allocate(1024);
// 4.从输入通道中获取数据,写入到缓冲区
while (inChannel.read(buf) != -1) { // 缓冲区:写数据模式
// 5.缓冲区切换成读取数据模式
buf.flip();
// 6.从缓冲区读取数据,放到通道中
outChannel.write(buf);
// 7.清空缓冲区(数据还存在,"被遗忘"状态,会重写覆盖)
buf.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (outChannel != null)
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (inChannel != null)
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fos != null)
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fis != null)
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
Instant end = Instant.now();
System.out.println("共花费时间: " + Duration.between(start,end).toMillis() + "毫秒"); // 共花费时间: 5057毫秒 5277毫秒 5014毫秒
}
// 利用通道完成文件的复制 (直接缓冲区)
@Test
public void test2() {
FileInputStream fis = null;
FileOutputStream fos = null;
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
// 1.准备通道连接的IO设备
fis = new FileInputStream("1.jpg");
fos = new FileOutputStream("2.jpg");
// 2.获取输入输出通道 (传输数据)
inChannel = fis.getChannel();
outChannel = fos.getChannel();
// 3.创建非接缓冲区(存储数据) // 获取直接缓冲区的方式一:ByteBuffer.allocateDirect()方法创建
ByteBuffer buf = ByteBuffer.allocateDirect(1024);
// 4.从输入通道中获取数据,写入到缓冲区
while (inChannel.read(buf) != -1) { // 缓冲区:写数据模式
// 5.缓冲区切换成读取数据模式
buf.flip();
// 6.从缓冲区读取数据,放到通道中
outChannel.write(buf);
// 7.清空缓冲区(数据还存在,"被遗忘"状态,会重写覆盖)
buf.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (outChannel != null)
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (inChannel != null)
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fos != null)
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fis != null)
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/** 使用直接缓冲区完成文件的复制(内存映射文件的方式)
*
* 内存文件映射实现原理:
* 将直接缓冲区的逻辑/虚拟空间地址(在JVM的直接内存中)与磁盘文件部分区域的物理地址相映射(保证是两块相同大小的区域,一一对应),
* 此时还数据还不在缓冲区,当我们访问到缓冲区对象时,查页表发现,缓冲区对象虚拟地址对应的物理地址不在内存中,而在磁盘中,于是引起缺页异常,此时将文件数
* 据从磁盘加载进内存中,程序和磁盘文件就都绕开和cpu交互完成了从磁盘读取文件进程序内存的IO操作。这种方式实际上是利用了CPU访问内存的特性,PC中数据都是虚拟
* 存储的,页表保存了物理地址与逻辑/虚拟地址之间的映射,物理地址可以是主存/内存,也可以是磁盘等的外部设备,当程序访问到虚拟地址,查询页表时发现数据不在内存里,
* 就会去磁盘等的外部设备中将数据加载进内存,当内存中的物理空间不够时,又回将内存中不常用的内存块/内存页,放到磁盘等的外部设备中去,即swap。
*/
@Test
public void test3() {
Instant start = Instant.now();
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
// CREATE_NEW:如果文件存在就报错;CREATE:如果文件存在就覆盖
inChannel = FileChannel.open(Paths.get("/Users/lotus/Desktop/juc.zip"), StandardOpenOption.READ);
outChannel = FileChannel.open(Paths.get("/Users/lotus/Desktop/juc_3.zip"), StandardOpenOption.READ,StandardOpenOption.WRITE, StandardOpenOption.CREATE);
// 内存文件映射 获取直接缓冲区的方式二:通过FileChannel.map()返回MappedByteBuffer
MappedByteBuffer inMappedBuf = inChannel.map(MapMode.READ_ONLY,0,inChannel.size());
MappedByteBuffer outMappedBuf = outChannel.map(MapMode.READ_WRITE,0,inChannel.size());
// 直接对缓冲区进行数据的读写操作
byte[] dst = new byte[inMappedBuf.limit()];
inMappedBuf.get(dst);
outMappedBuf.put(dst);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(inChannel != null)
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if(outChannel != null)
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
Instant end = Instant.now();
Duration duration = Duration.between(start,end);
System.out.println("共花费时间: " + duration.toMillis() + "毫秒"); // 共花费时间: 3411毫秒 8405毫秒 2271毫秒 2243毫秒
// 如果将文件放在Java项目文件夹下: java.lang.OutOfMemoryError: Java heap space
}
// 通道之间的数据传输(直接缓冲区)
@Test
public void test4() {
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
outChannel = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
// inChannel.transferTo(0,inChannel.size(),outChannel);
outChannel.transferFrom(inChannel,0,inChannel.size());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (inChannel != null)
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (outChannel != null)
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 分散读取与聚集写入
@Test
public void test5() {
FileChannel channel1 = null;
FileChannel channel2 = null;
try {
RandomAccessFile raf1 = new RandomAccessFile("CallbackTest.py","rw");
// 1.获取通道
channel1 = raf1.getChannel();
// 2.创建多个缓冲区
ByteBuffer buf1 = ByteBuffer.allocate(100);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
// 3.分散读取
ByteBuffer[] bufs = new ByteBuffer[]{buf1,buf2};
channel1.read(bufs);
// 缓冲区切换读模式
for(ByteBuffer buf : bufs) {
buf.flip();
}
System.out.println(new String(bufs[0].array(),0,bufs[0].limit()));
System.out.println("==================================================");
System.out.println(new String(bufs[1].array(),0,bufs[1].limit()));
// 4.聚集写入
RandomAccessFile raf2 = new RandomAccessFile("CallbackTest2.py","rw");
channel2 = raf2.getChannel();
channel2.write(bufs);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 5.关闭通道
try {
if (channel1 != null)
channel1.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (channel2 != null)
channel2.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 字符集
@Test
public void test6() {
Map<String,Charset> map = Charset.availableCharsets();
Set<Entry<String,Charset>> set = map.entrySet();
for (Entry<String,Charset> entry : set) {
System.out.println(entry.getKey() + "= " + entry.getValue());
}
}
// 字符集
@Test
public void test7() throws IOException {
Charset cs1 = Charset.forName("GBK"); // GBK : 1个中文占2个字节
//获取编码器
CharsetEncoder ce = cs1.newEncoder();
//获取解码器
CharsetDecoder cd = cs1.newDecoder();
CharBuffer cBuf = CharBuffer.allocate(1024);
cBuf.put("哈哈!");
// 编码
cBuf.flip();
ByteBuffer bBuf = ce.encode(cBuf);
for (int i = 0; i < 12; i++) {
System.out.println(bBuf.get());
}
System.out.println("=======================");
// 解码
bBuf.rewind(); // rewind()方法将position置0,清除mark,重读。
CharBuffer cBuf2 = cd.decode(bBuf);
for (int i = 0; i < 6; i++) {
System.out.println(cBuf2.get());
}
// System.out.println(cBuf2.toString());
}
}
|
课堂笔记:
系统调用/CPU/DMA/通道
|
1. 系统调用:用户态与内核态
Linux操作系统的体系架构分为用户态和内核态(或者用户空间和内核)。内核程序相当于一个 “大管家”,内核从本质上看是一种软件——控制计算机的硬件资源,并提供上层应用程序运行的环境。用户态即上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。API如:IO接口
内核态:在内核空间执行,通常是驱动程序,中断相关程序,内核调度程序,内存管理及其操作程序。
用户态:用户程序运行空间。
2. CPU: 中央处理器
内核是CPU的核心(芯片),一个CPU可以是多核的(多核处理器)
硬件资源都是由CPU来独立负责控制访问的,“程序直接控制方式”是由CPU全权负责与IO设备交互,控制完成IO操作。
这导致CPU占用率极高,就不能做其他的事了,CPU性能下降,CPU利用率低(未能充分利用CPU)
3. DMA:直接存储器
![]()
DMA:传统的数据传输方式,当Java程序通过系统调用发出读写IO请求时,首先DMA向CPU申请控制IO操作的权限、CPU授予权限,让DMA直接负责IO操作与磁盘等外部设备交互,但是如果DMA总线过多,总线冲突,也会造成大量申请cpu权限,性能下降,DMA完成IO操作以后,会向CPU发送中断信号,CPU将收回权限。
4. Channel:通道
![]()
通道:完全独立的处理器:专门用于IO操作的,虽附属于CPU,拥有自己的一套自己命令和传输方式。不用向CPU发起申请控制IO操作权限的请求,cpu利用率更高
|
三、Java代码在OS上执行的过程
Java程序在OS上的执行过程
|
通过JVM的javac编译器编译成 .class字节码文件,将字节码文件加载到内存(JVM执行引擎)中,通过执行引擎解释编译为OS能识别的本机机器指令(需要转成机器码:0101的数据串),由CPU的操作控制器OC去取,即CPU取址过程,由指令译码器ID(Instructions Decoder),解码分析出操作符和操作数的地址(操作数放在JVM的堆中),将译码后的CPU指令放到指令寄存器IR里,将操作数放入一级/二级缓存里,最后再由操作控制器OC控制运算单元去计算,至此完成了程序在OS上的一次执行。
补:instructions:指令,命令 decoder:译码器,解码器 encoder:编译器
|
执行过程流程图
|
![]() |
四、JVM内存与类加载
Java内存区域
|
![]() |
程序计数器
|
描述:
一块较小的内存区域,可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
程序计数器主要有两个作用:
计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,计数器的值则应为空(Undefined)。
此内存区域是唯一一个规范中没有规定任何OutOfMemoryError情况的区域。(每个线程只有一个程序计数器,内存占用少,故不存在内存溢出的情况)
|
Java虚拟机栈
|
描述:
每个方法被执行时都会同步创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。每一个方法被调用直至执行完毕的过程,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
局部变量表:
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,不等同于对象本身,可以是一个指向对象起始地址的指针等)和returnAddress类型(指向一条字节码指令的地址)。
这些数据类型以局部变量槽(Slot)来表示,其中64位长度的double和long占用两个变量槽,其余只占用一个。
设置虚拟机栈内存大小:
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小: java -Xss2M HackTheJava
该区域可能抛出以下异常:
1. 若 Java 虚拟机栈的内存大小不允许动态扩展,且线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
2. 栈进行动态扩展时如果无法申请到足够内存,并且垃圾回收器也无法提供更多内存的话,会抛出 OutOfMemoryError 异常。
|
本地方法栈
|
描述:
与虚拟栈所发挥的作用非常相似,区别是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的本地方法服务。
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
该区域可能抛出以下异常:
本地方法栈也会抛出 StackOverflowError和OutOfMemoryError异常。
|
Java堆
|
描述:
被所有线程共享的一块区域,在虚拟机启动时创建,是一块运行时数据区域。此内存区域的唯一目的是存放对象实例,几乎所有对象实例都在这里分配内存,是垃圾收集器管理的内存区域(GC堆)。
该区域可能抛出以下异常:
Java堆可以处于物理上不连续的内存空间中(不需要连续的内存),但在逻辑上它应该被视为连续的;能够可扩展的动态增加其内存。
如果在Java堆中没有内存来完成实例分配,并且堆也无法再扩展时会抛出OutOfMemoryError异常。
设置Java堆内存大小:
可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
java -Xms1M -Xmx2M HackTheJava
|
方法区
|
描述:
是各个线程共享的内存区域,它用于存放被虚拟机加载的类型信息(类的结构)、常量、静态变量、即时编译器编译后的代码缓存(方法和构造函数的代码)等数据。在虚拟机启动时创建,是一块运行时数据区域。
这个区域的垃圾回收目标主要是针对常量池的回收和对类型的卸载,但是一般难以实现。
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
为什么将永久代替换成元空间?
方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。和堆一样不需要连续的内存,并且可以动态扩展;如果方法区无法满足新的内存分配需求时,会抛出 OutOfMemoryError 异常。
运行时常量区(方法区)
运行时常量区是方法区的一部分。实际上存储的还是引用,实际的对象还是在Java堆上。
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译器生成的各种字面量与符号引用),这部分内容将在类加载后存放到方法区的运行时常量池中。受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。
除了在编译期生成的常量外,还可以动态生成,如String类的intern()方法。
|
直接内存
|
描述:
直接内存并不是虚拟机运行时数据区的一部分,但是也被频繁地使用;动态扩展时也会出现OutOfMemoryError 异常。
在 JDK 1.4 中新引入了 NIO 类,一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
|
五、JVM执行引擎
执行引擎概述
|
![]()
|
执行引擎的工作过程
|
|
Java代码编译和执行过程
|
![]()
大部分的程序代码转换成物理机的
目标代码或虚拟机能执行的指令集之前,都需要经过上图中的各个步骤。
Java代码编译是由Java源码编译器来完成,流程图如下所示:
![]()
Java字节码的执行是由JVM执行引擎来完成,流程图如下所示:
![]() |
什么是解释器( Interpreter),什么是JIT编译器?
|
解释执行方式: 通过解释器将字节码 ----逐行解释-----> 对应平台的机器码 ----执行----->
编译执行方式: 通过编译器将“热点代码<一个高频使用的函数>”字节码 ----编译-----> 对应平台的机器码 ----执行----->
设计初衷: JVM设计者们的初衷仅仅只是单纯地为了满足Java程序实现跨平台特性,因此避免采用静态编译的方式直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法。
|
为什么说Java是半编译半解释型语言?
|
|
机器码、指令、汇编语言
|
机器码:
指令:
指令集:
➢x86指令集,对应的是x86架构的平台
➢ARM指令集,对应的是ARM架构的平台
汇编语言:
➢由于计算机只认识指令码,所以用汇编语言编写的程序还必须翻译成机器指令码,计算机才能识别和执行。
小结:
1. 使用0101的二进制数代表指令的含义,即机器指令码/机器码,使用机器码来编程,即机器语言。
2. 为了提高机器语言的可读性,升级:机器码 ---> 机器指令(使用英文简写机器码代表的含义,包含操作码和操作数地址)------> 汇编语言(使用助记符代替机器指令的操作码,使用地址符号和标号代替操作数地址)
3. 不同硬件平台拥有自己的一套机器指令集:同一个操作<同一种含义的指令>,对应的机器指令<简写英文不同> 和 机器码<0、1序列不同> 可能不同
4. CPU决定机器指令,CPU不同,机器指令不同
5. 对于汇编语言来说想在PC上执行,由于PC机只能识别机器码:汇编语言 --- 转换成---> 机器指令 ----翻译----> 机器码
|
六、Java字节码的执行方式:解释执行 和 编译执行
解释执行 和 编译执行
|
![]()
简化版的过程图示:
|
相关基本概念
|
JVM:
一种能够运行Java字节码的虚拟机。
Java字节码:
是Java虚拟机执行的一种指令格式,字节码是已经经过编译(.class文件),但与特定机器码无关,需要解释器转译才能成为机器码的中间代码。
解释器:
是一种电脑程序,能够把高级编程语言一行一行直接翻译运行。解释器不会一次把整个程序翻译出来,只像一位“中间人”,每次运行程序时都要先转成另一种语言再作运行,因此解释器的程序运行速度比较缓慢。它每翻译一行程序叙述就立刻运行,然后再翻译下一行,再运行,如此不停地进行下去。它会先将源码翻译成另一种语言,以供多次运行而无需再经编译。其制成品无需依赖编译器而运行,程序运行速度比较快。
二进制文件:
广义的二进制文件即为文件,由文件在外部存储设备的存放方式为二进制而得名。狭义的二进制文件即指除文本文件以外的文件。文本文件的格式包括:ASCII、MIME、txt。
Java编译器:
将Java源文件(.java文件)编译成字节码文件(.class文件,是特殊的二进制文件,二进制字节码文件),这种字节码就是JVM的“机器语言”。javac.exe可以简单看成是Java编译器。
JIT编译器(注意与Java解释器区分):
JIT编译器是JRE的一部分。原本的Java程序都是要经过解释执行的,其执行速度肯定比可执行的二进制字节码程序慢。为了提高执行速度,引入了JIT。在运行时,JIT会把翻译过来的机器码保存起来,以备下次使用。而如果JIT对每条字节码都进行编译,则会负担过重,所以,JIT只会对经常执行的字节码进行编译,如循环,高频度使用的方法等。它会以整个方法为单位,一次性将整个方法的字节码编译为本地机器码,然后直接运行编译后的机器码。
即时编译(Just-in-time compilation: JIT):
又叫实时编译、及时编译。是指一种在运行时期把字节码编译成原生机器码的技术,一句一句翻译源代码,但是会将翻译过的代码缓存起来以降低性能耗损。这项技术是被用来改善虚拟机的性能的。
JVM:
JVM有自己完善的硬件架构,如处理器、堆栈(Stack)、寄存器等,还具有相应的指令系统(字节码就是一种指令格式)。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需要生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM是Java平台无关的基础。JVM负责运行字节码:JVM把每一条要执行的字节码交给解释器,翻译成对应的机器码,然后由解释器执行。JVM解释执行字节码文件就是JVM操作Java解释器进行解释执行字节码文件的过程。(还有JIT的作用)
注意:通常情况下,一个平台上的二进制可执行文件不能在其他平台上工作,因为此可执行文件包含了对目标处理器的机器语言。而Class文件这种特殊的二进制文件,是可以运行在任何支持Java虚拟机的硬件平台和操作系统上的!
|
Java编译执行和解释执行的意义
|
HotSpot VM是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。
但是如今的HotSpot VM中不仅内置有解释器,还内置有先进的JIT(Just In Time Compiler)编译器,在Java虚拟机运行时,解释器和即时编译器能够相互协作,各自取长补短。有一点需要注意,无论是采用解释器进行解释执行,还是采用即时编译器进行编译执行,最终字节码都需要被转换为对应平台的本地机器指令。
问:既然HotSpot VM中已经内置JIT编译器了,那么为什么还需要再使用解释器来“拖累”程序的执行性能呢?
对于服务端应用来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一个平衡点。
由于即时编译器将本地机器指令的编译推迟到了运行时,自此Java程序的运行性能已经达到了可以和C/C++程序一较高下的地步。这主要是因为JIT编译器可以针对那些频繁被调用的“热点代码”做出深度优化,而静态编译器的代码优化则无法完全推断出运行时热点,因此通过JIT编译器编译的本地机器指令比直接生成的本地机器指令拥有更高的执行效率也就理所当然了。
|
具体的流程图
|
![]() |