Browse Source

Merge branch 'lab' of https://gitee.com/sun1040084806/notes into lab

seamew 1 year ago
parent
commit
c90a687400

+ 16 - 0
面经/问答/JVM.md

@@ -6,6 +6,20 @@
 4. 空间分配担保失败在发生**Minor GC**之前,虚拟机会检查**老年代最大可用的连续空间**是否**大于新生代所有对象的总空间**,如果大于,则此次**Minor GC是安全的**。如果小于,则虚拟机会查看**HandlePromotionFailure**设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于**历次晋升到老年代的对象的平均大小**,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。
 5. 未指定老年代和新生代大小,堆伸缩时会产生fullgc,所以一定要配置-Xmx、-Xms
 
+## 什么时候会触发Minor Gc?
+
+Minor GC 通常在新生代中的 Eden 区域满时,或者 Survivor 区域中的对象年龄达到一定阈值时触发。下面分别介绍这两种情况:
+
+1. Eden 区域满时:当 Java 应用程序创建新对象时,这些对象会被分配到 Eden 区域中。当 Eden 区域满时,无法再分配出任何连续的内存空间,此时就需要进行 Minor GC 了。Minor GC 会将 Eden 区域中不再使用的对象清理掉,同时将存活的对象移到 Survivor 区域。
+2. Survivor 区域中的对象年龄达到一定阈值:当对象在 Survivor 区域中经历一定次数的垃圾回收后,其年龄就会增加。当对象年龄达到一定阈值时,就会被移到老年代中去。这个阈值的默认值为 15,也可以通过 JVM 参数 -XX:MaxTenuringThreshold 来进行设置。当 Survivor 区域中的对象年龄达到一定阈值时,也会触发 Minor GC。
+
+总的来说,Minor GC 是针对新生代中的垃圾回收,它通常在 Eden 区域满时或者 Survivor 区域中对象年龄达到一定阈值时被触发。Minor GC 的目的是清除新生代中不再使用的对象,同时将存活的对象移到 Survior 区域和老年代中。
+
+## Minor Gc 和 Full GC有什么不同呢?
+
+- **Major GC 是清理老年代。**
+- **Full GC 是清理整个堆空间—包括年轻代和老年代。**
+
 ## JVM调优
 
 -Xmx –Xms:指定 java 堆最大值(默认值是物理内存的 1/4(<1GB))和初始 java 堆最小值(默认值是物理内存的 1/64(<1GB))
@@ -130,3 +144,5 @@ jvm内存区域大体上可以分为线程私有和线程共有两大部分
 
 ![1](assets/061921034534396.png)
 
+
+

BIN
面经/问答/assets/image-20230506170615271.png


BIN
面经/问答/assets/image-20230506170616683.png


BIN
面经/问答/assets/image-20230508141542098.png


BIN
面经/问答/assets/image-20230508141543630.png


BIN
面经/问答/assets/image-20230508142326378.png


BIN
面经/问答/assets/image-20230508143024494.png


BIN
面经/问答/assets/image-20230508143143138.png


BIN
面经/问答/assets/image-20230508161022413.png


BIN
面经/问答/assets/image-20230508161218693.png


+ 28 - 0
面经/问答/基础.md

@@ -0,0 +1,28 @@
+## 为什么重写 equals() 时必须重写 hashCode() 方法?
+
+根据Java规范的规定,如果两个对象相等,那么它们的哈希值必须相等。也就是说如果 `equals` 方法判断两个对象是相等的,那这两个对象的 `hashCode` 值也要相等。如果重写 `equals()` 时没有重写 `hashCode()` 方法的话就可能会导致 `equals` 方法判断是相等的两个对象,`hashCode` 值却不相等。
+
+例如hashset集合中插入的对象重写 equals()但是没有重写 hashCode() 就会导致插入重复的元素。
+
+## 在java中,mian线程中申请某一个线程,main线程运行结束,申请的新线程为什么还会继续运行?
+
+在 Java 中,与其他操作系统一样,线程是通过调度器来管理执行的。当一个进程中所有的非守护线程都执行完毕后,该进程会自动结束并终止所有守护线程。因此,如果在 main 线程中创建了一个新线程,main 线程结束之后,该新线程仍然会继续运行。
+
+这是因为在Java中,除了守护线程以外,每个线程都是独立执行的,并且拥有自己独立的执行堆栈和执行上下文。在 main 线程中创建一个新线程后,该新线程会被 JVM 调度器分配 CPU 时间片,并独立运行。这时即使 main 线程结束,该新线程仍然可以继续运行,直到执行完毕或者被显式地终止。
+
+需要注意的是,如果该新线程是守护线程,那么在所有非守护线程执行完毕后,JVM 会自动终止所有守护线程并退出进程。因此,如果该新线程是守护线程,则在 main 线程结束之后可能会被强制终止,而不会继续运行。
+
+## java线程生命周期
+
+Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
+
+- NEW: 初始状态,线程被创建出来但没有被调用 `start()` 。
+- RUNNABLE: 运行状态,线程被调用了 `start()`等待运行的状态。
+- BLOCKED:阻塞状态,需要等待锁释放。
+- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
+- TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
+- TERMINATED:终止状态,表示该线程已经运行完毕。
+
+线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换
+
+![image-20230508161022413](assets/image-20230508161022413.png)

