diff --git a/Java/ALG.md b/Java/ALG.md
new file mode 100644
index 0000000..5067ae5
--- /dev/null
+++ b/Java/ALG.md
@@ -0,0 +1,1839 @@
+# ALG
+
+## 递归
+
+### 概述
+
+算法:解题方案的准确而完整的描述,是一系列解决问题的清晰指令,代表着用系统的方法解决问题的策略机制
+
+递归:程序调用自身的编程技巧
+
+递归:
+
+* 直接递归:自己的方法调用自己
+* 间接递归:自己的方法调用别的方法,别的方法又调用自己
+
+递归如果控制的不恰当,会形成递归的死循环,从而导致栈内存溢出错误
+
+
+
+参考书籍:https://book.douban.com/subject/35263893/
+
+
+
+***
+
+
+
+### 算法
+
+#### 核心思想
+
+递归的三要素(理论):
+
+1. 递归的终结点
+2. 递归的公式
+3. 递归的方向:必须走向终结点
+
+```java
+// f(x)=f(x-1)+1; f(1)=1; f(10)=?
+// 1.递归的终结点: f(1) = 1
+// 2.递归的公式:f(x) = f(x - 1) + 1
+// 3.递归的方向:必须走向终结点
+public static int f(int x){
+ if(x == 1){
+ return 1;
+ }else{
+ return f(x-1) + 1;
+ }
+}
+```
+
+
+
+***
+
+
+
+#### 公式转换
+
+```java
+// 已知: f(x) = f(x + 1) + 2, f(1) = 1。求:f(10) = ?
+// 公式转换
+// f(x-1)=f(x-1+1)+2 => f(x)=f(x-1)+2
+//(1)递归的公式: f(n) = f(n-1)- 2 ;
+//(2)递归的终结点: f(1) = 1
+//(3)递归的方向:必须走向终结点。
+public static int f(int n){
+ if(n == 1){
+ return 1;
+ }else{
+ return f(n-1) - 2;
+ }
+}
+```
+
+
+
+#### 注意事项
+
+以上理论只能针对于**规律化递归**,如果是非规律化是不能套用以上公式的!
+非规律化递归的问题:文件搜索,啤酒问题。
+
+
+
+***
+
+
+
+### 应用
+
+#### 猴子吃桃
+
+猴子第一天摘了若干个桃子,当即吃了一半,觉得好不过瘾,然后又多吃了一个。第二天又吃了前一天剩下的一半,觉得好不过瘾,然后又多吃了一个。以后每天都是如此。等到第十天再吃的时候发现只有1个桃子,问猴子第一天总共摘了多少个桃子?
+
+```java
+/*
+(1)公式: f(x+1)=f(x)-f(x)/2-1; ==> 2f(x+1) = f(x) - 2 ==> f(x)=2f(x+1)+2
+(2)终结点:f(10) = 1
+(3)递归的方向:走向了终结点
+*/
+
+public static int f(int x){
+ if(x == 10){
+ return 1;
+ } else {
+ return 2*f(x+1)+2
+ }
+}
+```
+
+
+
+***
+
+
+
+#### 递归求和
+
+```java
+//(1)递归的终点接:f(1) = 1
+//(2)递归的公式: f(n) = f(n-1) + n
+//(3)递归的方向必须走向终结点:
+public static int f(int n){
+ if(n == 1 ) return 1;
+ return f(n-1) + n;
+}
+```
+
+
+
+****
+
+
+
+#### 汉诺塔
+
+```java
+public class Hanoi {
+ public static void main(String[] args) {
+ hanoi('X', 'Y', 'Z', 3);
+ }
+
+ // 将n个块分治的从x移动到z,y为辅助柱
+ private static void hanoi(char x, char y, char z, int n) {
+ if (n == 1) {
+ System.out.println(x + "→" + z); // 直接将x的块移动到z
+ } else {
+ hanoi(x, z, y, n - 1); // 分治处理n-1个块,先将n-1个块借助z移到y
+ System.out.println(x + "→" + z); // 然后将x最下面的块(最大的)移动到z
+ hanoi(y, x, z, n - 1); // 最后将n-1个块从y移动到z,x为辅助柱
+ }
+ }
+}
+```
+
+时间复杂度 O(2^n)
+
+
+
+****
+
+
+
+#### 啤酒问题
+
+非规律化递归问题,啤酒 2 元 1 瓶,4 个盖子可以换 1 瓶,2 个空瓶可以换 1 瓶
+
+```java
+public class BeerDemo{
+ // 定义一个静态变量存储可以喝酒的总数
+ public static int totalNum;
+ public static int lastBottleNum;
+ public static int lastCoverNum;
+ public static void main(String[] args) {
+ buyBeer(10);
+ System.out.println("总数:"+totalNum);
+ System.out.println("剩余盖子:"+ lastCoverNum);
+ System.out.println("剩余瓶子:"+ lastBottleNum);
+ }
+ public static void buyBeer(int money){
+ int number = money / 2;
+ totalNum += number;
+ // 算出当前剩余的全部盖子和瓶子数,换算成金额继续购买。
+ int currentBottleNum = lastBottleNum + number ;
+ int currentCoverNum = lastCoverNum + number ;
+ // 把他们换算成金额
+ int totalMoney = 0 ;
+ totalMoney += (currentBottleNum/2)*2; // 除2代表可以换几个瓶子,乘2代表换算成钱,秒!
+ lastBottleNum = currentBottleNum % 2 ;// 取余//算出剩余的瓶子
+
+ totalMoney += (currentCoverNum / 4) * 2;
+ lastCoverNum = currentCoverNum % 4 ;
+
+ // 继续拿钱买酒
+ if(totalMoney >= 2){
+ buyBeer(totalMoney);
+ }
+ }
+}
+```
+
+
+
+
+
+***
+
+
+
+## 排序
+
+### 冒泡排序
+
+冒泡排序(Bubble Sort):两个数比较大小,较大的数下沉,较小的数冒起来
+
+算法描述:每次从数组的第一个位置开始两两比较,把较大的元素与较小的元素进行层层交换,最终把当前最大的一个元素存入到数组当前的末尾
+
+实现思路:
+
+1. 确定总共需要冒几轮:数组的长度-1
+2. 每轮两两比较几次
+
+
+
+```java
+// 0 1位置比较,大的放后面,然后1 2位置比较,大的继续放后面,一轮循环最后一位是最大值
+public class BubbleSort {
+ public static void main(String[] args) {
+ int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88};
+ int flag;//标记本趟排序是否发生了交换
+ //比较i和i+1,不需要再比最后一个位置
+ for (int i = 0; i < arr.length - 1; i++) {
+ flag = 0;
+ //最后i位不需要比,已经排序好
+ for (int j = 0; j < arr.length - 1 - i; j++) {
+ if (arr[j] > arr[j + 1]) {
+ int temp = arr[j];
+ arr[j] = arr[j + 1];
+ arr[j + 1] = temp;
+ flag = 1;//发生了交换
+ }
+ }
+ //没有发生交换,证明已经有序,不需要继续排序,节省时间
+ if(flag == 0) {
+ break;
+ }
+ }
+ System.out.println(Arrays.toString(arr));
+ }
+}
+```
+
+冒泡排序时间复杂度:最坏情况
+
+* 元素比较的次数为:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2`
+* 元素交换的次数为:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2`
+* 总执行次数为:`(N^2/2-N/2)+(N^2/2-N/2)=N^2-N`
+
+按照大 O 推导法则,保留函数中的最高阶项那么最终冒泡排序的时间复杂度为 O(N^2)
+
+
+
+***
+
+
+
+### 选择排序
+
+#### 简单选择
+
+选择排序(Selection-sort):一种简单直观的排序算法
+
+算法描述:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕
+
+实现思路:
+
+1. 控制选择几轮:数组的长度 - 1
+2. 控制每轮从当前位置开始比较几次
+
+
+
+```java
+public class SelectSort {
+ public static void main(String[] args) {
+ int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88};
+ for (int i = 0; i < arr.length - 1; i++) {
+ //获取最小索引位置
+ int minIndex = i;
+ for (int j = i + 1; j < arr.length; j++) {
+ if (arr[minIndex] > arr[j]) {
+ minIndex = j;
+ }
+ }
+ //交换元素
+ int temp = arr[i];
+ arr[i] = arr[minIndex];
+ arr[minIndex] = temp;
+ }
+ System.out.println(Arrays.toString(arr));
+ }
+}
+```
+
+选择排序时间复杂度:
+
+* 数据比较次数:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2`
+* 数据交换次数:`N-1`
+* 时间复杂度:`N^2/2-N/2+(N-1)=N^2/2+N/2-1`
+
+根据大 O 推导法则,保留最高阶项,去除常数因子,时间复杂度为 O(N^2)
+
+
+
+***
+
+
+
+#### 堆排序
+
+堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,堆结构是一个近似完全二叉树的结构,并同时满足子结点的键值或索引总是小于(或者大于)父节点
+
+优先队列:堆排序每次上浮过程都会将最大或者最小值放在堆顶,应用于优先队列可以将优先级最高的元素浮到堆顶
+
+实现思路:
+
+1. 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,并通过上浮对堆进行调整,此堆为初始的无序区,**堆顶为最大数**
+
+2. 将堆顶元素 R[1] 与最后一个元素 R[n] 交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区 Rn,且满足 R[1,2…n-1]<=R[n]
+
+3. 交换后新的堆顶 R[1] 可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将 R[1] 与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn),不断重复此过程直到有序区的元素个数为 n-1,则整个排序过程完成
+
+
+
+floor:向下取整
+
+```java
+public class HeapSort {
+ public static void main(String[] args) {
+ int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88};
+ heapSort(arr, arr.length - 1);
+ System.out.println(Arrays.toString(arr));
+ }
+
+ //high为数组最大索引
+ private static void heapSort(int[] arr, int high) {
+ //建堆,逆排序,因为堆排序定义的交换顺序是从当前结点往下交换,逆序排可以避免多余的交换
+ //i初始值是最后一个节点的父节点,如果参数是数组长度len,则 i = len / 2 -1
+ for (int i = (high - 1) / 2; i >= 0; i--) {
+ //调整函数
+ sift(arr, i, high);
+ }
+ //从尾索引开始排序
+ for (int i = high; i > 0; i--) {
+ //将最大的节点放入末尾
+ int temp = arr[0];
+ arr[0] = arr[i];
+ arr[i] = temp;
+ //继续寻找最大的节点
+ sift(arr, 0, i - 1);
+ }
+ }
+
+ //调整函数,调整arr[low]的元素,从索引low到high的范围调整
+ private static void sift(int[] arr, int low, int high) {
+ //暂存调整元素
+ int temp = arr[low];
+ int i = low, j = low * 2 + 1;//j是左节点
+ while (j <= high) {
+ //判断是否有右孩子,并且比较左右孩子中较大的节点
+ if (j < high && arr[j] < arr[j + 1]) {
+ j++; //指向右孩子
+ }
+ if (temp < arr[j]) {
+ arr[i] = arr[j];
+ i = j; //继续向下调整
+ j = 2 * i + 1;
+ } else {
+ //temp > arr[j],说明也大于j的孩子,探测结束
+ break;
+ }
+ }
+ //将被调整的节点放入最终的位置
+ arr[i] = temp;
+ }
+}
+```
+
+堆排序的时间复杂度是 O(nlogn)
+
+
+
+***
+
+
+
+### 插入排序
+
+#### 直接插入
+
+插入排序(Insertion Sort):在要排序的一组数中,假定前 n-1 个数已经排好序,现在将第 n 个数插到这个有序数列中,使得这 n 个数也是排好顺序的,如此反复循环,直到全部排好顺序
+
+
+
+```java
+public class InsertSort {
+ public static void main(String[] args) {
+ int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88};
+ for (int i = 1; i < arr.length; i++) {
+ for (int j = i; j > 0; j--) {
+ // 比较索引j处的值和索引j-1处的值,
+ // 如果索引j-1处的值比索引j处的值大,则交换数据,
+ // 如果不大,那么就找到合适的位置了,退出循环即可;
+ if (arr[j - 1] > arr[j]) {
+ int temp = arr[j];
+ arr[j] = arr[j - 1];
+ arr[j - 1] = temp;
+ }
+ }
+ }
+ System.out.println(Arrays.toString(arr));
+ }
+}
+```
+
+插入排序时间复杂度:
+
+* 比较的次数为:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2`
+* 交换的次数为:`(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)(N-1)/2=N^2/2-N/2`
+* 总执行次数为:`(N^2/2-N/2)+(N^2/2-N/2)=N^2-N`
+
+按照大 O 推导法则,保留函数中的最高阶项那么最终插入排序的时间复杂度为 O(N^2)
+
+
+
+***
+
+
+
+#### 希尔排序
+
+希尔排序(Shell Sort):也是一种插入排序,也称为缩小增量排序
+
+实现思路:
+
+1. 选定一个增长量 h,按照增长量 h 作为数据分组的依据,对数据进行分组
+2. 对分好组的每一组数据完成插入排序
+3. 减小增长量,最小减为 1,重复第二步操作
+
+
+
+希尔排序的核心在于间隔序列的设定,既可以提前设定好间隔序列,也可以动态的定义间隔序列,希尔排序就是插入排序增加了间隔
+
+```java
+public class ShellSort {
+ public static void main(String[] args) {
+ int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88};
+ // 确定增长量h的初始值
+ int h = 1;
+ while (h < arr.length / 2) {
+ h = 2 * h + 1;
+ }
+ // 希尔排序
+ while (h >= 1) {
+ // 找到待插入的元素
+ for (int i = h; i < arr.length; i++) {
+ // 把待插入的元素插到有序数列中
+ for (int j = i; j >= h; j -= h) {
+ // 待插入的元素是arr[j],比较arr[j]和arr[j-h]
+ if (arr[j] < arr[j - h]) {
+ int temp = arr[j];
+ arr[j] = arr[j - h];
+ arr[j - h] = temp;
+ }
+ }
+ }
+ // 减小h的值,减小规则为:
+ h = h / 2;
+ }
+ System.out.println(Arrays.toString(arr));
+ }
+}
+```
+
+在希尔排序中,增长量 h 并没有固定的规则,有很多论文研究了各种不同的递增序列,但都无法证明某个序列是最好的,所以对于希尔排序的时间复杂度分析就认为 O(nlogn)
+
+
+
+***
+
+
+
+### 归并排序
+
+#### 实现方式
+
+归并排序(Merge Sort):建立在归并操作上的一种有效的排序算法,该算法是采用分治法的典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
+
+实现思路:
+
+1. 一组数据拆分成两个元素相等的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是1为止
+2. 将相邻的两个子组进行合并成一个有序的大组
+3. 不断的重复步骤2,直到最终只有一个组为止
+
+
+
+归并步骤:每次比较两端最小的值,把最小的值放在辅助数组的左边
+
+
+
+
+
+
+
+
+
+
+
+***
+
+
+
+#### 实现代码
+
+```java
+public class MergeSort {
+ public static void main(String[] args) {
+ int[] arr = new int[]{55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88};
+ mergeSort(arr, 0, arr.length - 1);
+ System.out.println(Arrays.toString(arr));
+ }
+ // low为arr最小索引,high为最大索引
+ public static void mergeSort(int[] arr, int low, int high) {
+ // low == high 时说明只有一个元素了,直接返回
+ if (low < high) {
+ int mid = (low + high) / 2;
+ mergeSort(arr, low, mid); // 归并排序前半段
+ mergeSort(arr, mid + 1, high); // 归并排序后半段
+ merge(arr, low, mid, high); // 将两段有序段合成一段有序段
+ }
+ }
+
+ private static void merge(int[] arr, int low, int mid, int high) {
+ int index = 0;
+ // 定义左右指针
+ int left = low, right = mid + 1;
+ int[] assist = new int[high - low + 1];
+
+ while (left <= mid && right <= high) {
+ assist[index++] = arr[left] < arr[right] ? arr[left++] : arr[right++];
+ }
+ while (left <= mid) {
+ assist[index++] = arr[left++];
+ }
+ while (right <= high) {
+ assist[index++] = arr[right++];
+ }
+
+ for (int k = 0; k < assist.length; k++) {
+ arr[low++] = assist[k];
+ }
+ }
+}
+```
+
+
+
+用树状图来描述归并,假设元素的个数为 n,那么使用归并排序拆分的次数为 `log2(n)`,即层数,每次归并需要做 n 次对比,最终得出的归并排序的时间复杂度为 `log2(n)*n`,根据大O推导法则,忽略底数,最终归并排序的时间复杂度为 O(nlogn)
+
+归并排序的缺点:需要申请额外的数组空间,导致空间复杂度提升,是典型的**以空间换时间**的操作
+
+
+
+
+
+****
+
+
+
+### 快速排序
+
+快速排序(Quick Sort):通过**分治思想**对冒泡排序的改进,基本过程是通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,以此达到整个数据变成有序序列
+
+实现思路:
+
+1. 从数列中挑出一个元素,称为基准(pivot)
+2. 重新排序数列,所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边),在这个分区退出之后,该基准就处于数列的中间位置,这个称为分区(partition)操作;
+3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
+
+
+
+```java
+public class QuickSort {
+ public static void main(String[] args) {
+ int[] arr = {55, 22, 2, 5, 1, 3, 8, 5, 7, 4, 3, 99, 88};
+ quickSort(arr, 0, arr.length - 1);
+ System.out.println(Arrays.toString(arr));
+ }
+
+ public static void quickSort(int[] arr, int low, int high) {
+ // 递归结束的条件
+ if (low >= high) {
+ return;
+ }
+
+ int left = low;
+ int right = high;
+ // 基准数
+ int temp = arr[left];
+ while (left < right) {
+ // 用 >= 可以防止多余的交换
+ while (arr[right] >= temp && right > left) {
+ right--;
+ }
+ // 做判断防止相等
+ if (right > left) {
+ // 到这里说明 arr[right] < temp
+ arr[left] = arr[right];// 此时把arr[right]元素视为空
+ left++;
+ }
+ while (arr[left] <= temp && left < right) {
+ left++;
+ }
+ if (right > left) {
+ arr[right] = arr[left];
+ right--;
+ }
+ }
+ // left == right
+ arr[left] = temp;
+ quickSort(arr, low, left-1);
+ quickSort(arr, right + 1, high);
+ }
+}
+```
+
+快速排序和归并排序的区别:
+
+* 快速排序是另外一种分治的排序算法,将一个数组分成两个子数组,将两部分独立的排序
+* 归并排序的处理过程是由下到上的,先处理子问题,然后再合并。而快排正好相反,它的处理过程是由上到下的,先分区,然后再处理子问题
+* 快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将有序的子数组归并从而将整个数组排序,而快速排序的方式则是当两个数组都有序时,整个数组自然就有序了
+* 在归并排序中,一个数组被等分为两半,归并调用发生在处理整个数组之前,在快速排序中,切分数组的位置取决于数组的内容,递归调用发生在处理整个数组之后
+
+时间复杂度:
+
+* 最优情况:每一次切分选择的基准数字刚好将当前序列等分。把数组的切分看做是一个树,共切分了 logn 次,所以,最优情况下快速排序的时间复杂度为 O(nlogn)
+
+* 最坏情况:每一次切分选择的基准数字是当前序列中最大数或者最小数,这使得每次切分都会有一个子组,那么总共就得切分n次,所以最坏情况下,快速排序的时间复杂度为 O(n^2)
+
+
+
+* 平均情况:每一次切分选择的基准数字不是最大值和最小值,也不是中值,这种情况用数学归纳法证明,快速排序的时间复杂度为 O(nlogn)
+
+
+
+推荐视频:https://www.bilibili.com/video/BV1b7411N798?t=1001&p=81
+
+参考文章:https://blog.csdn.net/nrsc272420199/article/details/82587933
+
+
+
+
+
+****
+
+
+
+### 基数排序
+
+基数排序(Radix Sort):又叫桶排序和箱排序,借助多关键字排序的思想对单逻辑关键字进行排序的方法
+
+计数排序其实是桶排序的一种特殊情况,当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶,每个桶内的数据值都是相同的,省掉了桶内排序的时间
+
+按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前
+
+解释:先排低位再排高位,可以说明在高位相等的情况下低位是递增的,如果高位也是递增,则数据有序
+
+
+
+实现思路:
+
+- 获得最大数的位数,可以通过将最大数变为 String 类型,再求长度
+- 将所有待比较数值(正整数)统一为同样的数位长度,**位数较短的数前面补零**
+- 从最低位开始,依次进行一次排序
+- 从最低位排序一直到最高位(个位 → 十位 → 百位 → … →最高位)排序完成以后,数列就变成一个有序序列
+
+```java
+public class BucketSort {
+ public static void main(String[] args) {
+ int[] arr = new int[]{576, 22, 26, 548, 1, 3, 843, 536, 735, 43, 3, 912, 88};
+ bucketSort(arr);
+ System.out.println(Arrays.toString(arr));
+ }
+
+ private static void bucketSort(int[] arr) {
+ // 桶的个数固定为10个(个位是0~9),数组长度为了防止所有的数在同一行
+ int[][] bucket = new int[10][arr.length];
+ //记录每个桶中的有多少个元素
+ int[] elementCounts = new int[10];
+
+ //获取数组的最大元素
+ int max = arr[0];
+ for (int i = 1; i < arr.length; i++) {
+ max = max > arr[i] ? max : arr[i];
+ }
+ String maxEle = Integer.toString(max);
+ //将数组中的元素放入桶中,最大数的位数相当于需要几次放入桶中
+ for (int i = 0, step = 1; i < maxEle.length(); i++, step *= 10) {
+ for (int j = 0; j < arr.length; j++) {
+ //获取最后一位的数据,也就是索引
+ int index = (arr[j] / step) % 10;
+ //放入具体位置
+ bucket[index][elementCounts[index]] = arr[j];
+ //存储每个桶的数量
+ elementCounts[index]++;
+ }
+ //收集回数组
+ for (int j = 0, index = 0; j < 10; j++) {
+ //先进先出
+ int position = 0;
+ //桶中有元素就取出
+ while (elementCounts[j] > 0) {
+ arr[index] = bucket[j][position];
+ elementCounts[j]--;
+ position++;
+ index++;
+ }
+ }
+ }
+ }
+}
+```
+
+空间换时间
+
+
+
+推荐视频:https://www.bilibili.com/video/BV1b7411N798?p=86
+
+参考文章:https://www.toutiao.com/a6593273307280179715/?iid=6593273307280179715
+
+
+
+***
+
+
+
+### 算法总结
+
+#### 稳定性
+
+稳定性:在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中 `r[i]=r[j]`,且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的,否则称为不稳定的
+
+如果一组数据只需要一次排序,则稳定性一般是没有意义的,如果一组数据需要多次排序,稳定性是有意义的。
+
+
+
+* 冒泡排序:只有当 `arr[i]>arr[i+1]` 的时候,才会交换元素的位置,而相等的时候并不交换位置,所以冒泡排序是一种稳定排序算法
+* 选择排序:是给每个位置选择当前元素最小的,例如有数据{5(1),8 ,5(2), 3, 9 },第一遍选择到的最小元素为3,所以5(1)会和3进行交换位置,此时5(1)到了5(2)后面,破坏了稳定性,所以是不稳定的排序算法
+* 插入排序:比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么把要插入的元素放在相等元素的后面。相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的
+* 希尔排序:按照不同步长对元素进行插入排序,虽然一次插入排序是稳定的,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的
+* 归并排序在归并的过程中,只有 `arr[i] arr[mid]) {
+ start = mid + 1;
+ } else if (des < arr[mid]) {
+ end = mid - 1;
+ }
+ }
+ // 如果上述循环执行完毕还没有返回索引,说明根本不存在该元素值,直接返回-1
+ return -1;
+ }
+}
+```
+
+
+
+查找第一个匹配的元素:
+
+```java
+public static int binarySearch(int[] arr, int des) {
+ int start = 0;
+ int end = arr.length - 1;
+
+ while (start <= end) {
+ int mid = (start + end) / 2;
+ if (des == arr[mid]) {
+ //如果 mid 等于 0,那这个元素已经是数组的第一个元素,那肯定是我要找的
+ if (mid == 0 || a[mid - 1] != des) {
+ return mid;
+ } else {
+ //a[mid]前面的一个元素 a[mid-1]也等于 value,
+ //要找的元素肯定出现在[low, mid-1]之间
+ high = mid - 1
+ }
+ } else if (des > arr[mid]) {
+ start = mid + 1;
+ } else if (des < arr[mid]) {
+ end = mid - 1;
+ }
+ }
+ return -1;
+ }
+```
+
+
+
+
+
+***
+
+
+
+## 匹配
+
+### BF
+
+Brute Force 暴力匹配算法:
+
+```java
+public static void main(String[] args) {
+ String s = "seazean";
+ String t = "az";
+ System.out.println(match(s,t));//2
+}
+
+public static int match(String s,String t) {
+ int k = 0;
+ int i = k, j = 0;
+ //防止越界
+ while (i < s.length() && j < t.length()) {
+ if (s.charAt(i) == t.charAt(j)) {
+ ++i;
+ ++j;
+ } else {
+ k++;
+ i = k;
+ j = 0;
+ }
+ }
+ //说明是匹配成功
+ if (j >= t.length()) {
+ return k;
+ }
+ return 0;
+}
+```
+
+平均时间复杂度:O(m+n),最坏时间复杂度:O(m*n)
+
+
+
+***
+
+
+
+### RK
+
+把主串得长度记为 n,模式串得长度记为 m,通过哈希算法对主串中的 n-m+1 个子串分别求哈希值,然后逐个与模式串的哈希值比较大小,如果某个子串的哈希值与模式串相等,再去对比值是否相等(防止哈希冲突),那就说明对应的子串和模式串匹配了
+
+因为哈希值是一个数字,数字之间比较是否相等是非常快速的
+
+第一部分计算哈希值的时间复杂度为 O(n),第二部分对比的时间复杂度为 O(1),整体平均时间复杂度为 O(n),最坏为 O(n*m)
+
+
+
+***
+
+
+
+### KMP
+
+KMP 匹配:
+
+* next 数组的核心就是自己匹配自己,主串代表后缀,模式串代表前缀
+* nextVal 数组的核心就是回退失配
+
+```java
+public class Kmp {
+ public static void main(String[] args) {
+ String s = "acababaabc";
+ String t = "abaabc";
+ //[-1, 0, 0, 1, 1, 2]
+ System.out.println(Arrays.toString(getNext(t)));
+ //[-1, 0, -1, 1, 0, 2]
+ System.out.println(Arrays.toString(getNextVal(t)));
+ //5
+ System.out.println(kmp(s, t));
+ }
+
+ private static int kmp(String s, String t) {
+ int[] next = getNext(t);
+ int i = 0, j = 0;
+ while (i < s.length() && j < t.length()) {
+ //j==-1时说明第一个位置匹配失败,所以将s的下一个和t的首字符比较
+ if (j == -1 || s.charAt(i) == t.charAt(j)) {
+ i++;
+ j++;
+ } else {
+ //模式串右移,比较s的当前位置与t的next[j]位置
+ j = next[j];
+ }
+ }
+ if (j >= t.length()) {
+ return i - j + 1;
+ }
+ return -1;
+ }
+ //next数组
+ private static int[] getNext(String t) {
+ int[] next = new int[t.length()];
+ next[0] = -1;
+ int j = -1;
+ int i = 0;
+ while (i < t.length() - 1) {
+ // 根据已知的前j位推测第j+1位
+ // j=-1说明首位就没有匹配,即t[0]!=t[i],说明next[i+1]没有最大前缀,为0
+ if (j == -1 || t.charAt(i) == t.charAt(j)) {
+ // 因为模式串已经匹配到了索引j处,说明之前的位都是相等的
+ // 因为是自己匹配自己,所以模式串就是前缀,主串就是后缀,j就是最长公共前缀
+ // 当i+1位置不匹配时(i位之前匹配),可以跳转到j+1位置对比,next[i+1]=j+1
+ i++;
+ j++;
+ next[i] = j;
+ } else {
+ //i位置的数据和j位置的不相等,所以回退对比i和next[j]位置的数据
+ j = next[j];
+ }
+
+ }
+ return next;
+ }
+ //nextVal
+ private static int[] getNextVal(String t) {
+ int[] nextVal = new int[t.length()];
+ nextVal[0] = -1;
+ int j = -1;
+ int i = 0;
+ while (i < t.length() - 1) {
+ if (j == -1 || t.charAt(i) == t.charAt(j)) {
+ i++;
+ j++;
+ // 如果t[i+1] == t[next(i+1)]=next[j+1],回退后仍然失配,所以要继续回退
+ if (t.charAt(i) == t.charAt(j)) {
+ nextVal[i] = nextVal[j];
+ } else {
+ nextVal[i] = j;
+ }
+ } else {
+ j = nextVal[j];
+ }
+ }
+ return nextVal;
+ }
+}
+```
+
+平均和最坏时间复杂度都是 O(m+n)
+
+
+
+参考文章:https://www.cnblogs.com/tangzhengyue/p/4315393.html
+
+
+
+***
+
+
+
+## 树
+
+### 二叉树
+
+二叉树中,任意一个节点的度要小于等于 2
+
++ 节点:在树结构中,每一个元素称之为节点
++ 度:每一个节点的子节点数量称之为度
+
+
+
+
+
+****
+
+
+
+### 排序树
+
+#### 存储结构
+
+二叉排序树(BST),又称二叉查找树或者二叉搜索树
+
++ 每一个节点上最多有两个子节点
++ 左子树上所有节点的值都小于根节点的值
++ 右子树上所有节点的值都大于根节点的值
++ 不存在重复的节点
+
+
+
+
+
+
+
+***
+
+
+
+#### 代码实现
+
+* 节点类:
+
+ ```java
+ private static class TreeNode {
+ int key;
+ TreeNode left; //左节点
+ TreeNode right; //右节点
+
+ private TreeNode(int key) {
+ this.key = key;
+ }
+ }
+ ```
+
+* 查找节点:
+
+ ```java
+ // 递归查找
+ private static TreeNode search(TreeNode root, int key) {
+ //递归结束的条件
+ if (root == null) {
+ return null;
+ }
+ if (key == root.key) {
+ return root;
+ } else if (key > root.key) {
+ return search(root.right, key);
+ } else {
+ return search(root.left, key);
+ }
+ }
+
+ // 非递归
+ private static TreeNode search1(TreeNode root, int key) {
+ while (root != null) {
+ if (key == root.key) {
+ return root;
+ } else if (key > root.key) {
+ root = root.right;
+ } else {
+ root = root.left;
+ }
+ }
+ return null;
+ }
+ ```
+
+* 插入节点:
+
+ ```java
+ private static int insert(TreeNode root, int key) {
+ if (root == null) {
+ root = new TreeNode(key);
+ root.left = null;
+ root.right = null;
+ return 1;
+ } else {
+ if (key == root.key) {
+ return 0;
+ } else if (key > root.key) {
+ return insert(root.right, key);
+ } else {
+ return insert(root.left, key);
+ }
+ }
+ }
+ ```
+
+* 构造函数:
+
+ ```java
+ // 构造函数,返回根节点
+ private static TreeNode createBST(int[] arr) {
+ if (arr.length > 0) {
+ TreeNode root = new TreeNode(arr[0]);
+ for (int i = 1; i < arr.length; i++) {
+ insert(root, arr[i]);
+ }
+ return root;
+ }
+ return null;
+ }
+ ```
+
+* 删除节点:要删除节点12,先找到节点19,然后移动并替换节点12
+
+ 代码链接:https://leetcode-cn.com/submissions/detail/190232548/
+
+
+
+参考视频:https://www.bilibili.com/video/BV1iJ411E7xW?t=756&p=86
+
+图片来源:https://leetcode-cn.com/problems/delete-node-in-a-bst/solution/tu-jie-yi-dong-jie-dian-er-bu-shi-xiu-ga-edtn/
+
+
+
+***
+
+
+
+### 平衡树
+
+平衡二叉树(AVL)的特点:
+
++ 二叉树左右两个子树的高度差不超过 1
++ 任意节点的左右两个子树都是一颗平衡二叉树
+
+平衡二叉树旋转:
+
++ 旋转触发时机:当添加一个节点之后,该树不再是一颗平衡二叉树
+
++ 平衡二叉树和二叉查找树对比结构图
+
+
+
++ 左旋:将根节点的右侧往左拉,原先的右子节点变成新的父节点,并把多余的左子节点出让,给已经降级的根节点当右子节点
+
+ 
+
+* 右旋:将根节点的左侧往右拉,左子节点变成了新的父节点,并把多余的右子节点出让,给已经降级根节点当左子节点
+
+ 
+
+推荐文章:https://pdai.tech/md/algorithm/alg-basic-tree-balance.html
+
+
+
+
+***
+
+
+
+### 红黑树
+
+红黑树的特点:
+
+* 每一个节点可以是红或者黑
+
++ 红黑树不是高度平衡的,它的平衡是通过自己的红黑规则进行实现的
+
+红黑树的红黑规则有哪些:
+
+1. 每一个节点或是红色的,或者是黑色的
+2. 根节点必须是黑色
+3. 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为 Nil,这些 Nil 视为叶节点,每个叶节点 (Nil) 是黑色的
+4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况)
+5. 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
+
+红黑树与 AVL 树的比较:
+
+* AVL 树是更加严格的平衡,可以提供更快的查找速度,适用于读取**查找密集型任务**
+* 红黑树只是做到近似平衡,并不是严格的平衡,红黑树的插入删除比 AVL 树更便于控制,红黑树更适合于**插入修改密集型任务**
+
+- 红黑树整体性能略优于 AVL 树,AVL 树的旋转比红黑树的旋转多,更加难以平衡和调试,插入和删除的效率比红黑树慢
+
+
+
+
+
+红黑树添加节点的默认颜色为红色,效率高
+
+
+
+
+**红黑树添加节点后如何保持红黑规则:**
+
++ 根节点位置
+ + 直接变为黑色
++ 非根节点位置
+ + 父节点为黑色
+ + 不需要任何操作,默认红色即可
+ + 父节点为红色
+ + 叔叔节点为红色
+ 1. 将"父节点"设为黑色,将"叔叔节点"设为黑色
+ 2. 将"祖父节点"设为红色
+ 3. 如果"祖父节点"为根节点,则将根节点再次变成黑色
+ + 叔叔节点为黑色
+ 1. 将"父节点"设为黑色
+ 2. 将"祖父节点"设为红色
+ 3. 以"祖父节点"为支点进行旋转
+
+
+
+
+
+***
+
+
+
+### 并查集
+
+#### 基本实现
+
+并查集是一种树型的数据结构,有以下特点:
+
+* 每个元素都唯一的对应一个结点
+* 每一组数据中的多个元素都在同一颗树中
+* 一个组中的数据对应的树和另外一个组中的数据对应的树之间没有任何联系
+* 元素在树中并没有子父级关系的硬性要求
+
+
+
+可以高效地进行如下操作:
+
+* 查询元素 p 和元素 q 是否属于同一组
+* 合并元素 p 和元素 q 所在的组
+
+存储结构:
+
+
+
+合并方式:
+
+
+
+
+
+代码实现:
+
+* 类实现:
+
+ ```java
+ public class UF {
+ //记录节点元素和该元素所在分组的标识
+ private int[] eleAndGroup;
+ //记录分组的个数
+ private int count;
+
+ //初始化并查集
+ public UF(int N) {
+ //初始化分组数量
+ this.count = N;
+ //初始化eleAndGroup数量
+ this.eleAndGroup = new int[N];
+ //初始化eleAndGroup中的元素及其所在分组的标识符,eleAndGroup索引作为每个节点的元素
+ //每个索引处的值就是该组的索引,就是该元素所在的组的标识符
+ for (int i = 0; i < eleAndGroup.length; i++) {
+ eleAndGroup[i] = i;
+ }
+ }
+
+ //查询p所在的分组的标识符
+ public int find(int p) {
+ return eleAndGroup[p];
+ }
+
+ //判断并查集中元素p和元素q是否在同一分组中
+ public boolean connect(int p, int q) {
+ return find(p) == find(q);
+ }
+
+ //把p元素所在分组和q元素所在分组合并
+ public void union(int p, int q) {
+ //判断元素q和p是否已经在同一个分组中,如果已经在同一个分组中,则结束方法就可以了
+ if (connect(p, q)) {
+ return;
+ }
+ int pGroup = find(p);//找到p所在分组的标识符
+ int qGroup = find(q);//找到q所在分组的标识符
+
+ //合并组,让p所在组的 所有元素 的组标识符变为q所在分组的标识符
+ for (int i = 0; i < eleAndGroup.length; i++) {
+ if (eleAndGroup[i] == pGroup) {
+ eleAndGroup[i] = qGroup;
+ }
+ }
+ //分组个数-1
+ this.count--;
+ }
+ }
+ ```
+
+* 测试代码:
+
+ ```java
+ public static void main(String[] args) {
+ //创建并查集对象
+ UF uf = new UF(5);
+ System.out.println(uf);
+
+ //从控制台录入两个合并的元素,调用union方法合并,观察合并后并查集的分组
+ Scanner sc = new Scanner(System.in);
+
+ while (true) {
+ System.out.println("输入第一个要合并的元素");
+ int p = sc.nextInt();
+ System.out.println("输入第二个要合并的元素");
+ int q = sc.nextInt();
+ if (uf.connect(p, q)) {
+ System.out.println(p + "元素已经和" + q + "元素已经在同一个组");
+ continue;
+ }
+ uf.union(p, q);
+ System.out.println("当前并查集中还有:" + uf.count() + "个分组");
+ System.out.println(uf);
+ System.out.println("********************");
+ }
+ }
+ ```
+
+最坏情况下 union 算法的时间复杂度也是 O(N^2)
+
+
+
+****
+
+
+
+#### 优化实现
+
+让每个索引处的节点都指向它的父节点,当 eleGroup[i] = i 时,说明 i 是根节点
+
+
+
+```java
+//查询p所在的分组的标识符,递归寻找父标识符,直到找到根节点
+public int findRoot(int p) {
+ while (p != eleAndGroup[p]) {
+ p = eleAndGroup[p];
+ }
+ //p == eleGroup[p],说明p是根节点
+ return p;
+}
+
+//判断并查集中元素p和元素q是否在同一分组中
+public boolean connect(int p, int q) {
+ return findRoot(p) == findRoot(q);
+}
+
+//把p元素所在分组和q元素所在分组合并
+public void union(int p, int q) {
+ //找到p q对应的根节点
+ int pRoot = findRoot(p);
+ int qRoot = findRoot(q);
+ if (pRoot == qRoot) {
+ return;
+ }
+ //让p所在树的节点根节点为q的所在的根节点,只需要把根节点改一下,时间复杂度 O(1)
+ eleAndGroup[pRoot] = qRoot;
+ this.count-
+}
+```
+
+平均时间复杂度为 O(N),最坏时间复杂度是 O(N^2)
+
+
+
+继续优化:路径压缩,保证每次把小树合并到大树
+
+```java
+public class UF_Tree_Weighted {
+ private int[] eleAndGroup;
+ private int count;
+ private int[] size;//存储每一个根结点对应的树中的保存的节点的个数
+
+ //初始化并查集
+ public UF_Tree_Weighted(int N) {
+ this.count = N;
+ this.eleAndGroup = new int[N];
+ for (int i = 0; i < eleAndGroup.length; i++) {
+ eleAndGroup[i] = i;
+ }
+ this.size = new int[N];
+ //默认情况下,size中每个索引处的值都是1
+ for (int i = 0; i < size.length; i++) {
+ size[i] = 1;
+ }
+ }
+ //查询p所在的分组的标识符,父标识符
+ public int findRoot(int p) {
+ while (p != eleAndGroup[p]) {
+ p = eleAndGroup[p];
+ }
+ return p;
+ }
+
+ //判断并查集中元素p和元素q是否在同一分组中
+ public boolean connect(int p, int q) {
+ return findRoot(p) == findRoot(q);
+ }
+
+ //把p元素所在分组和q元素所在分组合并
+ public void union(int p, int q) {
+ //找到p q对应的根节点
+ int pRoot = findRoot(p);
+ int qRoot = findRoot(q);
+ if (pRoot == qRoot) {
+ return;
+ }
+ //判断pRoot对应的树大还是qRoot对应的树大,最终需要把较小的树合并到较大的树中
+ if (size[pRoot] < size[qRoot]) {
+ eleAndGroup[pRoot] = qRoot;
+ size[qRoot] += size[pRoot];
+ } else {
+ eleAndGroup[qRoot] = pRoot;
+ size[pRoot] += size[qRoot];
+ }
+ //组的数量-1、
+ this.count--;
+ }
+}
+```
+
+
+
+***
+
+
+
+#### 应用场景
+
+并查集存储的每一个整数表示的是一个大型计算机网络中的计算机:
+
+* 可以通过 connected(int p, int q) 来检测该网络中的某两台计算机之间是否连通
+* 可以调用 union(int p,int q) 使得 p 和 q 之间连通,这样两台计算机之间就可以通信
+
+畅通工程:某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府畅通工程的目标是使全省任何两个城镇间都可以实现交通,但不一定有直接的道路相连,只要互相间接通过道路可达即可,问最少还需要建设多少条道路?
+
+
+
+解题思路:
+
+1. 创建一个并查集 UF_Tree_Weighted(20)
+2. 分别调用 union(0,1)、union(6,9)、union(3,8)、union(5,11)、union(2,12)、union(6,10)、union(4,8),表示已经修建好的道路把对应的城市连接起来
+3. 如果城市全部连接起来,那么并查集中剩余的分组数目为 1,所有的城市都在一个树中,只需要获取当前并查集中剩余的数目减去 1,就是还需要修建的道路数目
+
+```java
+public static void main(String[] args)throws Exception {
+ Scanner sc = new Scanner(System.in);
+ //读取城市数目,初始化并查集
+ int number = sc.nextInt();
+ //读取已经修建好的道路数目
+ int roadNumber = sc.nextInt();
+ UF_Tree_Weighted uf = new UF_Tree_Weighted(number);
+ //循环读取已经修建好的道路,并调用union方法
+ for (int i = 0; i < roadNumber; i++) {
+ int p = sc.nextInt();
+ int q = sc.nextInt();
+ uf.union(p,q);
+ }
+ //获取剩余的分组数量
+ int groupNumber = uf.count();
+ //计算出还需要修建的道路
+ System.out.println("还需要修建"+(groupNumber-1)+"道路,城市才能相通");
+}
+```
+
+
+
+参考视频:https://www.bilibili.com/video/BV1iJ411E7xW?p=142
+
+
+
+***
+
+
+
+### 字典树
+
+#### 基本介绍
+
+Trie 树,也叫字典树,是一种专门处理字符串匹配的树形结构,用来解决在一组字符串集合中快速查找某个字符串的问题,Trie 树的本质就是利用字符串之间的公共前缀,将重复的前缀合并在一起
+
+* 根节点不包含任何信息
+* 每个节点表示一个字符串中的字符,从**根节点到红色节点的一条路径表示一个字符串**
+* 红色节点并不都是叶子节点
+
+
+
+
+
+注意:要查找的是字符串“he”,从根节点开始,沿着某条路径来匹配,可以匹配成功。但是路径的最后一个节点“e”并不是红色的,也就是说,“he”是某个字符串的前缀子串,但并不能完全匹配任何字符串
+
+
+
+***
+
+
+
+#### 实现Trie
+
+通过一个下标与字符一一映射的数组,来存储子节点的指针
+
+
+
+时间复杂度是 O(n)(n 表示要查找字符串的长度)
+
+```java
+public class Trie {
+ private TrieNode root = new TrieNode('/');
+
+ //插入一个字符
+ public void insert(char[] chars) {
+ TrieNode p = root;
+ for (int i = 0; i < chars.length; i++) {
+ //获取字符的索引位置
+ int index = chars[i] - 'a';
+ if (p.children[index] == null) {
+ TrieNode node = new TrieNode(chars[i]);
+ p.children[index] = node;
+ }
+ p = p.children[index];
+ }
+ p.isEndChar = true;
+ }
+
+ //查找一个字符串
+ public boolean find(char[] chars) {
+ TrieNode p = root;
+ for (int i = 0; i < chars.length; i++) {
+ int index = chars[i] - 'a';
+ if (p.children[index] == null) {
+ return false;
+ }
+ p = p.children[index];
+ }
+ if (p.isEndChar) {
+ //完全匹配
+ return true;
+ } else {
+ // 不能完全匹配,只是前缀
+ return false;
+ }
+ }
+
+
+ private class TrieNode {
+ char data;
+ TrieNode[] children = new TrieNode[26];//26个英文字母
+ boolean isEndChar = false;//结尾字符为true
+ public TrieNode(char data) {
+ this.data = data;
+ }
+ }
+}
+```
+
+
+
+***
+
+
+
+#### 优化Trie
+
+Trie 树是非常耗内存,采取空间换时间的思路。Trie 树的变体有很多,可以在一定程度上解决内存消耗的问题。比如缩点优化,对只有一个子节点的节点,而且此节点不是一个串的结束节点,可以将此节点与子节点合并
+
+
+
+
+
+参考文章:https://time.geekbang.org/column/article/72414
+
+
+
+***
+
+
+
+## 图
+
+图的邻接表形式:
+
+```java
+public class AGraph {
+ private VertexNode[] adjList; //邻接数组
+ private int vLen, eLen; //顶点数和边数
+
+ public AGraph(int vLen, int eLen) {
+ this.vLen = vLen;
+ this.eLen = eLen;
+ adjList = new VertexNode[vLen];
+ }
+ //弧节点
+ private class ArcNode {
+ int adjVex; //该边所指向的顶点的位置
+ ArcNode nextArc; //下一条边(弧)
+ //int info //添加权值
+
+ public ArcNode(int adjVex) {
+ this.adjVex = adjVex;
+ nextArc = null;
+ }
+ }
+
+ //表顶点
+ private class VertexNode {
+ char data; //顶点信息
+ ArcNode firstArc; //指向第一条边的指针
+
+ public VertexNode(char data) {
+ this.data = data;
+ firstArc = null;
+ }
+ }
+}
+```
+
+图的邻接矩阵形式:
+
+```java
+public class MGraph {
+ private int[][] edges; //邻接矩阵定义,有权图将int改为float
+ private int vLen; //顶点数
+ private int eLen; //边数
+ private VertexNode[] vex; //存放节点信息
+
+ public MGraph(int vLen, int eLen) {
+ this.vLen = vLen;
+ this.eLen = eLen;
+ this.edges = new int[vLen][vLen];
+ this.vex = new VertexNode[vLen];
+ }
+
+ private class VertexNode {
+ int num; //顶点编号
+ String info; //顶点信息
+
+ public VertexNode(int num) {
+ this.num = num;
+ this.info = null;
+ }
+ }
+}
+```
+
+
+
+图相关的算法需要很多的流程图,此处不再一一列举,推荐参考书籍《数据结构高分笔记》
+
+
+
+***
+
+
+
+## 位图
+
+### 基本介绍
+
+布隆过滤器:一种数据结构,是一个很长的二进制向量(位数组)和一系列随机映射函数(哈希函数),既然是二进制,每个空间存放的不是 0 就是 1,但是初始默认值都是 0,所以布隆过滤器不存数据只存状态
+
+
+
+这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且理论情况下,添加到集合中的元素越多,误报的可能性就越大
+
+
+
+***
+
+
+
+### 工作流程
+
+向布隆过滤器中添加一个元素 key 时,会通过多个 hash 函数得到多个哈希值,在位数组中把对应下标的值置为 1
+
+
+
+布隆过滤器查询一个数据,是否在二进制的集合中,查询过程如下:
+
+- 通过 K 个哈希函数计算该数据,对应计算出的 K 个 hash 值
+- 通过 hash 值找到对应的二进制的数组下标
+- 判断方法:如果存在一处位置的二进制数据是 0,那么该数据一定不存在。如果都是 1,则认为数据存在集合中(会误判)
+
+布隆过滤器优缺点:
+
+* 优点:
+ * 二进制组成的数组,占用内存极少,并且插入和查询速度都足够快
+ * 去重方便:当字符串第一次存储时对应的位数组下标设置为 1,当第二次存储相同字符串时,因为对应位置已设置为 1,所以很容易知道此值已经存在
+* 缺点:
+ * 随着数据的增加,误判率会增加:添加数据是通过计算数据的 hash 值,不同的字符串可能哈希出来的位置相同,导致无法确定到底是哪个数据存在,**这种情况可以适当增加位数组大小或者调整哈希函数**
+ * 无法删除数据:可能存在几个数据占据相同的位置,所以删除一位会导致很多数据失效
+
+* 总结:**布隆过滤器判断某个元素存在,小概率会误判。如果判断某个元素不在,那这个元素一定不在**
+
+
+
+参考文章:https://www.cnblogs.com/ysocean/p/12594982.html
+
+
+
+***
+
+
+
+### Guava
+
+引入 Guava 的依赖:
+
+```xml
+
+ com.google.guava
+ guava
+ 28.0-jre
+
+```
+
+指定误判率为(0.01):
+
+```java
+public static void main(String[] args) {
+ // 创建布隆过滤器对象
+ BloomFilter filter = BloomFilter.create(
+ Funnels.integerFunnel(),
+ 1500,
+ 0.01);
+ // 判断指定元素是否存在
+ System.out.println(filter.mightContain(1));
+ System.out.println(filter.mightContain(2));
+ // 将元素添加进布隆过滤器
+ filter.put(1);
+ filter.put(2);
+ System.out.println(filter.mightContain(1));
+ System.out.println(filter.mightContain(2));
+}
+```
+
+
+
+***
+
+
+
+### 实现布隆
+
+```java
+class MyBloomFilter {
+ //布隆过滤器容量
+ private static final int DEFAULT_SIZE = 2 << 28;
+ //bit数组,用来存放key
+ private static BitSet bitSet = new BitSet(DEFAULT_SIZE);
+ //后面hash函数会用到,用来生成不同的hash值,随意设置
+ private static final int[] ints = {1, 6, 16, 38, 58, 68};
+
+ //add方法,计算出key的hash值,并将对应下标置为true
+ public void add(Object key) {
+ Arrays.stream(ints).forEach(i -> bitSet.set(hash(key, i)));
+ }
+
+ //判断key是否存在,true不一定说明key存在,但是false一定说明不存在
+ public boolean isContain(Object key) {
+ boolean result = true;
+ for (int i : ints) {
+ //短路与,只要有一个bit位为false,则返回false
+ result = result && bitSet.get(hash(key, i));
+ }
+ return result;
+ }
+
+ //hash函数,借鉴了hashmap的扰动算法
+ private int hash(Object key, int i) {
+ int h;
+ return key == null ? 0 : (i * (DEFAULT_SIZE - 1) & ((h = key.hashCode()) ^ (h >>> 16)));
+ }
+}
+```
+
diff --git a/Java/JVM.md b/Java/JVM.md
new file mode 100644
index 0000000..76ccd64
--- /dev/null
+++ b/Java/JVM.md
@@ -0,0 +1,5704 @@
+# JVM
+
+## JVM概述
+
+### 基本介绍
+
+JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作
+
+特点:
+
+* Java 虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成
+* JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的**跨平台性**
+
+Java 代码执行流程:`Java 程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux)`
+
+JVM 结构:
+
+
+
+JVM、JRE、JDK 对比:
+
+* JDK(Java SE Development Kit):Java 标准开发包,提供了编译、运行 Java 程序所需的各种工具和资源
+* JRE( Java Runtime Environment):Java 运行环境,用于解释执行 Java 的字节码文件
+
+
+
+
+
+参考书籍:https://book.douban.com/subject/34907497/
+
+参考视频:https://www.bilibili.com/video/BV1PJ411n7xZ
+
+参考视频:https://www.bilibili.com/video/BV1yE411Z7AP
+
+
+
+***
+
+
+
+### 架构模型
+
+Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,Java 的指令都是根据栈来设计的,不同平台 CPU 架构不同,所以不能设计为基于寄存器架构
+
+* 基于栈式架构的特点:
+ * 设计和实现简单,适用于资源受限的系统
+ * 使用零地址指令方式分配,执行过程依赖操作栈,指令集更小,编译器容易实现
+ * 零地址指令:机器指令的一种,是指令系统中的一种不设地址字段的指令,只有操作码而没有地址码。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器(ACC)中,指令可直接访问寄存器
+ * 一地址指令:一个操作码对应一个地址码,通过地址码寻找操作数
+ * 不需要硬件的支持,可移植性更好,更好实现跨平台
+* 基于寄存器架构的特点:
+ * 需要硬件的支持,可移植性差
+ * 性能更好,执行更高效,寄存器比内存快
+ * 以一地址指令、二地址指令、三地址指令为主
+
+
+
+***
+
+
+
+### 生命周期
+
+JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡
+
+- **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点
+- **运行**:
+
+ - main() 方法是一个程序的初始起点,任何线程均可由在此处启动
+ - 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,**JVM 使用的是守护线程,main() 和其他线程使用的是用户线程**,守护线程会随着用户线程的结束而结束
+ - 执行一个 Java 程序时,真真正正在执行的是一个 **Java 虚拟机的进程**
+ - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多
+
+ Server 模式启动的 JVM 采用的是重量级的虚拟机,对程序采用了更多的优化;Client 模式启动的 JVM 采用的是轻量级的虚拟机
+- **死亡**:
+
+ - 当程序中的用户线程都中止,JVM 才会退出
+ - 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止
+ - 线程调用 Runtime 类 halt 方法或 System 类 exit 方法,并且 Java 安全管理器允许这次 exit 或 halt 操作
+
+
+
+
+
+***
+
+
+
+
+
+## 内存结构
+
+### 内存概述
+
+内存结构是 JVM 中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区
+
+JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行
+
+* Java1.8 以前的内存结构图:
+ 
+
+* Java1.8 之后的内存结果图:
+
+ 
+
+线程运行诊断:
+
+* 定位:jps 定位进程 ID
+* jstack 进程 ID:用于打印出给定的 Java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息
+
+常见 OOM 错误:
+
+* java.lang.StackOverflowError
+* java.lang.OutOfMemoryError:java heap space
+* java.lang.OutOfMemoryError:GC overhead limit exceeded
+* java.lang.OutOfMemoryError:Direct buffer memory
+* java.lang.OutOfMemoryError:unable to create new native thread
+* java.lang.OutOfMemoryError:Metaspace
+
+
+
+***
+
+
+
+### JVM内存
+
+#### 虚拟机栈
+
+##### Java 栈
+
+Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所需要的内存
+
+* 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(**一个方法一个栈帧**)
+
+* Java 虚拟机规范允许 **Java 栈的大小是动态的或者是固定不变的**
+
+* 虚拟机栈是**每个线程私有的**,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程
+
+* 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着:
+
+ * 局部变量表:存储方法里的 Java 基本数据类型以及对象的引用
+ * 动态链接:也叫指向运行时常量池的方法引用
+ * 方法返回地址:方法正常退出或者异常退出的定义
+ * 操作数栈或表达式栈和其他一些附加信息
+
+
+
+设置栈内存大小:`-Xss size` `-Xss 1024k`
+
+* 在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M
+
+虚拟机栈特点:
+
+* 栈内存**不需要进行GC**,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据
+
+* 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大)
+
+* 方法内的局部变量是否**线程安全**:
+ * 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析)
+ * 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
+
+异常:
+
+* 栈帧过多导致栈内存溢出 (超过了栈的容量),会抛出 OutOfMemoryError 异常
+* 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常
+
+
+
+***
+
+
+
+##### 局部变量
+
+局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量
+
+* 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题
+* 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中
+* 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁
+* 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收
+
+局部变量表最基本的存储单元是 **slot(变量槽)**:
+
+* 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据
+* 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量
+* 32 位以内的类型只占一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占两个 slot
+* 局部变量表中的槽位是可以**重复利用**的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的
+
+
+
+***
+
+
+
+##### 操作数栈
+
+栈:可以使用数组或者链表来实现
+
+操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop)
+
+* 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区
+
+* Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
+* 如果被调用的方法带有返回值的话,其**返回值将会被压入当前栈帧的操作数栈中**
+
+栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率
+
+基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术
+
+
+
+***
+
+
+
+##### 动态链接
+
+动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是**动态绑定**
+
+* 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用
+
+ 
+
+* 在 Java 源文件被编译成的字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中
+
+ 常量池的作用:提供一些符号和常量,便于指令的识别
+
+ 
+
+
+
+***
+
+
+
+##### 返回地址
+
+Return Address:存放调用该方法的 PC 寄存器的值
+
+方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置
+
+* 正常:调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的**下一条指令的地址**
+* 异常:返回地址是要通过异常表来确定
+
+正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者
+
+异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出
+
+两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值
+
+
+
+##### 附加信息
+
+栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息
+
+
+
+***
+
+
+
+#### 本地方法栈
+
+本地方法栈是为虚拟机执行本地方法时提供服务的
+
+JNI:Java Native Interface,通过使用 Java 本地接口程序,可以确保代码在不同的平台上方便移植
+
+* 不需要进行 GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常
+* 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一
+* 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序
+* 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限
+
+ * 本地方法可以通过本地方法接口来**访问虚拟机内部的运行时数据区**
+ * 直接从本地内存的堆中分配任意数量的内存
+ * 可以直接使用本地处理器中的寄存器
+
+
+原理:将本地的 C 函数(如 foo)编译到一个共享库(foo.so)中,当正在运行的 Java 程序调用 foo 时,Java 解释器利用 dlopen 接口动态链接和加载 foo.so 后再调用该函数
+
+* dlopen 函数:Linux 系统加载和链接共享库
+* dlclose 函数:卸载共享库
+
+
+
+
+
+图片来源:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md
+
+
+
+***
+
+
+
+#### 程序计数器
+
+Program Counter Register 程序计数器(寄存器)
+
+作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空)
+
+原理:
+
+* JVM 对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程
+* 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号
+
+特点:
+
+* 是线程私有的
+* **不会存在内存溢出**,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC
+
+Java 反编译指令:`javap -v Test.class`
+
+#20:代表去 Constant pool 查看该地址的指令
+
+```java
+0: getstatic #20 // PrintStream out = System.out;
+3: astore_1 // --
+4: aload_1 // out.println(1);
+5: iconst_1 // --
+6: invokevirtual #26 // --
+9: aload_1 // out.println(2);
+10: iconst_2 // --
+11: invokevirtual #26 // --
+```
+
+
+
+****
+
+
+
+#### 堆
+
+Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题
+
+存放哪些资源:
+
+* 对象实例:类初始化生成的对象,**基本数据类型的数组也是对象实例**,new 创建对象都使用堆内存
+* 字符串常量池:
+ * 字符串常量池原本存放于方法区,JDK7 开始放置于堆中
+ * 字符串常量池**存储的是 String 对象的直接引用或者对象**,是一张 string table
+* 静态变量:静态变量是有 static 修饰的变量,JDK8 时从方法区迁移至堆中
+* 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率
+
+设置堆内存指令:`-Xmx Size`
+
+内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常
+
+堆内存诊断工具:(控制台命令)
+
+1. jps:查看当前系统中有哪些 Java 进程
+2. jmap:查看堆内存占用情况 `jhsdb jmap --heap --pid 进程id`
+3. jconsole:图形界面的,多功能的监测工具,可以连续监测
+
+在 Java7 中堆内会存在**年轻代、老年代和方法区(永久代)**:
+
+* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候,GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间
+* Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区
+* Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理
+
+分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能
+
+```java
+public static void main(String[] args) {
+ // 返回Java虚拟机中的堆内存总量
+ long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
+ // 返回Java虚拟机使用的最大堆内存量
+ long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
+
+ System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M
+ System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M
+}
+```
+
+
+
+***
+
+
+
+#### 方法区
+
+方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆)
+
+方法区是一个 JVM 规范,**永久代与元空间都是其一种实现方式**
+
+方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError)
+
+方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现
+
+为了**避免方法区出现 OOM**,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,**静态变量和字符串常量池等放入堆中**
+
+类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表
+
+常量池表(Constant Pool Table)是 Class 文件的一部分,存储了**类在编译期间生成的字面量、符号引用**,JVM 为每个已加载的类维护一个常量池
+
+- 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等
+- 符号引用:类、字段、方法、接口等的符号引用
+
+运行时常量池是方法区的一部分
+
+* 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池
+* 类在解析阶段将这些符号引用替换成直接引用
+* 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()
+
+
+
+***
+
+
+
+### 本地内存
+
+#### 基本介绍
+
+虚拟机内存:Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报 OOM
+
+本地内存:又叫做**堆外内存**,线程共享的区域,本地内存这块区域是不会受到 JVM 的控制的,不会发生 GC;因此对于整个 Java 的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报 OOM
+
+本地内存概述图:
+
+
+
+
+
+***
+
+
+
+#### 元空间
+
+PermGen 被元空间代替,永久代的**类信息、方法、常量池**等都移动到元空间区
+
+元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制
+
+方法区内存溢出:
+
+* JDK1.8 以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space
+
+ ```sh
+ -XX:MaxPermSize=8m #参数设置
+ ```
+
+* JDK1.8 以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace
+
+ ```sh
+ -XX:MaxMetaspaceSize=8m #参数设置
+ ```
+
+元空间内存溢出演示:
+
+```java
+public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
+ public static void main(String[] args) {
+ int j = 0;
+ try {
+ Demo1_8 test = new Demo1_8();
+ for (int i = 0; i < 10000; i++, j++) {
+ // ClassWriter 作用是生成类的二进制字节码
+ ClassWriter cw = new ClassWriter(0);
+ // 版本号, public, 类名, 包名, 父类, 接口
+ cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
+ // 返回 byte[]
+ byte[] code = cw.toByteArray();
+ // 执行了类的加载
+ test.defineClass("Class" + i, code, 0, code.length); // Class 对象
+ }
+ } finally {
+ System.out.println(j);
+ }
+ }
+}
+```
+
+
+
+***
+
+
+
+#### 直接内存
+
+直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域
+
+
+
+直接内存详解参考:NET → NIO → 直接内存
+
+
+
+***
+
+
+
+### 变量位置
+
+变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的**声明位置**
+
+静态内部类和其他内部类:
+
+* **一个 class 文件只能对应一个 public 类型的类**,这个类可以有内部类,但不会生成新的 class 文件
+
+* 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到堆(待考证)
+
+类变量:
+
+* 类变量是用 static 修饰符修饰,定义在方法外的变量,随着 Java 进程产生和销毁
+* 在 Java8 之前把静态变量存放于方法区,在 Java8 时存放在堆中的静态变量区
+
+
+实例变量:
+
+* 实例(成员)变量是定义在类中,没有 static 修饰的变量,随着类的实例产生和销毁,是类实例的一部分
+* 在类初始化的时候,从运行时常量池取出直接引用或者值,**与初始化的对象一起放入堆中**
+
+局部变量:
+
+* 局部变量是定义在类的方法中的变量
+* 在所在方法被调用时**放入虚拟机栈的栈帧**中,方法执行结束后从虚拟机栈中弹出,
+
+类常量池、运行时常量池、字符串常量池有什么关系?有什么区别?
+
+* 类常量池与运行时常量池都存储在方法区,而字符串常量池在 Jdk7 时就已经从方法区迁移到了 Java 堆中
+* 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符
+* **在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池**
+* 对于文本字符,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池
+
+什么是字面量?什么是符号引用?
+
+* 字面量:java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示
+
+ ```java
+ int a = 1; //这个1便是字面量
+ String b = "iloveu"; //iloveu便是字面量
+ ```
+
+* 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用它的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址
+
+
+
+
+***
+
+
+
+
+
+## 内存管理
+
+### 内存分配
+
+#### 两种方式
+
+不分配内存的对象无法进行其他操作,JVM 为对象分配内存的过程:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象
+
+* 如果内存规整,使用指针碰撞(Bump The Pointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离
+* 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容
+
+
+
+***
+
+
+
+#### TLAB
+
+TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用 TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略**
+
+- 栈上分配使用的是栈来进行对象内存的分配
+- TLAB 分配使用的是 Eden 区域进行内存分配,属于堆内存
+
+堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
+
+问题:堆空间都是共享的么? 不一定,因为还有 TLAB,在堆中划分出一块区域,为每个线程所独占
+
+
+
+JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在 TLAB 空间分配内存失败时,JVM 就会通过**使用加锁机制确保数据操作的原子性**,从而直接在堆中分配内存
+
+栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存
+
+参数设置:
+
+* `-XX:UseTLAB`:设置是否开启 TLAB 空间
+
+* `-XX:TLABWasteTargetPercent`:设置 TLAB 空间所占用 Eden 空间的百分比大小,默认情况下 TLAB 空间的内存非常小,仅占有整个 Eden 空间的1%
+* `-XX:TLABRefillWasteFraction`:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配
+
+
+
+
+
+***
+
+
+
+#### 逃逸分析
+
+即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善性能的技术,在 HotSpot 实现中有多种选择:C1、C2 和 C1+C2,分别对应 Client、Server 和分层编译
+
+* C1 编译速度快,优化方式比较保守;C2 编译速度慢,优化方式比较激进
+* C1+C2 在开始阶段采用 C1 编译,当代码运行到一定热度之后采用 C2 重新编译
+
+逃逸分析并不是直接的优化手段,而是一个代码分析方式,通过动态分析对象的作用域,为优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸
+
+* 方法逃逸:当一个对象在方法中定义之后,被外部方法引用
+ * 全局逃逸:一个对象的作用范围逃出了当前方法或者当前线程,比如对象是一个静态变量、全局变量赋值、已经发生逃逸的对象、作为当前方法的返回值
+ * 参数逃逸:一个对象被作为方法参数传递或者被参数引用
+* 线程逃逸:如类变量或实例变量,可能被其它线程访问到
+
+如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配
+
+* 同步消除
+
+ 线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,不被其它线程访问到,那对象的读写就不会存在竞争,则可以消除对该对象的**同步锁**,通过 `-XX:+EliminateLocks` 可以开启同步消除 ( - 号关闭)
+
+* 标量替换
+
+ * 标量替换:如果把一个对象拆散,将其成员变量恢复到基本类型来访问
+ * 标量 (scalar) :不可分割的量,如基本数据类型和 reference 类型
+
+ 聚合量 (Aggregate):一个数据可以继续分解,对象一般是聚合量
+ * 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替
+ * 参数设置:
+
+ * `-XX:+EliminateAllocations`:开启标量替换
+ * `-XX:+PrintEliminateAllocations`:查看标量替换情况
+
+* 栈上分配
+
+ JIT 编译器在编译期间根据逃逸分析的结果,如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需 GC
+
+ User 对象的作用域局限在方法 fn 中,可以使用标量替换的优化手段在栈上分配对象的成员变量,这样就不会生成 User 对象,大大减轻 GC 的压力
+
+ ```java
+ public class JVM {
+ public static void main(String[] args) throws Exception {
+ int sum = 0;
+ int count = 1000000;
+ //warm up
+ for (int i = 0; i < count ; i++) {
+ sum += fn(i);
+ }
+ System.out.println(sum);
+ System.in.read();
+ }
+ private static int fn(int age) {
+ User user = new User(age);
+ int i = user.getAge();
+ return i;
+ }
+ }
+
+ class User {
+ private final int age;
+
+ public User(int age) {
+ this.age = age;
+ }
+
+ public int getAge() {
+ return age;
+ }
+ }
+ ```
+
+
+
+
+***
+
+
+
+#### 分代思想
+
+##### 分代介绍
+
+Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代
+
+- 新生代使用:复制算法
+- 老年代使用:标记 - 清除 或者 标记 - 整理 算法
+
+**Minor GC 和 Full GC**:
+
+- Minor GC:回收新生代,新生代对象存活时间很短,所以 Minor GC 会频繁执行,执行的速度比较快
+- Full GC:回收老年代和新生代,老年代对象其存活时间长,所以 Full GC 很少执行,执行速度会比 Minor GC 慢很多
+
+ Eden 和 Survivor 大小比例默认为 8:1:1
+
+
+
+
+
+
+
+***
+
+
+
+##### 分代分配
+
+工作机制:
+
+* **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当 Eden 区要满了时候,触发 YoungGC
+* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且当前对象的年龄会加 1,清空 Eden 区
+* 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区
+* To 区永远是空 Survivor 区,From 区是有数据的,每次 MinorGC 后两个区域互换
+* From 区和 To 区 也可以叫做 S0 区和 S1 区
+
+晋升到老年代:
+
+* **长期存活的对象进入老年代**:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中
+
+ `-XX:MaxTenuringThreshold`:定义年龄的阈值,对象头中用 4 个 bit 存储,所以最大值是 15,默认也是 15
+
+* **大对象直接进入老年代**:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发 GC 以获取足够的连续空间分配给大对象
+
+ `-XX:PretenureSizeThreshold`:大于此值的对象直接在老年代分配
+
+* **动态对象年龄判定**:如果在 Survivor 区中相同年龄的对象的所有大小之和超过 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代
+
+空间分配担保:
+
+* 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的
+* 如果不成立,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试着进行一次 Minor GC;如果小于或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC
+
+
+
+
+
+***
+
+
+
+### 回收策略
+
+#### 触发条件
+
+内存垃圾回收机制主要集中的区域就是线程共享区域:**堆和方法区**
+
+Minor GC 触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC
+
+FullGC 同时回收新生代、老年代和方法区,只会存在一个 FullGC 的线程进行执行,其他的线程全部会被**挂起**,有以下触发条件:
+
+* 调用 System.gc():
+
+ * 在默认情况下,通过 System.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发 FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用
+ * 不建议使用这种方式,应该让虚拟机管理内存。一般情况下,垃圾回收应该是自动进行的,无须手动触发;在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用 System.gc()
+
+* 老年代空间不足:
+
+ * 为了避免引起的 Full GC,应当尽量不要创建过大的对象以及数组
+ * 通过 -Xmn 参数调整新生代的大小,让对象尽量在新生代被回收掉不进入老年代,可以通过 `-XX:MaxTenuringThreshold` 调大对象进入老年代的年龄,让对象在新生代多存活一段时间
+
+* 空间分配担保失败
+
+* JDK 1.7 及以前的永久代(方法区)空间不足
+
+* Concurrent Mode Failure:执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC
+
+
+手动 GC 测试,VM参数:`-XX:+PrintGcDetails`
+
+```java
+public void localvarGC1() {
+ byte[] buffer = new byte[10 * 1024 * 1024];//10MB
+ System.gc(); //输出: 不会被回收, FullGC时被放入老年代
+}
+
+public void localvarGC2() {
+ byte[] buffer = new byte[10 * 1024 * 1024];
+ buffer = null;
+ System.gc(); //输出: 正常被回收
+}
+ public void localvarGC3() {
+ {
+ byte[] buffer = new byte[10 * 1024 * 1024];
+ }
+ System.gc(); //输出: 不会被回收, FullGC时被放入老年代
+ }
+
+public void localvarGC4() {
+ {
+ byte[] buffer = new byte[10 * 1024 * 1024];
+ }
+ int value = 10;
+ System.gc(); //输出: 正常被回收,slot复用,局部变量过了其作用域 buffer置空
+}
+```
+
+
+
+***
+
+
+
+#### 安全区域
+
+安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始 GC,只有在安全点才能停下
+
+- Safe Point 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太多可能导致运行时的性能问题
+- 大部分指令的执行时间都非常短,通常会根据是否具有让程序长时间执行的特征为标准,选择些执行时间较长的指令作为 Safe Point, 如方法调用、循环跳转和异常跳转等
+
+在 GC 发生时,让所有线程都在最近的安全点停顿下来的方法:
+
+- 抢先式中断:没有虚拟机采用,首先中断所有线程,如果有线程不在安全点,就恢复线程让线程运行到安全点
+- 主动式中断:设置一个中断标志,各个线程运行到各个 Safe Point 时就轮询这个标志,如果中断标志为真,则将自己进行中断挂起
+
+问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决
+
+安全区域 (Safe Region):指在一段代码片段中,**对象的引用关系不会发生变化**,在这个区域中的任何位置开始 GC 都是安全的
+
+运行流程:
+
+- 当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生 GC,JVM 会忽略标识为 Safe Region 状态的线程
+
+- 当线程即将离开 Safe Region 时,会检查 JVM 是否已经完成 GC,如果完成了则继续运行,否则线程必须等待 GC 完成,收到可以安全离开 SafeRegion 的信号
+
+
+
+***
+
+
+
+### 垃圾判断
+
+#### 垃圾介绍
+
+垃圾:**如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾**
+
+作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象
+
+垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收
+
+在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法**
+
+
+
+***
+
+
+
+#### 引用计数法
+
+引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1;当对象 A 的引用计数器的值为 0,即表示对象A不可能再被使用,可进行回收(Java 没有采用)
+
+优点:
+
+- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为 0,可以直接回收
+- 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报 OOM 错误
+- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象
+
+缺点:
+
+- 每次对象被引用时,都需要去更新计数器,有一点时间开销
+
+- 浪费 CPU 资源,即使内存够用,仍然在运行时进行计数器的统计。
+
+- **无法解决循环引用问题,会引发内存泄露**(最大的缺点)
+
+ ```java
+ public class Test {
+ public Object instance = null;
+ public static void main(String[] args) {
+ Test a = new Test();// a = 1
+ Test b = new Test();// b = 1
+ a.instance = b; // b = 2
+ b.instance = a; // a = 2
+ a = null; // a = 1
+ b = null; // b = 1
+ }
+ }
+ ```
+
+
+
+
+
+***
+
+
+
+#### 可达性分析
+
+##### GC Roots
+
+可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集
+
+GC Roots 对象:
+
+- 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等
+- 本地方法栈中引用的对象
+- 堆中类静态属性引用的对象
+- 方法区中的常量引用的对象
+- 字符串常量池(string Table)里的引用
+- 同步锁 synchronized 持有的对象
+
+**GC Roots 是一组活跃的引用,不是对象**,放在 GC Roots Set 集合
+
+
+
+***
+
+
+
+##### 工作原理
+
+可达性分析算法以根对象集合(GCRoots)为起始点,从上至下的方式搜索被根对象集合所连接的目标对象
+
+分析工作必须在一个保障**一致性的快照**中进行,否则结果的准确性无法保证,这也是导致 GC 进行时必须 Stop The World 的一个原因
+
+基本原理:
+
+- 可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链
+
+- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象
+
+- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象
+
+
+
+
+
+***
+
+
+
+##### 三色标记
+
+###### 标记算法
+
+三色标记法把遍历对象图过程中遇到的对象,标记成以下三种颜色:
+
+- 白色:尚未访问过
+- 灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问
+- 黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问完成
+
+当 Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为:
+
+1. 初始时,所有对象都在白色集合
+2. 将 GC Roots 直接引用到的对象挪到灰色集合
+3. 从灰色集合中获取对象:
+ * 将本对象引用到的其他对象全部挪到灰色集合中
+ * 将本对象挪到黑色集合里面
+4. 重复步骤 3,直至灰色集合为空时结束
+5. 结束后,仍在白色集合的对象即为 GC Roots 不可达,可以进行回收
+
+
+
+
+
+参考文章:https://www.jianshu.com/p/12544c0ad5c1
+
+
+
+****
+
+
+
+###### 并发标记
+
+并发标记时,对象间的引用可能发生变化,多标和漏标的情况就有可能发生
+
+**多标情况:**当 E 变为灰色或黑色时,其他线程断开的 D 对 E 的引用,导致这部分对象仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾**
+
+* 针对并发标记开始后的**新对象**,通常的做法是直接全部当成黑色,也算浮动垃圾
+* 浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除
+
+
+
+**漏标情况:**
+
+* 条件一:灰色对象断开了对一个白色对象的引用(直接或间接),即灰色对象原成员变量的引用发生了变化
+* 条件二:其他线程中修改了黑色对象,插入了一条或多条对该白色对象的新引用
+* 结果:导致该白色对象当作垃圾被 GC,影响到了程序的正确性
+
+
+
+代码角度解释漏标:
+
+```java
+Object G = objE.fieldG; // 读
+objE.fieldG = null; // 写
+objD.fieldG = G; // 写
+```
+
+为了解决问题,可以操作上面三步,**将对象 G 记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的 GC Roots 遍历完(并发标记),再遍历该集合(重新标记)
+
+> 所以**重新标记需要 STW**,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完
+
+解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理:
+
+* **写屏障 + 增量更新**:黑色对象新增引用,会将黑色对象变成灰色对象,最后对该节点重新扫描
+
+ 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标
+
+ 缺点:对黑色变灰的对象重新扫描所有引用,比较耗费时间
+
+* **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象
+
+ 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了,并且原始快照中本来就应该是灰色对象),最后重新扫描该对象的引用关系
+
+ SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标
+
+* **读屏障 (Load Barrier)**:破坏条件二,黑色对象引用白色对象的前提是获取到该对象,此时读屏障发挥作用
+
+以 Java HotSpot VM 为例,其并发标记时对漏标的处理方案如下:
+
+- CMS:写屏障 + 增量更新
+- G1:写屏障 + SATB
+- ZGC:读屏障
+
+
+
+***
+
+
+
+#### finalization
+
+Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
+
+垃圾回收此对象之前,会先调用这个对象的 finalize() 方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行后置处理,通常在这个方法中进行一些资源释放和清理,比如关闭文件、套接字和数据库连接等
+
+生存 OR 死亡:如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用,此对象需要被回收。但事实上这时候它们暂时处于缓刑阶段。**一个无法触及的对象有可能在某个条件下复活自己**,所以虚拟机中的对象可能的三种状态:
+
+- 可触及的:从根节点开始,可以到达这个对象
+- 可复活的:对象的所有引用都被释放,但是对象有可能在 finalize() 中复活
+- 不可触及的:对象的 finalize() 被调用并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为 **finalize() 只会被调用一次**,等到这个对象再被标记为可回收时就必须回收
+
+永远不要主动调用某个对象的 finalize() 方法,应该交给垃圾回收机制调用,原因:
+
+* finalize() 时可能会导致对象复活
+* finalize() 方法的执行时间是没有保障的,完全由 GC 线程决定,极端情况下,若不发生 GC,则 finalize() 方法将没有执行机会,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收
+* 一个糟糕的 finalize() 会严重影响 GC 的性能
+
+
+
+***
+
+
+
+#### 引用分析
+
+无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关,Java 提供了四种强度不同的引用类型
+
+1. 强引用:被强引用关联的对象不会被回收,只有所有 GCRoots 都不通过强引用引用该对象,才能被垃圾回收
+
+ * 强引用可以直接访问目标对象
+ * 虚拟机宁愿抛出 OOM 异常,也不会回收强引用所指向对象
+ * 强引用可能导致**内存泄漏**
+
+ ```java
+ Object obj = new Object();//使用 new 一个新对象的方式来创建强引用
+ ```
+
+2. 软引用(SoftReference):被软引用关联的对象只有在内存不够的情况下才会被回收
+
+ * **仅(可能有强引用,一个对象可以被多个引用)**有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
+ * 配合**引用队列来释放软引用自身**,在构造软引用时,可以指定一个引用队列,当软引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况
+ * 软引用通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用;如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时不会耗尽内存
+
+ ```java
+ Object obj = new Object();
+ SoftReference