目录
3.2. Java Native Interface (JNI)
前言
日常开发中,不知道有没有思考过这样的问题?
"为什么Java的
new Thread().start()
只需几微秒,而Linux的fork()
进程却要毫秒级?"
"为何Redis单线程却能支持10万QPS,而你的Java多线程程序卡在8千?"
或者如下图所示:
| 操作 | 耗时 | 状态切换次数 |
|---------------------|-------------|--------------|
| Java方法调用 | 3 ns | 0 |
| 系统调用(read) | 100 ns | 2 |
| 线程上下文切换 | 1 μs | 2+ |
| 进程创建(fork) | 1 ms | 10+ |
Java 方法调用为什么快?(3 ns)
-
纯用户态操作:
JVM 直接在用户空间执行方法调用,无需切换CPU特权级(无内核介入)。 -
直接跳转:
通过 栈帧 和 方法表 直接跳转,现代JVM还会用 JIT编译优化(内联等)。 -
硬件加速:
CPU 的流水线、分支预测等机制可以高效处理这种简单跳转。
而对于read方法为什么这么慢?可参考如下:
接下来就随着这篇文章来深入了解程序运行在操作系统里面的原理。
用户态和内核态是操作系统的两种运行状态,用于保护系统资源和确保安全。程序通过系统调用、异常或中断从用户态进入内核态。
系统调用是进程主动请求操作系统服务的方式,涉及CPU上下文切换,包括用户栈到内核栈的切换,并在完成后返回用户态。
下面将从硬件实现、操作系统原理及JVM设计三个层次进行深度剖析。
1、硬件层
分为特权级与保护机制,关于硬件这部分可以进行简单的了解。
1.1、CPU特权级(Ring Model)
如下图所示:
现代CPU通常采用多级特权环(x86架构为Ring 0~Ring 3):
-
Ring 0(内核态):执行所有指令(如
LGDT
加载全局描述符表、HLT
停机指令) -
Ring 3(用户态):受限指令会触发General Protection Fault(GPF)异常
-
特权级切换:通过
syscall
/sysenter
(快速系统调用指令)或中断门实现
关键点:硬件强制隔离用户态与内核态,任何越权操作都会触发CPU异常。
1.2、内存保护:MMU与页表
-
用户态进程:只能访问虚拟地址空间中用户区(如Linux的
0x00000000~0x7fffffff
) -
内核态:可访问全部地址空间(包括内核区
0xc0000000
以上) -
页表权限位:通过
PTE
(Page Table Entry)的U/S
位(User/Supervisor)控制访问权限
稍微扩展下:
-
U/S位(bit 2):控制访问权限
-
1
:用户态可访问 -
0
:仅内核态可访问
-
示例:Linux进程地址空间布局
用户态可访问:
0x00000000-0x7fffffff ┌───────────────┐
│ 用户空间 │
内核态独占:
0xc0000000-0xffffffff ┌───────────────┐
│ 内核空间 │
1.3、系统调用流程
当执行open()
系统调用时:
-
CPU从用户态(Ring 3)进入内核态(Ring 0)
-
MMU临时忽略U/S位检查
-
内核访问
struct file
等数据结构(位于内核内存区域) -
返回用户态时重新启用U/S位保护
如用户态代码访问内核态内存的时候:
// 用户态尝试读取内核地址会导致段错误
void *kernel_addr = (void *)0xffff888000000000;
printf("%d", *(int *)kernel_addr); // 触发#PF(Page Fault)异常
-
CPU检测到U/S=0的页在用户态被访问
-
触发Page Fault(错误码
0x00000005
:用户态+读操作) -
内核发送sigsegv信号终止进程
小结
从硬件特权级到JVM的巧妙封装,每一层设计都在平衡安全与效率。现代Java生态(如GraalVM、Project Loom)正在进一步模糊这一界限,但底层原理仍是性能优化的核心知识。
2、操作系统状态
2.1、分类
如下图所示:
用户态和内核态是操作系统的两种运行状态。
内核态:
-
定义:操作系统内核运行的特权模式。
-
特点:
-
可以执行所有CPU指令
-
可以访问全部内存空间
-
可以直接操作硬件设备
-
权限最高,但风险也大
-
处于内核态的 CPU 可以访问任意的数据,包括外围设备,比如网卡、硬盘等,处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况,一般处于特权级 0 的状态我们称之为内核态。
用户态:
-
定义:应用程序运行的普通权限模式。
-
特点:
-
只能访问受限的CPU指令集和内存空间
-
不能直接访问硬件设备
-
执行特权指令会导致异常
-
安全性高,一个进程崩溃不会影响整个系统
-
处于用户态的 CPU 只能访问受限资源,不能直接访问内存等硬件设备,不能直接访问内存等硬件设备,必须通过「系统调用」陷入到内核中,才能访问这些特权资源。
2.2、切换方式
如下图所示:
分为系统调用、中断和异常三种切换方式。
2.3、系统调用
系统调用实际上是一个软件中断,它将执行的上下文从用户模式切换到内核模式。操作系统内核作为更高的特权级别,可以访问保护的内存区域和硬件资源。这是一个非常重要的安全机制,因为它阻止了用户程序直接访问硬件和敏感信息。
以下是一些常见的系统调用:
1、文件操作:
open():打开或创建文件
read():读取文件内容
write():写入文件内容
close():关闭打开的文件
lseek():移动文件的读/写指针
2、进程管理:
fork():创建新的子进程
exit():结束进程
wait():暂停父进程,直到子进程结束
exec():在当前进程上下文中执行新的程序
3、内存管理:
brk()、sbrk():改变数据段的大小
mmap():创建一个新的映射区域
munmap():删除一个映射区域
4、设备管理:
ioctl():对设备进行控制
fcntl():执行各种文件操作
5、通信:
socket():创建一个新的套接字
bind():将套接字绑定到地址
listen()、accept():在套接字上监听连接
connect():发起到另一套接字的连接
send()、recv():发送/接收数据
shutdown():关闭套接字的部分功能
当程序发出系统调用时,它会提供一个系统调用的编号和一组参数来指定操作系统需要执行的具体任务。然后,CPU会将执行上下文切换到内核模式,并开始执行与编号对应的系统调用。
当发出系统调用的时候,如下图所示:
3、在Java领域的体现
操作系统内核态与用户态的划分是计算机系统安全的基石,而Java作为运行在用户态的高级语言,其与操作系统的交互涉及复杂的底层机制。
3.1. JVM与操作系统的交互
如下图所示:
-
JVM本身:大部分运行在用户态,但会通过系统调用与内核交互
-
系统调用示例:
-
文件I/O操作(
java.io
包) -
网络通信(
java.net
包) -
线程管理(
java.lang.Thread
)
-
3.2. Java Native Interface (JNI)
-
本地方法:通过JNI调用的本地代码可能涉及内核态操作
-
风险:错误的本地代码可能导致系统不稳定
3.3. 内存管理
-
Java堆内存:在用户态由JVM管理
-
直接内存(NIO):
ByteBuffer.allocateDirect()
可能涉及内核态的内存分配
3.4. 多线程实现
-
Java线程:通常映射到操作系统原生线程(1:1模型)
-
线程调度:最终由操作系统内核调度器在内核态完成
3.5. 性能考量
-
系统调用开销:频繁的I/O操作会导致用户态/内核态切换,影响性能
-
优化技术:
-
缓冲技术(如BufferedInputStream)
-
批量操作(如NIO的Selector)
-
零拷贝技术(FileChannel.transferTo)
-
4、系统调用
系统调用与进程管理,如下图所示:
更详细的调用可参考:
4.1. 系统调用(Syscall)
以Linux的read()
系统调用为例:
-
用户态触发:调用
libc
的read()
函数 -
软中断:
libc
通过int 0x80
(传统)或syscall
指令(x86-64)陷入内核 -
查表跳转:CPU根据中断描述符表(IDT)找到系统调用处理程序
-
内核执行:切换到内核栈,执行
sys_read()
函数 -
返回用户态:通过
iret
指令恢复用户态上下文
性能开销:一次系统调用约需100~300ns,主要消耗在寄存器保存/恢复和缓存失效(TLB flush)。
4.2. 线程与进程的实现
-
轻量级进程(LWP):Java线程对应内核线程(1:1模型,通过
clone()
系统调用创建) -
调度时机:
-
时间片耗尽(内核态时钟中断触发)
-
主动让出CPU(如
Thread.yield()
最终调用sched_yield()
)
-
-
上下文切换成本:约1~10μs(需切换页表、寄存器、TLB等)
5、注意事项
5.1. 减少模式切换的开销
-
批量IO:使用NIO的
Selector
合并就绪事件(单次系统调用处理多通道)-
减少不必要的系统调用:如合并小文件操作为批量操作
-
-
内存映射文件:
MappedByteBuffer
通过mmap()
直接映射文件到用户空间-
合理设置堆外内存:DirectByteBuffer不受GC管理,需注意内存泄漏
-
-
JNI优化:避免频繁跨越JNI边界(如合并多次本地方法调用)
-
谨慎使用本地方法:避免引入不稳定因素
-
-
线程池大小设置:考虑系统负载和上下文切换开销
5.2. 内核旁路(Kernel Bypass)技术
-
DPDK/Netty Epoll:绕过内核协议栈直接操作网卡(需特定驱动支持)
-
Java的Project Loom:虚拟线程(协程)减少内核线程切换开销
理解这些概念有助于Java开发者编写更高效、更稳定的应用程序,特别是在性能敏感或系统级编程场景中。