+ 199 - 0
面经/问答/并发编程.md

@@ -0,0 +1,199 @@
+## 为什么wait和notify必须放在synchronized中?
+
+在Java中,wait()和notify()是Object类的两个方法,它们用于实现线程间的协作。wait()使一个线程进入等待状态,直到另一个线程发出通知唤醒它。而notify()则用于唤醒正在等待的线程。
+
+wait()和notify()必须放在synchronized块中是因为这些方法依赖于对象的监视器锁(也称为互斥锁)。只有获得了对象的监视器锁(该锁即为调用wait方法的对象)的线程才能调用wait()和notify()方法。如果这些方法不在同步块中使用,就无法保证线程安全性。
+
+当一个线程调用wait()方法时,它会释放持有的锁并进入等待状态,等待其他线程调用notify()方法来唤醒它。如果wait()方法不在同步块中使用,那么该线程在释放锁之后可能已经被其他线程修改了。这样,即使其他线程调用了notify()方法唤醒了它,该线程也无法正确地处理唤醒事件。
+
+同样,notify()方法也必须在同步块中使用。如果一个线程在未获得锁的情况下调用notify()方法,那么它将无法通知任何等待的线程。因为它没有获取锁,所以它不能访问共享数据或执行必要的同步操作来确保正确的通知。
+
+因此,使用wait()和notify()方法时,必须在同步块中使用它们,以确保线程之间的安全性并避免出现竞态条件。
+
+## volatile 关键字
+
+在Java中,volatile关键字是用于保证多线程环境下变量的可见性和有序性。主要分为两大功能。
+
+1. 保证变量可见性:被volatile关键字声明代表变量是共享且不稳定的,每次使用它都到主存中进行读取,并且强制刷新到内存。
+2. 进制指令重排序:JVM 具有指令重排的特性,可以保证程序执行效率更高,但是volatile关键字会进制指令重排保证其有序性。
+
+## volatile 关键字底层原理
+
+`volatile`是通过编译器在生成字节码时,在指令序列中添加“**内存屏障**”来禁止指令重排序的。
+
+JMM层面的“**内存屏障**”:
+
+- **LoadLoad屏障**: 对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
+- **StoreStore屏障**:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
+- **LoadStore屏障**:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作执行前,保证Load1要读取的数据被读取完毕。
+- **StoreLoad屏障**: 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
+
+JVM的实现会在volatile读写前后均加上内存屏障,在一定程度上保证有序性。如下所示:
+
+> **LoadLoad**
+> **volatile 读操作**
+> **LoadStore**
+>
+> **StoreStore**
+> **volatile 写操作**
+> **StoreLoad**
+
+**禁止指令重排,汇编层面**
+
+>  **lock 前缀**:lock不是内存屏障,而是一种锁。执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU
+
+## synchronized 关键字
+
+`synchronized` 是 Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
+
+在JDK1.6之前synchronized关键字基于操作系统mutux互斥变量实现,该操作较为重量级,但是在JDK1.6之后进行的JVM层面的锁优化。效率大大提升,优化后的锁主要分为一下四类
+
+1. 无锁状态
+2. 偏向锁状态
+3. 轻量级锁状态
+4. 重量级锁状态
+
+锁可以升级,但不能降级。即:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的。首先程序判断该对象是否有锁,如果没有锁则将无锁状态转换为偏向锁状态,这里通过CAS操作进行加锁,如果加锁成功之后,就不会再有解锁等操作了。假如有两个线程来竞争该锁话,那么偏向锁就失效了,进而升级成轻量级锁了。
+
+轻量级之所以是轻量级锁,是因为它仅仅使用 CAS 进行操作来获取锁。如果获取成功那么会直接获取锁,如果失败,当前线程便尝试使用自旋来获取锁。当竞争线程的自旋次数达到界限值(`threshold`),轻量级锁将会膨胀为重量级锁。
+
+重量级锁(`heavy weight lock`),是使用操作系统互斥量(`mutex`)来实现的传统锁。 当所有对锁的优化都失效时,将退回到重量级锁。它与轻量级锁不同竞争的线程不再通过自旋来竞争线程, 而是直接进入堵塞状态,此时不消耗CPU,然后等拥有锁的线程释放锁后,唤醒堵塞的线程, 然后线程再次竞争锁。但是注意,当锁膨胀(`inflate`)为重量锁时,就不能再退回到轻量级锁。
+
+## synchronized 和 volatile 有什么区别?
+
+* `volatile` 关键字是线程同步的轻量级实现,所以性能肯定比`synchronized`关键字要好 。
+
+* `volatile` 关键字只能用于变量而 `synchronized` 关键字可以修饰方法以及代码块 。
+
+* `volatile` 关键字能保证数据的可见性,但不能保证数据的原子性。`synchronized` 关键字两者都能保证。
+
+* `volatile`关键字主要用于解决变量在多个线程之间的可见性,而 `synchronized` 关键字解决的是多个线程之间访问资源的同步性。
+
+## ReentrantLock锁
+
+`ReentrantLock` 实现了 `Lock` 接口,是一个可重入且独占式的锁,他的底层是基于CAS+AQS+LockSupport实现的
+
+* CAS
+
+CAS是Compare and Swap的缩写,即比较并交换。它是一种无锁算法,在多线程编程中用于实现同步操作。简单来说,CAS操作包括三个操作数:内存位置V、预期值A和新值B。当且仅当内存位置V的值等于预期值A时,才将该位置的值更新为新值B。
+
+* AQS
+
+AQS抽象队列同步器。它是Java并发包中锁和同步工具的核心实现上面的所说的LOCK锁就是基于AQS实现的
+
+AQS提供了一种通用的框架,用于实现线程间的协作和同步操作。它的核心思想是使用一个先进先出的等待队列来管理线程状态,同时支持独占模式和共享模式两种同步方式。
+
+![image-20230506170616683](assets/image-20230506170616683.png)
+
+* LockSupport
+
+基于C语言底层实现,它可以阻塞线程以等待许可证,或者取消线程的阻塞状态,而不需要使用传统的synchronized关键字或Object.wait()/notify()方法。
+
+## synchronized 和 ReentrantLock 有什么区别?
+
+* synchronized 依赖于 JVM 而 `ReentrantLock` 依赖于 API
+* `ReentrantLock`可以指定是公平锁还是非公平锁。而`synchronized`只能是非公平锁。
+* `synchronized`是以代码块形式实现的,因此只能对整个代码块进行同步操作,无法在代码块内部实现一些细粒度的控制。而`ReentrantLock`可以通过`Condition`对象实现线程间的协作和控制。
+
+## LockSupport和synchronized的区别
+
+* LockSupport不需要获取锁对象,因此避免了可能出现的死锁问题。
+
+* LockSupport可以响应中断,而Object.wait()/notify()方法无法响应中断。
+
+* LockSupport可以先执行unpark()方法,然后再执行park()方法,而Object.notify()方法必须在对应的wait()方法之前执行。
+
+## 线程池
+
+线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。线程池的优点和好处主要有以下三点
+
+* **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
+
+* **提高响应速度**。当任务到达时,任务可以不需要等到线程创建就能立即执行。
+
+* **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
+
+### 常见的线程池:
+
+* **`FixedThreadPool`**:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
+
+* **`SingleThreadExecutor`:** 该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
+
+* **`CachedThreadPool`:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
+
+* **`ScheduledThreadPool`**:该返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。
+
+### 为什么不推荐使用内置线程池?
+
+* **`FixedThreadPool` 和 `SingleThreadExecutor`**:使用的是无界的 `LinkedBlockingQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。
+
+* **`CachedThreadPool`**:使用的是同步队列 `SynchronousQueue`, 允许创建的线程数量为 `Integer.MAX_VALUE` ,可能会创建大量线程,从而导致 OOM。
+
+* **`ScheduledThreadPool` 和 `SingleThreadScheduledExecutor`** : 使用的无界的延迟阻塞队列`DelayedWorkQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。
+
+总之一句话,他们的队列长度都是MAX_VALUE,可能导致OOM
+
+### 线程池核心参数
+
+* **`corePoolSize` :** 任务队列未达到队列容量时,最大可以同时运行的线程数量。
+
+* **`maximumPoolSize` :** 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
+
+* **`workQueue`:** 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
+
+1. 当线程数小于核心线程数时,创建线程。
+
+2. 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列
+
+3. 当线程数大于等于核心线程数,且任务队列已满
+
+   * 若线程数小于最大线程数,创建线程。
+
+   * 若线程数等于最大线程数,抛出异常,拒绝任务,执行拒绝策略
+
+### 线程池的饱和策略有哪些?
+
+* **`ThreadPoolExecutor.AbortPolicy`:** 抛出 `RejectedExecutionException`来拒绝新任务的处理。
+* **`ThreadPoolExecutor.CallerRunsPolicy`:** 调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
+* **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。
+* **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。
+
+我们这里使用自定义的饱和策略,即为新建一个线程去处理饱和任务
+
+### 如何设定线程池的大小?
+
+* **CPU 密集型任务(N+1)**
+
+* **I/O 密集型任务(2N)**
+
+CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
+
+## ThreadLocal
+
+通常情况下,我们创建的变量是可以被任何一个线程访问并修改的,想要每一个线程都有自己私有的本地变量就就需要使用ThreadLocal类,**`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。**
+
+`ThreadLocal`底层为每一个线程维护了一个`ThreadLocalMap`,他的set方法如下
+
+```java
+public void set(T value) {
+    //获取当前请求的线程
+    Thread t = Thread.currentThread();
+    //取出 Thread 类内部的 threadLocals 变量(哈希表结构)
+    ThreadLocalMap map = getMap(t);
+    if (map != null)
+        // 将需要存储的值放入到这个哈希表中
+        map.set(this, value);
+    else
+        createMap(t, value);
+}
+ThreadLocalMap getMap(Thread t) {
+    return t.threadLocals;
+}
+```
+
+## ThreadLocal 内存泄露问题是怎么导致的?
+
+`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
+
+这样一来,`ThreadLocalMap` 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。`ThreadLocalMap` 实现中已经考虑了这种情况,使用完 `ThreadLocal`方法后最好手动调用`remove()`方法
+

