Skip to content

Commit 03ef533

Browse files
committed
Update Java Notes
1 parent b415ff4 commit 03ef533

File tree

1 file changed

+28
-25
lines changed

1 file changed

+28
-25
lines changed

Java.md

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8557,13 +8557,12 @@ select 和 poll 对比:
85578557
- select 会修改描述符,而 poll 不会
85588558
- select 的描述符类型使用数组实现,有描述符的限制;而 poll 使用**链表**实现,没有描述符数量的限制
85598559
- poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高
8560-
- 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定
8561-
8562-
* select 和 poll 速度都比较慢,每次调用都需要将全部描述符数组从应用进程缓冲区复制到内核缓冲区
85638560

8561+
* select 和 poll 速度都比较慢,每次调用都需要将全部描述符数组 fd 从应用进程缓冲区复制到内核缓冲区,同时每次都需要在内核遍历传递进来的所有 fd ,这个开销在 fd 很多时会很大
85648562
* 几乎所有的系统都支持 select,但是只有比较新的系统支持 poll
85658563
* select 和 poll 的时间复杂度 O(n),对 socket 进行扫描时是线性扫描,即采用轮询的方法,效率较低,因为并不知道具体是哪个 socket 具有事件,所以随着 FD 的增加会造成遍历速度慢的**线性下降**性能问题
85668564
* poll 还有一个特点是水平触发,如果报告了 fd 后,没有被处理,那么下次 poll 时会再次报告该 fd
8565+
* 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定
85678566

85688567

85698568

@@ -8579,16 +8578,14 @@ select 和 poll 对比:
85798578

85808579
###### 函数
85818580

8582-
epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵**红黑树**上,通过回调函数内核会将 I/O 准备好的描述符加入到一个**链表**中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符
8581+
epoll 使用事件的就绪通知方式,通过 epoll_ctl() 向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵**红黑树**上,一旦该 fd 就绪,内核通过 callback 回调函数将 I/O 准备好的描述符加入到一个**链表**中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符
85838582

85848583
```c
85858584
int epoll_create(int size);
85868585
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
85878586
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
85888587
```
85898588

8590-
epoll 使用事件的就绪通知方式,通过 epoll_ctl 注册 fd,一旦该 fd 就绪,内核就会采用 callback 的回调机制来激活该 fd,epoll_wait 便可以收到通知
8591-
85928589
* epall_create:一个系统函数,函数将在内核空间内开辟一块新的空间,可以理解为 epoll 结构空间,返回值为 epoll 的文件描述符编号,所以 epoll 使用一个文件描述符管理多个描述符
85938590

85948591
* epall_ctl:epoll 的事件注册函数,select 函数是调用时指定需要监听的描述符和事件,epoll 先将用户感兴趣的描述符事件注册到 epoll 空间。此函数是非阻塞函数,用来增删改 epoll 空间内的描述符,参数解释:
@@ -8687,19 +8684,22 @@ else
86878684

86888685
epoll 的特点:
86898686