+ 191 - 0
面经/问答/操作系统.md

@@ -0,0 +1,191 @@
+## 虚拟内存
+
+**虚拟内存**是逻辑存在的内存,他的主要作用的简化内存管理。
+
+总的来说虚拟内存提供了一下几个功能
+
+1. **隔离进程**:物理内存通过虚拟地址空间访问,虚拟地址空间与进程一一对应。每个进程都认为自己拥有了整个物理内存,进程之间彼此隔离,一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。
+2. **提升物理内存利用率**:有了虚拟地址空间后,操作系统只需要将进程当前正在使用的部分数据或指令加载入物理内存。
+3. **简化内存管理**:进程都有一个一致且私有的虚拟地址空间,程序员不用和真正的物理内存打交道,而是借助虚拟地址空间访问物理内存,从而简化了内存管理。
+4. **提供更大的可使用内存空间**:可以让程序拥有超过系统物理内存大小的可用内存空间。这是因为当物理内存不够用时,可以利用磁盘充当,将物理内存页(通常大小为 4 KB)保存到磁盘文件(会影响读写速度),数据或代码页会根据需要在物理内存与磁盘之间移动。
+
+## 虚拟地址与物理内存地址是如何映射的?
+
+MMU**(Memory Management Unit,内存管理单元)**将虚拟地址翻译为物理地址的主要机制有 3 种:
+
+1. 分段机制
+2. 分页机制
+3. 段页机制
+
+其中,现代操作系统广泛采用分页机制,需要重点关注!
+
+### 分段机制
+
+程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。**不同的段是有不同的属性的,所以就用分段(Segmentation)的形式把这些段分离出来。**
+
+![image-20230508141543630](assets/image-20230508141543630.png)
+
+分段机制容易出现外部内存碎片,即在段与段之间留下碎片空间(不足以映射给虚拟地址空间中的段)。从而造成物理内存资源利用率的降低。解决「外部内存碎片」的问题就是**内存交换**(内存交换空间就是swap空间)。但是**如果内存交换的时候,交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿。**所以为了解决内存分段的「外部内存碎片和内存交换效率低」的问题,就出现了内存分页。
+
+### 分页机制
+
+**分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小**。在 Linux 下,每一页的大小为 `4KB`。
+
+![image-20230508142326378](assets/image-20230508142326378.png)
+
+页表是存储在内存里的,**内存管理单元** (MMU)就做将虚拟内存地址转换成物理地址的工作。
+
+内存分页由于内存空间都是预先划分好的,也就不会像内存分段一样,在段与段之间会产生间隙非常小的内存,这正是分段会产生外部内存碎片的原因。而**采用了分页,页与页之间是紧密排列的,所以不会有外部碎片。**但是,因为内存分页机制分配内存的最小单位是一页,即使程序不足一页大小,我们最少只能分配一个页,所以页内会出现内存浪费,所以针对**内存分页机制会有内部内存碎片**的现象。
+
+#### 多级页表
+
+为了解决一级页表过大,导致占用内存过多的问题。所以就产生了多级页表
+
+####  TLB
+
+多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了几道转换的工序,这显然就降低了这俩地址转换的速度,也就是带来了时间上的开销。
+
+TLB即为页表缓存、转址旁路缓存、快表等。有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。
+
+![image-20230508143024494](assets/image-20230508143024494.png)
+
+
+
+### 段页机制
+
+内存分段和内存分页并不是对立的,它们是可以组合起来在同一个系统中使用的,那么组合起来后,通常称为**段页式内存管理**。
+
+虚拟内存地址结构就由**段号、段内页号和页内位移**三部分组成。
+
+![image-20230508143143138](assets/image-20230508143143138.png)
+
+## 缺页中断
+
+进程访问的虚拟地址在页表中查不到时,系统会产生一个**缺页异常**,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。
+
+缺页中断主要分为一下两种:
+
+* **硬性页缺失(Hard Page Fault)**:物理内存中没有对应的物理页。此时MMU就会建立相应的虚拟页和物理页的映射关系。
+
+如果发生硬性页缺失,CPU会看是否有空闲的物理内存,如果有,就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系。
+
+如果物理内存中没有空闲的物理页面(有虚拟内存可以用,不会发生OOM)可用的话。操作系统就必须将物理内存中的一个物理页淘汰出去,这样就可以腾出空间来加载新的页面了。更新页表主要有一下几种算法
+
+1. **最佳页面置换算法(OPT,Optimal)**:优先选择淘汰的页面是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若干页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现,只是理论最优的页面置换算法,可以作为衡量其他置换算法优劣的标准。
+
+2. **先进先出页面置换算法(FIFO,First In First Out)** : 最简单的一种页面置换算法,总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。该算法易于实现和理解,一般只需要通过一个 FIFO 队列即可需求。不过,它的性能并不是很好。
+
+3. **最近最久未使用页面置换算法(LRU ,Least Recently Used)**:LRU 算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。LRU 算法是根据各页之前的访问情况来实现,因此是易于实现的。OPT 算法是根据各页未来的访问情况来实现,因此是不可实现的。
+
+4. **最少使用页面置换算法(LFU,Least Frequently Used)** : 和 LRU 算法比较像,不过该置换算法选择的是之前一段时间内使用最少的页面作为淘汰页。
+
+* **软性页缺失(Soft Page Fault)**:物理内存中有对应的物理页,但虚拟页还未和物理页建立映射。此时MMU就会建立相应的虚拟页和物理页的映射关系。
+
+## 在 4GB 物理内存的机器上,申请 8G 内存会怎么样?
+
+- 在 32 位操作系统,因为进程理论上最大能申请 3 GB 大小的虚拟内存,所以直接申请 8G 内存,会申请失败。
+- 在 64位 位操作系统,因为进程理论上最大能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请 8G 内存也是没问题,因为申请的内存是虚拟内存。如果这块虚拟内存被访问了,要看系统有没有 Swap 分区:
+  - 如果没有 Swap 分区,因为物理空间不够,进程会被操作系统杀掉,原因是 OOM(内存溢出);
+  - 如果有 Swap 分区,即使物理内存只有 4GB,程序也能正常使用 8GB 的内存,进程可以正常运行;
+
+## 进程和线程
+
+* **进程(Process)** 是指计算机中正在运行的一个程序实例。
+* **线程(Thread)** 也被称为轻量级进程,更加轻量。多个线程可以在同一个进程中同时执行,并且共享进程的资源比如内存空间、文件句柄、网络连接等。
+
+### PCB 是什么?包含哪些信息?
+
+**PCB(Process Control Block)** 即进程控制块,是操作系统中用来管理和跟踪进程的数据结构,每个进程都对应着一个独立的 PCB。你可以将 PCB 视为进程的大脑。
+
+当操作系统创建一个新进程时,会为该进程分配一个唯一的进程 ID,并且为该进程创建一个对应的进程控制块。当进程执行时,PCB 中的信息会不断变化,操作系统会根据这些信息来管理和调度进程。
+
+**进程描述信息:**
+
+- 进程标识符:标识各个进程,每个进程都有一个并且唯一的标识符;
+- 用户标识符:进程归属的用户,用户标识符主要为共享和保护服务;
+
+**进程控制和管理信息:**
+
+- 进程当前状态,如 new、ready、running、waiting 或 blocked 等;
+- 进程优先级:进程抢占 CPU 时的优先级;
+
+**资源分配清单:**
+
+- 有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的 I/O 设备信息。
+
+**CPU 相关信息:**
+
+- CPU 中各个寄存器的值,当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行。
+
+### 进程间的通信方式有哪些?
+
+1. **管道/匿名管道(Pipes)**:用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
+
+2. **信号(Signal)**:信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
+
+3. **消息队列(Message Queuing)**:消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。**消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点。**
+
+4. **信号量(Semaphores)**:信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
+
+5. **共享内存(Shared memory)**:使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
+
+6. **套接字(Sockets)** : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
+
+### 进程和线程的区别
+
+- 线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。
+- 线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
+- 线程执行开销小,但不利于资源的管理和保护;而进程正相反。
+
+### 线程的通信方式
+
+* 互斥锁
+* 信号量
+
+### 线程间的同步的方式有哪些?
+
+下面是几种常见的线程同步的方式:
+
+1. **互斥锁**:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
+2. **读写锁:允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
+3. **信号量**:它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
+4. **屏障**:屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。
+5. **事件** :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
+
+## 进程生命周期
+
+我们一般把进程大致分为 5 种状态,这一点和线程很像!
+
+- **创建状态(new)**:进程正在被创建,尚未到就绪状态。
+- **就绪状态(ready)**:进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
+- **运行状态(running)**:进程正在处理器上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。
+- **阻塞状态(waiting)**:又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
+- **结束状态(terminated)**:进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。
+
+![image-20230508161218693](assets/image-20230508161218693.png)
+
+## 死锁
+
+死锁(Deadlock)描述的是这样一种情况:多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。
+
+产生死锁的四个必要条件
+
+1. **互斥**:资源必须处于非共享模式,即一次只有一个进程可以使用。如果另一进程申请该资源,那么必须等待直到该资源被释放为止。
+
+2. **占有并等待**:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。
+
+3. **非抢占**:资源不能被抢占。只能在持有资源的进程完成任务后,该资源才会被释放。
+
+4. **循环等待**:有一组等待进程 `{P0, P1,..., Pn}`, `P0` 等待的资源被 `P1` 占有,`P1` 等待的资源被 `P2` 占有,......,`Pn-1` 等待的资源被 `Pn` 占有,`Pn` 等待的资源被 `P0` 占有。
+
+## 如何预防死锁
+
+破坏第一个条件 **互斥条件**:使得资源是可以同时访问的,这是种简单的方法,磁盘就可以用这种方法管理,但是我们要知道,有很多资源 **往往是不能同时访问的** ,所以这种做法在大多数的场合是行不通的。
+
+破坏第三个条件 **非抢占**:也就是说可以采用 **剥夺式调度算法**,但剥夺式调度方法目前一般仅适用于 **主存资源** 和 **处理器资源** 的分配,并不适用于所有的资源,会导致 **资源利用率下降**。
+
+所以一般比较实用的 **预防死锁的方法**,是通过考虑破坏第二个条件和第四个条件。
+
+1. **静态分配策略**:一个进程必须在执行前就申请到它所需要的全部资源
+2. **层次分配策略**:一个进程得到某一次的一个资源后,它只能再申请较高一层的资源
+