8687+
* epoll 仅适用于 Linux 系统
86908688
* epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中
8691-
* 没有最大并发连接的限制,能打开的 fd 的上限远大于1024(1G的内存上能监听约10万个端口
8692-
* epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait 不断轮询监听列表,当设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中进入睡眠的进程,所以 epoll 实际上是**事件驱动(每个事件关联上fd)**的
8689+
* 没有最大描述符数量(并发连接)的限制,打开 fd 的上限远大于1024(1G 内存能监听约10万个端口
8690+
* epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait 只是轮询就绪链表。当监听列表有设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中阻塞的进程,所以 epoll 实际上是**事件驱动(每个事件关联上fd)**的
86938691
* epoll 内核中根据每个 fd 上的 callback 函数来实现,只有活跃的 socket 才会主动调用 callback,所以使用 epoll 没有前面两者的线性下降的性能问题,效率提高
8694-
* epoll 仅适用于 Linux 系统
8695-
* epoll 比 select 和 poll 更加灵活而且没有描述符数量限制
8696-
* epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次, 利用 mmap() 文件映射内存加速与内核空间的消息传递,减少复制开销
8692+
8693+
* epoll 每次注册新的事件到 epoll 句柄中时,会把所有的 fd 拷贝进内核,但不是每次 epoll_wait 的时重复拷贝,对比前面两种,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次。 epoll 也可以利用 mmap() 文件映射内存加速与内核空间的消息传递,减少复制开销
8694+
* 前面两者要把 current 往设备等待队列中挂一次,epoll 也只把 current 往等待队列上挂一次,但是这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列,这样节省不少的开销
86978695
* epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符,也不会产生像 select 和 poll 的不确定情况
8698-
* select,poll 每次调用都要把 fd 集合从用户态往内核态拷贝一次,并且要把 current 往设备等待队列中挂一次,而 epoll 只要一次拷贝,而且把 current 往等待队列上挂也只挂一次(注意这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列),这也能节省不少的开销(看流程图会有更好的认识)
8699-
8696+
8697+
87008698

87018699
参考文章:https://www.jianshu.com/p/dfd940e7fca2
87028700

8701+
参考文章:https://www.cnblogs.com/anker/p/3265058.html
8702+
87038703

87048704

87058705
***
@@ -8740,7 +8740,7 @@ epoll 的特点:
87408740
内核空间:内核代码、内核调度程序、进程描述符(内核堆栈、thread_info进程描述符)
87418741

87428742
* 进程描述符和用户的进程是一一对应的
8743-
* SYS_API系统调用:如 read、write系统调用就是 0X80 中断
8743+
* SYS_API 系统调用:如 read、write系统调用就是 0X80 中断
87448744
* 进程描述符pd:进程从用户态切换到内核态时,需要保存用户态时的上下文信息,
87458745
* 线程上下文:用户程序基地址,程序计数器、cpu cache、寄存器等,方便程序切回用户态时恢复现场
87468746
* 内核堆栈:系统调用函数也是要创建变量的,这些变量在内核堆栈上分配
@@ -8759,7 +8759,8 @@ epoll 的特点:
87598759

87608760
* 硬中断:如网络传输中,数据到达网卡后,网卡经过一系列操作后发起硬件中断
87618761
* 软中断:如程序运行过程中本身产生的一些中断
8762-
- 如进行系统调用 system_call,则发起 `0X80` 中断
8762+
- 如进行系
8763+
- 发起 `0X80` 中断
87638764
- 如程序执行碰到除 0 异常
87648765

87658766
系统调用 system_call 函数所对应的中断指令编号是 0X80(十进制是8×16=128),而该指令编号对应的就是系统调用程序的入口,所以称系统调用为 80 中断
@@ -8794,7 +8795,7 @@ DMA (Direct Memory Access) :直接存储器访问,让外部设备不通过 C
87948795
把内存数据传输到网卡然后发送:
87958796

87968797
* 没有 DMA:CPU 读内存数据到 CPU 高速缓存,再写到网卡,这样就把 CPU 的速度拉低到和网卡一个速度
8797-
* 使用DMA:把内存数据读到 socket 内核缓存区(CPU复制),CPU 分配给 DMA 开始**异步**操作,DMA 读取 socket 缓冲区到 DMA 缓冲区,然后写到网卡。DMA 执行完后中断 CPU,这时 socket 内核缓冲区为空,CPU 从用户态切换到内核态,执行中断处理程序,将需要使用 socket 缓冲区的阻塞进程移到运行队列
8798+
* 使用 DMA:把内存数据读到 socket 内核缓存区(CPU复制),CPU 分配给 DMA 开始**异步**操作,DMA 读取 socket 缓冲区到 DMA 缓冲区,然后写到网卡。DMA 执行完后中断 CPU,这时 socket 内核缓冲区为空,CPU 从用户态切换到内核态,执行中断处理程序,将需要使用 socket 缓冲区的阻塞进程移到运行队列
87988799

87998800
一个完整的 DMA 传输过程必须经历 DMA 请求、DMA 响应、DMA 传输、DMA 结束四个步骤:
88008801

@@ -8818,14 +8819,16 @@ DMA 方式是一种完全由硬件进行组信息传送的控制方式,通常
88188819

88198820
传统的 I/O 操作进行了 4 次用户空间与内核空间的上下文切换,以及 4 次数据拷贝:
88208821

8821-
* JVM发出read() 系统调用,OS 上下文切换到内核模式(切换1)并将数据从网卡或硬盘等通过 DMA 读取到内核空间缓冲区(拷贝1)
8822+
* JVM 发出 read() 系统调用,OS 上下文切换到内核模式(切换1)并将数据从网卡或硬盘等通过 DMA 读取到内核空间缓冲区(拷贝1)
88228823
* OS 内核将数据复制到用户空间缓冲区(拷贝2),然后 read 系统调用返回,又会导致一次内核空间到用户空间的上下文切换(切换2)
88238824
* JVM 处理代码逻辑并发送 write() 系统调用,OS 上下文切换到内核模式(切换3)并从用户空间缓冲区复制数据到内核空间缓冲区(拷贝3)
88248825
* write 系统调用返回,导致内核空间到用户空间的再次上下文切换(切换4),将内核空间缓冲区中的数据写到 hardware(拷贝4)
88258826

8827+
流程图中的箭头反过来也成立,可以从网卡获取数据
8828+
88268829
![](https://gitee.com/seazean/images/raw/master/Java/IO-BIO工作流程.png)
88278830

8828-
read 调用图示:
8831+
read 调用图示:read、write 都是系统调用指令
88298832

88308833
<img src="https://gitee.com/seazean/images/raw/master/Java/IO-缓冲区读写.png" style="zoom: 67%;" />
88318834

@@ -12303,7 +12306,9 @@ public static void main(String[] args) {
1230312306

1230412307
方法区是一个 JVM 规范,**永久代与元空间都是其一种实现方式**
1230512308

12306-
方法区的大小不必是固定的,可以动态扩展;方法区大小很难确定,因此加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError);对这块区域进行垃圾回收主要是对常量池的回收和对类的卸载,比较难实现
12309+
方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError)
12310+
12311+
方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现
1230712312

1230812313
为了**避免方法区出现OOM**,在JDK8中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,静态变量和字符串常量池等放入堆中
1230912314

@@ -12317,8 +12322,6 @@ public static void main(String[] args) {
1231712322
* 类在解析阶段将符号引用替换成直接引用
1231812323
* 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()
1231912324

12320-
方法区的 GC:针对常量池的回收及对类型的卸载
12321-
1232212325

1232312326

1232412327
***
@@ -12398,9 +12401,9 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制
1239812401

1239912402
##### 基本介绍
1240012403

12401-
直接内存是Java堆外的、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域
12404+
直接内存是 Java 堆外的、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域
1240212405

12403-
Direct Memory优点
12406+
Direct Memory 优点
1240412407

1240512408
* Java 的 NIO 库允许 Java 程序使用直接内存,用于数据缓冲区,使用 native 函数直接分配堆外内存
1240612409
* 读写性能高,读写频繁的场合可能会考虑使用直接内存
@@ -14175,7 +14178,7 @@ public class Test {
1417514178
* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
1417614179
* 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类
1417714180
* MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, 就必须先使用findStaticVarHandle来初始化要调用的类
14178-
* 补充:当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化
14181+
* 补充:当一个接口中定义了 JDK8 新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化
1417914182

1418014183
**被动引用**:所有引用类的方式都不会触发初始化,称为被动引用
1418114184

@@ -14213,7 +14216,7 @@ init指的是实例构造器,主要作用是在类实例化过程中执行,
1421314216
2. 该类没有在其他任何地方被引用
1421414217
3. 该类的类加载器的实例已被GC
1421514218

14216-
在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的,但是由我们自定义的类加载器加载的类是可能被卸载
14219+
在JVM生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,由我们自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,所以这些类始终是可及的
1421714220

1421814221

1421914222

0 commit comments

Comments
 (0)