master 9bb89cc659bc cached
115 files
1.4 MB
689.6k tokens
1 requests
Download .txt
Showing preview only (2,585K chars total). Download the full file or copy to clipboard to get everything.
Repository: Swayingleaves/JavaDeveloperBrain
Branch: master
Commit: 9bb89cc659bc
Files: 115
Total size: 1.4 MB

Directory structure:
gitextract_u9dhcn2c/

├── .gitignore
├── 23-Java知识点梳理.xmind
├── Go/
│   └── go.md
├── Java-JVM/
│   ├── JVM的启动过程.md
│   ├── JVM调优.md
│   ├── Java即时编译.md
│   ├── Java对象头.md
│   ├── 内存分配与回收策略.md
│   ├── 内存结构.md
│   ├── 垃圾回收.md
│   └── 类加载机制.md
├── Java-基础/
│   ├── JavaIO.md
│   ├── Java关键字.md
│   ├── Java类型.md
│   ├── Java长期支持版本.md
│   ├── Object.md
│   ├── String.md
│   ├── 反射.md
│   ├── 容器-collection.md
│   ├── 容器-map.md
│   ├── 异常.md
│   ├── 数组.md
│   ├── 泛型.md
│   ├── 继承.md
│   ├── 虚拟线程.md
│   └── 虚拟线程2.md
├── Java-多线程/
│   ├── AQS.md
│   ├── CAS.md
│   ├── Java对象头.md
│   ├── ThreadLocal.md
│   ├── volatile.md
│   ├── 线程.md
│   ├── 线程池.md
│   └── 锁机制.md
├── LICENSE
├── Linux/
│   └── linux.md
├── Mybatis/
│   └── mybatis.md
├── Netty/
│   └── netty.md
├── README.md
├── Redis/
│   ├── Redis.md
│   └── Redis常见面试题.md
├── Spring/
│   ├── Spring.md
│   ├── SpringBoot.md
│   └── SpringMVC.md
├── SpringCloud/
│   └── springcloud.md
├── TODO.md
├── 分布式相关/
│   ├── ApacheSeata.md
│   ├── BASE.md
│   ├── CAP.md
│   ├── 一致性算法.md
│   ├── 分布式ID.md
│   ├── 分布式事务.md
│   └── 分布式锁.md
├── 商城类问题/
│   └── 商城类问题.md
├── 场景设计/
│   └── 场景设计.md
├── 大数据/
│   ├── Flink/
│   │   └── Flink.md
│   ├── Hadoop/
│   │   ├── Hadoop.md
│   │   └── hadoop面试问题.md
│   ├── Hive/
│   │   └── Hive.md
│   └── Spark/
│       └── Spark.md
├── 容器技术/
│   ├── docker.md
│   └── k8s.md
├── 性能测试/
│   └── docker安装配置influxdb-v2+grafana+jemter.md
├── 数据库/
│   ├── Elasticsearch.md
│   ├── Hbase.md
│   ├── MongoDB.md
│   ├── MySQL.md
│   ├── Nebula.md
│   ├── SQL面试题.md
│   └── count.md
├── 数据结构和算法/
│   ├── 01-最长回文子串.md
│   ├── LFU.md
│   ├── LRU.md
│   ├── hotcode.md
│   ├── leetcodetop150.md
│   ├── 二叉树.md
│   ├── 二叉树遍历.md
│   ├── 加减乘除.md
│   └── 排序算法.md
├── 智力题/
│   └── 智力题.md
├── 架构/
│   ├── DDD领域驱动设计.md
│   ├── 系统设计.md
│   └── 计算和储存分离.md
├── 消息队列/
│   ├── Kafka.md
│   ├── Pulsar.md
│   ├── RabbitMQ.md
│   ├── Redis.md
│   ├── RocketMQ.md
│   ├── Zookeeper.md
│   └── mq常见面试题.md
├── 职业规划和学习习惯/
│   └── 职业规划和学习习惯.md
├── 计算机网络/
│   ├── CDN.md
│   ├── HTTP.md
│   ├── HTTP面试题.md
│   ├── IP报文.md
│   ├── JWT.md
│   ├── TCP_IP.md
│   ├── TCP报文.md
│   ├── TLS.md
│   ├── UDP报文.md
│   ├── cookie和session.md
│   ├── 网络协议分层.md
│   ├── 网络攻击行为.md
│   └── 跨域.md
├── 设计模式/
│   ├── 单例模式.md
│   ├── 工厂模式.md
│   ├── 策略模式.md
│   ├── 装饰者模式.md
│   ├── 设计模式面试题.md
│   └── 责任链模式.md
└── 面试解答/
    ├── HR会问什么.md
    ├── 面试解答2021-06.md
    ├── 面试解答2021-07.md
    ├── 面试解答2021-09.md
    └── 面试解答2021-10.md

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Created by .ignore support plugin (hsz.mobi)

.idea


================================================
FILE: Go/go.md
================================================

# Go和Java的区别 


# go和Java的线程模型区别

# go和Java的GC区别


================================================
FILE: Java-JVM/JVM的启动过程.md
================================================

* [JVM的启动过程](#jvm的启动过程)
    * [1、JVM的装入环境和配置](#1jvm的装入环境和配置)
    * [2、装载JVM](#2装载jvm)
    * [3、初始化JVM,获得本地调用接口](#3初始化jvm获得本地调用接口)
    * [4、运行Java程序](#4运行java程序)


# JVM的启动过程
## 1、JVM的装入环境和配置

在学习这个之前,我们需要了解一件事情,就是JDK和JRE的区别。

JDK是面向开发人员使用的SDK,它提供了Java的开发环境和运行环境,JDK中包含了JRE。

JRE是Java的运行环境,是面向所有Java程序的使用者,包括开发者。

JRE = 运行环境 = JVM。

如果安装了JDK,会发现电脑中有两套JRE,一套位于/Java/jre.../下,一套位于/Java/jdk.../jre下。那么问题来了,一台机器上有两套以上JRE,谁来决定运行那一套呢?这个任务就落到java.exe身上,java.exe的任务就是找到合适的JRE来运行java程序。

java.exe按照以下的顺序来选择JRE:

1、自己目录下有没有JRE

2、父目录下有没有JRE

3、查询注册表: HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment\"当前JRE版本号"\JavaHome

这几步的主要核心是为了找到JVM的绝对路径。


jvm.cfg的路径为:JRE路径\lib\"CPU架构"\jvm.fig

jvm.cfg的内容大致如下:


-client KNOWN
-server KNOWN
-hotspot ALIASED_TO -client
-classic WARN
-native ERROR
-green ERROR

KNOWN 表示存在 、IGNORE 表示不存在 、ALIASED_TO 表示给别的JVM去一个别名
WARN 表示不存在时找一个替代 、ERROR 表示不存在抛出异常

## 2、装载JVM
通过第一步找到JVM的路径后,Java.exe通过LoadJavaVM来装入JVM文件。
LoadLibrary装载JVM动态连接库,然后把JVM中的到处函数JNI_CreateJavaVM和JNI_GetDefaultJavaVMIntArgs 挂接到InvocationFunction 变量的CreateJavaVM和GetDafaultJavaVMInitArgs 函数指针变量上。JVM的装载工作完成。

## 3、初始化JVM,获得本地调用接口

调用InvocationFunction -> CreateJavaVM也就是JVM中JNI_CreateJavaVM方法获得JNIEnv结构的实例。

## 4、运行Java程序

JVM运行Java程序的方式有两种:jar包 与 Class

运行jar 的时候,Java.exe调用GetMainClassName函数,该函数先获得JNIEnv实例然后调用JarFileJNIEnv类中getManifest(),从其返回的Manifest对象中取getAttrebutes("Main-Class")的值,即jar 包中文件:META-INF/MANIFEST.MF指定的Main-Class的主类名作为运行的主类。之后main函数会调用Java.c中LoadClass方法装载该主类(使用JNIEnv实例的FindClass)。

运行Class的时候,main函数直接调用Java.c中的LoadClass方法装载该类。

================================================
FILE: Java-JVM/JVM调优.md
================================================

* [原则](#原则)
* [jvm调优](#jvm调优)
  * [JVM调优目标](#jvm调优目标)
  * [举例](#举例)
  * [JVM调优的步骤](#jvm调优的步骤)
  * [JVM参数解析及调优](#jvm参数解析及调优)


# 原则
- 大多数的Java应用不需要进行JVM优化;
- 大多数导致GC问题的原因是代码层面的问题导致的(代码层面);
- 上线之前,应先考虑将机器的JVM参数设置到最优;
- 减少创建对象的数量(代码层面);
- 减少使用全局变量和大对象(代码层面);
- 优先架构调优和代码调优,JVM优化是不得已的手段(代码、架构层面);
- 分析GC情况优化代码比优化JVM参数更好(代码层面);
# jvm调优
### JVM调优目标
- 延迟:GC低停顿和GC低频率;
- 低内存占用;
- 高吞吐量;
### 举例
- Heap 内存使用率 <= 70%;
- Old generation内存使用率<= 70%;
- avgpause <= 1秒;
- Full gc 次数0 或 avg pause interval >= 24小时 ;
### JVM调优的步骤
1. 分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点;
2. 确定JVM调优量化目标;
3. 确定JVM调优参数(根据历史JVM参数来调整);
4. 依次调优内存、延迟、吞吐量等指标;
5. 对比观察调优前后的差异;
6. 不断的分析和调整,直到找到合适的JVM参数配置;
7. 找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。
### JVM参数解析及调优
- -Xmx4g –Xms4g –Xmn1200m –Xss512k -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:PermSize=100m -XX:MaxPermSize=256m -XX:MaxTenuringThreshold=15
- XX:+PrintGCDetails
  - -Xlog
- -Xmx4g:堆内存最大值为4GB。
- -Xms4g:初始化堆内存大小为4GB。
  - 初始化堆内存大小,默认为物理内存的1/64(小于1GB)。
- -Xmn1200m:设置年轻代大小为1200MB。增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
- -Xss512k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1MB,以前每个线程堆栈大小为256K。应根据应用线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
- -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
- -XX:SurvivorRatio=8:设置年轻代中Eden区与Survivor区的大小比值。设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
- -XX:PermSize=100m:初始化永久代大小为100MB。
- -XX:MaxPermSize=256m:设置持久代大小为256MB。
- -XX:MaxTenuringThreshold=15:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
- -XX:MaxDirectMemorySize=1G:直接内存。报java.lang.OutOfMemoryError: Direct buffer memory异常可以上调这个值。
- -XX:+DisableExplicitGC:禁止运行期显式地调用System.gc()来触发fulll GC。
- -XX:ConcGCThreads=4:CMS垃圾回收器并行线程线,推荐值为CPU核心数。
- -XX:ParallelGCThreads=8:新生代并行收集器的线程数。
- -XX:+HeapDumpOnOutOfMemoryError 当首次遭遇内存溢出时Dump出此时的堆内 ,再使用jhat分析
- 内存优化示例
  - java heap:参数-Xms和-Xmx,建议扩大至3-4倍FullGC后的老年代空间占用。
  - 永久代:-XX:PermSize和-XX:MaxPermSize,建议扩大至1.2-1.5倍FullGc后的永久带空间占用。
  - 新生代:-Xmn,建议扩大至1-1.5倍FullGC之后的老年代空间占用。
  - 老年代:2-3倍FullGC后的老年代空间占用。
- 延迟优化示例
  - 应用程序可接受的平均停滞时间: 此时间与测量的Minor
  - GC持续时间进行比较。可接受的Minor GC频率:Minor
  - GC的频率与可容忍的值进行比较。
  - 可接受的最大停顿时间:最大停顿时间与最差情况下FullGC的持续时间进行比较。
  - 可接受的最大停顿发生的频率:基本就是FullGC的频率。
  - 新生代空间越大,Minor GC的GC时间越长,频率越低。如果想减少其持续时长,就需要减少其空间大小。如果想减小其频率,就需要加大其空间大小。

================================================
FILE: Java-JVM/Java即时编译.md
================================================
# 什么是
- 常见的编译型语言如C++,通常会把代码直接编译成CPU所能理解的机器码来运行。而Java为了实现“一次编译,处处运行”的特性,把编译的过程分成两部分,首先它会先由javac编译成通用的中间形式——字节码,然后再由解释器逐条将字节码解释为机器码来执行。所以在性能上,Java通常不如C++这类编译型语言
- 为了优化Java的性能 ,JVM在解释器之外引入了即时(Just In
  Time)编译器:当程序运行时,解释器首先发挥作用,代码可以直接执行。随着时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。解释器这时可以作为编译运行的降级手段,在一些不可靠的编译优化出现问题时,再切换回解释执行,保证程序可以正常运行
- 即时编译器极大地提高了Java程序的运行速度,而且跟静态编译相比,即时编译器可以选择性地编译热点代码,省去了很多编译时间,也节省很多的空间。目前,即时编译器已经非常成熟了,在性能层面甚至可以和编译型语言相比。不过在这个领域,大家依然在不断探索如何结合不同的编译方式,使用更加智能的手段来提升程序的运行速度。

# Java的执行过程

## Java的执行过程整体可以分为两个部分

1. 第一步由javac将源码编译成字节码,在这个过程中会进行词法分析、语法分析、语义分析,编译原理中这部分的编译称为前端编译
2. 接下来无需编译直接逐条将字节码解释执行,在解释执行的过程中,虚拟机同时对程序运行的信息进行收集,在这些信息的基础上,编译器会逐渐发挥作用,它会进行后端编译——把字节码编译成机器码,但不是所有的代码都会被编译,只有被JVM认定为的热点代码,才可能被编译。
   - **热点代码** JVM中会设置一个阈值,当方法或者代码块的在一定时间内的调用次数超过这个阈值时就会被编译,存入codeCache中。当下次执行时,再遇到这段代码,就会从codeCache中读取机器码,直接执行,以此来提升程序运行的性能

## 1.JVM中的编译器

JVM中集成了两种编译器

### Client Compiler

- Client Compiler注重启动速度和局部的优化
- HotSpot VM带有一个Client Compiler C1编译器。这种编译器启动速度快,但是性能比较Server Compiler来说会差一些。C1会做三件事
    - 局部简单可靠的优化,比如字节码上进行的一些基础优化,方法内联、常量传播等,放弃许多耗时较长的全局优化。
    - 将字节码构造成高级中间表示(High-level Intermediate Representation,以下称为HIR),HIR与平台无关,通常采用图结构,更适合JVM对程序进行优化。
    - 最后将HIR转换成低级中间表示(Low-level Intermediate
      Representation,以下称为LIR),在LIR的基础上会进行寄存器分配、窥孔优化(局部的优化方式,编译器在一个基本块或者多个基本块中,针对已经生成的代码,结合CPU自己指令的特点,通过一些认为可能带来性能提升的转换规则或者通过整体的分析,进行指令转换,来提升代码性能)等操作,最终生成机器码。

### Server Compiler

- Server Compiler则更加关注全局的优化,性能会更好,但由于会进行更多的全局分析,所以启动速度会变慢
- Server Compiler主要关注一些编译耗时较长的全局优化,甚至会还会根据程序运行的信息进行一些不可靠的激进优化。这种编译器的启动时间长,适用于长时间运行的后台程序,它的性能通常比Client
  Compiler高30%以上。目前,Hotspot虚拟机中使用的Server Compiler有两种:C2和Graal
    - C2 Compiler
        - C2编译器在进行编译优化时,会使用一种控制流与数据流结合的图数据结构,称为Ideal Graph。 Ideal
          Graph表示当前程序的数据流向和指令间的依赖关系,依靠这种图结构,某些优化步骤(尤其是涉及浮动代码块的那些优化步骤)变得不那么复杂
        - Ideal
          Graph的构建是在解析字节码的时候,根据字节码中的指令向一个空的Graph中添加节点,Graph中的节点通常对应一个指令块,每个指令块包含多条相关联的指令,JVM会利用一些优化技术对这些指令进行优化,比如Global
          Value Numbering、常量折叠等,解析结束后,还会进行一些死代码剔除的操作。生成Ideal
          Graph后,会在这个基础上结合收集的程序运行信息来进行一些全局的优化,这个阶段如果JVM判断此时没有全局优化的必要,就会跳过这部分优化。
        - 构建Ideal Graph其实就是把指令添加到Ideal Graph,然后形成一个指向这个指令的节点,并且用各种优化手段优化这些指令
        - 无论是否进行全局优化,Ideal Graph都会被转化为一种更接近机器层面的MachNode Graph,最后编译的机器码就是从MachNode Graph中得的,生成机器码前还会有一些包括寄存器分配、窥孔优化等操作
    - Graal Compiler
        - 从JDK 9开始,Hotspot VM中集成了一种新的Server Compiler,Graal编译器。相比C2编译器,Graal有这样几种关键特性
        - JVM会在解释执行的时候收集程序运行的各种信息,然后编译器会根据这些信息进行一些基于预测的激进优化,比如分支预测,根据程序不同分支的运行概率,选择性地编译一些概率较大的分支。Graal比C2更加青睐这种优化,所以Graal的峰值性能通常要比C2更好
            - 使用Java编写,对于Java语言,尤其是新特性,比如Lambda、Stream等更加友好。
            - 更深层次的优化,比如虚函数的内联、部分逃逸分析等
        - Graal编译器可以通过Java虚拟机参数-XX:+UnlockExperimentalVMOptions -XX:
          +UseJVMCICompiler启用。当启用时,它将替换掉HotSpot中的C2编译器,并响应原本由C2负责的编译请求。

## 2.分层编译

### 什么是

- 在Java 7以前,需要研发人员根据服务的性质去选择编译器
- 对于需要快速启动的,或者一些不会长期运行的服务,可以采用编译效率较高的C1,对应参数-client。长期运行的服务,或者对峰值性能有要求的后台服务,可以采用峰值性能更好的C2,对应参数-server
- Java 7开始引入了分层编译的概念,它结合了C1和C2的优势,追求启动速度和峰值性能的一个平衡。分层编译将JVM的执行状态分为了五个层次

### 五个层级

- 层次
    - 解释执行。
    - 执行不带profiling的C1代码。
        - profiling就是收集能够反映程序执行状态的数据。其中最基本的统计数据就是方法的调用次数,以及循环回边的执行次数
    - 执行仅带方法调用次数以及循环回边执行次数profiling的C1代码。
    - 执行带所有profiling的C1代码。
    - 执行C2代码。
- 特点
    - 通常情况下,C2代码的执行效率要比C1代码的高出30%以上。C1层执行的代码,按执行效率排序从高至低则是1层>2层>3层。
    - 这5个层次中,1层和4层都是终止状态,当一个方法到达终止状态后,只要编译后的代码并没有失效,那么JVM就不会再次发出该方法的编译请求的。
    - 服务实际运行时,JVM会根据服务运行情况,从解释执行开始,选择不同的编译路径,直到到达终止状态
- 常见的编译路径
  ![](../img/jvm/常见的编译路径.png)
    - 图中第①条路径,代表编译的一般情况,热点方法从解释执行到被3层的C1编译,最后被4层的C2编译。
    -
    如果方法比较小(比如Java服务中常见的getter/setter方法),3层的profiling没有收集到有价值的数据,JVM就会断定该方法对于C1代码和C2代码的执行效率相同,就会执行图中第②条路径。在这种情况下,JVM会在3层编译之后,放弃进入C2编译,直接选择用1层的C1编译运行。
    - 在C1忙碌的情况下,执行图中第③条路径,在解释执行过程中对程序进行profiling ,根据信息直接由第4层的C2编译。
    - 前文提到C1中的执行效率是1层>2层>3层,第3层一般要比第2层慢35%以上,所以在C2忙碌的情况下,执行图中第④条路径。这时方法会被2层的C1编译,然后再被3层的C1编译,以减少方法在3层的执行时间。
    - 如果编译器做了一些比较激进的优化,比如分支预测,在实际运行时发现预测出错,这时就会进行反优化,重新进入解释执行,图中第⑤条执行路径代表的就是反优化。
- 总的来说,C1的编译速度更快,C2的编译质量更高,分层编译的不同编译路径,也就是JVM根据当前服务的运行情况来寻找当前服务的最佳平衡点的一个过程。从JDK 8开始,JVM默认开启分层编译。

## 3.即时编译的触发

### Java虚拟机根据方法的调用次数以及循环回边的执行次数来触发即时编译
- 循环回边
    - 循环回边是一个控制流图中的概念,程序中可以简单理解为往回跳转的指令,比如下面这段代码:
      ```java
      public void nlp(Object obj) {
         int sum = 0;
         for (int i = 0; i < 200; i++) {
             sum += i;
         }
      }
      ```

    - 上面这段代码经过编译生成下面的字节码。其中,偏移量为18的字节码将往回跳至偏移量为4的字节码中。在解释执行时,每当运行一次该指令,Java虚拟机便会将该方法的循环回边计数器加1。 
    - 字节码 
      ```java
        public void nlp(java.lang.Object); 
          Code:
             0: iconst_0 
             1: istore_1 
             2: iconst_0 
             3: istore_2 
             4: iload_2 
             5: sipush 200 
             8: if_icmpge 21 
             11: iload_1 
             12: iload_2 
             13:iadd 
             14: istore_1 
             15: iinc 2, 1 
             18: goto 4 
             21: return
      ```
      
- 在即时编译过程中,编译器会识别循环的头部和尾部。上面这段字节码中,循环体的头部和尾部分别为偏移量为11的字节码和偏移量为15的字节码。编译器将在循环体结尾增加循环回边计数器的代码,来对循环进行计数

当方法的调用次数和循环回边的次数的和,超过由参数`-XX:CompileThreshold`指定的阈值时(使用C1时,默认值为1500;使用C2时,默认值为10000),就会触发即时编译

开启分层编译的情况下,`-XX:CompileThreshold`参数设置的阈值将会失效,触发编译会由以下的条件来判断 

- 方法调用次数大于由参数`-XX:TierXInvocationThreshold`指定的阈值乘以系数。 
- 方法调用次数大于由参数`-XX:TierXMINInvocationThreshold`指定的阈值乘以系数,并且方法调用次数和循环回边次数之和大于由参数-XX:TierXCompileThreshold指定的阈值乘以系数时。 
### 分层编译触发条件公式 
- `i >TierXInvocationThreshold * s || (i > TierXMinInvocationThreshold * s && i + b > TierXCompileThreshold * s)`i为调用次数,b是循环回边次数 
- 上述满足其中一个条件就会触发即时编译,并且JVM会根据当前的编译方法数以及编译线程数动态调整系数s。

# 编译优化
即时编译器会对正在运行的服务进行一系列的优化,包括字节码解析过程中的分析,根据编译过程中代码的一些中间形式来做局部优化,还会根据程序依赖图进行全局优化,最后才会生成机器码
## 1. 中间表达形式(Intermediate Representation)
在编译原理中,通常把编译器分为前端和后端,前端编译经过词法分析、语法分析、语义分析生成中间表达形式(Intermediate Representation,以下称为IR),后端会对IR进行优化,生成目标代码
- Java字节码就是一种IR,但是字节码的结构复杂,字节码这样代码形式的IR也不适合做全局的分析优化。现代编译器一般采用图结构的IR,静态单赋值(Static Single Assignment,SSA)IR是目前比较常用的一种。这种IR的特点是每个变量只能被赋值一次,而且只有当变量被赋值之后才能使用
  - SSA IR
   ``` 
   Plain Text
    {
        a = 1;
        a = 2;
        b = a;
    }
  ```
  - 上述代码中我们可以轻易地发现a = 1的赋值是冗余的,但是编译器不能。传统的编译器需要借助数据流分析,从后至前依次确认哪些变量的值被覆盖掉。不过,如果借助了SSA IR,编译器则可以很容易识别冗余赋值。
  - 上面代码的SSA IR形式的伪代码可以表示为
  ```Plain Text
  {
      a_1 = 1;
      a_2 = 2;
      b_1 = a_2;
  }
  ```
  - 由于SSA IR中每个变量只能赋值一次,所以代码中的a在SSA IR中会分成a_1、a_2两个变量来赋值,这样编译器就可以很容易通过扫描这些变量来发现a_1的赋值后并没有使用,赋值是冗余的
- 我们可以将编译器的每一种优化看成一个图优化算法,它接收一个IR图,并输出经过转换后的IR图。编译器优化的过程就是一个个图节点的优化串联起来的
## 2.方法内联
- 方法内联,是指在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。JIT大部分的优化都是在内联的基础上进行的,方法内联是即时编译器中非常重要的一环。
- Java服务中存在大量getter/setter方法,如果没有方法内联,在调用getter/setter时,程序执行时需要保存当前方法的执行位置,创建并压入用于getter/setter的栈帧、访问字段、弹出栈帧,最后再恢复当前方法的执行。内联了对 getter/setter的方法调用后,上述操作仅剩字段访问
- 编译器的大部分优化都是在方法内联的基础上。所以一般来说,内联的方法越多,生成代码的执行效率越高。但是对于即时编译器来说,内联的方法越多,编译时间也就越长,程序达到峰值性能的时刻也就比较晚
### 虚函数内联(多态、接口)
内联是JIT提升性能的主要手段,但是虚函数使得内联是很难的,因为在内联阶段并不知道他们会调用哪个方法。例如,我们有一个数据处理的接口,这个接口中的一个方法有三种实现add、sub和multi,JVM是通过保存虚函数表Virtual Method Table(以下称为VMT)存储class对象中所有的虚函数,class的实例对象保存着一个VMT的指针,程序运行时首先加载实例对象,然后通过实例对象找到VMT,通过VMT找到对应方法的地址,所以虚函数的调用比直接指向方法地址的classic call性能上会差一些。很不幸的是,Java中所有非私有的成员函数的调用都是虚调用
- 比如如下代码将会优化
```java
    public class SimpleInliningTest {
    public static void main(String[] args) throws InterruptedException {
        VirtualInvokeTest obj = new VirtualInvokeTest();
        VirtualInvoke1 obj1 = new VirtualInvoke1();
        for (int i = 0; i < 100000; i++) {
            invokeMethod(obj);
            invokeMethod(obj1);
        }
        Thread.sleep(1000);
    }
    public static void invokeMethod(VirtualInvokeTest obj) {
        obj.methodCall();
    }
    private static class VirtualInvokeTest {
        public void methodCall() {
            System.out.println("virtual call");
        }
    }
    private static class VirtualInvoke1 extends VirtualInvokeTest {
        @Override
        public void methodCall() {
            super.methodCall();
        }
    }
}
```

- 如下代码多个实现未被优化
```java
    public class SimpleInliningTest {
    public static void main(String[] args) throws InterruptedException {
        VirtualInvokeTest obj = new VirtualInvokeTest();
        VirtualInvoke1 obj1 = new VirtualInvoke1();
        VirtualInvoke2 obj2 = new VirtualInvoke2();
        for (int i = 0; i < 100000; i++) {
            invokeMethod(obj);
            invokeMethod(obj1);
            invokeMethod(obj2);
        }
        Thread.sleep(1000);
    }
    public static void invokeMethod(VirtualInvokeTest obj) {
        obj.methodCall();
    }
    private static class VirtualInvokeTest {
        public void methodCall() {
            System.out.println("virtual call");
        }
    }
    private static class VirtualInvoke1 extends VirtualInvokeTest {
        @Override
        public void methodCall() {
            super.methodCall();
        }
    }
    private static class VirtualInvoke2 extends VirtualInvokeTest {
        @Override
        public void methodCall() {
            super.methodCall();
        }
    }
}
```

## 3. 逃逸分析
逃逸分析是“一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针”。Java虚拟机的即时编译器会对新建的对象进行逃逸分析,判断对象是否逃逸出线程或者方法。即时编译器判断对象是否逃逸的依据有两种
- 对象是否被存入堆中(静态字段或者堆中对象的实例字段),一旦对象被存入堆中,其他线程便能获得该对象的引用,即时编译器就无法追踪所有使用该对象的代码位置。
- 对象是否被传入未知代码中,即时编译器会将未被内联的代码当成未知代码,因为它无法确认该方法调用会不会将调用者或所传入的参数存储至堆中,这种情况,可以直接认为方法调用的调用者以及参数是逃逸的。
### 1、锁消除
- 在学习Java并发编程时会了解锁消除,而锁消除就是在逃逸分析的基础上进行的。
- 如果即时编译器能够证明锁对象不逃逸,那么对该锁对象的加锁、解锁操作没就有意义。因为线程并不能获得该锁对象。在这种情况下,即时编译器会消除对该不逃逸锁对象的加锁、解锁操作。实际上,编译器仅需证明锁对象不逃逸出线程,便可以进行锁消除。由于Java虚拟机即时编译的限制,上述条件被强化为证明锁对象不逃逸出当前编译的方法。不过,基于逃逸分析的锁消除实际上并不多见。
### 2、栈上分配
- 我们都知道Java的对象是在堆上分配的,而堆是对所有对象可见的。同时,JVM需要对所分配的堆内存进行管理,并且在对象不再被引用时回收其所占据的内存
- 如果逃逸分析能够证明某些新建的对象不逃逸,那么JVM完全可以将其分配至栈上,并且在new语句所在的方法退出时,通过弹出当前方法的栈桢来自动回收所分配的内存空间。这样一来,我们便无须借助垃圾回收器来处理不再被引用的对象
### 3、标量替换
过Hotspot虚拟机,并没有进行实际的栈上分配,而是使用了标量替换这一技术。所谓的标量,就是仅能存储一个值的变量,比如Java代码中的基本类型。与之相反,聚合量则可能同时存储多个值,其中一个典型的例子便是Java的对象。编译器会在方法内将未逃逸的聚合量分解成多个标量,以此来减少堆上分配。下面是一个标量替换的例子
  ```java
    public class Example{
      @AllArgsConstructor
      class Cat{
          int age;
          int weight;
      }
      public static void example(){
          Cat cat = new Cat(1,10);
          addAgeAndWeight(cat.age,Cat.weight);
      }
  }
  ```
  - 经过逃逸分析,cat对象未逃逸出example()的调用,因此可以对聚合量cat进行分解,得到两个标量age和weight,进行标量替换后的伪代码:
    ```java
        public class Example{
        @AllArgsConstructor
        class Cat{
            int age;
            int weight;
        }
        public static void example(){
            int age = 1;
            int weight = 10;
            addAgeAndWeight(age,weight);
        }
    }
    ```

### 4、部分逃逸分析
部分逃逸分析也是Graal对于概率预测的应用。通常来说,如果发现一个对象逃逸出了方法或者线程,JVM就不会去进行优化,但是Graal编译器依然会去分析当前程序的执行路径,它会在逃逸分析基础上收集、判断哪些路径上对象会逃逸,哪些不会。然后根据这些信息,在不会逃逸的路径上进行锁消除、栈上分配这些优化手段。
## 4. Loop Transformations
C2编译器在构建Ideal Graph后会进行很多的全局优化,其中就包括对循环的转换,最重要的两种转换就是循环展开和循环分离
### 1、循环展开
循环展开是一种循环转换技术,它试图以牺牲程序二进制码大小为代价来优化程序的执行速度,是一种用空间换时间的优化手段。
- 循环展开通过减少或消除控制程序循环的指令,来减少计算开销,这种开销包括增加指向数组中下一个索引或者指令的指针算数等。如果编译器可以提前计算这些索引,并且构建到机器代码指令中,那么程序运行时就可以不必进行这种计算。也就是说有些循环可以写成一些重复独立的代码。比如下面这个循环
```java
    public void loopRolling(){
        for(int i = 0;i<200;i++){
            delete(i);
        }
    }
```
- 上面的代码需要循环删除200次,通过循环展开可以得到下面这段代码:
```java
    public void loopRolling(){
        for(int i = 0;i<200;i+=5){
            delete(i);
            delete(i+1);
            delete(i+2);
            delete(i+3);
            delete(i+4);
        }
    }
```
- 这样展开就可以减少循环的次数,每次循环内的计算也可以利用CPU的流水线提升效率。当然这只是一个示例,实际进行展开时,JVM会去评估展开带来的收益,再决定是否进行展开。
### 2、循环分离
循环分离也是循环转换的一种手段。它把循环中一次或多次的特殊迭代分离出来,在循环外执行。举个例子,下面这段代码
```java
int a = 10;
for(int i = 0;i<10;i++){
    b[i] = x[i] + x[a];
    a = i;
}
```
可以看出这段代码除了第一次循环a = 10以外,其他的情况a都等于i-1。所以可以把特殊情况分离出去,变成下面这段代码
```java
b[0] = x[0] + 10;
for(int i = 1;i<10;i++){
    b[i] = x[i] + x[i-1];
}
```
这种等效的转换消除了在循环中对a变量的需求,从而减少了开销。
## 5. 窥孔优化与寄存器分配
前文提到的窥孔优化是优化的最后一步,这之后就会程序就会转换成机器码,窥孔优化就是将编译器所生成的中间代码(或目标代码)中相邻指令,将其中的某些组合替换为效率更高的指令组,常见的比如强度削减、常数合并等,看下面这个例子就是一个强度削减的例子
- 强度削减
  - y1=x1*3  经过强度削减后得到  y1=(x1<<1)+x1
  - 编译器使用移位和加法削减乘法的强度,使用更高效率的指令组
- 寄存器分配
  - 寄存器分配也是一种编译的优化手段,在C2编译器中普遍的使用。它是通过把频繁使用的变量保存在寄存器中,CPU访问寄存器的速度比内存快得多,可以提升程序的运行速度。
  - 寄存器分配和窥孔优化是程序优化的最后一步。经过寄存器分配和窥孔优化之后,程序就会被转换成机器码保存在codeCache中

# 参考文章
- https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html

================================================
FILE: Java-JVM/Java对象头.md
================================================
# Java对象头

由于Java面向对象的思想,在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。

# 对象头形式

JVM中对象头的方式有以下两种(以32位JVM为例):

## 普通对象

```text
|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (32 bits)         |    Klass Word (32 bits) |
|------------------------------------|-------------------------|
```

## 数组对象

```text
|---------------------------------------------------------------------------------|
|                                 Object Header (96 bits)                         |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|
```

# 对象头的组成

## Mark Word

这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。
为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:

```text
|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                              | lock:2 |    Marked for GC   |
|-------------------------------------------------------|--------------------|
```

其中各部分的含义如下:

lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。

|biased_lock|    lock|    状态|
|:----------|:----|:-----|
|0|    01    |无锁|
|1|    01    |偏向锁|
|0|    00    |轻量级锁|
|0|    10    |重量级锁|
|0|    11    |GC标记|

- biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
- age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
- identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
- thread:持有偏向锁的线程ID。
- epoch:偏向时间戳。
- ptr_to_lock_record:指向栈中锁记录的指针。
- ptr_to_heavyweight_monitor:指向管程Monitor的指针。

64位下的标记字与32位的相似,不再赘述:
```text
|------------------------------------------------------------------------------|--------------------|
|                                  Mark Word (64 bits)                         |       State        |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|------------------------------------------------------------------------------|--------------------|
| thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|------------------------------------------------------------------------------|--------------------|
|                       ptr_to_lock_record:62                         | lock:2 | Lightweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                     ptr_to_heavyweight_monitor:62                   | lock:2 | Heavyweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                                                                     | lock:2 |    Marked for GC   |
|------------------------------------------------------------------------------|--------------------|
```
## class pointer
这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。

如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

1. 每个Class的属性指针(即静态变量)
2. 每个对象的属性指针(即对象变量)
3. 普通对象数组的每个元素指针

## array length
如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。


# 参考文章

- https://www.jianshu.com/p/3d38cba67f8b

================================================
FILE: Java-JVM/内存分配与回收策略.md
================================================

* [对象优先在 Eden 分配](#对象优先在-eden-分配)
* [大对象直接进入老年代](#大对象直接进入老年代)
* [长期存活的对象进入老年代](#长期存活的对象进入老年代)
* [动态对象年龄判定](#动态对象年龄判定)
* [空间分配担保](#空间分配担保)

### 对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
### 大对象直接进入老年代
- 大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
- 经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象
- -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
### 长期存活的对象进入老年代
- 为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
- -XX:MaxTenuringThreshold 用来定义年龄的阈值
### 动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold 中要求的年龄。
### 空间分配担保
- 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的
- 如果不成立的话则虚拟机会先查看- XX:HandlePromotionFailure参数的设置值是否允许担保失败,如果允许那么就会继续检查老年代 最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者-XX: HandlePromotionFailure设置不允许冒险,那么就要进行一次 Full GC。
  - 解释一下“冒险”是冒了什么风险:前面提到过,新生代使用复制收集算法,但为了内存利用率, 只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况 ——最极端的情况就是内存回收后新生代中所有对象都存活,需要老年代进行分配担保,把Survivor无 法容纳的对象直接送入老年代,这与生活中贷款担保类似。老年代要进行这样的担保,前提是老年代 本身还有容纳这些对象的剩余空间,但一共有多少对象会在这次回收中活下来在实际完成内存回收之 前是无法明确知道的,所以只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,与 老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

================================================
FILE: Java-JVM/内存结构.md
================================================
# 内存结构
## JDK1.7和1.8对比
<img src="../img/jvm/java7内存结构.png" alt="java7内存结构"  width="50%" /><img src="../img/jvm/java8内存结构.png" alt="java8内存结构"  width="50%" />

# 程序计数器
- 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的 字节码的行号指示器
- 字节码解释器工作时就是通过改变这个计数器 的值来选取下一条需要执行的字节码指令
# Java虚拟机栈
虚拟机栈描述的是Java方法执行的线程内存模型:
- 每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧[1](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信 息
- 每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小,在 JDK 1.4 中默认为 256K,而在 JDK1.5+ 默认为 1M:
  - java -Xss2M HackTheJava
- 异常
  - 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
  - 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
# 本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机 栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。
# 堆
“几乎”所有的对象实例都在这里分配内存
## 堆的分代(g1前)
- 新生代 Eden:Survivor=8:1:1
  - Eden
  - Survivor
    - to
    - from
- 老年代
- JVM参数 新生代:老年代=1:2
  - -Xms 初始值
  - -Xmx 最大值

## 新的堆内存结构

G1 之前的 JVM 内存模型

<img src="../img/jvm/g1之前的垃圾收集器.png" align="center"  width="60%" />

G1收集器的内存模型

<img src="../img/jvm/G1内存布局.png" align="center"  width="60%" />

堆内存会被切分成为很多个固定大小区域(Region),每个是连续范围的虚拟内存。

堆内存中一个区域 (Region) 的大小,可以通过 -XX:G1HeapRegionSize 参数指定,大小区间最小 1M 、最大 32M ,总之是 2 的幂次方。

默认是将堆内存按照 2048 份均分。

每个 Region 被标记了 E、S、O 和 H,这些区域在逻辑上被映射为 Eden,Survivor 和老年代。

存活的对象从一个区域转移(即复制或移动)到另一个区域。区域被设计为并行收集垃圾,可能会暂停所有应用线程。如上图所示,区域可以分配到 Eden,survivor 和老年代。

此外,还有第四种类型,被称为巨型区域(Humongous Region)。

Humongous 区域主要是为存储超过 50% 标准 region 大小的对象设计,它用来专门存放巨型对象。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储。为了能找到连续的 H 区,有时候不得不启动 Full GC 。

## 逃逸分析
逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
- 逃逸分析的基本行为就是分析对象动态作用域
- 举例
  - 方法逃逸
    - 当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸
    - 代码
      - ```java
        public static StringBuffer craeteStringBuffer(String s1, String s2) {
           StringBuffer sb = new StringBuffer();
           sb.append(s1);
           sb.append(s2);
           return sb;
        }
        ```
      - StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部
      - 上述代码如果想要StringBuffer sb不逃出方法,可以这样写
      - ```java
        public static String createStringBuffer(String s1, String s2) {
            StringBuffer sb = new StringBuffer();
            sb.append(s1);
            sb.append(s2);
            return sb.toString();
        }
        ```
      - 不直接返回 StringBuffer,那么StringBuffer将不会逃逸出方法
  - 线程逃逸
    - 甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸
- 使用逃逸分析,编译器可以对代码做如下优化
  - 一、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
  - 二、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
    - JVM参数
      - -XX:+DoEscapeAnalysis : 表示开启逃逸分析
      - -XX:-DoEscapeAnalysis : 表示关闭逃逸分析
    - 对象的栈上内存分配
      - 我们知道,在一般情况下,对象和数组元素的内存分配是在堆内存上进行的。但是随着JIT编译器的日渐成熟,很多优化使这种分配策略并不绝对,JIT编译器就可以在编译期间根据逃逸分析的结果,来决定是否可以将对象的内存分配从堆转化为栈
  - 三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
# 方法区
## 1.7

<img src="../img/jvm/java7堆和方法区.png" align="center"  width="50%" />

用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK

## 1.8 
开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。

## 方法区的回收
- 因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高
- 主要是对常量池的回收和对类的卸载
- 为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能
- 类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载
  - 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
  - 加载该类的 ClassLoader 已经被回收。
  - 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
# 运行时常量池
运行时常量池是方法区的一部分。 
- Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
  - 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生 成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
- 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
# 直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分
- 在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了 在Java堆和Native堆中来回复制数据。
  - 受到 本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制

# 参考文章
- https://www.hollischuang.com/archives/2398

================================================
FILE: Java-JVM/垃圾回收.md
================================================
# 判断一个对象能否被回收
## 引用计数法
- 为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
- 在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
## 可达性算法

<div style="text-align: center;">
    <img src="../img/jvm/gcRoot.png" width="70%" />
</div>

以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
## 哪些可以作为是根节点
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 正在运⾏的线程
- 锁住的对象

这种算法的优点是简单、高效,但缺点是有可能出现"内存泄漏",即存在一些对象已经无用了,但是由于这些对象与GC Roots对象之间存在间接引用,导致这些对象不能被回收。

除了可达性分析算法外,还有一些高级的垃圾回收算法,如G1垃圾收集器的Region To Space算法,以及ZGC垃圾收集器的读屏障算法等。这些算法都能够更加精确地判断对象是否可以被回收,以及更加高效地进行垃圾回收。

## 方法区的回收
## finalize()
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。
- 经过可达性分析后如果对象没有域root有任何相关的引用链,则会被标记,随后经过一次筛选,筛选的条件就是是否需要执行finalize()方法,如果对象没有覆盖finalize方法,或者已经被虚拟机调用过。则没必要执行
- 如果需要执行,则把对象放在一个名为F-Queue的队列中中,并由一个优先级低的由虚拟机创建的线程去执行他们的finalize()方法,这里的执行只是触发,并不一定要等待他们执行完,否则如果该方法之心缓慢甚至死循环,则会阻塞队列
## 引用类型
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。

引用类型的种类
### 强引用
- 被强引用关联的对象不会被回收。
- 使用 new 一个新对象的方式来创建强引用。`Object obj = new Object();`
### 软引用
  - 被软引用关联的对象只有在内存不够的情况下才会被回收。
  - 使用 SoftReference 类来创建软引用。
  ```java
    Object obj = new Object();
    SoftReference<Object> sf = new SoftReference<>(obj);
    obj = null;//使对象只被软引用关联
  ```
### 弱引用
- 被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
- 使用 WeakReference 类来创建弱引用。
  ```java
    Object obj = new Object();
    WeakReference<Object> sf = new WeakReference<>(obj);
    obj = null;
  ```
### 虚引用
- 又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。
- 为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。
- 使用 PhantomReference 来创建虚引用。
  ```java
    Object obj = new Object();
    PhantomReference<Object> sf = new PhantomReference<>(obj,null);
    obj = null;
  ```
# 分代收集理论
当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)[1]的理论进 行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分 代假说之上:
- 1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
- 2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消 亡。

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分 出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区 域之中存储

# GC定义
## 新生代收集(Minor GC/Young GC)
指目标只是新生代的垃圾收集。
- 当Eden区满了的时候,会触发Young GC
## 老年代收集(Major GC/Old GC)
指目标只是老年代的垃圾收集。目前只有CMS收集器会有单 独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。
## 混合收集(Mixed GC)
指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收 集器会有这种行为。
## 整堆收集(Full GC) 
收集整个Java堆和方法区的垃圾收集
- 在发生Young GC的时候,虚拟机会检测之前每次晋升到老年代的平均大小是否大于年老代的剩余空间,如果大于,则直接进行Full GC;
- 如果小于,但设置了Handle PromotionFailure,那么也会执行Full GC。
  - -XX:HandlePromotionFailure:是否设置空间分配担保 JDK7及以后这个参数就失效了. 只要老年代的连续空间大于新生代对象的总大小或者历次晋升到老年代的对象的平均大小就进行MinorGC,否则FullGC
- 永久代空间不足,会触发Full GC
- System.gc()也会触发Full GC
- 堆中分配很大的对象 所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。
# 回收算法
## 标记-清除

<div style="text-align: center;">
    <img src="../img/jvm/标记清除.png" width="70%" />
</div>

### 原理
首先标记出所有需要回 收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回 收所有未被标记的对象
### 优缺点
- 优点
  - 简单
- 缺点
  - 标记和清除过程效率都不高;
  - 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
## 标记-复制

<div style="text-align: center;">
    <img src="../img/jvm/标记复制.png" width="70%" />
</div>

### 原理
- 为了解决标记-清除算法面对大量可回收对象时执行效率低 的问题
- 它将可用 内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着 的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
- 新生代采用此算法
### 优缺点
- 优点
  - 不会产生内存碎片
- 缺点
  - 空间浪费
  - 对象存活率较高时就要进行较多的复制操作,效率将会降低
## 标记-整理

<div style="text-align: center;">
    <img src="../img/jvm/标记整理.png" width="70%" />
</div>

### 原理
- 让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
- 老年代采用此算法
### 优缺点
- 优点
  - 不会产生内存碎片
- 缺点
  - 需要移动大量对象,处理效率比较低。

# Hotspot算法实现细节
## 根节点枚举GC Roots
- 必须暂定用户线程STW
- 需要在一个能保障一致性的快照中进行
  - 如果在分析过程对象引用关系不断变化肯定是不行的
- 但是如果在此时依次枚举成百上万的关系肯定会耗费大大量的时间
- 所以应当是能直接获取到哪些地方存放着对象的引用
- 一个数据结构OopMap
  - 一旦类加载完成,就会把对象内什么偏移量上是什么类型的数据计算出来,在特定的位置记录下栈里和寄存器里哪些位置是引用
  - 相当于把引用已经存在了这里,扫描时直接来拿就好了
## 安全点Safe Point
- 在OopMap的协助下,HotSpot可以快速准确地完成GC Roots枚举,但一个很现实的问题随之而 来:可能导致引用关系变化,或者说导致OopMap内容变化的指令非常多,如果为每一条指令都生成 对应的OopMap,那将会需要大量的额外存储空间,这样垃圾收集伴随而来的空间成本就会变得无法 忍受的高昂。
- 实际上HotSpot也的确没有为每条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录 了这些信息,这些位置被称为安全点(Safepoint)。
- 让线程跑到安全点再GC
  - 抢先式中断
    - GC时中断所有的用户线程
    - 如果发现该线程不在安全点则恢复,直到它跑到了安全点
      - 现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件。
  - 主动式中断
    - 不主动中断线程,设置一个标志位,让线程自己主动轮询这个标志位,一旦发现标志位为真,就自己在最近的安全点主动中断挂起
## 安全区域Safe Region
如果线程未获取到处理器时间则无法到达安全点
- 于是设置一个安全区域,在这个区域的任意地方开始GC都是安全的
  - 执行到安全区时标识自己已进入,当离开时检查是否完成了根节点枚举,完成了继续执行,否则一直等待直到收到离开安全区的信号为止
## 记忆集Remembered Set与卡表Card Table
在Java虚拟机(JVM)中,垃圾收集器(GC)负责回收不再使用的对象以释放内存。Java堆内存通常被分为几个部分,主要包括:

1. 年轻代(Young Generation):新创建的对象被分配在这里,大部分对象在这里很快被回收。
2. 老年代(Old Generation 或 Tenured Generation):经过一定次数垃圾回收仍然存活的对象会被移到这里。
3. 永久代(Permanent Generation,在Java 8中被元空间Metaspace取代):存放类和方法信息等。

跨代引用,即一个老年代对象引用了一个年轻代对象,或者反过来,会影响垃圾收集器的效率。在JVM中,主要使用了两种算法来处理跨代引用的问题:

1. **记忆集(Remembered Set)**:用于记录从老年代到年轻代的引用。垃圾收集器在进行Minor GC(只清理年轻代的GC)时,只会检查年轻代内存。然而,如果存在老年代对象引用年轻代对象,这些年轻代的对象即使没有在年轻代内部被引用,也不能被当作垃圾回收。记忆集就是用来避免整个老年代扫描这种情况的,它记录下哪些老年代的区域包含指向年轻代对象的引用,从而只扫描这些区域。

2. **卡表(Card Table)**:它是记忆集的一种实现,通常使用一个字节数组来实现。堆被分成了许多小的区域,这些区域称为“卡(cards)”。每个卡对应于记忆集中的一个条目。当老年代对象持有年轻代对象的引用时,这个引用所在的卡被标记为脏卡(dirty card),表示该区域有横跨不同代的引用。Minor GC时,垃圾收集器将只检查这些脏卡对应的老年代部分,来更新跨代引用。

处理好跨代引用对于垃圾回收的性能至关重要。一个高效的GC必须尽可能快地确定哪些对象是垃圾,而跨代引用如果处理不当,会大大增加GC的时间,因此记忆集和卡表等机制是GC优化中非常关键的组成部分。
## 写屏障Write Barrier
- 还没有解决卡表元素如何维 护的问题,例如它们何时变脏、谁来把它们变脏等
- 卡表元素何时变脏的答案是很明确的
  - 有其他分代区域中对象引用了本区域对象时,其对应的 卡表元素就应该变脏
  - 变脏时间点原则上应该发生在引用类型字段赋值的那一刻
- 但问题是如何变 脏,即如何在对象赋值的那一刻去更新维护卡表呢?假如是解释执行的字节码,那相对好处理,虚拟 机负责每条字节码指令的执行,有充分的介入空间;但在编译执行的场景中呢?经过即时编译后的代 码已经是纯粹的机器指令流了,这就必须找到一个在机器码层面的手段,把维护卡表的动作放到每一 个赋值操作之中。
  - 在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的
  - 写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切 面[2],在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的 前后都在写屏障的覆盖范畴内
    - 在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值 后的则叫作写后屏障(Post-Write Barrier)
    - 应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令,一旦收集器在写屏障中增加了更新 卡表操作,无论更新的是不是老年代对新生代对象的引用,每次只要对引用进行更新,就会产生额外 的开销,不过这个开销与Minor GC时扫描整个老年代的代价相比还是低得多的。
## 并发的可达性分析
### 为什么需要并发标记
首先GC的前提是:基于一个能保证一致性的快照中,这就意味必须要STW,STW的停顿时间是和需要标记的堆的对象数量成正比的,所以能相对削弱这部分的停顿时间,收益是客观的,如果不冻结用户线程就需要GC线程和用户线程同时运行,这就需要并发标记
### 三色标记Tri-color Marking(并发标记使用的方法)
#### 什么是三色标记

<div style="text-align: center;">
    <img src="../img/jvm/三色标记1.png" width="70%" />
</div>

在遍历对象图的过程中,把访问的对象都按照是否访问过标记成3种颜色
- `白色` 还未被垃圾回收器访问过,开始阶段所有对象都是白色的,在结束阶段如果对象还是白色则不可达
- `黑色` 对象已经被垃圾回收器访问过,且这个对象的所有引用都已经被扫描过,是安全存活的
- `灰色` 对象被垃圾回收器访问过,但是这个对象至少存在一个引用没有被访问过

**问题**:

如果现在GC和用户线程同时运行,GC在标记颜色,用户线程在修改引用关系,就会出现并发标记的问题
##### 并发标记存在的问题
- 把原本死亡的对象标记为存活
  - 其实这个是小问题因为这就产生了浮动的垃圾,下次清理就好了
  - 出现浮动垃圾
    - 本来应该被回收的对象,但是因为并发标记的原因,被标记为存活,这就产生了浮动垃圾
    - 例子:GC线程先标记了A为存活,用户线程修改A=null,这时候A依然可达,就是浮动垃圾
- 把原本存活的对象标记为死亡【对象消失的情况】
  - 这个问题就非常严重了,需要的对象被回收,程序肯定会发生错误
  - 例子:某个对象 A 在标记过程中被标记为黑色(即标记已经完成),表示 A 和 A 所引用的所有对象都已被访问。但在标记完成后,应用程序线程修改了 A 的引用,将其引用指向了一个新对象 B。此时,如果 B 尚未被标记,而 A 已经是黑色,则 B 可能永远不会被标记为存活对象。这种情况会导致对象 B 被误认为是死亡对象,即使它实际上仍然被引用并存活。

##### 正常标记

<div style="text-align: center;">
    <img src="../img/jvm/正常标记.png" width="70%" />
</div>


##### 并发标记-对象消失的情况
- 正在扫描的灰色对象的一个引用被切断,与一个原本已经被扫描过的黑色对象产生了引用关系

<div style="text-align: center;">
    <img src="../img/jvm/对象消失情况1.png" width="50%" />
</div>

- 原本是引用链的一部分,但是被切断了并与原本扫描的黑色节点产生了引用关系

<div style="text-align: center;">
    <img src="../img/jvm/对象消失情况2.png" width="50%" />
</div>

#### 对象消失的条件
当且仅当以下两个条件同时满足时会产生对象消失问题

- 条件一 赋值器插入了一条或者多条黑色到白色对象的引用
- 条件二 赋值器删除了全部灰色对象到白色对象的直接或间接引用

这两个条件只要破坏一个即可

<div style="text-align: center;">
    <img src="../img/jvm/对象消失的条件.png" width="50%" />
</div>

#### 对象消失的情况-解决方案

##### 写屏障(Write Barrier)
写屏障是一种技术,用于捕获应用程序线程对对象引用的修改。当应用程序修改一个引用时,写屏障可以确保新引用的对象也被正确地标记。例如:

- 增量更新(Incremental Update):当一个对象已经是黑色时,如果其引用发生了变化(指向一个新的白色对象),则写屏障会将新的引用对象标记为灰色,确保它在之后的标记过程中被扫描。增量更新破坏的是第一个条件,在新增一条引用时,将该记录保存。实际的实现中,通常是将引用相关的节点进行重新标记

- 原始快照(Snapshot-at-the-Beginning,SATB):在标记开始时快照所有的引用关系,写屏障会将旧的引用标记为灰色,即使它被更新。这可以确保所有的对象都会被正确标记,而不会因为引用更新而漏标。原始快照破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,并发扫描结束后,在将这些记录重新扫描一次。

##### 读屏障(Read Barrier)
读屏障是在读取引用对象时触发的机制,通常用于更加激进的垃圾收集器(例如 ZGC)。当应用线程试图访问某个对象时,读屏障可以确保这个对象是被正确标记的。

### 三色标记法与现代垃圾回收器
对于读写屏障,以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下:

- CMS:写屏障 + 增量更新
- G1:写屏障 + SATB(原始快照)
- ZGC:读屏障

#### 为什么G1用SATB?CMS用增量更新?
增量更新:黑色对象新增一条指向白色对象的引用,那么要进行深入扫描白色对象及它的引用对象。

原始快照:灰色对象删除了一条指向白色对象的引用,实际上就产生了浮动垃圾,好处是不需要像 CMS 那样 remark,再走一遍 root trace 这种相当耗时的流程。

我的理解:SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象,而CMS对增量引用的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度扫描。

# 垃圾回收器
## Serial收集器
### 流程

<div style="text-align: center;">
    <img src="../img/jvm/Serial收集器工作流程.png" width="70%" />
</div>

### 特点
- 单线程
- 停顿时间长
- 但对应资源紧张的服务器却是最优的选择
  - 单核
  - 内存小
  - 因为它没有切换线程的开销
- 它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
## ParNew收集器
### 流程

<div style="text-align: center;">
    <img src="../img/jvm/ParNew收集器工作流程.png" width="70%" />
</div>

### 特点
- 多线程
- 实际就是Serial的多线程版本
- 只有它能与CMS 收集器配合工作。
## ParallelScavenge收集器
### 特点
- 基于标记-复制算法
- 多线程
- 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量
  - 这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值
- 吞吐量优先收集器
### JVM参数
- -XX:MaxGCPauseMillis
  - 大于0的毫秒值
  - 控制最大垃圾收集停顿时间,收集器尽力暴走内存回收花费的时间不超过用户设定的值
  - 如果设置小一些,使得垃圾回收快一点,其实是通过减少新生代的大小来实现的,回收300M肯定比回收500M花的时间少,但是GC 也会变得更频繁,吞吐量就下来了
- -XX:GCTimeRatio
  - 直接设置吞吐量大小,也就是GC时间占总时间的比率
  - 大于0小于100的整数
  - 譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5% (即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间
- -XX:+UseAdaptiveSizePolicy
  - 激活后不需要人工指定新生代的大小、新生代和老年代的比值、晋升老年代的大小等细节参数,虚拟机会自适应,自动调节
## SerialOld收集器
### 流程

<div style="text-align: center;">
    <img src="../img/jvm/SerialOld收集器工作流程.png" width="70%" />
</div>

### 特点
- Serial的老年代版本
- 单线程
- 使用标记-整理算法
## ParallelOld收集器
### 流程

<div style="text-align: center;">
    <img src="../img/jvm/ParallelOld收集器工作流程.png" width="70%" />
</div>

### 特点
- Parallel Scavenge收集器的老年代版本
- 多线程
- 注重吞吐量
## CMS收集器
### 流程

<div style="text-align: center;">
    <img src="../img/jvm/CMS收集器工作流程.png" width="70%" />
</div>

- 初始标记
  - 仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
  - 需要STW
- 并发标记
  - 从GC Roots的直接关联对象开始遍历整个对 象图的过程
  - 并发
- 重新标记
  - 为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的 标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一 些,但也远比并发标记阶段的时间短
  - 并发
- 并发清除
  - 清理删除掉标记阶段判断的已经死亡的 对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
  - 并发
### 特点
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS
  - 并发阶段用户线程也在产生垃圾,只能下一次清理
  - 浮动垃圾
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
## G1收集器
### 流程

<div style="text-align: center;">
    <img src="../img/jvm/G1收集器工作流程.png" width="70%" />
</div>

- 初始标记
  - 仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。
  - STW
- 并发标记
  - 从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象
  - 并发
- 最终标记
  - 对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录
  - STW
- 筛选回收
  - 负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行 完成的。
  - STW
### 特点
- 主要面向服务端应用的垃圾收集器。
- 基于Region的堆内存布局
  - 不在基于分代,而是回收任何区域,基于哪块内存存放的垃圾数量最多,回收收益最大
  - G1不再坚持固定大小以及固定数量的 分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间

    <div style="text-align: center;">
    <img src="../img/jvm/G1内存布局.png" width="50%" />
    </div>

    - 收集器能够对扮演不同角色的 Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的 旧对象都能获取很好的收集效果
    - 跨Region引用对象如何解决
      - 使用记忆集避免全堆作为GC Roots扫描,
      - 但是G1的记忆集设计更复杂,在本质上是一种哈希表,key是别的region的起始位置,value是一个集合储存卡表的索引号,这里的卡表是“双向的”(卡表是我指向谁,但是这种结构还记录谁指向我)比之前的要复杂
    - 并发标记新对象的标记
      - 原始快照
      - 每一个Region上都设计了两个名为TAMS(top at mark start)的指针,把region的一部分空间划分出来用于并发回收过程中的新对象的内存分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上
  - 每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免 在整个Java堆中进行全区域的垃圾收集
- Humongous区域
专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设 定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象, 将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代 的一部分来进行看待
## ZGC收集器

ZGC(The Z Garbage Collector)是JDK 11中推出的一款低延迟垃圾回收器,它的设计目标包括:

- 停顿时间不超过10ms;
- 停顿时间不会随着堆的大小,或者活跃对象的大小而增加;
- 支持8MB~4TB级别的堆(未来支持16TB)。

从设计目标来看,我们知道ZGC适用于大内存低延迟服务的内存管理和回收。本文主要介绍ZGC在低延时场景中的应用和卓越表现,文章内容主要分为四部分:

- GC之痛:介绍实际业务中遇到的GC痛点,并分析CMS收集器和G1收集器停顿时间瓶颈;
- ZGC原理:分析ZGC停顿时间比G1或CMS更短的本质原因,以及背后的技术原理;
- ZGC调优实践:重点分享对ZGC调优的理解,并分析若干个实际调优案例;
- 升级ZGC效果:展示在生产环境应用ZGC取得的效果。

### GC之痛
很多低延迟高可用Java服务的系统可用性经常受GC停顿的困扰。GC停顿指垃圾回收期间STW(Stop The World),当STW时,所有应用线程停止活动,等待GC停顿结束。以美团风控服务为例,部分上游业务要求风控服务65ms内返回结果,并且可用性要达到99.99%。但因为GC停顿,我们未能达到上述可用性目标。当时使用的是CMS垃圾回收器,单次Young GC 40ms,一分钟10次,接口平均响应时间30ms。通过计算可知,有(40ms + 30ms) * 10次 / 60000ms = 1.12%的请求的响应时间会增加0 ~ 40ms不等,其中30ms * 10次 / 60000ms = 0.5%的请求响应时间会增加40ms。可见,GC停顿对响应时间的影响较大。为了降低GC停顿对系统可用性的影响,我们从降低单次GC时间和降低GC频率两个角度出发进行了调优,还测试过G1垃圾回收器,但这三项措施均未能降低GC对服务可用性的影响。

### CMS与G1停顿时间瓶颈
在介绍ZGC之前,首先回顾一下CMS和G1的GC过程以及停顿时间的瓶颈。CMS新生代的Young GC、G1和ZGC都基于标记-复制算法,但算法具体实现的不同就导致了巨大的性能差异。

标记-复制算法应用在CMS新生代(ParNew是CMS默认的新生代垃圾回收器)和G1垃圾回收器中。标记-复制算法可以分为三个阶段:

- 标记阶段,即从GC Roots集合开始,标记活跃对象;
- 转移阶段,即把活跃对象复制到新的内存地址上;
- 重定位阶段,因为转移导致对象的地址发生了变化,在重定位阶段,所有指向对象旧地址的指针都要调整到对象新的地址上。

下面以G1为例,通过G1中标记-复制算法过程(G1的Young GC和Mixed GC均采用该算法),分析G1停顿耗时的主要瓶颈。G1垃圾回收周期如下图所示:

<div style="text-align: center;">
    <img src="../img/jvm/G1垃圾回收周期.png" width="70%" />
</div>

G1的混合回收过程可以分为标记阶段、清理阶段和复制阶段。

#### 标记阶段停顿分析

初始标记阶段:初始标记阶段是指从GC Roots出发标记全部直接子节点的过程,该阶段是STW的。由于GC Roots数量不多,通常该阶段耗时非常短。
并发标记阶段:并发标记阶段是指从GC Roots开始对堆中对象进行可达性分析,找出存活对象。该阶段是并发的,即应用线程和GC线程可以同时活动。并发标记耗时相对长很多,但因为不是STW,所以我们不太关心该阶段耗时的长短。
再标记阶段:重新标记那些在并发标记阶段发生变化的对象。该阶段是STW的。
#### 清理阶段停顿分析

清理阶段清点出有存活对象的分区和没有存活对象的分区,该阶段不会清理垃圾对象,也不会执行存活对象的复制。该阶段是STW的。
#### 复制阶段停顿分析

复制算法中的转移阶段需要分配新内存和复制对象的成员变量。转移阶段是STW的,其中内存分配通常耗时非常短,但对象成员变量的复制耗时有可能较长,这是因为复制耗时与存活对象数量与对象复杂度成正比。对象越复杂,复制耗时越长。
四个STW过程中,初始标记因为只标记GC Roots,耗时较短。再标记因为对象数少,耗时也较短。清理阶段因为内存分区数量少,耗时也较短。转移阶段要处理所有存活的对象,耗时会较长。因此,G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW。为什么转移阶段不能和标记阶段一样并发执行呢?主要是G1未能解决转移过程中准确定位对象地址的问题。

G1的Young GC和CMS的Young GC,其标记-复制全过程STW,这里不再详细阐述。

### ZGC原理
全并发的ZGC

与CMS中的ParNew和G1类似,ZGC也采用标记-复制算法,不过ZGC对该算法做了重大改进:ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于10ms目标的最关键原因。

ZGC垃圾回收周期如下图所示

<div style="text-align: center;">
    <img src="../img/jvm/ZGC垃圾回收周期.png" width="70%" />
</div>


ZGC只有三个STW阶段:初始标记,再标记,初始转移。其中,初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。

### ZGC关键技术
ZGC通过着色指针和读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移。大致原理描述如下:并发转移中“并发”意味着GC线程在转移对象的过程中,应用线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误。而在ZGC中,应用线程访问对象将触发“读屏障”,如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。那么,JVM是如何判断对象被移动过呢?就是利用对象引用的地址,即着色指针。下面介绍着色指针和读屏障技术细节。

#### 着色指针
> 着色指针是一种将信息存储在指针中的技术。

ZGC仅支持64位系统,它把64位虚拟地址空间划分为多个子空间,如下图所示:

<div style="text-align: center;">
    <img src="../img/jvm/ZGC-虚拟地址空间.png" width="70%" />
</div>


其中,[0~4TB) 对应Java堆,[4TB ~ 8TB) 称为M0地址空间,[8TB ~ 12TB) 称为M1地址空间,[12TB ~ 16TB) 预留未使用,[16TB ~ 20TB) 称为Remapped空间。

当应用程序创建对象时,首先在堆空间申请一个虚拟地址,但该虚拟地址并不会映射到真正的物理地址。ZGC同时会为该对象在M0、M1和Remapped地址空间分别申请一个虚拟地址,且这三个虚拟地址对应同一个物理地址,但这三个空间在同一时间有且只有一个空间有效。ZGC之所以设置三个虚拟地址空间,是因为它使用“空间换时间”思想,去降低GC停顿时间。“空间换时间”中的空间是虚拟空间,而不是真正的物理空间。后续章节将详细介绍这三个空间的切换过程。

与上述地址空间划分相对应,ZGC实际仅使用64位地址空间的第0~41位,而第42~45位存储元数据,第47~63位固定为0。

<div style="text-align: center;">
    <img src="../img/jvm/ZGC-虚拟地址空间2.png" width="70%" />
</div>


ZGC将对象存活信息存储在42~45位中,这与传统的垃圾回收并将对象存活信息放在对象头中完全不同。

#### 读屏障
> 读屏障是JVM向应用代码插入一小段代码的技术。当应用线程从堆中读取对象引用时,就会执行这段代码。需要注意的是,仅“从堆中读取对象引用”才会触发这段代码。

读屏障示例:
```java
Object o = obj.FieldA   // 从堆中读取引用,需要加入屏障
<Load barrier>
Object p = o  // 无需加入屏障,因为不是从堆中读取引用
o.dosomething() // 无需加入屏障,因为不是从堆中读取引用
int i =  obj.FieldB  //无需加入屏障,因为不是对象引用
```
ZGC中读屏障的代码作用:在对象标记和转移过程中,用于确定对象的引用地址是否满足条件,并作出相应动作。

### ZGC并发处理演示
接下来详细介绍ZGC一次垃圾回收周期中地址视图的切换过程:

- 初始化:ZGC初始化之后,整个内存空间的地址视图被设置为Remapped。程序正常运行,在内存中分配对象,满足一定条件后垃圾回收启动,此时进入标记阶段。
- 并发标记阶段:第一次进入标记阶段时视图为M0,如果对象被GC标记线程或者应用线程访问过,那么就将对象的地址视图从Remapped调整为M0。所以,在标记阶段结束之后,对象的地址要么是M0视图,要么是Remapped。如果对象的地址是M0视图,那么说明对象是活跃的;如果对象的地址是Remapped视图,说明对象是不活跃的。
- 并发转移阶段:标记结束后就进入转移阶段,此时地址视图再次被设置为Remapped。如果对象被GC转移线程或者应用线程访问过,那么就将对象的地址视图从M0调整为Remapped。

其实,在标记阶段存在两个地址视图M0和M1,上面的过程显示只用了一个地址视图。之所以设计成两个,是为了区别前一次标记和当前标记。也即,第二次进入并发标记阶段后,地址视图调整为M1,而非M0。

着色指针和读屏障技术不仅应用在并发转移阶段,还应用在并发标记阶段:将对象设置为已标记,传统的垃圾回收器需要进行一次内存访问,并将对象存活信息放在对象头中;而在ZGC中,只需要设置指针地址的第42~45位即可,并且因为是寄存器访问,所以速度比访问内存更快。

<div style="text-align: center;">
    <img src="../img/jvm/ZGC并发处理.png" width="70%" />
</div>

### ZGC调优实践
ZGC不是“银弹”,需要根据服务的具体特点进行调优。网络上能搜索到实战经验较少,调优理论需自行摸索,我们在此阶段也耗费了不少时间,最终才达到理想的性能。本文的一个目的是列举一些使用ZGC时常见的问题,帮助大家使用ZGC提高服务可用性

#### 调优基础知识
理解ZGC重要配置参数

以我们服务在生产环境中ZGC参数配置为例,说明各个参数的作用:

重要参数配置样例:
```java
-Xms10G -Xmx10G 
-XX:ReservedCodeCacheSize=256m -XX:InitialCodeCacheSize=256m 
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC 
-XX:ConcGCThreads=2 -XX:ParallelGCThreads=6 
-XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5 
-XX:+UnlockDiagnosticVMOptions -XX:-ZProactive 
-Xlog:safepoint,classhisto*=trace,age*,gc*=info:file=/opt/logs/logs/gc-%t.log:time,tid,tags:filecount=5,filesize=50m 
```

-Xms -Xmx:堆的最大内存和最小内存,这里都设置为10G,程序的堆内存将保持10G不变。 -XX:ReservedCodeCacheSize -XX:InitialCodeCacheSize:设置CodeCache的大小, JIT编译的代码都放在CodeCache中,一般服务64m或128m就已经足够。我们的服务因为有一定特殊性,所以设置的较大,后面会详细介绍。 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC:启用ZGC的配置。 -XX:ConcGCThreads:并发回收垃圾的线程。默认是总核数的12.5%,8核CPU默认是1。调大后GC变快,但会占用程序运行时的CPU资源,吞吐会受到影响。 -XX:ParallelGCThreads:STW阶段使用线程数,默认是总核数的60%。 -XX:ZCollectionInterval:ZGC发生的最小时间间隔,单位秒。 -XX:ZAllocationSpikeTolerance:ZGC触发自适应算法的修正系数,默认2,数值越大,越早的触发ZGC。 -XX:+UnlockDiagnosticVMOptions -XX:-ZProactive:是否启用主动回收,默认开启,这里的配置表示关闭。 -Xlog:设置GC日志中的内容、格式、位置以及每个日志的大小。

#### 理解ZGC触发时机

相比于CMS和G1的GC触发机制,ZGC的GC触发机制有很大不同。ZGC的核心特点是并发,GC过程中一直有新的对象产生。如何保证在GC完成之前,新产生的对象不会将堆占满,是ZGC参数调优的第一大目标。因为在ZGC中,当垃圾来不及回收将堆占满时,会导致正在运行的线程停顿,持续时间可能长达秒级之久。

ZGC有多种GC触发机制,总结如下:

- 阻塞内存分配请求触发:当垃圾来不及回收,垃圾将堆占满时,会导致部分线程阻塞。我们应当避免出现这种触发方式。日志中关键字是“Allocation Stall”。
- 基于分配速率的自适应算法:最主要的GC触发方式,其算法原理可简单描述为”ZGC根据近期的对象分配速率以及GC时间,计算出当内存占用达到什么阈值时触发下一次GC”。自适应算法的详细理论可参考彭成寒《新一代垃圾回收器ZGC设计与实现》一书中的内容。通过ZAllocationSpikeTolerance参数控制阈值大小,该参数默认2,数值越大,越早的触发GC。我们通过调整此参数解决了一些问题。日志中关键字是“Allocation Rate”。
- 基于固定时间间隔:通过ZCollectionInterval控制,适合应对突增流量场景。流量平稳变化时,自适应算法可能在堆使用率达到95%以上才触发GC。流量突增时,自适应算法触发的时机可能会过晚,导致部分线程阻塞。我们通过调整此参数解决流量突增场景的问题,比如定时活动、秒杀等场景。日志中关键字是“Timer”。
- 主动触发规则:类似于固定间隔规则,但时间间隔不固定,是ZGC自行算出来的时机,我们的服务因为已经加了基于固定时间间隔的触发机制,所以通过-ZProactive参数将该功能关闭,以免GC频繁,影响服务可用性。 日志中关键字是“Proactive”。
- 预热规则:服务刚启动时出现,一般不需要关注。日志中关键字是“Warmup”。
- 外部触发:代码中显式调用System.gc()触发。 日志中关键字是“System.gc()”。
- 元数据分配触发:元数据区不足时导致,一般不需要关注。 日志中关键字是“Metadata GC Threshold”。

#### 理解ZGC日志

一次完整的GC过程,需要注意的点已在图中标出。

<div style="text-align: center;">
    <img src="../img/jvm/ZGC-GC日志.png" width="70%" />
</div>


注意:该日志过滤了进入安全点的信息。正常情况,在一次GC过程中还穿插着进入安全点的操作。

GC日志中每一行都注明了GC过程中的信息,关键信息如下:

- Start:开始GC,并标明的GC触发的原因。上图中触发原因是自适应算法。
- Phase-Pause Mark Start:初始标记,会STW。
- Phase-Pause Mark End:再次标记,会STW。
- Phase-Pause Relocate Start:初始转移,会STW。
- Heap信息:记录了GC过程中Mark、Relocate前后的堆大小变化状况。High和Low记录了其中的最大值和最小值,我们一般关注High中Used的值,如果达到100%,在GC过程中一定存在内存分配不足的情况,需要调整GC的触发时机,更早或者更快地进行GC。
- GC信息统计:可以定时的打印垃圾收集信息,观察10秒内、10分钟内、10个小时内,从启动到现在的所有统计信息。利用这些统计信息,可以排查定位一些异常点。

日志中内容较多,关键点已用红线标出,含义较好理解,更详细的解释大家可以自行在网上查阅资料。

<div style="text-align: center;">
    <img src="../img/jvm/ZGC-GC日志2.png" width="70%" />
</div>


#### 理解ZGC停顿原因

我们在实战过程中共发现了6种使程序停顿的场景,分别如下:

- GC时,初始标记:日志中Pause Mark Start。
- GC时,再标记:日志中Pause Mark End。
- GC时,初始转移:日志中Pause Relocate Start。
- 内存分配阻塞:当内存不足时线程会阻塞等待GC完成,关键字是”Allocation Stall”。

<div style="text-align: center;">
    <img src="../img/jvm/ZGC-GC日志3.png" width="70%" />
</div>

- 安全点:所有线程进入到安全点后才能进行GC,ZGC定期进入安全点判断是否需要GC。先进入安全点的线程需要等待后进入安全点的线程直到所有线程挂起。
- dump线程、内存:比如jstack、jmap命令。

<div style="text-align: center;">
    <img src="../img/jvm/ZGC-GC日志4.png" width="70%" />
</div>


<div style="text-align: center;">
    <img src="../img/jvm/ZGC-虚拟地址空间.png" width="70%" />
</div>


### 调优案例
我们维护的服务名叫Zeus,它是美团的规则平台,常用于风控场景中的规则管理。规则运行是基于开源的表达式执行引擎Aviator。Aviator内部将每一条表达式转化成Java的一个类,通过调用该类的接口实现表达式逻辑。

Zeus服务内的规则数量超过万条,且每台机器每天的请求量几百万。这些客观条件导致Aviator生成的类和方法会产生很多的ClassLoader和CodeCache,这些在使用ZGC时都成为过GC的性能瓶颈。接下来介绍两类调优案例。

内存分配阻塞,系统停顿可达到秒级

#### 案例一:秒杀活动中流量突增,出现性能毛刺

日志信息:对比出现性能毛刺时间点的GC日志和业务日志,发现JVM停顿了较长时间,且停顿时GC日志中有大量的“Allocation Stall”日志。

分析:这种案例多出现在“自适应算法”为主要GC触发机制的场景中。ZGC是一款并发的垃圾回收器,GC线程和应用线程同时活动,在GC过程中,还会产生新的对象。GC完成之前,新产生的对象将堆占满,那么应用线程可能因为申请内存失败而导致线程阻塞。当秒杀活动开始,大量请求打入系统,但自适应算法计算的GC触发间隔较长,导致GC触发不及时,引起了内存分配阻塞,导致停顿。

解决方法:

1. 开启”基于固定时间间隔“的GC触发机制:-XX:ZCollectionInterval。比如调整为5秒,甚至更短。
2. 增大修正系数-XX:ZAllocationSpikeTolerance,更早触发GC。ZGC采用正态分布模型预测内存分配速率,模型修正系数ZAllocationSpikeTolerance默认值为2,值越大,越早的触发GC,Zeus中所有集群设置的是5。

#### 案例二:压测时,流量逐渐增大到一定程度后,出现性能毛刺

日志信息:平均1秒GC一次,两次GC之间几乎没有间隔。

分析:GC触发及时,但内存标记和回收速度过慢,引起内存分配阻塞,导致停顿。

解决方法:增大-XX:ConcGCThreads, 加快并发标记和回收速度。ConcGCThreads默认值是核数的1/8,8核机器,默认值是1。该参数影响系统吞吐,如果GC间隔时间大于GC周期,不建议调整该参数。

GC Roots 数量大,单次GC停顿时间长

#### 案例三: 单次GC停顿时间30ms,与预期停顿10ms左右有较大差距

日志信息:观察ZGC日志信息统计,“Pause Roots ClassLoaderDataGraph”一项耗时较长。

分析:dump内存文件,发现系统中有上万个ClassLoader实例。我们知道ClassLoader属于GC Roots一部分,且ZGC停顿时间与GC Roots成正比,GC Roots数量越大,停顿时间越久。再进一步分析,ClassLoader的类名表明,这些ClassLoader均由Aviator组件生成。分析Aviator源码,发现Aviator对每一个表达式新生成类时,会创建一个ClassLoader,这导致了ClassLoader数量巨大的问题。在更高Aviator版本中,该问题已经被修复,即仅创建一个ClassLoader为所有表达式生成类。

解决方法:升级Aviator组件版本,避免生成多余的ClassLoader。

#### 案例四:服务启动后,运行时间越长,单次GC时间越长,重启后恢复

日志信息:观察ZGC日志信息统计,“Pause Roots CodeCache”的耗时会随着服务运行时间逐渐增长。

分析:CodeCache空间用于存放Java热点代码的JIT编译结果,而CodeCache也属于GC Roots一部分。通过添加-XX:+PrintCodeCacheOnCompilation参数,打印CodeCache中的被优化的方法,发现大量的Aviator表达式代码。定位到根本原因,每个表达式都是一个类中一个方法。随着运行时间越长,执行次数增加,这些方法会被JIT优化编译进入到Code Cache中,导致CodeCache越来越大。

解决方法:JIT有一些参数配置可以调整JIT编译的条件,但对于我们的问题都不太适用。我们最终通过业务优化解决,删除不需要执行的Aviator表达式,从而避免了大量Aviator方法进入CodeCache中。

值得一提的是,我们并不是在所有这些问题都解决后才全量部署所有集群。即使开始有各种各样的毛刺,但计算后发现,有各种问题的ZGC也比之前的CMS对服务可用性影响小。所以从开始准备使用ZGC到全量部署,大概用了2周的时间。在之后的3个月时间里,我们边做业务需求,边跟进这些问题,最终逐个解决了上述问题,从而使ZGC在各个集群上达到了一个更好表现。

### 升级ZGC效果
#### 延迟降低
TP(Top Percentile)是一项衡量系统延迟的指标:TP999表示99.9%请求都能被响应的最小耗时;TP99表示99%请求都能被响应的最小耗时。

在Zeus服务不同集群中,ZGC在低延迟(TP999 < 200ms)场景中收益较大:

- TP999:下降12~142ms,下降幅度18%~74%。
- TP99:下降5~28ms,下降幅度10%~47%。 

超低延迟(TP999 < 20ms)和高延迟(TP999 > 200ms)服务收益不大,原因是这些服务的响应时间瓶颈不是GC,而是外部依赖的性能。

#### 吞吐下降
对吞吐量优先的场景,ZGC可能并不适合。例如,Zeus某离线集群原先使用CMS,升级ZGC后,系统吞吐量明显降低。究其原因有二:第一,ZGC是单代垃圾回收器,而CMS是分代垃圾回收器。单代垃圾回收器每次处理的对象更多,更耗费CPU资源;第二,ZGC使用读屏障,读屏障操作需耗费额外的计算资源。

### 总结
ZGC作为下一代垃圾回收器,性能非常优秀。ZGC垃圾回收过程几乎全部是并发,实际STW停顿时间极短,不到10ms。这得益于其采用的着色指针和读屏障技术。

Zeus在升级JDK 11+ZGC中,通过将风险和问题分类,然后各个击破,最终顺利实现了升级目标,GC停顿也几乎不再影响系统可用性。

最后推荐大家升级ZGC,Zeus系统因为业务特点,遇到了较多问题,而风控其他团队在升级时都非常顺利。

## Shenandoah GC
Shenandoah GC是一种面向高吞吐量、低延迟的垃圾回收器,它与G1 GC类似,也是基于标记-清除算法。与G1 GC不同的是,Shenandoah GC在运行过程中会自动调整堆内存大小,以适应不同负载。同时,Shenandoah GC支持在运行过程中动态调整GC参数,以适应不同负载。

### 1. Shenandoah GC 的最大特点
- 并发压缩:Shenandoah GC 的最大特点是其**并发压缩(Concurrent Compaction)**能力。在垃圾收集过程中,Shenandoah GC 可以与应用线程并发地压缩堆内存空间。这意味着在整个垃圾收集过程中,只有极短的停顿时间,因为传统 GC 中需要长时间暂停应用线程来进行内存压缩的部分被并发完成了。

- 极低停顿时间:Shenandoah GC 设计的核心目标是将垃圾收集的停顿时间控制在 10 毫秒以下,即使在大内存(如数百 GB)的情况下,也能实现这一目标。

### 2. Shenandoah GC 的其他特点
- 区域化堆布局:与 G1 GC 类似,Shenandoah GC 也将堆分成多个区域(Region),这些区域可以独立地进行垃圾收集和压缩。

- 并发阶段:Shenandoah GC 的大部分垃圾收集工作在并发阶段完成,包括标记、更新引用、压缩等操作。这减少了应用线程的停顿时间。

- 标记-清除-压缩算法:Shenandoah 使用标记-清除-压缩的算法,其中压缩阶段是并发的。这是它实现低停顿的关键之一。

- “Brooks Pointer”:Shenandoah 使用一种称为 “Brooks Pointer” 的技术来支持并发压缩。每个对象都有一个额外的指针(Brooks Pointer),指向其自身或移动后的新位置,这样即使在压缩过程中,应用线程也能正确地访问对象。

### 3. Shenandoah GC 与其他垃圾收集器的区别
#### 3.1 与 G1 GC 的区别
- 停顿时间:
  - G1 GC:G1 GC 的设计目标是提供可预测的停顿时间,它通过增量收集来减少长时间停顿。G1 GC 的堆压缩在混合收集阶段进行,可能会引发较长的停顿。
  - Shenandoah GC:Shenandoah GC 在所有阶段都尽量并发执行,特别是压缩阶段,这使得 Shenandoah GC 的停顿时间更短。

- 并发压缩:

  - G1 GC:在 G1 GC 中,老年代的压缩(或者说堆压缩)会导致较长时间的停顿。
  - Shenandoah GC:Shenandoah 的压缩是并发进行的,因此不会导致长时间的停顿。
#### 3.2 与 ZGC 的区别
- 支持范围:

  - ZGC:ZGC 主要设计用于处理超大堆(TB 级)的内存场景,并且也以极低的停顿时间为目标。ZGC 使用的是基于颜色指针的算法(Colored Pointers)和读屏障(Read Barriers)。
  - Shenandoah GC:Shenandoah 支持的堆大小虽然大,但不及 ZGC 的 TB 级别。Shenandoah 的目标是提供低停顿时间的同时在大多数场景中都能高效运行。

- 复杂性:
  - ZGC:ZGC 的实现相对复杂,它需要操作系统和硬件支持(如页表保护和内存映射)来管理并发的内存回收和指针更新。
  - Shenandoah GC:Shenandoah 的设计相对简单,它依赖于 "Brooks Pointer" 和标准的指针操作,而不是像 ZGC 那样复杂的机制。
#### 3.3 与 Parallel GC 的区别
目标:

- Parallel GC:追求高吞吐量,垃圾收集时会暂停所有应用线程,进行并行收集。
- Shenandoah GC:追求低停顿时间,通过并发收集来最大限度地减少应用线程的暂停。
### 4. 总结
Shenandoah GC 的最大特点是其并发压缩能力,这使得它在垃圾收集过程中几乎不会长时间暂停应用线程,从而实现极低的停顿时间。与 G1 GC 和 ZGC 相比,Shenandoah GC 的目标是为中到大型堆内存提供低停顿时间的垃圾收集,同时其实现方式也相对较为直接和简单。Shenandoah 适合那些对停顿时间非常敏感的应用场景。


# 不同版本jdk默认的垃圾收集器
| JDK版本 | 默认垃圾收集器 |
| --- | --- |
| JDK 8 | Parallel |
| JDK 11 | G1 |
| JDK 17 | G1 |
| JDK 21 | G1 |

# 参考文章
- https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html
- https://www.cnblogs.com/hongdada/p/14578950.html

================================================
FILE: Java-JVM/类加载机制.md
================================================
# 类加载机制
### 有哪些类加载器
![](../img/jvm/类加载器.png)
#### 1.引导类加载器 `bootstrap classloader`
启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的
#### 2.扩展类加载器 `extensions classloader`
它负责加载`JAVA_HOME/lib/ext`目录下或者由系统变量`-Djava.ext.dir`指定位路径中的类库,开发者可以直接使用标准扩展类加载器
#### 3.应用程序类加载器 `application classloader`
应用程序加载器是指 Sun公司实现的`sun.misc.Launcher$AppClassLoader`。它负责加载系统类路径`java -classpath`或`-D java.class.path` 指定路径下的类库,也就是我们经常用到的classpath路径
#### 4.自定义类加载器 `java.lang.classloder`
继承java.lang.ClassLoader类的方式
# 生命周期
### 1. 加载
通过类的完全限定名称获取定义该类的二进制字节流。
- 其中二进制字节流可以从以下方式中获取
  - 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
  - 从网络中获取,最典型的应用是 Applet。
  - 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass的代理类的二进制字节流。
  - 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。
- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口
### 2. 验证
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
### 3. 准备
准备阶段是正式为类变量(即静态变量,被static修饰的变量,并非实例变量)分配内存并设置类变量初 始值的阶段
- 类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存
  - 初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。 `public static int value = 123;`
  - 如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。例如下面的常量 value 被初始化为 123 而不是0。 `public static final int value = 123;`
- 实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
### 4. 解析
将常量池的符号引用替换为直接引用的过程
- 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何 形式的字面量,只要使用时能无歧义地定位到目标即可
- 直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能 间接定位到目标的句柄

举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。

在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
### 5. 初始化
在类的class文件中,包含两个特殊的方法:`<clinit>`和`<init>`。这两个方法由编译器自动生成,分别代表类构造器和构造函数。其中构造函数可以由编程人员实现,而类构造器则由编译器自动生成。而初始化阶段则负责调用类构造器,来初始化变量和资源。
`<clinit>`方法由编译器自动收集类的赋值动作和静态语句块(static{}块)中的语句合并生成的,它有以下特点:

编译器收集的顺序由源文件中语句的顺序决定,静态语句块只能访问到在它之前定义的变量,在它之后定义的变量,它只能进行赋值操作,但不能访问。

```java
public class Test {
  static {
      // 变量赋值编译可以正常通过
      value = 2;
      // 访问变量编译失败
      System.out.println(i);
  }

    static int value = 1;
}
```
- 虚拟机保证在子类的`<clinit>`方法执行之前,父类的`<clinit>`方法已经执行完毕。因此父类中的操作对于子类都是可见的。
- 接口的`<clinit>`方法执行之前,不需要先执行父接口的`<clinit>`方法,只有父接口中定义的变量被使用时,父接口才会初始化。同时接口的实现类在初始化时也不会执行父接口的`<clinit>`方法。
- ``<clinit>``方法不是必须的,如果一个类或者接口没有变量赋值和静态语句块,则编译器可以不生成`<clinit>`方法。
- 虚拟机会保证`<clinit>`方法在多线程中被正确的加锁、同步。如果多个线程同时去初始化一个类,那么只有一个线程去执行`<clinit>`方法,其他线程会被阻塞。


# 双亲委派模型
该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器
![](../img/jvm/类加载器.png)
## 工作过程
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载
### 好处
保证了基础类是唯一的
- 它使得类有了层次的划分。就拿`java.lang.Object`来说,你加载它经过一层层委托最终是由Bootstrap ClassLoader来加载的,也就是最终都是由Bootstrap ClassLoader去找`<JAVA_HOME>\lib`中`rt.jar`里面的`java.lang.Object`加载到JVM中。
- 这样如果有不法分子自己造了个`java.lang.Object`,里面嵌了不好的代码,如果我们是按照双亲委派模型来实现的话,最终加载到JVM中的只会是我们`rt.jar`里面的东西,也就是这些核心的基础类代码得到了保护。因为这个机制使得系统中只会出现一个`java.lang.Object`。不会乱套了。你想想如果我们JVM里面有两个Object,那岂不是天下大乱了
### ClassLoader用过吗?用在哪些方面
#### 1、依赖冲突
实际项目中如果出现需要同时依赖多个版本的jar包,可以自定义类加载器分别加载各自的依赖jar即可
#### 2、热加载
可以让RestartClassLoader为自定义的类加载器,其核心是loadClass的加载方式,我们发现其通过修改了双亲委托机制,默认优先从自己加载,如果自己没有加载到,从从parent进行加载。这样保证了业务代码可以优先被RestartClassLoader加载。进而通过重新加载RestartClassLoader即可完成应用代码部分的重新加载。
#### 3、加密保护
- 打包时加密class文件
- 加载时使用实现解密的classloader

## 怎么打破双亲委派模型?
- 自定义类加载器,并使用自定义类加载器
- SPI也打破了双亲委派模型
- Tomcat的模型

## Tomcat是如何隔离Web应用的?

Tomcat通过自定义的类加载器WebAppClassLoader打破了双亲委托机制,目的就是为了优化加载Web应用目录下的类。Tomcat 作为 Servlet 容器,它负责加载我们Servlet 类,此外它还负责加载 Servlet 所依赖的 JAR 包。并且Tomcat 本身也是也是一个 Java 程序,因此它需要加载自己的类和依赖的 JAR 包。

如果Tomcat里面运行了两个Web应用程序,两个Web应用程序中有同名的Servlet,但功能不同,Tomcat需要同时加载和管理这两个同名的 Servlet 类,保证它们不会冲突,因此 Web 应用之间的类需要隔离。

如果如两个 Web 应用都依赖同一个第三方的 JAR 包,比如 Spring,那 Spring 的 JAR 包被加载到内存后,Tomcat 要保证这两个 Web 应用能够共享,也就是说Spring 的 JAR 包只被加载一次,否则随着依赖的第三方JAR 包增多,JVM 的内存会膨胀。

同时,跟JVM一样,我们需要隔离Tomcat本身的类和Web应用类。
### Tomcat的类加载器的层次结构
为了解决AppClassLoader,同类名的Servlet类只能被加载一次。Tomcat 自定义一个类加载器WebAppClassLoader, 并且给每个 Web 应用创建一个类加载器实例。Context 容器组件对应一个 Web应用,因此,每个 Context 容器负责创建和维护一个WebAppClassLoader 加载器实例。原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同。这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间,每一个 Web 应用都有自己的类空间,Web 应用之间通过各自的类加载器互相隔离。

![](../img/jvm/Tomcat类加载器图.png)

为了Tomcat可以存放多个Web应用,Tomcat实现了Web应用的隔离,从而达到可以加载不同Web应用下相同名的Servlet类,从而编写了自己的类加载器,其内部类加载器模型如上图所示
1. SharedClassLoader:该类加载器存在是为了解决不同Web应用之间共享类库,并且不会重复加载相同的类。它作为WebAppClassLoader的父加载器,专门加载Web应用之间的共享类。
2. CatalinaClassloader:该类加载器专门加载Tomcat自身的类,从而和web应用的类做一个隔离。
3. CommonClassLoad:CatalinaClassLader实现了Tomcat类和web应用类的隔离,如果二者之间需要共享一些类怎么办?这里就需要CommonClassLoad,它所加载的所有类都可以被SharedClassLoader和CatalinaClassLoader使用,从而实现web应用和tomcat对一些类的共享。
### 补充Spring的加载问题
在 JVM 的实现中有一条隐含的规则,默认情况下,如果一个类由类加载器 A 加载,那么这个类的依赖类也是由相同的类加载器加载。比如 Spring 作为一个 Bean 工厂,它需要创建业务类的实例,并且在创建业务类实例之前需要加载这些类。Spring 是通过调用Class.forName来加载业务类的。

我在前面提到,Web 应用之间共享的 JAR 包可以交给SharedClassLoader 来加载,从而避免重复加载。Spring作为共享的第三方 JAR 包,它本身是由SharedClassLoader 来加载的,Spring 又要去加载业务类,按照前面那条规则,加载 Spring 的类加载器也会用来加载业务类,但是业务类在 Web 应用目录下,不在SharedClassLoader 的加载路径下,这该怎么办呢?

于是线程上下文加载器登场了,它其实是一种类加载器传递机制。为什么叫作“线程上下文加载器”呢,因为这个类加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中就能把这个类加载器取出来用。因此 Tomcat 为每个 Web 应用创建一个WebAppClassLoarder 类加载器,并在启动 Web 应用的线程里设置线程上下文加载器,这样 Spring 在启动时就将线程上下文加载器取出来,用来加载 Bean。

# 参考文章
- https://www.cnblogs.com/jay-wu/p/11590571.html
- https://juejin.cn/post/6844903733952774152
- https://blog.csdn.net/Wangdiankun/article/details/105819963


================================================
FILE: Java-基础/JavaIO.md
================================================
* [文件io](#文件io)
  * [磁盘操作](#磁盘操作)
  * [字节流](#字节流)
  * [字符流](#字符流)
* [序列化](#序列化)
  * [什么是序列化](#什么是序列化)
  * [序列化的实现](#序列化的实现)
  * [案例](#案例)
  * [transient](#transient)
* [网络io](#网络io)
  * [InetAddress](#inetaddress)
  * [URL](#url)
  * [socket](#socket)
  * [特点](#特点)
* [NIO](#nio)
  * [什么是NIO](#什么是nio)
  * [流与块](#流与块)
  * [通道 Channel](#通道-channel)
  * [缓冲区Buffer](#缓冲区buffer)
  * [选择器 Selector](#选择器-selector)
    * [选择器](#选择器)
      * [什么是选择器](#什么是选择器)
    * [流程](#流程)
  * [NIO的bug](#nio的bug)
# 文件io
## 磁盘操作
```java
public static void listAllFiles(File dir){
    if(dir == null || !dir.exists()){
        return;
    }
    if (dir.isFile()){
        System.out.println(dir.getName());
        return;
    }
    for (File file : dir.listFiles()) {
        listAllFiles(file);
    }
}
```
## 字节流
可以处理任意类型的数据
```java
public static void copyFile(String src, String dist) throws IOException {
    FileInputStream in = new FileInputStream(src);
    FileOutputStream out = new FileOutputStream(dist);

    byte[] buffer = new byte[20 * 1024];
    int cnt;
    //read()最多读取buffer.length个字节
    //返回的是实际读取的个数
    //返回-1的时候表示读到eof,即文件尾
    while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {
        out.write(buffer, 0, cnt);
    }
    in.close();
    out.close();
}
```
- inputStream
  - FileInputStream
- outputStream
  - FileOutputStream
## 字符流
- 只能处理字符类型的数据
```java
public static void readFileContent(String filePath) throws IOException {
    FileReader fileReader = new FileReader(filePath);
    BufferedReader bufferedReader = new BufferedReader(fileReader);
    String line;
    while ((line = bufferedReader.readLine()) != null) {
        System.out.println(line);
    }
    //装饰者模式使得BufferedReader 组合了一个Reader对象
    //在调用BufferedReader的close()方法时会去调用Reader的close()方法
    //因此只要一个close()即可
    bufferedReader.close();
}
```

- 编码与解码
  - 编码就是把字符转换为字节,而解码是把字节重新组合成字符。
- Reader
- InputStreamReader
实现从字节流解码成字符流
- Writer
- InputStreamWriter
实现字符流编码成为字节流

# 序列化
## 什么是序列化
- 序列化就是一种用来处理对象流的机制,将对象的内容进行流化。可以对流化后的对象进行读写操作,可以将流化后的对象传输于网络之间。序列化是为了解决在对象流读写操作时所引发的问题
- 方便存储和传输
- 序列化:`ObjectOutputStream.writeObject()`
- 反序列化:`ObjectInputStream.readObject()`
## 序列化的实现
将需要被序列化的类实现Serialize接口
## 案例
```java
private static class A implements Serializable{
    private int x;
    private String y;

    public A(int x, String y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public String toString() {
        return "A{" +
                "x=" + x +
                ", y='" + y + '\'' +
                '}';
    }
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
    A a1= new A(123,"abc");
    String objectFile = "a1";

    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile));
    objectOutputStream.writeObject(a1);
    objectOutputStream.close();

    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile));
    A a2 = (A) objectInputStream.readObject();
    objectInputStream.close();
    System.out.println(a2);
}
```
```text
输出:
A{x=123, y='abc'}
```
## transient
- transient 关键字可以使一些属性不会被序列化
- ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
  - `private transient Object[] elementData;`

# 网络io
## InetAddress
用于表示网络上的硬件资源,即 IP 地址;
- InetAddress.getByName(String host);
- InetAddress.getByAddress(byte[] address);
## URL
案例
```java
public static void main(String[] args) throws IOException {
    URL url = new URL("http://www.baidu.com");
    //字节流
    InputStream is = url.openStream();
    //字符流
    InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
    //提供缓存功能
    BufferedReader br = new BufferedReader(isr);
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
    br.close();
}
```
## socket
```java
public class IOServer {
    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(8000);
        //接收新线程
        new Thread(() -> {
            while (true) {
                try {
                    //1-阻塞方法获取新连接
                    Socket socket = serverSocket.accept();
                    //2-每一个新连接都创建一个线程,负责读取数据
                    new Thread(() -> {
                        try {
                            int len;
                            byte[] data = new byte[1024];
                            InputStream inputStream = socket.getInputStream();
                            //3-按字节流方式读取数据
                            while ((len = inputStream.read(data)) != -1) {
                                System.out.println(new String(data, 0, len));
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }).start();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}
```
![](../img/io/socket示意图.png)
## 特点
同步阻塞,一个线程只能处理一个请求,如果要处理多个请求需要开启多个线程

# NIO
## 什么是NIO
新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O
## 流与块
I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
## 通道 Channel
- 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据
- 特点
  - 通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
- 类型
  - FileChannel:从文件中读写数据;
  - DatagramChannel:通过 UDP 读写网络中数据;
  - SocketChannel:通过 TCP 读写网络中数据;
  - ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
## 缓冲区Buffer
- 发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
- 缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
- 类型
  - ByteBuffer
  - CharBuffer
  - ShortBuffer
  - IntBuffer
  - LongBuffer
  - FloatBuffer
  - DoubleBuffer
- 缓冲区状态变量
  - capacity:最大容量;
  - position:当前已经读写的字节数;
    - 下一个要被读取或者写入的元素的索引
  - limit:还可以读写的字节数。
    - 缓冲区中第一个不能被读或者写的位置
- 状态变量的改变过程
  1. 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变
     ![](../img/io/nio状态变量改变过程1.png)
  2. 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。
     ![](../img/io/nio状态变量改变过程2.png)
  3. 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将position 设置为 0。
     ![](../img/io/nio状态变量改变过程3.png)
  4. 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
     ![](../img/io/nio状态变量改变过程4.png)
  5. 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
     ![](../img/io/nio状态变量改变过程5.png)
- 文件 NIO 实例
```java
public static void fastCopy(String src, String dist) throws IOException {
    //获取源文件的输入字节流
    FileInputStream fin = new FileInputStream(src);
    //获取输入字节流的文件通道
    FileChannel fcin = fin.getChannel();
    //获取目标文件的输出字节流
    FileOutputStream fout = new FileOutputStream(dist);
    //获取输出字节流的文件通道
    FileChannel fcout = fout.getChannel();
    //为缓冲区分配1024字节
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    while (true) {
        //从输入通道中读取数据到缓冲区
        int r = fcin.read(buffer);
        //read()返回 -1 表示EOF
        if (r == -1) {
            break;
        }
        //切换读写
        buffer.flip();
        //把缓冲区的内容写入输出文件中
        fcout.write(buffer);
        //清空缓冲区
        buffer.clear();
    }
}
```
## 选择器 Selector
### 选择器
#### 什么是选择器
![](../img/io/nio选择器.png)
- NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。
- NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
- 通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。
- 因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。
- 应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,因为 FileChannel 配置非阻塞也没有意义。
### 流程
- 创建选择器
`Selector selector = Selector.open();`
- 将通道注册到选择器上
  ```
  ServerSocketChannel ssChannel = ServerSocketChannel.open();
  ssChannel.configureBlocking(false);
  ssChannel.register(selector, SelectionKey.OP_ACCEPT);
  ```
  - 事件
    - `SelectionKey.OP_CONNECT`
    - `SelectionKey.OP_ACCEPT`
    - `SelectionKey.OP_READ`
    - `SelectionKey.OP_WRITE`
- 监听事件
  `int num = selector.select();`
  - 它会一直阻塞直到有至少一个事件到达。
- 获取到达的事件
```java
Set<SelectionKey> keys = selector.selectedKets();
Iterator<SelectionKey> keyIterator = keys.iterator();
while(keyIterator.hasNext()){
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()){
        //....
    } else if (key.isReadable()){
        //....    
    }
    keyIterator.remove();
}
``` 
- 事件循环
  因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。
```java
while(true){
    int num = selector.select();
    Set<SelectionKey> keys = selector.selectedKets();
    Iterator<SelectionKey> keyIterator = keys.iterator();
    while(keyIterator.hasNext()){
        SelectionKey key = keyIterator.next();
        if(key.isAcceptable()){
            //....
        } else if (key.isReadable()){
            //....    
        }
        keyIterator.remove();
    }    
}
```
- nio示例
```java
public class NIOServer {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        ssChannel.configureBlocking(false);
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);

        ServerSocket serverSocket = ssChannel.socket();
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
        serverSocket.bind(address);

        while (true) {
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = keys.iterator();

            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();

                if (key.isAcceptable()) {
                    ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
                    //服务器会为每个新连接创建一个SocketChannel
                    SocketChannel sChannel = ssChannel1.accept();
                    sChannel.configureBlocking(false);

                    //这个新连接主要用于从客服端读取数据
                    sChannel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    SocketChannel sChannel = (SocketChannel) key.channel();
                    System.out.println(readDataFromSocketChannel(sChannel));
                    sChannel.close();
                }
                keyIterator.remove();
            }
        }
    }

    private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        StringBuilder data = new StringBuilder();
        while (true) {
            buffer.clear();
            int n = sChannel.read(buffer);
            if (n == -1) {
                break;
            }
            buffer.flip();
            int limit = buffer.limit();
            char[] dst = new char[limit];
            for (int i = 0; i < limit; i++) {
                dst[i] = (char) buffer.get(i);
            }
            data.append(dst);
            buffer.clear();
        }
        return data.toString();
    }
}
```
```java
public static class NIOClient{
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1",8888);
        OutputStream out = socket.getOutputStream();
        String s = "hello world";
        out.write(s.getBytes(StandardCharsets.UTF_8));
        out.close();
    }
}
```
## NIO的bug
JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该BUG发生概率降低了一些而已,它并没有被根本解决

**Selector BUG出现的原因**

因为poll和epoll对于突然中断的连接socket会对返回的eventSet事件集合置为EPOLLHUP或者EPOLLERR,eventSet事件集合发生了变化,这就导致Selector会被唤醒,唤醒后遍历,若Selector的轮询结果为空,也没有wakeup或新消息处理,则发生空轮询,CPU使用率100%,

**Netty的解决办法**
- 对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数,
- 若在某个周期内连续发生N次空轮询,则触发了epoll死循环bug。
- 重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。


================================================
FILE: Java-基础/Java关键字.md
================================================
* [final](#final)
  * [修饰变量](#修饰变量)
  * [修饰方法](#修饰方法)
  * [修饰类](#修饰类)
  * [底层原理](#底层原理)
  * [final关键字的好处](#final关键字的好处)
  * [为什么匿名内部类引用外部类的变脸必须是final](#为什么匿名内部类引用外部类的变脸必须是final)
* [static](#static)
  * [静态变量](#静态变量)
  * [静态方法](#静态方法)
  * [静态语句块](#静态语句块)
  * [静态内部类](#静态内部类)
  * [初始化顺序](#初始化顺序)
  * [底层原理](#底层原理-1)
    * [final fianaly finanize](#final-fianaly-finanize)

# final
## 修饰变量
- 声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量
  - 对于基本类型,final 使数值不变;
  - 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
- 修饰成员变量需要显式初始化
  - 声明时初始化
  - 构造时对其赋值
## 修饰方法
- 声明方法不能被子类重写。
- private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。
## 修饰类
声明类不允许被继承
## 底层原理
- 写final域的重排序规则
  - 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  - (先写入final变量,后调用该对象引用)
  - 原因:编译器会在final域的写之后,插入一个StoreStore屏障
- 读final域的重排序规则
  - 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序
  - (先读对象的引用,后读final变量)
  - 编译器会在读final域操作的前面插入一个LoadLoad屏障
## final关键字的好处
1. final关键字提高了性能。JVM和Java应用都会缓存final变量。
2. final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
3. 使用final关键字,JVM会对方法、变量及类进行优化。
## 为什么匿名内部类引用外部类的变量必须是final
- 匿名内部类在编译的时候会编译为一个单独的类,并且匿名内部类引用的外部类的变量会作为构造函数的参数传入
- 如果匿名内部类能访问非final的变量,但如果修改了这个非final的变量会影响匿名内部类,因为匿名内部类感知不到数据已修改造成数据不同步
- 就好比说你创建了我,并且给了我一个你的东西使用但是你又不让我能直接拿到(引用传递)非要我copy一个一模一样的(值传递),但是运行的时候你把这点东西改了,而我又感知不了,就造成数据不一致
- 所以和Java8 lambda表达式需要变量为final一样,如果把值传递改为引用传递就不会报错了

## chatGPT的回答
```text
q:AVA final的底层原理
a:

在Java中,使用final关键字修饰的变量具有不可变性,即它们的值一旦被赋值就不能再被修改。final关键字可以用于变量、方法和类,但它们的底层实现方式是不同的。

对于final变量,Java编译器会在编译时将它们替换为常量,即在使用到final变量的地方直接用变量的值替代。这种替换是在编译期完成的,因此不会影响运行时性能。

对于final方法,它们的实现方式与普通方法基本相同,但是在子类中不能被重写。这是因为final方法在编译期已经被绑定,所以无法在运行时动态绑定到子类的方法。

对于final类,它们的实现方式与普通类基本相同,但是不能被继承。这是因为final类在编译期已经被声明为不可继承,所以无法在运行时进行继承。

总之,final关键字的底层实现方式是通过在编译期进行常量替换、绑定和声明为不可继承来实现的。由于这种实现方式是在编译期完成的,所以不会对运行时性能产生影响,同时也保证了final变量、方法和类的不可变性。
```

# static
## 静态变量
又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份
## 静态方法
静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。
## 静态语句块
静态语句块在类初始化时运行一次。
## 静态内部类
非静态内部类依赖于外部类的实例,而静态内部类不需要
## 初始化顺序
- 静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。
- 静态代码块<>静态变量>实例变量>普通语句块>构造函数的初始化
  - 存在继承的情况下,初始化顺序为:
    1. 父类(静态变量、静态语句块)
    2. 子类(静态变量、静态语句块)
    3. 父类(实例变量、普通语句块)
    4. 父类(构造函数)
    5. 子类(实例变量、普通语句块)
    6. 子类(构造函数)
- ![](../img/基础/java关键字初始化顺序1.png)
  - 输出结果
    ```
    静态变量
    静态初始化块
    变量
    初始化块
    构造器
    ``` 
- ![](../img/基础/java关键字初始化顺序2.png)
  - 输出结果
  ```
       父类--静态变量
       父类--静态初始化块
       子类--静态变量
       子类--静态初始化块
       子类main方法
       父类--变量
       父类--初始化块
       父类--构造器
       i=9, j=0
       子类--变量
       子类--初始化块
       子类--构造器
       i=9,j=20
    ```
## 底层原理
- 使用static修饰的变量或方法是类级别的,它们属于类本身而不是类的实例。因此,static变量和方法在类被加载时就已经被初始化,并且存储在JVM的方法区中。
- JVM的方法区是用于存储类信息、常量、静态变量、编译器编译后的代码等数据的一块内存区域。在JVM启动时,方法区被创建,并且在整个JVM生命周期中都存在。
- 当JVM加载一个类时,它会将类的信息读入内存,并在方法区中创建一个运行时常量池,用于存储字面量、符号引用等信息。同时,JVM会为类的静态变量分配内存并进行初始化,将它们存储在方法区中。静态方法也会在方法区中创建,并且可以直接通过类名调用。
- 因此,使用static修饰的对象会在类加载时被初始化并存储在JVM的方法区中。它们不属于类的实例,而是属于类本身,可以通过类名直接访问。这种初始化方式具有一定的效率和内存优势,但也需要注意静态变量和方法的作用域和生命周期问题。



### final fianaly finanize
- final 用于声明属性,方法和类, 分别表示属性不可变, 方法不可覆盖, 类不可继承.
- finally 是异常处理语句结构的一部分,表示总是执行.
- finalize Finalize是object类中的一个方法,子类可以重写finalize()方法实现对资源的回收。垃圾回收只负责回收内存,并不负责资源的回收,资源回收要由程序员完成,Java虚拟机在垃圾回收之前会先调用垃圾对象的finalize方法用于使对象释放资源(如关闭连接、关闭文件),之后才进行垃圾回收,这个方法一般不会显式地调用,在垃圾回收时垃圾回收器会主动调用。


================================================
FILE: Java-基础/Java类型.md
================================================
* [基本类型](#基本类型)
  * [float或者double怎么判断=0?](#float或者double怎么判断0)
* [包装类型](#包装类型)
  * [包装类型的equals](#包装类型的equals)

# 基本类型
|  基本类型   | 大小  | 备注  |
|  ----  | ----  | --- |
| int  | 4字节 32位 | Java默认的整数类型 |
| short  |  2字节 16位 | --- |
| byte  | 1字节 8位 | --- |
| float  | 4字节 32位 | --- |
| double  | 8字节 64位 | Java默认的小数类型 |
| long  | 8字节 64位 | --- |
| boolean  | 1字节 |  true/false  |
| char  | 2字节 16位 | --- |

### float或者double怎么判断=0?

- 一般而言用误差内比较float为1e-6,double为1e-15,即如果Math.abs(a)<1e-6即可认为=0
- 如果非常严格要求精度 BigDecimal

# 包装类型

|  包装类型   | 缓存  |
|  ----  | ----  | 
| Integer  | IntegerCache [-128,127] | 
| Short  |  ShortCache.cache [-128,127] | 
| Byte  | [-128, 127] | 
| Float  | 4字节 32位 | 
| Double  | 8字节 64位 |
| Long  | [-128, 127] |
| Boolean  | TRUE FALSE | 
| Character  | [0, 127] | 

## 包装类型的equals
- 包装类的equals首先会判断是否是本类型如果不是直接返回false
- 否则比较值
- 装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。`Integer i = 10 等价于 Integer i = Integer.valueOf(10)
  int n = i 等价于 int n = i.intValue()`; 

## 判断创建多少个对象
因为大多包装类型都用cache,在判断时考虑是否超出了cache范围,否则会创建新对象

## new Integer(1) 和 Integer.valueOf(1)的区别是什么

Integer.valueOf(1)如果范围在`[-128,127]`之间是直接使用缓存不会创建对象
```java
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
```

================================================
FILE: Java-基础/Java长期支持版本.md
================================================
* [Java8](#java8)
  * [Lambda表达式](#lambda表达式)
  * [函数式接口](#函数式接口)
  * [默认方法](#默认方法)
  * [方法引用](#方法引用)
  * [Stream API](#stream-api)
    * [Collections](#collections)
  * [Optional](#optional)
  * [Date Time API](#date-time-api)
  * [重复注解](#重复注解)
  * [Base64](#base64)
  * [JVM的新特性](#jvm的新特性)
* [Java11](#java11)
  * [增加了一系列好用的字符串处理方法](#增加了一系列好用的字符串处理方法)
  * [用于 Lambda 参数的局部变量语法](#用于-lambda-参数的局部变量语法)
  * [标准化HTTP Client](#标准化http-client)
  * [单个命令编译运行源代码](#单个命令编译运行源代码)
  * [ZGC](#zgc)
# Java8
## Lambda表达式
```java
语法格式:
(parameters) -> expression 或 (parameters) -> { statements;}
代码示例:
Arrays.asList("jay","Eason",""SHE).foreach((s) -> System.out.print(s));
```
允许把函数作为一个方法的参数(函数作为参数传递到方法中)
## 函数式接口
- @FunctionalInterface
```java
@FunctionalInterface
public interface Runnable{
    public abstract void run();
}
```
- 只有一个函数
## 默认方法
默认方法就是一个在接口里面有了一个实现的方法。它允许将新方法添加到接口,但不强制实现了该接口的类必须实现新的方法。
```java
public interface ISingerService{
    //默认方法
  default void sing(){
     System.out.printLn("唱歌");
  }
  void writeSong();
}

public class JaySingerServiceImpl implements ISingerService{
    @Override
    public void writeSong(){
      System.out.printLn("写了一首七里香");
    }
}
```
## 方法引用 
可以直接引用已有Java类或对象(实例)的方法或构造器。
```java
//利用函数式接口Consumer的accept方法实现打印,Lambda表达式如下
Consumer<String> consumer = x -> System.out.printLn(x);
consumer.accept("jay");
//引用PrintStream类(也就是System.out的类型)的printLn方法,这就是方法引用
consumer = System.out::println;
consumer.accept("sss");
```
## Stream API 
### Collections
- filter 筛选
- map流映射
- reduce 将流中的元素组合起来
- collect 返回集合
- sorted 排序
- flatMap 流转换
- limit返回指定流个数
- distinct去除重复元素
## Optional
用来解决NullPointerException。Optional代替if...else解决空指针问题,使代码更加简洁。
## Date Time API
LocalDate /LocalDateTime
## 重复注解
重复注解,即一个注解可以在一个类、属性或者方法上同时使用多次;用@Repeatable定义重复注解

```java
import java.lang.annotation.Repeatable;

@Repeatable(ScheduleTimes.class)
public @interface ScheduleTime{
    String value();
}

public @interface ScheduleTimes{
  ScheduleTimes[] value();
}

public class ScheduleTimeTask{
    
    @ScheduleTime("10")
    @ScheduleTime("12")
    public void doSomething(){};
}
```
## Base64
Java 8把Base64编码的支持加入到官方库中~
- `String encoded = Base64.getEncoder().encodeToString(str.getBytes( StandardCharsets.UTF_8));`
- `String decoded = new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8);`
## JVM的新特性
使用元空间Metaspace代替持久代(PermGen space),JVM参数使用-XX:MetaSpaceSize和-XX:MaxMetaspaceSize设置大小。
# Java11
## 增加了一系列好用的字符串处理方法
- isBlank() 判空。
- strip() 去除首尾空格
- stripLeading() 去除字符串首部空格
- stripTrailing() 去除字符串尾部空格
- lines() 分割获取字符串流。
- repeat() 复制字符串
## 用于 Lambda 参数的局部变量语法
```java
var map = new HashMap<String,Object>();
map.put("aaa","aaa");
map.forEach((k,v)->{
    System.out.println(k+":"+v);    
})
```
## 标准化HTTP Client
Java 9 引入Http Client API,Java 10对它更新,Java 11 对它进行标准化。这几个版本后,Http Client几乎被完全重写,支持HTTP/1.1和HTTP/2 ,也支持 websockets。
## 单个命令编译运行源代码
- Java 11之前
  - // 编译 `javac Jay.java`
  - // 运行 `java Jay`
- Java 11之后 `java Jay.java`
## ZGC
可伸缩低延迟垃圾收集器
- GC 停顿时间不超过 10ms
- 既能处理几百 MB 的小堆,也能处理几个 TB 的大堆
- 应用吞吐能力不会下降超过 15%(与 G1 回收算法相比)
- 方便在此基础上引入新的 GC 特性和利用 colord
- 针以及 Load barriers 优化奠定基础
- 当前只支持 Linux/x64 位平台

# Java17

## 封闭类和接口(Sealed Classes and Interfaces, JEP 409):

引入封闭类和接口,允许开发者限制哪些类可以扩展或实现它们。这有助于设计更可预测的类层次结构。

语法示例:
```java

public sealed class Shape permits Circle, Square { }
```

## 模式匹配 for switch (Pattern Matching for switch, Preview, JEP 406):

扩展 switch 语句,以支持基于模式匹配的分支操作,这使得 switch 更加灵活和强大。

语法示例:
```java
switch (obj) {
  case Integer i -> System.out.println("Integer: " + i);
  case String s -> System.out.println("String: " + s);
  default -> System.out.println("Other: " + obj);
}
```
## 强度减少(Strong Encapsulation, JEP 403):

更严格地限制模块化系统中的反射访问,使默认情况下不能通过反射访问模块的内部 API。
## 外部函数和内存 API (Foreign Function & Memory API, Incubator, JEP 412):

引入新的 API,使 Java 程序能够安全、高效地调用非 Java 代码(如 C 函数),并操作非堆内存。
## 增强的伪随机数生成器(Enhanced Pseudo-Random Number Generators, JEP 356):

增加了一组新的接口和实现,以提高伪随机数生成的灵活性和可扩展性。
## 弃用和移除:

永久删除 Applets,并移除 RMI Activation,这些旧的功能已经不再使用。

# Java21

## 结构化并发(Structured Concurrency, JEP 428):

通过将多个任务作为单个结构化单元来管理,以简化多线程编程,增强可读性和可维护性。
## 模式匹配 for switch (Pattern Matching for switch, JEP 441):

这是对 Java 17 中引入的模式匹配 switch 的正式化,在 Java 21 中已经成为标准功能。
## 记录模式(Record Patterns, JEP 440):

扩展了模式匹配功能,使其能够匹配 Record 类型,方便解构和处理 Record 类的字段。
## 虚拟线程(Virtual Threads, JEP 444):

引入了虚拟线程的概念,极大地简化了并发编程,使得创建和管理大量线程变得更加轻量级。
## String 模式匹配和增强(String Templates, JEP 430):

新增了字符串模板功能,简化了字符串插值操作。
## 外部函数和内存 API (Foreign Function & Memory API, JEP 442):

这项功能在 Java 17 中作为孵化器引入,而在 Java 21 中则被进一步增强和正式化。
## 垃圾收集器改进:

在垃圾收集器方面,特别是 ZGC 和 G1 等垃圾收集器,做了大量优化以提升性能和稳定性。
## 向量 API (Vector API, JEP 438):

进一步改进和稳定,允许开发者利用向量化硬件加速批量计算任务

================================================
FILE: Java-基础/Object.md
================================================

* [equals](#equals)
* [hashcode](#hashcode)
* [toString](#tostring)
* [clone](#clone)
* [wait](#wait)
* [notify](#notify)
* [finalize](#finalize)
* [重写了equals后为什么要重写hashcode,如果不重写,会有什么影响](#重写了equals后为什么要重写hashcode如果不重写会有什么影响)

# equals
- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
- 对于引用类型,== 判断两个变量是否引用同一个对象(即是否指向同一个内存地址),而 equals() 判断引用的对象是否等价(默认情况下,Object 类中的 equals() 方法与 == 相同,但许多类重写了这个方法,需要判断重写后的方法返回的内容来判断是否相同来判断对象是否相等)。
# hashcode
- hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。
- 在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。
  - 重写equals的一般步骤 
    - 检查两个对象是否为同一个对象,如果是,则返回true。 
    - 检查传入的对象是否为空,如果是,则返回false。 
    - 检查传入的对象是否与当前对象属于同一类,如果不是,则返回false。 
    - 将传入的对象转换成当前对象的类型。 
    - 比较两个对象的属性是否相等,如果都相等,则返回true,否则返回false。
- hashCode方法实际上必须要完成的一件事情就是,为该equals方法认定为相同的对象返回相同的哈希值
# toString
- 一般生成对象字符串
# clone
- 浅拷贝
- 引用一个对象,拷贝基本数据类型的值,引用类型只拷贝引用
- 深拷贝
- 引用不同对象,拷贝基本数据类型的值,引用类型创建新对象
# wait
- 挂起线程
# notify
- 唤醒线程
# finalize
- GC时调用,回收资源

# 重写了equals后为什么要重写hashcode,如果不重写,会有什么影响
- 每个覆盖了equals方法的类中,必须覆盖hashCode。如果不这么做,就违背了hashCode的通用约定。进而导致该类无法结合所以与散列的集合一起正常运作,这里指的是HashMap、HashSet、HashTable、ConcurrentHashMap。
  - hashcode源码注释中的大致意思是:当我们将equals方法重写后有必要将hashCode方法也重写,这样做才能保证不违背hashCode方法中“相同对象必须有相同哈希值”的约定
- 比如hashmap里的put操作就会判断key的hashcode是否相等,如果出现重写了equals但未重写hashcode,则可能出现这种情况:hashMap.put("k","v1"),hashMap.put("k":"v2"),而不是使用v2替换v1的值,这样我们的HashMap就乱套了

================================================
FILE: Java-基础/String.md
================================================

* [final](#final)
  * [不可变的原因](#不可变的原因)
* [储存数据](#储存数据)
* [string pool](#string-pool)
  * [new String("abc")会创建几个对象](#new-stringabc会创建几个对象)
  * [intern()](#intern)
  * [str1 + " a nice day"](#str1---a-nice-day)
  * ["a" + "b" + "c"](#a--b--c)
* [StringBuilder](#stringbuilder)
* [StringBuffer](#stringbuffer)

# final
## 不可变的原因
在Java中,String类型被设计成final的主要原因是为了保证字符串的不可变性。这意味着一旦一个字符串被创建,它的值就不能再被改变。这种不可变性带来了很多好处,例如:

- 线程安全:由于字符串是不可变的,所以在多线程环境下,不需要担心并发访问的问题。

- 缓存哈希值:由于字符串的哈希值是不可变的,所以可以在哈希表等数据结构中缓存字符串的哈希值,提高性能。

- 安全性:不可变的字符串可以防止在处理字符串时,因为修改字符串而导致的潜在安全问题,例如SQL注入攻击。

- 简化代码:由于字符串是不可变的,所以在编写代码时,不需要担心字符串值被改变的情况,这使得代码更加简单和可读。

此外,String类型作为Java中最常用的类型之一,设计成final还可以提高字符串的性能,因为编译器可以对字符串的操作进行一些优化,例如重复使用相同的字符串对象、避免创建新的字符串对象等。
# 储存数据
- Java8内部使用char数组储存数据
  - `private final char value[];`
- Java9后使用byte数组同时使用coder来标识使用哪种编码
  - `private final byte[] value;`
  - `private final byte coder;`
# string pool
### new String("abc")会创建几个对象
- 使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对象)。
- "abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面量;
- 而使用 new 的方式会在堆中创建一个字符串对象。
### intern()
字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法 进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。
```
下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一
个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用
的是同一个字符串。
String s1 = new String("aaa"); 
String s2 = new String("aaa"); 
System.out.println(s1 == s2); // false
 
String s3 = s1.intern(); 
String s4 = s1.intern(); 
System.out.println(s3 == s4); // true

如果是采用 "bbb" 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。
String s5 = "bbb"; 
String s6 = "bbb"; 
System.out.println(s5 == s6); // true
```
### str1 + " a nice day"
- 编译为 new StringBuilder().append(str1).append(" a nice day");
- 但是如果str1被final修饰,此变量会在初始化时加载到常量池,所以会直接变为str1的值"value"+"a nice day"
### "a" + "b" + "c"
编译优化不会创建对象
# StringBuilder
- 可变 byte数组没有被final修饰
- 线程不安全
- 速度快
# StringBuffer
- 可变 byte数组没有被final修饰
- 线程安全
  - 方法被synchronized修饰
- 速度慢

大量字符串操作使用他们
  - 操作少量的数据: 适用 String
  - 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  - 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

# String 的hashcode()方法
String也是遵守equals的标准的,也就是 s.equals(s1)为true,则s.hashCode()==s1.hashCode()也为true。此处并不关注eqauls方法,而是讲解 hashCode()方法,String.hashCode()有点意思,而且在面试中也可能被问到。先来看一下代码:
```java
public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
```
## 为什么要选31作为乘数呢?
从网上的资料来看,一般有如下两个原因:

- 31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。另外一些相近的质数,比如37、41、43等等,也都是不错的选择。那么为啥偏偏选中了31呢?请看第二个原因。
- 31可以被 JVM 优化,31 * i = (i << 5) - i。

# 参考文章
- https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483956&idx=1&sn=1c19164967621fa5449a7830d006c8f9&scene=19#wechat_redirect

================================================
FILE: Java-基础/反射.md
================================================

* [什么是反射](#什么是反射)
* [java.lang.reflect](#javalangreflect)
* [反射的优缺点](#反射的优缺点)
    * [优点](#优点)
        * [可扩展性](#可扩展性)
        * [类浏览器和可视化开发环境](#类浏览器和可视化开发环境)
        * [调试器和测试工具](#调试器和测试工具)
    * [缺点](#缺点)
        * [性能开销](#性能开销)
        * [安全限制](#安全限制)
        * [内部暴露](#内部暴露)


## 什么是反射
- 每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
- 类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用Class.forName("com.mysql.jdbc.Driver") 这种方式来控制类的加载,该方法会返回一个 Class 对象。
- 反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
## java.lang.reflect
- Field :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
- Method :可以使用 invoke() 方法调用与 Method 对象关联的方法;
- Constructor :可以用 Constructor 创建新的对象。
## 反射的优缺点
### 优点
#### 可扩展性
应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
#### 类浏览器和可视化开发环境
一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
#### 调试器和测试工具
调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。
### 缺点
#### 性能开销
反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些
非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
#### 安全限制
使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的
环境中运行,如 Applet,那么这就是个问题了
#### 内部暴露
由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使
用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因
此当平台发生改变的时候,代码的行为就有可能也随着变化。

## java反射中,Class.forName和classloader的区别
java中class.forName()和classLoader都可用来对类进行加载。

class.forName()前者除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。

而classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象
```java
public static Class<?> forName(String className)
            throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
```
```java
public static Class<?> forName(String name, boolean initialize,ClassLoader loader)throws ClassNotFoundException{
    Class<?> caller = null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        // Reflective call to get caller class is only needed if a security manager
        // is present.  Avoid the overhead of making this call otherwise.
        caller = Reflection.getCallerClass();
        if (sun.misc.VM.isSystemDomainLoader(loader)) {
            ClassLoader ccl = ClassLoader.getClassLoader(caller);
            if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                sm.checkPermission(
                    SecurityConstants.GET_CLASSLOADER_PERMISSION);
            }
        }
    }
    return forName0(name, initialize, loader, caller);
}
```
# 参考文章
- https://blog.csdn.net/qq_27093465/article/details/52262340

================================================
FILE: Java-基础/容器-collection.md
================================================

* [Collection](#collection)
* [List](#list)
* [常用的子类](#常用的子类)
  * [Vector](#vector)
  * [LinkedList](#linkedlist)
  * [ArrayList](#arraylist)
  * [CopyOnWriteArrayList](#copyonwritearraylist)
  * [集合对比](#集合对比)
* [Set](#set)
  * [HashSet](#hashset)
  * [LinkedHashSet](#linkedhashset)
  * [TreeSet](#treeset)
* [queue](#queue)
  * [添加](#添加)
  * [删除](#删除)
  * [获取](#获取)
  * [BlockingQueue](#blockingqueue)
    * [插入](#插入)
    * [移除](#移除)
    * [获取数据](#获取数据)
    * [常见的实现Queue queue = new LinkedList&lt;&gt;();](#常见的实现queue-queue--new-linkedlist)
* [参考文章](#参考文章)

# Collection
![](../img/基础/Java容器类关系图.png)
# List
- 实现了Collection接口
- List接口特性:是有序的,元素是可重复的
- 允许元素为null
# 常用的子类
## Vector
- 底层结构是数组,初始容量为10,每次增长2倍
- 它是线程同步的,已被ArrayList替代
- Vector 也是一个动态数组结构,一个元老级别的类,早在 jdk1.1 就引入进来了,之后在 jdk1.2 里引进 ArrayList,ArrayList 可以说是 Vector 的一个迷你版,ArrayList 大部分的方法和 Vector 比较相似!
- 两者不同的是,Vector 中的方法都加了synchronized,保证操作是线程安全的,但是效率低,而 ArrayList 所有的操作都是非线程安全的,执行效率高,但不安全!
- 对于 Vector,虽然可以在多线程环境下使用,但是在迭代遍历元素的时候依然会报错,抛ConcurrentModificationException异常!
- 在 JDK 中 Vector 已经属于过时的类,官方不建议在程序中采用,如果想要在多线程环境下使用 Vector,建议直接使用并发包中的CopyOnWriteArrayList!
- Stack
  - Stack 是 Vector 的一个子类,本质也是一个动态数组结构,不同的是,它的数据结构是先进后出,取名叫栈!
  - 不过,关于 Java 中 Stack 类,有很多的质疑声,栈更适合用队列结构来实现,这使得 Stack 在设计上不严谨,因此,官方推荐使用 Deque 下的类来是实现栈!
## LinkedList
- 底层结构是双向链表
- 实现了Deque接口,因此我们可以像操作栈和队列一样操作它
- 线程非同步
- LinkedList 是一个双向链表结构,在任意位置插入、删除都很方便,但是不支持随机取值,每次都只能从一端开始遍历,直到找到查询的对象,然后返回;不过,它不像 ArrayList 那样需要进行内存拷贝,因此相对来说效率较高,但是因为存在额外的前驱和后继节点指针,因此占用的内存比 ArrayList 多一些。
## ArrayList
- 底层结构是数组,初始容量为10,每次增长1.5倍
  - ArrayList的扩容
    - 原理
      - 调用系统函数的copy方法
      - 一个数组,可以不断地添加元素,而不出现数组下标越界异常。怎么实现?扩容
- 在增删时候,需要数组的拷贝复制(navite 方法由C/C++实现),性能还是不差的!
- 线程非同步
- ArrayList 是一个动态数组结构,支持随机存取,在指定的位置插入、删除效率低(因为要移动数组元素);如果内部数组容量不足则自动扩容,扩容系数为原来的1.5倍,因此当数组很大时,效率较低。扩容也是调用Arrays.copyOf方法
- 当然,插入删除也不是效率非常低,在某些场景下,比如尾部插入、删除,因为不需要移动数组元素,所以效率也很高哦!
- ArrayList 是一个非线程安全的类,在多线程环境下使用迭代器遍历元素时,会报错,抛ConcurrentModificationException异常!
  - 迭代器
    - 迭代器删除元素
    - 单线程和多线程的区别 https://blog.csdn.net/weixin_35681869/article/details/113812708
- addAll方法
  - 底层使用native arraycopy方法,内存拷贝数组速度会更快
  - 大数据量时推荐使用,小数据量时与for循环对比不明显
- 如果要从列表的中间添加元素是怎么实现的?
  - 将当前在该位置的元素index(如果有)和任何后续元素向右移动,也是直接使用System.arraycopy
  - arr[index] = data
  - size+1

## CopyOnWriteArrayList
- 原理:在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将新数组交由array变量指向。
- 写加锁,读不加锁 ReentrantLock
- 缺点:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
- 适合在读多写少的场景下使用
- iterator
  - 返回一个拷贝数据的对象COWIterator

CopyOnWrite容器是一种并发容器,它通过在修改容器时先将容器复制一份,然后在复制的容器上进行修改,最后将原容器引用指向复制的容器,从而实现并发访问的线程安全。因为每次修改都需要复制整个容器,所以CopyOnWrite容器在修改操作上的性能相对较差,但是在读操作上的性能比较好,因为读操作不需要加锁。

虽然CopyOnWrite容器能够实现线程安全,但是由于每次修改都需要复制整个容器,所以对于实时性要求较高的场景,使用CopyOnWrite容器可能会导致数据的实时一致性问题。因为当多个线程同时进行写操作时,每个线程都会在自己的副本上进行修改,这就导致了在某个时间点,不同线程所看到的数据可能不一致。因此,如果要求数据的实时一致性,使用CopyOnWrite容器可能不是最佳选择。
## 集合对比
- ArrayList(动态数组结构),查询快(随意访问或顺序访问),增删慢,但在末尾插入删除,速度与LinkedList相差无几,但是是非线程安全的!
- LinkedList(双向链表结构),查询慢,增删快,也是非线程安全的!
- Vector(动态数组结构),因为方法加了同步锁,相比 ArrayList 执行都慢,基本不再使用,如果需要在多线程下使用,推荐使用并发容器中的CopyOnWriteArrayList来操作,效率高!
- Stack(栈结构)继承于Vector,数据是先进后出,基本不再使用,如果要实现栈,推荐使用 Deque 下的 ArrayDeque,效率比 Stack 高!
`https://juejin.im/post/6844903728324018189`
# Set
- 实现了Collection接口
- Set接口特性:无序的,元素不可重复
- 底层大多数是Map结构的实现
- 常用的三个子类都是非同步的
## HashSet
- 底层数据结构是哈希表(是一个元素为链表的数组) + 红黑树
- 实际上就是封装了HashMap
- 元素无序,可以为null
## LinkedHashSet
- 底层数据结构由哈希表(是一个元素为链表的数组)和双向链表组成。
- 父类是HashSet
- 实际上就是LinkHashMap
- 元素可以为null
## TreeSet
- 底层实际上是一个TreeMap实例(红黑树)
- 可以实现排序的功能
- 元素不能为null
# queue
## 添加
offer,add 区别:
- 一些队列有大小限制,因此如果想在一个满的队列中加入一个新项,多出的项就会被拒绝。
- 这时新的 offer 方法就可以起作用了。它不是对调用 add() 方法抛出一个 unchecked 异常,而只是得到由 offer() 返回的 false。
## 删除
poll,remove 区别:
- remove() 和 poll() 方法都是从队列中删除第一个元素。remove() 的行为与 Collection 接口的版本相似, 但是新的 poll() 方法在用空集合调用时不是抛出异常,只是返回 null。因此新的方法更适合容易出现异常条件的情况。
## 获取
peek,element区别:
- element() 和 peek() 用于在队列的头部查询元素。与 remove() 方法类似,在队列为空时, element() 抛出一个异常,而 peek() 返回 null。
## BlockingQueue
### 插入
- add(e) 抛出异常
- offer(e) 特殊值
- put(e) 阻塞
- offer(e, time, unit) 超时
### 移除
- remove() 抛出异常
- poll() 特殊值
- take() 阻塞
- poll(time, unit) 超时
### 获取数据
- element() 抛出异常
- peek() 特殊值

### 常见的实现
Queue<T> queue = new LinkedList<>();

BlockingQueue(阻塞队列)详解 https://www.cnblogs.com/aspirant/p/8657801.html

# 参考文章
- https://segmentfault.com/a/1190000021237438
- https://mp.weixin.qq.com/s/H6lxTfpedzzDz2QXihhdmw
- https://segmentfault.com/a/1190000023308658
- https://blog.csdn.net/weixin_39797532/article/details/112337531
- https://pdai.tech/md/java/collection/java-collection-all.html






================================================
FILE: Java-基础/容器-map.md
================================================
# Map
![](../img/基础/Java容器类关系图.png)
## HashMap
- 存储的结构是key-value键值对,不像Collection是单列集合
- 阅读Map前最好知道什么是散列表和红黑树
## 特点
- k和v允许为null,存储无序
- 非同步
  - HashMap 虽然很强大,但是它是非线程安全的,也就是说,如果在多线程环境下使用,可能因为程序自动扩容操作将单向链表转变成了循环链表,在查询遍历元素的时候,造成程序死循环!此时 CPU 直接会飙到 100%!
    - 如果我们想在多线程环境下使用 HashMap,其中一个推荐的解决办法就是使用 java 并发包下的 ConcurrentHashMap 类!
- 散列表容量大于64且链表大于8时,转成红黑树
  - 为什么是8
    - 源码的注释,根据泊松分布原理发生冲突 并且链表长度为8的概率已经非常小了不到千万分之一
- 底层是散列表+红黑树。初始容量为16,装载因子为0.75,每次扩容2倍
  - 为什么是0.75,时间和空间的权衡,如果太小比如0.5那么空间为一半的时候就发生扩容,浪费空间,太大如1,满了才扩容,hash冲突的概率大大增加,那为什么不是0.6或者0.8呢,这里估计是调优得出的参数 可以看看这篇文章 https://segmentfault.com/a/1190000023308658
  - ![](../img/hashmap/HashMap链表结构.png)


## 结构
### Node[] table,即哈希桶数组
```java
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }
```
### 插入
#### 插入流程
- ①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容; 
- ②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③; 
- ③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals; 
- ④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤; 
- ⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可; 
- ⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

![](../img/hashmap/HashMap插入流程.png)

#### 插入代码
```java

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

public V put(K key, V value) {
    //对key做hash
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //tab节点数组为空则新建
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //计算index-hash,是否存在,不存在直接封装为node插入
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
    // hash相同,值存在情况    
        Node<K,V> e; K k;
        //hash相同, key相同,覆盖原值
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //节点是红黑树
        else if (p instanceof TreeNode)
            //使用红黑树的插入操作
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //节点是链表
            //遍历链表
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    //尾插
                    p.next = newNode(hash, key, value, null);
                    //判断链表长度是否为8,是则链表转为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //hash 相同,key 相同,跳出循环,在后面覆盖值
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 节点数量大于 阈值则扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
```

#### hash冲突使用链地址法
- 链接地址法的思路是将哈希值相同的元素构成一个同义词的单链表
- `int threshold; `            // 扩容阈值
  - `threshold`就是在此Load factor和length(数组长度)对应下允许的最大元素数目,超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍
- `final float loadFactor;`    // 负载因子
  - 0.75
- `transient int modCount;`  // 出现线程问题时,负责及时抛异常
- `transient int size;`     // HashMap中实际存在的Node数量
- 解决hash冲突的几种方法 https://cloud.tencent.com/developer/article/1672781
#### HashMap的容量为什么要初始化为2的n次幂
```java
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
 ```
- 向集合中添加元素时,会使用(n - 1) & hash的计算方法来得出该元素在集合中的位置
- 扩容时调用resize()方法中的部分源码,可以看出会新建一个tab,然后遍历旧的tab,将旧的元素经过e.hash & (newCap - 1)的计算添加进新的tab中,还是用(n - 1) & hash的计算方法

可见这个(n - 1) & hash的计算方法有着千丝万缕的关系,符号&是按位与的计算,这是位运算,特别高效,按位与&的计算方法是,只有当对应位置的数据都为1时,运算结果也为1,当HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞

#### 为什么计算hash要无符号右移16位
- Key的哈希值会与该值的高16位做异或操作,进一步增加随机性,如果只是单纯的返回hashcode,做运算的始终是低16位,而hashmap长度大多数小于2的16次方
```java
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
```
#### 为什么计算hash是异或
- 按位与运算符(&)
  - 两位同时为“1”,结果才为“1”,否则为0,结果偏向于0
- 按位或运算符(|)
  - 参加运算的两个对象只要有一个为1,其值为1。结果偏向于1
- 异或运算符(^)
  - 参加运算的两个对象,如果两个相应位为“异”(值不同),则该位结果为1,否则为0。所以更好的扰动了

### 查找
```java
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
    
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //检查首节点
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            //判断是否为红黑树
            if (first instanceof TreeNode)
                //红黑树的查找方式
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //否则遍历桶
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
```
### 扩容
```java
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    //原数组大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //原数组扩容阈值
    int oldThr = threshold;
    int newCap, newThr = 0;
    //原数组大小不为0
    if (oldCap > 0) {
        //超过最大值就不再扩充,随你碰撞吧
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //没超过就扩充为原来的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    //原数组阈值不为0
    else if (oldThr > 0) // initial capacity was placed in threshold
        //新数组大小为阈值
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        //原数组阈值为0,则新数组的大小和阈值为默认
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //如果新阈值为0,设置阈值
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        //遍历原数组的每个节点,插入到新数组
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    //采用 高低位方式转移原数组到新数组的位置,解决环链问题,详细的看下面的解析
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
```
### jdk1.7多线程扩容死循环问题
```java
void transfer(Entry[] newTable, boolean rehash) {
   int newCapacity = newTable.length;
   // 外层循环遍历数组槽(slot)
   for (Entry<K,V> e : table) {
       // 内层循环遍历单链表
       while(null != e) {
           // 记录当前节点的next节点
           Entry<K,V> next = e.next;
           if (rehash) {
               e.hash = null == e.key ? 0 : hash(e.key);
           }
           // 找到元素在新数组中的槽(slot)
           int i = indexFor(e.hash, newCapacity);
           // 用头插法将元素插入新的数组
           e.next = newTable[i];
           newTable[i] = e;
           // 遍历下一个节点
           e = next;
       }
   }
}
```
#### 在单线程情况下,假设A、B、C三个节点处在一个链表上,扩容后依然处在一个链表上,代码执行过程如下:
![](../img/基础/jdk1.7hashmap单线程扩容流程.png)

需要注意的几点是
- 单链表在转移的过程中会被反转
- table是线程共享的,而newTable是不共享的
- 执行table = newTable后,其他线程就可以看到转移线程转移后的结果了
#### 多线程下扩容
其实主要结合这4步,然后结合图示就明白了
```java
  Entry<K,V> next = e.next;
  e.next = newTable[i];
  newTable[i] = e;
  e = next;
```
![](../img/基础/jdk1.7hashmap多线程扩容流程.png)
### jdk1.8的扩容
```java
// 低位链表头节点,尾结点
// 低位链表就是扩容前后,所处的槽(slot)的下标不变
// 如果扩容前处于table[n],扩容后还是处于table[n]
Node<K,V> loHead = null, loTail = null;
// 高位链表头节点,尾结点
// 高位链表就是扩容后所处槽(slot)的下标 = 原来的下标 + 新容量的一半
// 如果扩容前处于table[n],扩容后处于table[n + newCapacity / 2]
// Node<K,V> hiHead = null, hiTail = null;
```

```java
Node<K,V> next;
do {
    next = e.next;
    //当前槽上的链表在扩容前和扩容后,所在的槽(slot)下标是否一致
    if ((e.hash & oldCap) == 0) {
        if (loTail == null)
            loHead = e;
        else
            loTail.next = e;
        loTail = e;
    }
    else {
        if (hiTail == null)
            hiHead = e;
        else
            hiTail.next = e;
        hiTail = e;
    }
} while ((e = next) != null);
if (loTail != null) {
    loTail.next = null;
    // 低位链表在扩容后,所处槽的下标不变
    newTab[j] = loHead;
}
if (hiTail != null) {
    hiTail.next = null;
    // 高位链表在扩容后,所处槽的下标 = 原来的下标 + 扩容前的容量(也就是扩容后容量的一半)
    newTab[j + oldCap] = hiHead;
}
```

等到链表被分成高位链表和低位链表后,再一次性转移到新的table。这样就完成了单链表在扩容过程中的转移,使用两条链表的好处就是转移前后的链表不会倒置,顺序一致则不会因为多线程扩容而导致死循环

### JDK1.8hashmap 依然不安全的原因
JDK1.8 中,由于多线程对HashMap进行put操作,调用了HashMap#putVal(),具体原因:假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全

### 为什么要使用红黑树而不是AVL
- 1、为什么不直接使用树
  - 大部分情况hashmap的数据量发生hash冲突的概率其实是很小的,此时使用链表是最佳选择
- 2、为什么是红黑树?
  - AVL树是完全平衡二叉树,在节点插入时、删除时都会调整树结构来平衡,因此会消耗更多的时间
  - 虽然AVL的查找时间由于树高度更低而更快,但是插入和删除花费时间比红黑树更长,在hashmap这种情况下更适用红黑树
### 为什么不是B+树
- b+树的特点是矮胖,这样叶子结点可以存储大量数据,减少磁盘IO,并不是说不可以,但是相对应用场景来讲,用红黑树更加适合

### 为什么不使用跳表
- 跳表的随机性:跳表的层次结构是基于随机的,它并不能像红黑树那样提供严格的平衡性。尽管跳表的时间复杂度也是 O(log n),但跳表实现依赖随机数生成来构建多级索引,可能会导致性能不如红黑树稳定。
- 空间开销:跳表需要维护多层索引,会增加额外的空间消耗,特别是在 HashMap 中,每个桶原本只是链表结构,如果改为跳表,整个结构变得更复杂,需要更多的指针和索引,内存开销增加。

### 如果HashMap里有100万条数据,remove掉90万条,HashMap的数组会不会变小
在Java中,HashMap的数组大小在HashMap创建时确定,并且通常不会自动缩小。因此,当你从HashMap中删除大量条目后,HashMap的数组大小不会减小,但是HashMap的占用内存大小也不会一直增加。

即使从HashMap中删除了90%的条目,HashMap的“capacity”和“load factor”将保持不变。这意味着虽然内存中保留了之前容纳100万条目的数组,但实际上用于存储数据的内存将会减少,因为90%的项已被删除。

尽管数组大小不会在删除后自动缩小,但随着元素被移除而留下的空间会被重新利用。在Java中,HashMap通过垃圾回收器来释放不再使用的内存,因此即使HashMap中有大量数据被删除,这些内存空间最终会被垃圾回收器回收。

总而言之,尽管HashMap的数组大小不会缩小,但是通过删除大量数据后,Java的垃圾回收器会逐渐回收未使用的内存空间,因此HashMap的占用空间不会一直增加。Java的垃圾回收机制会确保内存空间被有效地管理和重用。

## LinkedHashMap
- 底层是散列表+红黑树+双向链表,父类是HashMap
- 允许为null,插入有序
- 非同步
- 提供插入顺序和访问顺序两种,访问顺序是符合LRU算法的,一般用于扩展(默认是插入顺序)
- 迭代与初始容量无关(迭代的是维护的双向链表)
- 大多使用HashMap的API,只不过在内部重写了某些方法,维护了双向链表

### 复杂度
- 增删查改时间复杂度均为O(1)
- containsKey()时间复杂度是O(1)
- containsValue()时间复杂度是O(N)
## TreeMap
- 底层是红黑树,保证了时间复杂度为log(n)
- 可以对其进行排序,使用Comparator或者Comparable
- 只要compare或者CompareTo认定该元素相等,那就相等
- 非同步
- 自然排序(手动排序),元素不能为null

### 复杂度
- 增删查改时间复杂度均为O(logN)
- containsKey()时间复杂度O(logN)
- containsValue()时间复杂度O(N),因为value是无序的,所以要依次遍历

## ConcurrentHashMap
- 底层是散列表+红黑树,支持高并发操作
- key和value都不能为null
- 线程是安全的,利用CAS算法和部分操作上锁实现
- get方法是非阻塞,无锁的。重写Node类,通过volatile修饰next来实现每次获取都是最新设置的值
  ```java
  volatile V val;
  volatile Node<K,V> next;
  ```
- 在高并发环境下,统计数据(计算size…等等)其实是无意义的,因为在下一时刻size值就变化了。
- 在 JDK1.7 中,ConcurrentHashMap 类采用了分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组(Segment),每个小数组有 n 个 HashEntry 组成,其中 Segment 继承自ReentrantLock(可重入锁),从而实现并发控制!
- 从 jdk1.8 开始,ConcurrentHashMap 类取消了 Segment 分段锁,采用 CAS + synchronized来保证并发安全,数据结构跟 jdk1.8 中 HashMap 结构保持一致,都是 数组 + 链表(当链表长度大于8时,链表结构转为红黑树)结构。
- jdk1.8 中的 ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 效率又提升了 N 倍!
### ConcurrentHashMap扩容机制
#### 1、ConcurrentHashMap 在 JDK 1.7 中的实现

在 JDK 1.7 版本及之前的版本中,ConcurrentHashMap 为了解决 HashTable 会锁住整个 hash 表的问题,提出了分段锁的解决方案,分段锁就是将一个大的 hash 表分解成若干份小的 hash 表,需要加锁时就针对小的 hash 表进行加锁,从而来提升 hash 表的性能。JDK1.7 中的 ConcurrentHashMap 引入了 Segment 对象,将整个 hash 表分解成一个一个的 Segment 对象,每个 Segment 对象呢可以看作是一个细粒度的 HashMap。

Segment 对象继承了 ReentrantLock 类,因为 Segment 对象它就变成了一把锁,这样就可以保证数据的安全。 在 Segment 对象中通过 HashEntry 数组来维护其内部的 hash 表。每个 HashEntry 就代表了 map 中的一个 K-V,如果发生 hash 冲突时,在该位置就会形成链表。

JDK1.7 中,ConcurrentHashMap 的整体结构可以描述为下图的样子:
![](../img/基础/jdk1.7concurrentHashmap整体结构.png)

##### put 方法
```java
 public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    // 二次哈希,以保证数据的分散性,避免哈希冲突
    int hash = hash(key.hashCode());
    int j = (hash >>> segmentShift) & segmentMask;
    // Unsafe 调用方式,直接获取相应的 Segment
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}
```

在 put 方法中,首先是通过二次哈希减小哈希冲突的可能性,根据 hash 值以 Unsafe 调用方式,直接获取相应的 Segment,最终将数据添加到容器中是由 segment对象的 put 方法来完成。Segment对象的 put 方法源代码如下:

```java
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // 无论如何,确保获取锁 scanAndLockForPut会去查找是否有key相同Node
    ConcurrentHashMap.HashEntry<K,V> node = tryLock() ? null :
            scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        ConcurrentHashMap.HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        ConcurrentHashMap.HashEntry<K,V> first = entryAt(tab, index);
        for (ConcurrentHashMap.HashEntry<K,V> e = first;;) {
            // 更新已存在的key
            if (e != null) {
                K k;
                if ((k = e.key) == key ||
                        (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                if (node != null)
                    node.setNext(first);
                else
                    node = new ConcurrentHashMap.HashEntry<K,V>(hash, key, value, first);
                int c = count + 1;
                // 判断是否需要扩容
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}
```
由于 Segment 对象本身就是一把锁,所以在新增数据的时候,相应的 Segment对象块是被锁住的,其他线程并不能操作这个 Segment 对象,这样就保证了数据的安全性,在扩容时也是这样的,在 JDK1.7 中的 ConcurrentHashMap扩容只是针对 Segment 对象中的 HashEntry 数组进行扩容,还因为 Segment 对象是一把锁,所以在 rehash 的过程中,其他线程无法对 segment 的 hash 表做操作,这就解决了 HashMap 中 put 数据引起的闭环问题
#### 2、ConcurrentHashMap 在 JDK 1.8 中的实现
先从容器安全说起,在容器安全上,1.8 中的 ConcurrentHashMap 放弃了 JDK1.7 中的分段技术,而是采用了 CAS 机制 + synchronized 来保证并发安全性,但是在 ConcurrentHashMap 实现里保留了 Segment 定义,这仅仅是为了保证序列化时的兼容性而已,并没有任何结构上的用处

在存储结构上,JDK1.8 中 ConcurrentHashMap 放弃了 HashEntry 结构而是采用了跟 HashMap 结构非常相似,采用 Node 数组加链表(链表长度大于8时转成红黑树)的形式,Node 节点设计如下:

```java
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
        ...省略...
 }       
```
跟 HashMap 一样 Key 字段被 final 修饰,说明在生命周期内,key 是不可变的, val 字段被 volatile 修饰了,这就保证了 val 字段的可见性。

JDK1.8 中的 ConcurrentHashMap 结构如下图所示:

![](../img/基础/jdk1.8concurrentHashmap整体结构.png)

##### putVal 方法
```java
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 如果 key 为空,直接返回
    if (key == null || value == null) throw new NullPointerException();
    // 两次 hash ,减少碰撞次数
    int hash = spread(key.hashCode());
    // 记录链表节点得个数
    int binCount = 0;
    // 无条件得循环遍历整个 node 数组,直到成功
    for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
        ConcurrentHashMap.Node<K,V> f; int n, i, fh;
        // lazy-load 懒加载的方式,如果当前 tab 容器为空,则初始化 tab 容器
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();

        // 通过Unsafe.getObjectVolatile()的方式获取数组对应index上的元素,如果元素为空,则直接无所插入
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //// 利用CAS去进行无锁线程安全操作
            if (casTabAt(tab, i, null,
                    new ConcurrentHashMap.Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 如果 fh == -1 ,说明正在扩容,那么该线程也去帮扩容
        else if ((fh = f.hash) == MOVED)
            // 协作扩容操作
            tab = helpTransfer(tab, f);
        else {
            // 如果上面都不满足,说明存在 hash 冲突,则使用 synchronized 加锁。锁住链表或者红黑树的头结点,来保证操作安全
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {

                    if (fh >= 0) {// 表示该节点是链表
                        binCount = 1;
                        // 遍历该节点上的链表
                        for (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //这里涉及到相同的key进行put就会覆盖原先的value
                            if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                            (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            ConcurrentHashMap.Node<K,V> pred = e;
                            if ((e = e.next) == null) {//插入链表尾部
                                pred.next = new ConcurrentHashMap.Node<K,V>(hash, key,
                                        value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof ConcurrentHashMap.TreeBin) {// 该节点是红黑树节点
                        ConcurrentHashMap.Node<K,V> p;
                        binCount = 2;
                        if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
                                value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            // 插入完之后,判断链表长度是否大于8,大于8就需要转换为红黑树
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                // 如果存在相同的key ,返回原来的值
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //统计 size,并且检测是否需要扩容
    addCount(1L, binCount);
    return null;
}
```
putVal 方法主要做了以下几件事:
  - 第一步、在 ConcurrentHashMap 中不允许 key val 字段为空,所以第一步先校验key value 值,key、val 两个字段都不能是 null 才继续往下走,否则直接返回一个 NullPointerException 错误,这点跟 HashMap 有区别,HashMap 是可以允许为空的。
  - 第二步、判断容器是否初始化,如果容器没有初始化,则调用 initTable 方法初始化,initTable 方法如下:
##### initTable方法 
```java
/**
 * Initializes table, using the size recorded in sizeCtl.
 */
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        // 负数表示正在初始化或扩容,等待
        if ((sc = sizeCtl) < 0)
            // 自旋等待
            Thread.yield(); // lost initialization race; just spin
        // 执行 CAS 操作,期望将 sizeCtl 设置为 -1,-1 是正在初始化的标识
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
        // CAS 抢到了锁
            try {
            // 对 table 进行初始化,初始化长度为指定值,或者默认值 16
                if ((tab = table) == null || tab.length == 0) {
                    // sc 在初始化的时候用户可能会自定义,如果没有自定义,则是默认的
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    // 创建数组
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    // 指定下次扩容的大小,相当于 0.75 × n
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}
```
Table 本质上就是一个 Node 数组,其初始化过程也就是对 Node 数组的初始化过程,方法中使用了 CAS 策略执行初始化操作。初始化流程为:
1. 判断 sizeCtl 值是否小于 0,如果小于 0 则表示 ConcurrentHashMap 正在执行初始化操作,所以需要先等待一会,如果其它线程初始化失败还可以顶替上去
2. 如果 sizeCtl 值大于等于 0,则基于 CAS 策略抢占标记 sizeCtl 为 -1,表示 ConcurrentHashMap 正在执行初始化,然后构造 table,并更新 sizeCtl 的值
3. 根据双哈希之后的 hash 值找到数组对应的下标位置,如果该位置未存放节点,也就是说不存在 hash 冲突,则使用 CAS 无锁的方式将数据添加到容器中,并且结束循环。
4. 如果并未满足第三步,则会判断容器是否正在被其他线程进行扩容操作,如果正在被其他线程扩容,则放弃添加操作,加入到扩容大军中(ConcurrentHashMap 扩容操作采用的是多线程的方式,后面我们会讲到),扩容时并未跳出死循环,这一点就保证了容器在扩容时并不会有其他线程进行数据添加操作,这也保证了容器的安全性。
5. 如果 hash 冲突,则进行链表操作或者红黑树操作(如果链表树超过8,则修改链表为红黑树),在进行链表或者红黑树操作时,会使用 synchronized 锁把头节点被锁住了,保证了同时只有一个线程修改链表,防止出现链表成环。
6. 进行 addCount(1L, binCount) 操作,该操作会更新 size 大小,判断是否需要扩容,addCount 方法源码如下:

##### addCount方法
```java
// X传入的是1,check 传入的是 putVal 方法里的 binCount,没有hash冲突的话为0,冲突就会大于1
private final void addCount(long x, int check) {
ConcurrentHashMap.CounterCell[] as; long b, s;
// 统计ConcurrentHashMap里面节点个数
if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
    ConcurrentHashMap.CounterCell a; long v; int m;
    boolean uncontended = true;
    if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
                    U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
        fullAddCount(x, uncontended);
        return;
    }
    if (check <= 1)
        return;
    s = sumCount();
}
// check就是binCount,binCount 最小都为0,所以这个条件一定会为true
if (check >= 0) {
    ConcurrentHashMap.Node<K,V>[] tab, nt; int n, sc;
    // 这儿是自旋,需同时满足下面的条件
    // 1. 第一个条件是map.size 大于 sizeCtl,也就是说需要扩容
    // 2. 第二个条件是`table`不为null
    // 3. 第三个条件是`table`的长度不能超过最大容量
    while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
            (n = tab.length) < MAXIMUM_CAPACITY) {
        int rs = resizeStamp(n);
        // 该判断表示已经有线程在进行扩容操作了
        if (sc < 0) {
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                break;
            // 如果可以帮助扩容,那么将 sc 加 1. 表示多了一个线程在帮助扩容
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                transfer(tab, nt);
        }
        // 如果不在扩容,将 sc 更新:标识符左移 16 位 然后 + 2. 也就是变成一个负数。高 16 位是标识符,低 16 位初始是 2
        else if (U.compareAndSwapInt(this, SIZECTL, sc,
                (rs << RESIZE_STAMP_SHIFT) + 2))
            transfer(tab, null);
        s = sumCount();
    }
}
```
addCount 方法做了两个工作:
1. 对 map 的 size 加一
2. 检查是否需要扩容,或者是否正在扩容。如果需要扩容,就调用扩容方法,如果正在扩容,就帮助其扩容。

##### transfer 扩容方法
以下两种情况下可能触发扩容操作

- 调用 put 方法新增元素之后,会调用 addCount 方法来更新 size 大小,并检查是否需要进行扩容,当数组元素个数达到阈值时,会触发transfer方法
- 触发了 tryPresize 操作, tryPresize 操作会触发扩容操作,有两种情况会触发 tryPresize 操作:
  - 第一种情况:当某节点的链表元素个数达到阈值 8 时,这时候需要将链表转成红黑树,在结构转换之前会,会先判断数组长度 n 是否小于阈值MIN_TREEIFY_CAPACITY,默认是64,如果小于则会调用tryPresize方法把数组长度扩大到原来的两倍,并触发transfer方法,重新调整节点的位置。
  - 第二种情况:在 putAll 操作时会先触发 tryPresize 操作。

```java
private final void transfer(ConcurrentHashMap.Node<K,V>[] tab, ConcurrentHashMap.Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    // 多线程扩容,每核处理的量小于16,则强制赋值16
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    // nextTab 为空,先实例化一个新的数组
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            // 新数组的大小是原来的两倍
            ConcurrentHashMap.Node<K,V>[] nt = (ConcurrentHashMap.Node<K,V>[])new ConcurrentHashMap.Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        // 更新成员变量
        nextTable = nextTab;
        // 更新转移下标,就是 老的 tab 的 length
        transferIndex = n;
    }
    // bound :该线程此次可以处理的区间的最小下标,超过这个下标,就需要重新领取区间或者结束扩容
    // advance: 该参数
    int nextn = nextTab.length;
    // 创建一个 fwd 节点,用于占位。当别的线程发现这个槽位中是 fwd 类型的节点,则跳过这个节点。
    ConcurrentHashMap.ForwardingNode<K,V> fwd = new ConcurrentHashMap.ForwardingNode<K,V>(nextTab);
    // advance 变量指的是是否继续递减转移下一个桶,如果为 true,表示可以继续向后推进,反之,说明还没有处理好当前桶,不能推进
    boolean advance = true;
    // 完成状态,如果是 true,表示扩容结束
    boolean finishing = false; // to ensure sweep before committing nextTab
    // 死循环,i 表示下标,bound 表示当前线程可以处理的当前桶区间最小下标
    for (int i = 0, bound = 0;;) {
        ConcurrentHashMap.Node<K,V> f; int fh;
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt
                    (this, TRANSFERINDEX, nextIndex,
                            nextBound = (nextIndex > stride ?
                                    nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            synchronized (f) {
            // 这儿多判断一次,是否为了防止可能出现的remove()操作
                if (tabAt(tab, i) == f) {
                    // 旧链表上该节点的数据,会被分成低位和高位,低位就是在新链表上的位置跟旧链表上一样,
                    // 高位就是在新链表的位置是旧链表位置加上旧链表的长度
                    ConcurrentHashMap.Node<K,V> ln, hn;
                    if (fh >= 0) {
                        int runBit = fh & n;
                        ConcurrentHashMap.Node<K,V> lastRun = f;
                        for (ConcurrentHashMap.Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        for (ConcurrentHashMap.Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            // 该节点哈希值与旧链表长度与运算,结果为0,则在低位节点上,反之,在高位节点上
                            if ((ph & n) == 0)
                                ln = new ConcurrentHashMap.Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new ConcurrentHashMap.Node<K,V>(ph, pk, pv, hn);
                        }
                        setTabAt(nextTab, i, ln);
                        // 在nextTable i + n 位置处插上链表
                        setTabAt(nextTab, i + n, hn);
                        // 在table i 位置处插上ForwardingNode 表示该节点已经处理过了
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    else if (f instanceof ConcurrentHashMap.TreeBin) {
                        // 如果是TreeBin,则按照红黑树进行处理,处理逻辑与上面一致
                        // 红黑树的逻辑跟节点一模一样,最后也会分高位和低位
                        ConcurrentHashMap.TreeBin<K,V> t = (ConcurrentHashMap.TreeBin<K,V>)f;
                        ConcurrentHashMap.TreeNode<K,V> lo = null, loTail = null;
                        ConcurrentHashMap.TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        for (ConcurrentHashMap.Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            ConcurrentHashMap.TreeNode<K,V> p = new ConcurrentHashMap.TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        // 如果树的节点数小于等于 6,那么转成链表,反之,创建一个新的树
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new ConcurrentHashMap.TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new ConcurrentHashMap.TreeBin<K,V>(hi) : t;
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}
```
transfer 大致做了以下几件事件:
1. 计算出每个线程每次可以处理的个数,根据 Map 的长度,计算出每个线程(CPU)需要处理的桶(table数组的个数),默认每个线程每次处理 16 个桶,如果小于 16 个,则强制变成 16 个桶。
2. 对 nextTab 初始化,如果传入的新 table nextTab 为空,则对 nextTab 初始化,默认是原 table 的两倍
3. 引入 ForwardingNode、advance、finishing 变量来辅助扩容,ForwardingNode 表示该节点已经处理过,不需要在处理,advance 表示该线程是否可以下移到下一个桶(true:表示可以下移),finishing 表示是否结束扩容(true:结束扩容,false:未结束扩容) ,具体的逻辑就不说了
4. 跳过一些其他细节,直接到数据迁移这一块,在数据转移的过程中会加 synchronized 锁,锁住头节点,同步化操作,防止 putVal 的时候向链表插入数据
5. 进行数据迁移,如果这个桶上的节点是链表或者红黑树,则会将节点数据分为低位和高位,计算的规则是通过该节点的 hash 值跟为扩容之前的 table 容器长度进行位运算(&),如果结果为 0 ,则将数据放在新表的低位(当前 table 中为 第 i 个位置,在新表中还是第 i 个位置),结果不为 0 ,则放在新表的高位(当前 table 中为第 i 个位置,在新表中的位置为 i + 当前 table 容器的长度)。
6. 如果桶挂载的是红黑树,不仅需要分离出低位节点和高位节点,还需要判断低位和高位节点在新表以链表还是红黑树的形式存放。

## HashTable
- 与hashmap对比较大的区别是方法加锁,使用synchronized,线程安全
- Hashtable key和value不为空

## IdentityHashMap
- IdentityHashMap 从名字上看,感觉表示唯一的 HashMap,然后并不是,别被它的名称所欺骗哦。
- IdentityHashMap 的数据结构很简单,底层实际就是一个 Object 数组,在存储上并没有使用链表来存储,而是将 K 和 V 都存放在 Object 数组上。
- 当添加元素的时候,会根据 Key 计算得到散列位置,如果发现该位置上已经有改元素,直接进行新值替换;如果没有,直接进行存放。当元素个数达到一定阈值时,Object 数组会自动进行扩容处理。
- IdentityHashMap 的实现也不同于 HashMap,虽然也是数组,不过 IdentityHashMap 中没有用到链表,解决冲突的方式是计算下一个有效索引,并且将数据 key 和 value 紧挨着存入 map 中,即table[i]=key、table[i+1]=value;
- IdentityHashMap 允许key、value都为null,当key为null的时候,默认会初始化一个Object对象作为key;
## WeakHashMap
- WeakHashMap 是 Map 体系中一个很特殊的成员,它的特殊之处在于 WeakHashMap 里的元素可能会被 GC 自动删除,即使程序员没有显示调用 remove() 或者 clear() 方法。
- 换言之,当向 WeakHashMap 中添加元素的时候,再次遍历获取元素,可能发现它已经不见了
- WeakHashMap 的 key 使用了弱引用类型,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
- 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
- WeakHashMap 跟普通的 HashMap 不同,在存储数据时,key 被设置为弱引用类型,而弱引用类型在 java 中,可能随时被 jvm 的 gc 回收,所以再次通过获取对象时,可能得到空值,而 value 是在访问数组内容的时候,进行清除。
- 可能很多人觉得这样做很奇葩,其实不然,WeekHashMap 的这个特点特别适用于需要缓存的场景。
- 在缓存场景下,由于系统内存是有限的,不能缓存所有对象,可以使用 WeekHashMap 进行缓存对象,即使缓存丢失,也可以通过重新计算得到,不会造成系统错误。
- 比较典型的例子,Tomcat 中的 ConcurrentCache 类就使用了 WeekHashMap 来实现数据缓存。

# 参考文章
- https://segmentfault.com/a/1190000021237438
- https://mp.weixin.qq.com/s/H6lxTfpedzzDz2QXihhdmw
- https://segmentfault.com/a/1190000023308658
- https://blog.csdn.net/weixin_39797532/article/details/112337531
- https://pdai.tech/md/java/collection/java-collection-all.html






================================================
FILE: Java-基础/异常.md
================================================

* [Throwable](#throwable)
  * [Error](#error)
  * [Exception](#exception)
* [try catch finally机制](#try-catch-finally机制)
* [OOM &amp;&amp; SOF](#oom--sof)
  * [发生了内存泄露或溢出怎么办](#发生了内存泄露或溢出怎么办)
  * [内存泄漏的场景](#内存泄漏的场景)
  * [内存溢出的场景](#内存溢出的场景)
    * [Java Heap 溢出](#java-heap-溢出)
    * [虚拟机栈和本地方法栈溢出](#虚拟机栈和本地方法栈溢出)
    * [运行时常量池溢出](#运行时常量池溢出)
    * [方法区溢出](#方法区溢出)
    * [java.lang.OutOfMemoryError: GC overhead limit exceeded](#javalangoutofmemoryerror-gc-overhead-limit-exceeded)
  * [SOF (堆栈溢出 StackOverflow)](#sof-堆栈溢出-stackoverflow)
  * [如何避免发生内存泄露和溢出](#如何避免发生内存泄露和溢出)
* [参考文章](#参考文章)

## Throwable
![](../img/基础/Java异常.png)
### Error
表示 JVM 无法处理的错误
- OutOfMemoryError
- StackOverflowError
### Exception
- 受检异常
  - 需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复;
    - IOException
      - ClassNotFoundException
- 非受检异常
  - 是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复
    - RuntimeException
      - NullPointerException
      - IllegalArgumentException
## try catch finally机制
- 有当try代码块发生异常的时候,才会执行到catch代码块
- 不管try中是否发生异常,finally都会执行。
  - 以下两种情况例外:
    - 一:try中不发生异常时,try块中有System.exit(0);
    - 二:try中发生异常时,catch中有System.exit(0);
    - 说明:System.exit(0) 代码的作用的退出虚拟机;
- 若finally块内有return语句,则以finally块内的return为准
   - 如果try 或者 catch内也有return 其实是先执行了try 或者 catch代码块中的return语句的,
   - 但是由于finally的机制,执行完try或者catch内的代码以后并不会立刻结束函数,还会执行finally块代码,
   - 若finally也有return语句,则会覆盖try块或者catch块中的return语句
- 若finally代码块中有return语句,则屏蔽catch代码块中抛出的异常

## OOM && SOF
OutOfMemoryError异常: 除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能,

内存泄露:指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用。

内存溢出:指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。

从定义上可以看出内存泄露是内存溢出的一种诱因,不是唯一因素。

栈溢出:当应用程序递归太深而发生堆栈溢出时,抛出该错误。
### 发生了内存泄露或溢出怎么办
一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess
java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。

(1)通过参数 -XX:+HeapDumpOnOutOfMemoryError 让虚拟机在出现OOM异常的时候Dump出内存映像以便于分析。

(2)一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。(到底是出现了内存泄漏还是内存溢出)

哪些对象被怀疑为内存泄漏,哪些对象占的空间最大及对象的调用关系,还可以分析线程状态,可以观察到线程被阻塞在哪个对象上,从而判断系统的瓶颈。

(3)如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象时通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。 找到引用信息,可以准确的定位出内存泄漏的代码位置。(HashMap中的元素的某些属性改变了,影响了hashcode的值会发生内存泄漏)

(4)如果不存在内存泄漏,就应当检查虚拟机的参数(-Xmx与-Xms)的设置是否适当,是否可以调大;修改代码逻辑,把某些对象生命周期过长,持有状态时间过长等情况的代码修改。



### 内存泄漏的场景
   (1)使用静态的集合类

静态的集合类的生命周期和应用程序的生命周期一样长,所以在程序结束前容器中的对象不能被释放,会造成内存泄露。

解决办法是最好不使用静态的集合类,如果使用的话,在不需要容器时要将其赋值为null。

修改hashset中对象的参数值,且参数是计算哈希值的字段

(2)单例模式可能会造成内存泄露(长生命周期的对象持有短生命周期对象的引用)

单例模式只允许应用程序存在一个实例对象,并且这个实例对象的生命周期和应用程序的生命周期一样长,如果单例对象中拥有另一个对象的引用的话,这个被引用的对象就不能被及时回收。

解决办法是单例对象中持有的其他对象使用弱引用,弱引用对象在GC线程工作时,其占用的内存会被回收掉。

(3)数据库、网络、输入输出流,这些资源没有显示的关闭

垃圾回收只负责内存回收,如果对象正在使用资源的话,Java虚拟机不能判断这些对象是不是正在进行操作,比如输入输出,也就不能回收这些对象占用的内存,所以在资源使用完后要调用close()方法关闭。

### 内存溢出的场景
#### Java Heap 溢出

在jvm规范中,堆中的内存是用来生成对象实例和数组的。 

如果细分,堆内存还可以分为年轻代和年老代,年轻代包括一个eden区和两个survivor区。
   
当生成新对象时,内存的申请过程如下:

- jvm先尝试在eden区分配新建对象所需的内存;
- 如果内存大小足够,申请结束,否则下一步;
- jvm启动youngGC,试图将eden区中不活跃的对象释放掉,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
- Survivor区被用来作为Eden及old的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
- 当OLD区空间不够时,JVM会在OLD区进行full GC;
- full GC后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”: outOfMemoryError:java heap space


#### 虚拟机栈和本地方法栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
不断创建线程,如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
这里需要注意当栈的大小越大可分配的线程数就越少。
用Xss设置


#### 运行时常量池溢出
异常信息:java.lang.OutOfMemoryError:PermGen space
如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。
该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。


#### 方法区溢出
异常信息:java.lang.OutOfMemoryError: PermGen space

方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。


所以如果程序加载的类过多,或者使用反射、gclib等这种动态代理生成类的技术,就可能导致该区发生内存溢出

方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。

我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小

#### java.lang.OutOfMemoryError: GC overhead limit exceeded
原因:执行垃圾收集的时间比例太大, 有效的运算量太小. 默认情况下, 如果GC花费的时间超过 98%, 并且GC回收的内存少于 2%, JVM就会抛出这个错误。

目的是为了让应用终止,给开发者机会去诊断问题。一般是应用程序在有限的内存上创建了大量的临时对象或者弱引用对象,从而导致该异常。

解决方法:
1. 大对象在使用之后指向null。
2. 增加参数,-XX:-UseGCOverheadLimit,关闭这个特性;
3. 增加heap大小,-Xmx1024m



### SOF (堆栈溢出 StackOverflow)
StackOverflowError 的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。 因为栈一般默认为1-2M,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1M而导致溢出。

栈溢出的原因:
- 递归调用
- 大量循环或死循环
- 全局变量是否过多
- 数组、List、map数据过大



### 如何避免发生内存泄露和溢出
1. 尽早释放无用对象的引用
2. 使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域
3. 尽量少用静态变量,因为静态变量存放在永久代(方法区),永久代基本不参与垃圾回收
4. 避免在循环中创建对象
5. 开启大型文件或从数据库一次拿了太多的数据很容易造成内存溢出,所以在这些地方要大概计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。


# 参考文章
- https://www.cnblogs.com/haimishasha/p/11329510.html

================================================
FILE: Java-基础/数组.md
================================================

* [初始化](#初始化)
  * [静态初始化](#静态初始化)
  * [动态初始化](#动态初始化)
* [Java 数组和内存](#java-数组和内存)
* [参考文章](#参考文章)


# 初始化
## 静态初始化
int[] a = {1,2,3};
## 动态初始化
int[] a = new int[3];

# Java 数组和内存
Java 数组在内存中的存储是这样的:

数组对象(这里可以看成一个指针)存储在栈中。

数组元素存储在堆中。

如下图所示:只有当 JVM 执行 new String[] 时,才会在堆中开辟相应的内存区域。数组对象 array 可以视为一个指针,指向这块内存的存储地址。

![](../img/基础/Java数组内存.png)

# 参考文章
- https://dunwu.github.io/javacore/basics/java-array.html#_1-4-java-%E6%95%B0%E7%BB%84%E5%92%8C%E5%86%85%E5%AD%98

================================================
FILE: Java-基础/泛型.md
================================================

* [什么是泛型?为什么要使用泛型?](#什么是泛型为什么要使用泛型)
* [泛型的使用](#泛型的使用)
* [泛型上下边界](#泛型上下边界)
    * [上边界](#上边界)
    * [下边界](#下边界)

## java 中泛型标记符:

- E - Element (在集合[*主要分为Collection和Map两个接口]中使用,因为集合中存放的是元素)
- T - Type(Java 类)
- K - Key(键)
- V - Value(值)
- N - Number(数值类型)
- ? - 表示不确定的 java 类型
## 什么是泛型?为什么要使用泛型?
- 泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
- 泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
## 泛型的使用 
- 泛型类
- 泛型接口
- 泛型方法

参考 https://blog.csdn.net/zhanshixiang/article/details/82559259

## 泛型上下边界
### 上边界
extends
- 类型必须继承指定的某个类(class),或实现某个接口(interface)。
### 下边界
super
- 指定的类型不能小于操作的类,即指定类及其父类
### 边界参考 https://www.jianshu.com/p/00d50c1e46a9

================================================
FILE: Java-基础/继承.md
================================================

* [访问修饰符](#访问修饰符)
* [抽象类与接口](#抽象类与接口)
  * [抽象类](#抽象类)
  * [接口](#接口)
* [super](#super)
* [重写与重载](#重写与重载)
  * [重写(Override)](#重写override)
  * [重载(Overload)](#重载overload)
* [多态的实现与原理](#多态的实现与原理)


## 访问修饰符
- private
- protected
- default
- public


|访问权限|本类|本包的类|子类|非子类的外包类|
|---|---|---|---|---|
|public	    |是	|是	|是	|是|
|protected	|是	|是	|是	|否|
|default	|是	|是	|否	|否|
|private	|是	|否	|否	|否|
## 抽象类与接口
### 抽象类
- 抽象类和抽象方法都使用 abstract 关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。
- 抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。
### 接口
- 接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。
- 从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。
- 接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。
- 接口的字段默认都是 static 和 final 的。
## super
- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。
- 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。
## 重写与重载
### 重写(Override)
存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法
- 子类方法的访问权限必须大于等于父类方法;
- 子类方法的返回类型必须是父类方法返回类型或为其子类型。
- 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型
### 重载(Overload)
- 存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。
- 应该注意的是,返回值不同,其它都相同不算是重载。
## 多态的实现与原理
多态指同一个行为具有不同的表现形式。java多态主要体现在两个方面:重写和重载。 重载时,调用相同的方法名,但可以根据参数的不同,对应不同的方法行为。 重写时,子类继承父类方法,并重写,通过父类引用指向子类对象,调用相同的方法名时,也可产生不同的行为。

多态允许具体访问时实现方法的动态绑定。Java对于动态绑定的实现主要依赖于方法表,通过继承和接口的多态实现有所不同
- 继承
  - 在执行某个方法时,在方法区中找到该类的方法表,再确认该方法在方法表中的偏移量,找到该方法后如果被重写则直接调用,否则认为没有重写父类该方法,这时会按照继承关系搜索父类的方法表中该偏移量对应的方法
- 接口
  - Java 允许一个类实现多个接口,从某种意义上来说相当于多继承,这样同一个接口的的方法在不同类方法表中的位置就可能不一样了。所以不能通过偏移量的方法,而是通过搜索完整的方法表

# 参考文章
- https://www.nowcoder.com/questionTerminal/e83cdcb915f246a4866dac132128259a

================================================
FILE: Java-基础/虚拟线程.md
================================================
# 虚拟线程

## 背景
2022-09-20,JDK 19 发布了GA版本,备受瞩目的协程功能也算尘埃落地,不过,此次 GA版本并不是以协程来命名,而是使用了 Virtual

Thread(虚拟线程),并且是 preview预览版本。小编最早关注到协程功能是在 2020年,那时孵化项目叫做 Java project Loom,
使用的是 Fiber(直译为:纤维,意译为:轻量级线程,即协程),但是 GA版本为何最终被定义为 Virtual Thread(虚拟线程),原因不得而知。

- GA: General Availability,正式发布的版本,在国外都是用 GA来指代 release版本;
- JEP: JDK Enhancement Proposal, JDK增强建议,JEP是一个JDK核心技术相关的增强建议文档;

为什么需要虚拟线程
既然 Java官方推出一个和线程这么相近的概念,必定是要解决线程的某些问题,因此,我们先回顾下线程的一些特点:

Java中的线程是对操作系统线程的一个简单包装,线程的创建,调度和销毁等都是由操作系统完成;
线程切换需要消耗CPU时间,这部分时间是与业务无关的;
线程的性能直接受操作系统处理能力的影响;

因此,线程是一种重量级的资源,作为一名 Java程序员应该深有体会。所以,为了更好的管理线程,Java采用了池化(线程池)的方式进行管理线程,避免线程频繁创建和销毁带来的开销。但是,尽管线程池避免线程大部分创建和销毁的开销,但是线程的调度还是直接受操作系统的影响,那么有没有更好的方式来打破这种限制,因此,虚拟线程就孕育而生。
在 JDK 19源码中,官方直接在 java.lang包下新增一个 VirtualThread类来表示虚拟线程,为了更好的区分虚拟线程和原有的 Thread线程,官方给 Thread类赋予了一个高大上的名字:平台线程。
下面给出了 JDK 19中虚拟线程的 Diagram截图以及平台线程和系统线程的关系图:

![虚拟线程的 Diagram截图以及平台线程和系统线程的关系图.png](..%2Fimg%2FJava%E5%9F%BA%E7%A1%80%2F%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B%E7%9A%84%20Diagram%E6%88%AA%E5%9B%BE%E4%BB%A5%E5%8F%8A%E5%B9%B3%E5%8F%B0%E7%BA%BF%E7%A8%8B%E5%92%8C%E7%B3%BB%E7%BB%9F%E7%BA%BF%E7%A8%8B%E7%9A%84%E5%85%B3%E7%B3%BB%E5%9B%BE.png)

![os和系统线程.png](..%2Fimg%2FJava%E5%9F%BA%E7%A1%80%2Fos%E5%92%8C%E7%B3%BB%E7%BB%9F%E7%BA%BF%E7%A8%8B.png)

## 如何创建虚拟线程

### 1.通过 Thread.startVirtualThread()创建
如下示例代码,通过 Thread.startVirtualThread()可以创建一个新的并且已启动的虚拟线程,该方法等价于 Thread.ofVirtual().start(task):
```java
public class VirtualThreadTest {

    public static void main(String[] args) {
        CustomThread customThread = new CustomThread();
// 创建并且启动虚拟线程  
        Thread.startVirtualThread(customThread);
    }
}

class CustomThread implements Runnable {
    @Override
    public void run() {
        System.out.println("CustomThread run");
    }
}
```
### 2.通过 Thread.ofVirtual()创建
如下示例代码,通过 Thread.ofVirtual().unstarted()方式可以创建一个新的未启动的虚拟线程,然后通过 Thread.start()来启动线程,也可以通过 Thread.ofVirtual().start()直接创建一个新的并已启动的虚拟线程:

```java
public class VirtualThreadTest {

    public static void main(String[] args) {
        CustomThread customThread = new CustomThread();
// 创建并且不启动虚拟线程,然后 unStarted.start()方法启动虚拟线程  
        Thread unStarted = Thread.ofVirtual().unstarted(customThread);
        unStarted.start();

// 等同于  
        Thread.ofVirtual().start(customThread);
    }
}

class CustomThread implements Runnable {
    @Override
    public void run() {
        System.out.println("CustomThread run");
    }
}  
```
### 3.通过 ThreadFactory创建
如下示例代码,通过 ThreadFactory.newThread()方式就能创建一个虚拟线程,然后通过 Thread.start()来启动线程:
```java
public class VirtualThreadTest {

    public static void main(String[] args) {
        CustomThread customThread = new CustomThread();
// 获取线程工厂类  
        ThreadFactory factory = Thread.ofVirtual().factory();
// 创建虚拟线程  
        Thread thread = factory.newThread(customThread);
// 启动线程  
        thread.start();
    }
}

class CustomThread implements Runnable {
    @Override
    public void run() {
        System.out.println("CustomThread run");
    }
}
```

### 4.通过 Executors.newVirtualThreadPerTaskExecutor()创建
如下示例代码,通过 JDK自带的Executors工具类方式创建一个虚拟线程,然后通过 executor.submit()来启动线程:
```java
public class VirtualThreadTest {

    public static void main(String[] args) {
        CustomThread customThread = new CustomThread();
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
        executor.submit(customThread);
    }
}

class CustomThread implements Runnable {
    @Override
    public void run() {
        System.out.println("CustomThread run");
    }
}
```

通过上述列举的 4种创建虚拟线程的方式可以看出,官方为了降低虚拟线程的门槛,尽力复用原有的Thread线程类,这样可以平滑的过渡到虚拟线程的使用。不过,在
Java 19中,虚拟线程还是一个预览功能,默认关闭,需要使用参数 --enable-preview 来启用该功能,预览功能源码和启动虚拟线程指令如下:
```java
// Thread 源码,通过 @PreviewFeature 注解来标注 虚拟线程为 预览功能  
public class Thread implements Runnable {
    /**
     * Creates a virtual thread to execute a task and schedules it to execute.  
     This method is equivalent to: Thread.ofVirtual().start(task);  
     Params: task – the object to run when the thread executes  
     Returns: a new, and started, virtual thread  
     Throws: UnsupportedOperationException – if preview features are not enabled  
     Since: 19  
     See Also: Inheritance when creating threads  
     * @param task
     * @return
     */
    @PreviewFeature(feature = PreviewFeature.Feature.VIRTUAL_THREADS)
    public static Thread startVirtualThread(Runnable task) {
        Objects.requireNonNull(task);
        // 判断是否开启虚拟线程功能  
        PreviewFeatures.ensureEnabled();
        var thread = ThreadBuilders.newVirtualThread(null, null, 0, task);
        thread.start();
        return thread;
    }

    // 异常信息提醒 可以通过 --enable-preview 开启虚拟线程功能  
    public static void ensureEnabled() {
        if (!isEnabled()) {
            throw new UnsupportedOperationException(
                    "Preview Features not enabled, need to run with --enable-preview");
        }
    }
}  

```
IDEA 中配置 --enable-preview 如下图:

![启用虚拟线程idea.png](..%2Fimg%2FJava%E5%9F%BA%E7%A1%80%2F%E5%90%AF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8Bidea.png)


为了更好的感受虚拟线程的性能,我们模拟一个对比测试用例:分别使用虚拟线程和线程池执行10w个任务,每个线程任务睡眠10ms,统计各自的总耗时和创建的最大平台线程总数,示例代码如下:

```java
// 虚拟线程
public class VirtualThreadTest {
    static List<Integer> list = new ArrayList<>();

    public static void main(String[] args) {
// 开启一个线程来监控当前的平台线程(系统线程)总数
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
            ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false);
            saveMaxThreadNum(threadInfo.length);
        }, 10, 10, TimeUnit.MILLISECONDS);

        long start = System.currentTimeMillis();
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
        for (int i = 0; i < 10000; i++) {
            executor.submit(() -> {
// 线程睡眠 10ms,可以等同于模拟业务耗时10ms
                try {
                    TimeUnit.MILLISECONDS.sleep(10);
                } catch (InterruptedException e) {

                }
            });
        }
        executor.close();
        System.out.println("max:" + list.get(0) + " platform thread/os thread");
        System.out.printf("totalMillis:%dms\n", System.currentTimeMillis() - start);
    }
}

public class ThreadTest {
    static List<Integer> list = new ArrayList<>();
    public static void main(String[] args) {
// 开启一个线程来监控当前的平台线程(系统线程)总数
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
            ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false);
            saveMaxThreadNum(threadInfo.length);
        }, 1, 1, TimeUnit.SECONDS);

        long start = System.currentTimeMillis();
        ExecutorService executor = Executors.newFixedThreadPool(200);
        for (int i = 0; i < 100000; i++) {
            executor.submit(() -> {
                try {
// 线程睡眠 10ms,可以等同于模拟业务耗时10ms
                    TimeUnit.MILLISECONDS.sleep(10);
                } catch (InterruptedException e) {

                }
            });
        }
        executor.close();
        System.out.println("max:" + list.get(0) + " platform thread/os thread");
        System.out.printf("totalMillis:%dms\n", System.currentTimeMillis() - start);
    }
}

    // 保存平台线程的创建的最大总数
    public static List<Integer> saveMaxThreadNum(int num) {
        if (list.isEmpty()) {
            list.add(num);
        } else {
            Integer integer = list.get(0);
            if (num > integer) {
                list.add(0, num);
            }
        }
        return list;
    }
```
两个示例的运行结果:

![运行结果.png](..%2Fimg%2FJava%E5%9F%BA%E7%A1%80%2F%E8%BF%90%E8%A1%8C%E7%BB%93%E6%9E%9C.png)

通过运行结果可以发现:


- 使用虚拟线程执行 10w个任务总耗时为:129ms,最大创建了 18个平台线程;
- 使用线程池执行 10w个任务总耗时为:6103 ms,最大创建了 207个平台线程;
- 两者总耗时差50倍,最大创建的平台线程总数差 10倍,因此性能差可想而知;

### 核心源码解析
首先从 VirtualThread类开始,源码如下:
```java
/**
 * A thread that is scheduled by the Java virtual machine rather than the operating system.  
 */
final class VirtualThread extends BaseVirtualThread {

    /**
     * Creates a new {@code VirtualThread} to run the given task with the given  
     * scheduler. If the given scheduler is {@code null} and the current thread  
     * is a platform thread then the newly created virtual thread will use the  
     * default scheduler. If given scheduler is {@code null} and the current  
     * thread is a virtual thread then the current thread's scheduler is used.  
     *
     * @param scheduler the scheduler or null  
     * @param name thread name  
     * @param characteristics characteristics  
     * @param task the task to execute  
     */
    VirtualThread(Executor scheduler, String name, int characteristics, Runnable task) {
        super(name, characteristics, /*bound*/ false);
        Objects.requireNonNull(task);

// choose scheduler if not specified  
        if (scheduler == null) {
            Thread parent = Thread.currentThread();
            if (parent instanceof VirtualThread vparent) {
                scheduler = vparent.scheduler;
            } else {
                scheduler = DEFAULT_SCHEDULER;
            }
        }

        this.scheduler = scheduler;
        this.cont = new VThreadContinuation(this, task);
        this.runContinuation = this::runContinuation;
    }

    /**
     * 创建默认的调度器  
     * Creates the default scheduler.  
     */
    @SuppressWarnings("removal")
    private static ForkJoinPool createDefaultScheduler() {
        ForkJoinWorkerThreadFactory factory = pool -> {
            PrivilegedAction<ForkJoinWorkerThread> pa = () -> new CarrierThread(pool);
            return AccessController.doPrivileged(pa);
        };
        PrivilegedAction<ForkJoinPool> pa = () -> {
            int parallelism, maxPoolSize, minRunnable;
            String parallelismValue = System.getProperty("jdk.virtualThreadScheduler.parallelism");
            String maxPoolSizeValue = System.getProperty("jdk.virtualThreadScheduler.maxPoolSize");
            String minRunnableValue = System.getProperty("jdk.virtualThreadScheduler.minRunnable");
            if (parallelismValue != null) {
                parallelism = Integer.parseInt(parallelismValue);
            } else {
                parallelism = Runtime.getRuntime().availableProcessors();
            }
            if (maxPoolSizeValue != null) {
                maxPoolSize = Integer.parseInt(maxPoolSizeValue);
                parallelism = Integer.min(parallelism, maxPoolSize);
            } else {
                maxPoolSize = Integer.max(parallelism, 256);
            }
            if (minRunnableValue != null) {
                minRunnable = Integer.parseInt(minRunnableValue);
            } else {
                minRunnable = Integer.max(parallelism / 2, 1);
            }
            Thread.UncaughtExceptionHandler handler = (t, e) -> { };
            boolean asyncMode = true; // FIFO  
            return new ForkJoinPool(parallelism, factory, handler, asyncMode,
                    0, maxPoolSize, minRunnable, pool -> true, 30, SECONDS);
        };
        return AccessController.doPrivileged(pa);
    }
}  

```
通过 VirtualThread类的源码可以总结出:

- VirtualThread继承 BaseVirtualThread类,BaseVirtualThread类继承 Thread类;
- 虚拟线程是 JVM进行调度的,而不是操作系统;
- VirtualThread类是一个终态类,因此该类无法被继承,无法被扩展;

VirtualThread类,只提供了一个构造器,接收 4个参数:

- Executor scheduler:如果给定的调度器为空并且当前线程是平台线程,那么新创建的虚拟线程将使用默认调度程序(底层采用 ForkJoinPool),如果给定的调度器为空并且当前线程是虚拟线程,则使用当前线程的调度程序
- String name:自定义线程名
- int characteristics:线程特征值
- Runnable task:需要执行的任务

然后我们看下 JDK中创建虚拟线程的源码:
```java
public class Thread implements Runnable {
    /**
     * Creates a virtual thread to execute a task and schedules it to execute.  
     This method is equivalent to: Thread.ofVirtual().start(task);  
     Params: task – the object to run when the thread executes  
     Returns: a new, and started, virtual thread  
     Throws: UnsupportedOperationException – if preview features are not enabled  
     Since: 19  
     See Also: Inheritance when creating threads  
     * @param task
     * @return
     */
    @PreviewFeature(feature = PreviewFeature.Feature.VIRTUAL_THREADS)
    public static Thread startVirtualThread(Runnable task) {
        Objects.requireNonNull(task);
// 判断是否开启虚拟线程功能  
        PreviewFeatures.ensureEnabled();
        var thread = ThreadBuilders.newVirtualThread(null, null, 0, task);
        thread.start();
        return thread;
    }

    // 异常信息提醒 可以通过 --enable-preview 开启虚拟线程功能  
    public static void ensureEnabled() {
        if (!isEnabled()) {
            throw new UnsupportedOperationException(
                    "Preview Features not enabled, need to run with --enable-preview");
        }
    }
}

class ThreadBuilders {
    static Thread newVirtualThread(Executor scheduler,
                                   String name,
                                   int characteristics,
                                   Runnable task) {
        if (ContinuationSupport.isSupported()) {
            return new VirtualThread(scheduler, name, characteristics, task);
        } else {
            if (scheduler != null)
                throw new UnsupportedOperationException();
            return new BoundVirtualThread(name, characteristics, task);
        }
    }

    /**
     * Returns a builder for creating a virtual {@code Thread} or {@code ThreadFactory}
     * that creates virtual threads.  
     *
     * @apiNote The following are examples using the builder:  
     * {@snippet :
     * // Start a virtual thread to run a task.  
     * Thread thread = Thread.ofVirtual().start(runnable);  
     *
     * // A ThreadFactory that creates virtual threads  
     * ThreadFactory factory = Thread.ofVirtual().factory();  
     * }
     *
     * @return A builder for creating {@code Thread} or {@code ThreadFactory} objects.  
     * @throws UnsupportedOperationException if preview features are not enabled  
     * @since 19
     */
    @PreviewFeature(feature = PreviewFeature.Feature.VIRTUAL_THREADS)
    public static Builder.OfVirtual ofVirtual() {
        PreviewFeatures.ensureEnabled();
        return new ThreadBuilders.VirtualThreadBuilder();
    }
}  

```
Thread.startVirtualThread()创建虚拟线程,会调用ThreadBuilders.newVirtualThread(),最终调用 new VirtualThread()构造器来创建虚拟线程。

从上文我们在介绍虚拟线程创建的 4种方式也可以看出,虚拟线程创建的入口在 Thread 或者 Executors 类中,和以前使用线程或者线程池的习惯保持一致。

```java
final class VirtualThread extends BaseVirtualThread {
    /**
     * Mounts this virtual thread onto the current platform thread. On  
     * return, the current thread is the virtual thread.  
     */
    @ChangesCurrentThread
    private void mount() {
// sets the carrier thread  
        Thread carrier = Thread.currentCarrierThread();
        setCarrierThread(carrier);

// sync up carrier thread interrupt status if needed  
        if (interrupted) {
            carrier.setInterrupt();
        } else if (carrier.isInterrupted()) {
            synchronized (interruptLock) {
// need to recheck interrupt status  
                if (!interrupted) {
                    carrier.clearInterrupt();
                }
            }
        }

// set Thread.currentThread() to return this virtual thread  
        carrier.setCurrentThread(this);
    }


    /**
     * Unmounts this virtual thread from the carrier. On return, the  
     * current thread is the current platform thread.  
     */
    @ChangesCurrentThread
    private void unmount() {
// set Thread.currentThread() to return the platform thread  
        Thread carrier = this.carrierThread;
        carrier.setCurrentThread(carrier);

// break connection to carrier thread, synchronized with interrupt  
        synchronized (interruptLock) {
            setCarrierThread(null);
        }
        carrier.clearInterrupt();
    }
}  

```
mount() 和 unmount() 是虚拟线程两个核心方法:

- mount(),可以将此虚拟线程挂载到当前平台线程上,返回时,当前线程是虚拟线程;
- unmount(),从载体线程卸载此虚拟线程,返回时,当前线程是平台线程

通过这两个方式可以看出虚拟线程是搭载在平台线程上运行,运行结束后,从平台线程上卸载。

### 3种线程的关系
VirtualThread,Platform Thread,OS Thread 三者的关系如下图:

![三线程关系.png](..%2Fimg%2FJava%E5%9F%BA%E7%A1%80%2F%E4%B8%89%E7%BA%BF%E7%A8%8B%E5%85%B3%E7%B3%BB.png)

说明:
- 在现有的线程模型下,一个 Java线程相当于一个操作系统线程,多个虚拟线程需要挂载在一个平台线程(载体线程)上,每个平台线程和系统线程一一对应。因此,VirtualThread是属于 JVM级别的线程,由JVM调度,它是非常轻量级的资源,使用完后立即被销毁,因此就不需要像平台线程一样使用池化(线程池)。
- 虚拟线程在执行到 IO 操作或 Blocking操作时,会自动切换到其他虚拟线程执行,从而避免当前线程等待,可以高效通过少数线程去调度大量虚拟线程,最大化提升线程的执行效率。

## 总结

- Virtual Thread将会在性能上带来的巨大提高,不过,目前业界80~90%的代码还跑在 Java 8上,等 JDK
- 19投入实际生产环境,可能需要一个漫长的过程;
- 虚拟线程高度复用了现有的 Thread线程的功能,方便现有方式平滑迁移到虚拟线程;
- 虚拟线程是将 Thread作为载体线程,它并没有改变原来的线程模型;
- 虚拟线程是 JVM调度的,而不是操作系统调度;
- 使用虚拟线程可以显著提高程序吞吐量;
- 虚拟线程适合 并发任务数量很高 或者 IO密集型的场景,对于 计算密集型任务还需通过过增加CPU核心解决,或者利用分布式计算资源来来解决;
- 虚拟线程目前只是一个预览功能,只能从源码和简单的测试来分析,并无真实生产环境的验证;

曾一段时间内,JDK一直致力于 Reactor响应式编程,试图从这条路子来提升 Java的性能,但是最终发现:响应式编程难理解,难调试,难使用,

因此又把焦点转向了同步编程,为了改善性能,虚拟线程诞生了。或许虚拟线程很难在短时间内运用到实际生产中,但是通过官方的JDK版本发布,我们可以看到:尽管是 Oracle这样的科技型巨头也会走弯路,了解 JDK的动态,可以帮助我们更好的把握学习 Java的重心以及后面的发展趋势。


# 参考文章
- 文章来源 https://juejin.cn/post/7258180488358084669



================================================
FILE: Java-基础/虚拟线程2.md
================================================
# jdk21虚拟线程
jdk21已经正式发布了虚拟线程

虚拟线程和系统线程的绑定关系
![](../img/Java基础/虚拟线程和系统线程.png)

## 虚拟线程实现原理
虚拟线程是一种轻量级(用户模式)线程,这种线程是由Java虚拟机调度,而不是操作系统。虚拟线程占用空间小,任务切换开销几乎可以忽略不计,因此可以极大量地创建和使用。总体来看,虚拟线程实现如下:

```java
virtual thread = continuation + scheduler
```

虚拟线程会把任务(一般是java.lang.Runnable)包装到一个Continuation实例中:

- 当任务需要阻塞挂起的时候,会调用Continuation的yield操作进行阻塞
- 当任务需要解除阻塞继续执行的时候,Continuation会被继续执行

Scheduler也就是执行器,会把任务提交到一个载体线程池中执行:

- 执行器是java.util.concurrent.Executor的子类
- 虚拟线程框架提供了一个默认的ForkJoinPool用于执行虚拟线程任务

下文会把carrier thread称为"载体线程",指的是负责执行虚拟线程中任务的平台线程,或者说运行虚拟线程的平台线程称为它的载体线程

操作系统调度系统线程,而Java平台线程与系统线程一一映射,所以平台线程被操作系统调度,但是虚拟线程是由JVM调度。JVM把虚拟线程分配给平台线程的操作称为mount(挂载),反过来取消分配平台线程的操作称为unmount(卸载):

- mount操作:虚拟线程挂载到平台线程,虚拟线程中包装的Continuation栈数据帧或者引用栈数据会被拷贝到平台线程的线程栈,这是一个从堆复制到栈的过程
- unmount操作:虚拟线程从平台线程卸载,大多数虚拟线程中包装的Continuation栈数据帧会留在堆内存中

这个mount -> run -> unmount过程用伪代码表示如下:

```java
mount();
try {
    Continuation.run();
} finally {
    unmount();
}
```

从Java代码的角度来看,虚拟线程和它的载体线程暂时共享一个OS线程实例这个事实是不可见,因为虚拟线程的堆栈跟踪和线程本地变量与平台线程是完全隔离的。JDK中专门是用了一个FIFO模式的ForkJoinPool作为虚拟线程的调度程序,从这个调度程序看虚拟线程任务的执行流程大致如下:

- 调度器(线程池)中的平台线程等待处理任务

![](../img/Java基础/调度器中的平台线程等待处理任务.png))

- 一个虚拟线程被分配平台线程,该平台线程作为运载线程执行虚拟线程中的任务

![](../img/Java基础/虚拟线程2.png))

- 虚拟线程运行其Continuation,从而执行基于Runnable包装的用户任务

![](../img/Java基础/虚拟线程3.png))

- 虚拟线程任务执行完成,标记Continuation终结,标记虚拟线程为终结状态,清空一些上下文变量,运载线程"返还"到调度器(线程池)中作为平台线程等待处理下一个任务

![](../img/Java基础/虚拟线程4.png)

上面是描述一般的虚拟线程任务执行情况,在执行任务时候首次调用Continuation#run()获取锁(ReentrantLock)的时候会触发Continuation的yield操作让出控制权,等待虚拟线程重新分配运载线程并且执行,见下面的代码:

```java
public class VirtualThreadLock {

    public static void main(String[] args) throws Exception {
        ReentrantLock lock = new ReentrantLock();
        Thread.startVirtualThread(() -> {
            lock.lock();     // <------ 这里确保锁已经被另一个虚拟线程持有
        });
        Thread.sleep(1000);
        Thread.startVirtualThread(() -> {
            System.out.println("first");
            lock.lock();
            try {
                System.out.println("second");
            } finally {
                lock.unlock();
            }
            System.out.println("third");
        });
        Thread.sleep(Long.MAX_VALUE);
    }
}
```

- 虚拟线程中任务执行时候首次调用Continuation#run()执行了部分任务代码,然后尝试获取锁,会导致Continuation的yield操作让出控制权(任务切换),也就是unmount,运载线程栈数据会移动到Continuation栈的数据帧中,保存在堆内存,虚拟线程任务完成(但是虚拟线程没有终结,同时其Continuation也没有终结和释放),运载线程被释放到执行器中等待新的任务;如果Continuation的yield操作失败,则会对运载线程进行park调用,阻塞在运载线程上

![](../img/Java基础/虚拟线程5.png)

- 当锁持有者释放锁之后,会唤醒虚拟线程获取锁(成功后),虚拟线程会重新进行mount,让虚拟线程任务再次执行,有可能是分配到另一个运载线程中执行,Continuation栈会的数据帧会被恢复到运载线程栈中,然后再次调用Continuation#run()恢复任务执行:

![](../img/Java基础/虚拟线程6.png)

- 最终虚拟线程任务执行完成,标记Continuation终结,标记虚拟线程为终结状态,清空一些上下文变量,运载线程"返还"到调度器(线程池)中作为平台线程等待处理下一个任务

Continuation组件十分重要,它既是用户真实任务的包装器,也是任务切换虚拟线程与平台线程之间数据转移的一个句柄,它提供的yield操作可以实现任务上下文的中断和恢复。由于Continuation被封闭在java.base/jdk.internal.vm下,可以通过增加编译参数--add-exports java.base/jdk.internal.vm=ALL-UNNAMED暴露对应的功能,从而编写实验性案例,IDEA中可以按下图进行编译参数添加:

![](../img/Java基础/虚拟线程7.png)

然后编写和运行下面的例子:

```java
import jdk.internal.vm.Continuation;
import jdk.internal.vm.ContinuationScope;

public class ContinuationDemo {

    public static void main(String[] args) {
        ContinuationScope scope = new ContinuationScope("scope");
        Continuation continuation = new Continuation(scope, () -> {
            System.out.println("Running before yield");
            Continuation.yield(scope);
            System.out.println("Running after yield");
        });
        System.out.println("First run");
        // 第一次执行Continuation.run
        continuation.run();
        System.out.println("Second run");
        // 第二次执行Continuation.run
        continuation.run();
        System.out.println("Done");
    }
}

// 运行代码,神奇的结果出现了
First run
Running before yield
Second run
Running after yield
Done

```
这里可以看出Continuation的奇妙之处,Continuation实例进行yield调用后,再次调用其run方法就可以从yield的调用之处往下执行,从而实现了程序的中断和恢复。

## 源码分析
 todo

# 文章参考
- https://www.cnblogs.com/throwable/p/16758997.html

================================================
FILE: Java-多线程/AQS.md
================================================
# AQS(AbstractQueuedSynchronizer)
## 工作原理概要
AbstractQueuedSynchronizer又称为队列同步器(后面简称AQS),它是用来构建锁或其他同步组件的基础框架,内部通过一个int类型的成员变量state来控制同步状态

当state=0时,则说明没有任何线程占有共享资源的锁,当state=1时,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待

AQS内部通过内部类Node构成FIFO的同步队列来完成线程获取锁的排队工作,同时利用内部类ConditionObject构建等待队列

当Condition调用wait()方法后,线程将会加入等待队列中

而当Condition调用signal()方法后,线程将从等待队列转移动同步队列中进行锁竞争。

注意这里涉及到两种队列,一种的同步队列,当线程请求锁而等待的后将加入同步队列等待,而另一种则是等待队列(可有多个),通过Condition调用await()方法释放锁后,将加入等待队列
## 同步队列模型
### 属性
- `private transient volatile Node head;` //指向同步队列队头
- `private transient volatile Node tail;` //指向同步的队尾
- `private volatile int state;` //同步状态,0代表锁未被占用,1代表锁已被占用
### 示意图
![](../img/Java多线程/同步队列模型示意图.png)
- head指向同步队列的头部,注意head为空结点,不存储信息
- tail则是同步队列的队尾
- 同步队列采用的是双向链表的结构这样可方便队列进行结点增删操作
- state变量则是代表同步状态,执行当线程调用lock方法进行加锁后,如果此时state的值为0,则说明当前线程可以获取到锁(在本篇文章中,锁和同步状态代表同一个意思),同时将state设置为1,表示获取成功。如果state已为1,也就是当前锁已被其他线程持有,那么当前执行线程将被封装为Node结点加入同步队列等待
### Node
属性
```java
static final class Node {
    //共享模式
    static final Node SHARED = new Node();
    //独占模式
    static final Node EXCLUSIVE = null;

    //标识线程已处于结束状态
    static final int CANCELLED =  1;
    //等待被唤醒状态
    static final int SIGNAL    = -1;
    //条件状态,
    static final int CONDITION = -2;
    //在共享模式中使用表示获得的同步状态会被传播
    static final int PROPAGATE = -3;

    //等待状态,存在CANCELLED、SIGNAL、CONDITION、PROPAGATE 4种
    volatile int waitStatus;

    //同步队列中前驱结点
    volatile Node prev;

    //同步队列中后继结点
    volatile Node next;

    //请求锁的线程
    volatile Thread thread;

    //等待队列中的后继结点,这个与Condition有关,稍后会分析
    Node nextWaiter;

    //判断是否为共享模式
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    //获取前驱结点
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    //.....
}
```
- `SHARED`和`EXCLUSIVE`常量分别代表共享模式和独占模式
  - 共享模式是一个锁允许多条线程同时操作,如信号量Semaphore采用的就是基于AQS的共享模式实现的
  - 独占模式则是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待,如ReentranLock
- `waitStatus`  则表示当前被封装成Node结点的等待状态
  - `CANCELLED`
  值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化
  - `SIGNAL`
  值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行
  - `CONDITION`
  值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁
  - `PROPAGATE`
  值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态
  - `0状态`
  值为0,代表初始化状态
- `pre` 指向当前Node结点的前驱结点
- `next` 指向当前Node结点的后继结点
- `thread` 存储的请求锁的线程
- `nextWaiter` 与Condition相关,代表等待队列中的后继结点

# ReentrantLock
ReentrantLock内部存在3个实现类,分别是Sync、NonfairSync、FairSync
## Sync (extends AbstractQueuedSynchronizer)
Sync继承自AQS实现了解锁tryRelease()方法
## NonfairSync (extends Sync) 非公平锁
### lock
```java
/**
 * 非公平锁实现
 */
static final class NonfairSync extends Sync {
    //加锁
    final void lock() {
        //执行CAS操作,获取同步状态
        if (compareAndSetState(0, 1))
       //成功则将独占锁线程设置为当前线程  
          setExclusiveOwnerThread(Thread.currentThread());
        else
            //否则再次请求同步状态
            acquire(1);
    }
}
```
这里获取锁时,首先对同步状态执行CAS操作,尝试把state的状态从0设置为1,如果返回true则代表获取同步状态成功,也就是当前线程获取锁成,可操作临界资源,如果返回false,则表示已有线程持有该同步状态(其值为1),获取锁失败,注意这里存在并发的情景,也就是可能同时存在多个线程设置state变量,因此是CAS操作保证了state变量操作的原子性。返回false后,执行 acquire(1)方法,该方法是AQS中的方法,它对中断不敏感,即使线程获取同步状态失败,进入同步队列,后续对该线程执行中断操作也不会从同步队列中移出,方法如下

```java
public final void acquire(int arg) {
    //再次尝试获取同步状态
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
```
这里传入参数arg表示要获取同步状态后设置的值(即要设置state的值),因为要获取锁,而status为0时是释放锁,1则是获取锁,所以这里一般传递参数为1,进入方法后首先会执行tryAcquire(arg)方法,在前面分析过该方法在AQS中并没有具体实现,而是交由子类实现,因此该方法是由ReetrantLock类内部实现的

```java
//NonfairSync类
static final class NonfairSync extends Sync {

    protected final boolean tryAcquire(int acquires) {
         return nonfairTryAcquire(acquires);
     }
 }

//Sync类
abstract static class Sync extends AbstractQueuedSynchronizer {

  //nonfairTryAcquire方法
  final boolean nonfairTryAcquire(int acquires) {
      final Thread current = Thread.currentThread();
      int c = getState();
      //判断同步状态是否为0,并尝试再次获取同步状态
      if (c == 0) {
          //执行CAS操作
          if (compareAndSetState(0, acquires)) {
              setExclusiveOwnerThread(current);
              return true;
          }
      }
      //如果当前线程已获取锁,属于重入锁,再次获取锁后将status值加1
      else if (current == getExclusiveOwnerThread()) {
          int nextc = c + acquires;
          if (nextc < 0) // overflow
              throw new Error("Maximum lock count exceeded");
          //设置当前同步状态,当前只有一个线程持有锁,因为不会发生线程安全问题,可以直接执行 setState(nextc);
          setState(nextc);
          return true;
      }
      return false;
  }
  //省略其他代码
}
```
从代码执行流程可以看出,这里做了两件事

1. 一是尝试再次获取同步状态,如果获取成功则将当前线程设置为OwnerThread,否则失败,
2. 二是判断当前线程current是否为OwnerThread,如果是则属于重入锁,state自增1,并获取锁成功,返回true,反之失败,返回false,也就是tryAcquire(arg)执行失败,返回false。

需要注意的是nonfairTryAcquire(int acquires)内部使用的是CAS原子性操作设置state值,可以保证state的更改是线程安全的,因此只要任意一个线程调用nonfairTryAcquire(int acquires)方法并设置成功即可获取锁,不管该线程是新到来的还是已在同步队列的线程,毕竟这是非公平锁,并不保证同步队列中的线程一定比新到来线程请求(可能是head结点刚释放同步状态然后新到来的线程恰好获取到同步状态)先获取到锁,这点跟后面还会讲到的公平锁不同。ok~,接着看之前的方法acquire(int arg)

```java
public final void acquire(int arg) {
    //再次尝试获取同步状态
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
```

如果tryAcquire(arg)返回true,acquireQueued自然不会执行,这是最理想的,因为毕竟当前线程已获取到锁,如果tryAcquire(arg)返回false,则会执行addWaiter(Node.EXCLUSIVE)进行入队操作,由于ReentrantLock属于独占锁,因此结点类型为Node.EXCLUSIVE,下面看看addWaiter方法具体实现

```java
private Node addWaiter(Node mode) {
    //将请求同步状态失败的线程封装成结点
    Node node = new Node(Thread.currentThread(), mode);

    Node pred = tail;
    //如果是第一个结点加入肯定为空,跳过。
    //如果非第一个结点则直接执行CAS入队操作,尝试在尾部快速添加
    if (pred != null) {
        node.prev = pred;
        //使用CAS执行尾部结点替换,尝试在尾部快速添加
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果第一次加入或者CAS操作没有成功执行enq入队操作
    enq(node);
    return node;
}
```

创建了一个Node.EXCLUSIVE类型Node结点用于封装线程及其相关信息,其中tail是AQS的成员变量,指向队尾(这点前面的我们分析过AQS维持的是一个双向的链表结构同步队列),如果是第一个结点,则为tail肯定为空,那么将执行enq(node)操作,如果非第一个结点即tail指向不为null,直接尝试执行CAS操作加入队尾,如果CAS操作失败还是会执行enq(node),继续看enq(node):

```java
private Node enq(final Node node) {
    //死循环
    for (;;) {
         Node t = tail;
         //如果队列为null,即没有头结点
         if (t == null) { // Must initialize
             //创建并使用CAS设置头结点
             if (compareAndSetHead(new Node()))
                 tail = head;
         } else {//队尾添加新结点
             node.prev = t;
             if (compareAndSetTail(t, node)) {
                 t.next = node;
                 return t;
             }
         }
     }
    }
```
这个方法使用一个死循环进行CAS操作,可以解决多线程并发问题。这里做了两件事,一是如果还没有初始同步队列则创建新结点并使用compareAndSetHead设置头结点,tail也指向head,二是队列已存在,则将新结点node添加到队尾。注意这两个步骤都存在同一时间多个线程操作的可能,如果有一个线程修改head和tail成功,那么其他线程将继续循环,直到修改成功,这里使用CAS原子操作进行头结点设置和尾结点tail替换可以保证线程安全,从这里也可以看出head结点本身不存在任何数据,它只是作为一个牵头结点,而tail永远指向尾部结点(前提是队列不为null)。
![](../img/Java多线程/node节点数据结构.png)

添加到同步队列后,结点就会进入一个自旋过程,即每个结点都在观察时机待条件满足获取同步状态,然后从同步队列退出并结束自旋,回到之前的acquire()方法,自旋过程是在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法中执行的,代码如下

```java
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        //自旋,死循环
        for (;;) {
            //获取前驱结点
            final Node p = node.predecessor();
            当且仅当p为头结点才尝试获取同步状态
            if (p == head && tryAcquire(arg)) {
                //将node设置为头结点
                setHead(node);
                //清空原来头结点的引用便于GC
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //如果前驱结点不是head,判断是否挂起线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            //最终都没能获取同步状态,结束该线程的请求
            cancelAcquire(node);
    }
}

```
当前线程在自旋(死循环)中获取同步状态,当且仅当前驱结点为头结点才尝试获取同步状态,这符合FIFO的规则,即先进先出,其次head是当前获取同步状态的线程结点,只有当head释放同步状态唤醒后继结点,后继结点才有可能获取到同步状态,因此后继结点在其前继结点为head时,才进行尝试获取同步状态,其他时刻将被挂起。进入if语句后调用setHead(node)方法,将当前线程结点设置为head

```java
//设置为头结点
private void setHead(Node node) {
        head = node;
        //清空结点数据
        node.thread = null;
        node.prev = null;
}
```
设置为node结点被设置为head后,其thread信息和前驱结点将被清空,因为该线程已获取到同步状态(锁),正在执行了,也就没有必要存储相关信息了,head只有保存指向后继结点的指针即可,便于head结点释放同步状态后唤醒后继结点,执行结果如下图
![](../img/Java多线程/node节点数据结构2.png)

从图可知更新head结点的指向,将后继结点的线程唤醒并获取同步状态,调用setHead(node)将其替换为head结点,清除相关无用数据。当然如果前驱结点不是head,那么执行如下

```java
//如果前驱结点不是head,判断是否挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())

      interrupted = true;
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //获取当前结点的等待状态
        int ws = pred.waitStatus;
        //如果为等待唤醒(SIGNAL)状态则返回true
        if (ws == Node.SIGNAL)
            return true;
        //如果ws>0 则说明是结束状态,
        //遍历前驱结点直到找到没有结束状态的结点
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //如果ws小于0又不是SIGNAL状态,
            //则将其设置为SIGNAL状态,代表该结点的线程正在等待唤醒。
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

private final boolean parkAndCheckInterrupt() {
        //将当前线程挂起
        LockSupport.park(this);
        //获取线程中断状态,interrupted()是判断当前中断状态,
        //并非中断线程,因此可能true也可能false,并返回
        return Thread.interrupted();
}
```
shouldParkAfterFailedAcquire()方法的作用是判断当前结点的前驱结点是否为SIGNAL状态(即等待唤醒状态),如果是则返回true。如果结点的ws为CANCELLED状态(值为1>0),即结束状态,则说明该前驱结点已没有用应该从同步队列移除,执行while循环,直到寻找到非CANCELLED状态的结点。倘若前驱结点的ws值不为CANCELLED,也不为SIGNAL(当从Condition的条件等待队列转移到同步队列时,结点状态为CONDITION因此需要转换为SIGNAL),那么将其转换为SIGNAL状态,等待被唤醒。

若shouldParkAfterFailedAcquire()方法返回true,即前驱结点为SIGNAL状态同时又不是head结点,那么使用parkAndCheckInterrupt()方法挂起当前线程,称为WAITING状态,需要等待一个unpark()操作来唤醒它,到此ReetrantLock内部间接通过AQS的FIFO的同步队列就完成了lock()操作,这里我们总结成逻辑流程图
![](../img/Java多线程/aqs-lock流程.png)

关于获取锁的操作,这里看看另外一种可中断的获取方式,即调用ReentrantLock类的lockInterruptibly()或者tryLock()方法,最终它们都间接调用到doAcquireInterruptibly()
```java
 private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //直接抛异常,中断线程的同步状态请求
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
```
最大的不同是
```java
if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
     //直接抛异常,中断线程的同步状态请求
       throw new InterruptedException();
```
检测到线程的中断操作后,直接抛出异常,从而中断线程的同步状态请求,移除同步队列,ok~,加锁流程到此
### unlock
```java
//ReentrantLock类的unlock
public void unlock() {
    sync.release(1);
}

//AQS类的release()方法
public final boolean release(int arg) {
    //尝试释放锁
    if (tryRelease(arg)) {

        Node h = head;
        if (h != null && h.waitStatus != 0)
            //唤醒后继结点的线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

//ReentrantLock类中的内部类Sync实现的tryRelease(int releases) 
protected final boolean tryRelease(int releases) {

      int c = getState() - releases;
      if (Thread.currentThread() != getExclusiveOwnerThread())
          throw new IllegalMonitorStateException();
      boolean free = false;
      //判断状态是否为0,如果是则说明已释放同步状态
      if (c == 0) {
          free = true;
          //设置Owner为null
          setExclusiveOwnerThread(null);
      }
      //设置更新同步状态
      setState(c);
      return free;
  }
```
释放同步状态的操作相对简单些,tryRelease(int releases)方法是ReentrantLock类中内部类自己实现的,因为AQS对于释放锁并没有提供具体实现,必须由子类自己实现。释放同步状态后会使用unparkSuccessor(h)唤醒后继结点的线程,这里看看unparkSuccessor(h)

```java
private void unparkSuccessor(Node node) {
    //这里,node一般为当前线程所在的结点。
    int ws = node.waitStatus;
    if (ws < 0)//置零当前线程所在的结点状态,允许失败。
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;//找到下一个需要唤醒的结点s
    if (s == null || s.waitStatus > 0) {//如果为空或已取消
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);//唤醒
}
```
从代码执行操作来看,这里主要作用是用unpark()唤醒同步队列中最前边未放弃线程(也就是状态为CANCELLED的线程结点s)。此时,回忆前面分析进入自旋的函数acquireQueued(),s结点的线程被唤醒后,会进入acquireQueued()函数的if (p == head && tryAcquire(arg))的判断,如果p!=head也不会有影响,因为它会执行shouldParkAfterFailedAcquire(),由于s通过unparkSuccessor()操作后已是同步队列中最前边未放弃的线程结点,那么通过shouldParkAfterFailedAcquire()内部对结点状态的调整,s也必然会成为head的next结点,因此再次自旋时p==head就成立了,然后s把自己设置成head结点,表示自己已经获取到资源了,最终acquire()也返回了,这就是独占锁释放的过程。
ok~,关于独占模式的加锁和释放锁的过程到这就分析完,总之呢,在AQS同步器中维护着一个同步队列,当线程获取同步状态失败后,将会被封装成Node结点,加入到同步队列中并进行自旋操作,当当前线程结点的前驱结点为head时,将尝试获取同步状态,获取成功将自己设置为head结点。在释放同步状态时,则通过调用子类(ReetrantLock中的Sync内部类)的tryRelease(int releases)方法释放同步状态,释放成功则唤醒后继结点的线程。

## FairSync (extends Sync) 公平锁

### lock
与非公平锁不同的是,在获取锁的时,公平锁的获取顺序是完全遵循时间上的FIFO规则,也就是说先请求的线程一定会先获取锁,后来的线程肯定需要排队,这点与前面我们分析非公平锁的nonfairTryAcquire(int acquires)方法实现有锁不同,下面是公平锁中tryAcquire()方法的实现
```java
//公平锁FairSync类中的实现
protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
            //注意!!这里先判断同步队列是否存在结点
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
```
该方法与nonfairTryAcquire(int acquires)方法唯一的不同是在使用CAS设置尝试设置state值前,调用了hasQueuedPredecessors()判断同步队列是否存在结点,如果存在必须先执行完同步队列中结点的线程,当前线程进入等待状态。这就是非公平锁与公平锁最大的区别,即公平锁在线程请求到来时先会判断同步队列是否存在结点,如果存在先执行同步队列中的结点线程,当前线程将封装成node加入同步队列等待。而非公平锁呢,当线程请求到来时,不管同步队列是否存在线程结点,直接尝试获取同步状态,获取成功直接访问共享资源,但请注意在绝大多数情况下,非公平锁才是我们理想的选择,毕竟从效率上来说非公平锁总是胜于公平锁。

## Condition
在并发编程中,每个Java对象都存在一组监视器方法,如wait()、notify()以及notifyAll()方法,通过这些方法,我们可以实现线程间通信与协作(也称为等待唤醒机制),如生产者-消费者模式,而且这些方法必须配合着synchronized关键字使用,关于这点,如果想有更深入的理解,可观看博主另外一篇博文【 深入理解Java并发之synchronized实现原理】,与synchronized的等待唤醒机制相比Condition具有更多的灵活性以及精确性,这是因为notify()在唤醒线程时是随机(同一个锁),而Condition则可通过多个Condition实例对象建立更加精细的线程控制,也就带来了更多灵活性了,我们可以简单理解为以下两点
- 通过Condition能够精细的控制多线程的休眠与唤醒。
- 对于一个锁,我们可以为多个线程间建立不同的Condition。

### 主要方法
```java
public interface Condition {

 /**
  * 使当前线程进入等待状态直到被通知(signal)或中断
  * 当其他线程调用singal()或singalAll()方法时,该线程将被唤醒
  * 当其他线程调用interrupt()方法中断当前线程
  * await()相当于synchronized等待唤醒机制中的wait()方法
  */
 void await() throws InterruptedException;

 //当前线程进入等待状态,直到被唤醒,该方法不响应中断要求
 void awaitUninterruptibly();

 //调用该方法,当前线程进入等待状态,直到被唤醒或被中断或超时
 //其中nanosTimeout指的等待超时时间,单位纳秒
 long awaitNanos(long nanosTimeout) throws InterruptedException;

  //同awaitNanos,但可以指明时间单位
  boolean await(long time, TimeUnit unit) throws InterruptedException;

 //调用该方法当前线程进入等待状态,直到被唤醒、中断或到达某个时
 //间期限(deadline),如果没到指定时间就被唤醒,返回true,其他情况返回false
  boolean awaitUntil(Date deadline) throws InterruptedException;

 //唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须
 //获取与Condition相关联的锁,功能与notify()相同
  void signal();

 //唤醒所有等待在Condition上的线程,该线程从等待方法返回前必须
 //获取与Condition相关联的锁,功能与notifyAll()相同
  void signalAll();
}
```
### Condition的使用案例-生产者消费者模式
这里我们通过一个卖烤鸭的案例来演示多生产多消费者的案例,该场景中存在两条生产线程t1和t2,用于生产烤鸭,也存在两条消费线程t3,t4用于消费烤鸭,4条线程同时执行,需要保证只有在生产线程产生烤鸭后,消费线程才能消费,否则只能等待,直到生产线程产生烤鸭后唤醒消费线程,注意烤鸭不能重复消费。ResourceByCondition类中定义product()和consume()两个方法,分别用于生产烤鸭和消费烤鸭,并且定义ReentrantLock锁,用于控制product()和consume()的并发,由于必须在烤鸭生成完成后消费线程才能消费烤鸭,否则只能等待,因此这里定义两组Condition对象,分别是producer_con和consumer_con,前者拥有控制生产线程,后者拥有控制消费线程,这里我们使用一个标志flag来控制是否有烤鸭,当flag为true时,代表烤鸭生成完毕,生产线程必须进入等待状态同时唤醒消费线程进行消费,消费线程消费完毕后将flag设置为false,代表烤鸭消费完成,进入等待状态,同时唤醒生产线程生产烤鸭,具体代码如下
```java
package com.zejian.concurrencys;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by zejian on 2017/7/22.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class ResourceByCondition {
    private String name;
    private int count = 1;
    private boolean flag = false;

    //创建一个锁对象。
    Lock lock = new ReentrantLock();

    //通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者。
    Condition producer_con = lock.newCondition();
    Condition consumer_con = lock.newCondition();

    /**
     * 生产
     * @param name
     */
    public  void product(String name)
    {
        lock.lock();
        try
        {
            while(flag){
                try{
                    producer_con.await();
                }catch(InterruptedException e){
                    
                }
            }
            this.name = name + count;
            count++;
            System.out.println(Thread.currentThread().getName()+"...生产者5.0..."+this.name);
            flag = true;
            consumer_con.signal();//直接唤醒消费线程
        }
        finally
        {
            lock.unlock();
        }
    }

    /**
     * 消费
     */
    public  void consume()
    {
        lock.lock();
        try
        {
            while(!flag){
                try{
                    consumer_con.await();
                }catch(InterruptedException e)
                {}
            }
            System.out.println(Thread.currentThread().getName()+"...消费者.5.0......."+this.name);//消费烤鸭1
            flag = false;
            producer_con.signal();//直接唤醒生产线程
        }
        finally
        {
            lock.unlock();
        }
    }
}

```
执行代码
```java
package com.zejian.concurrencys;
/**
 * Created by zejian on 2017/7/22.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class Mutil_Producer_ConsumerByCondition {

    public static void main(String[] args) {
        ResourceByCondition r = new ResourceByCondition();
        Mutil_Producer pro = new Mutil_Producer(r);
        Mutil_Consumer con = new Mutil_Consumer(r);
        //生产者线程
        Thread t0 = new Thread(pro);
        Thread t1 = new Thread(pro);
        //消费者线程
        Thread t2 = new Thread(con);
        Thread t3 = new Thread(con);
        //启动线程
        t0.start();
        t1.start();
        t2.start();
        t3.start();
    }
}

/**
 * @decrition 生产者线程
 */
class Mutil_Producer implements Runnable {
    private ResourceByCondition r;

    Mutil_Producer(ResourceByCondition r) {
        this.r = r;
    }

    public void run() {
        while (true) {
            r.product("北京烤鸭");
        }
    }
}

/**
 * @decrition 消费者线程
 */
class Mutil_Consumer implements Runnable {
    private ResourceByCondition r;

    Mutil_Consumer(ResourceByCondition r) {
        this.r = r;
    }

    public void run() {
        while (true) {
            r.consume();
        }
    }
}
```
正如代码所示,我们通过两者Condition对象单独控制消费线程与生产消费,这样可以避免消费线程在唤醒线程时唤醒的还是消费线程,如果是通过synchronized的等待唤醒机制实现的话,就可能无法避免这种情况,毕竟同一个锁,对于synchronized关键字来说只能有一组等待唤醒队列,而不能像Condition一样,同一个锁拥有多个等待队列。synchronized的实现方案如下,
```java
public class KaoYaResource {

    private String name;
    private int count = 1;//烤鸭的初始数量
    private boolean flag = false;//判断是否有需要线程等待的标志
    /**
     * 生产烤鸭
     */
    public synchronized void product(String name){
        while(flag){
            //此时有烤鸭,等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.name=name+count;//设置烤鸭的名称
        count++;
        System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
        flag=true;//有烤鸭后改变标志
        notifyAll();//通知消费线程可以消费了
    }

    /**
     * 消费烤鸭
     */
    public synchronized void consume(){
        while(!flag){//如果没有烤鸭就等待
            try{this.wait();}catch(InterruptedException e){}
        }
        System.out.println(Thread.currentThread().getName()+"...消费者........"+this.name);//消费烤鸭1
        flag = false;
        notifyAll();//通知生产者生产烤鸭
    }
}
```
如上代码,在调用notify()或者 notifyAll()方法时,由于等待队列中同时存在生产者线程和消费者线程,所以我们并不能保证被唤醒的到底是消费者线程还是生产者线程,而Codition则可以避免这种情况。嗯,了解完Condition的使用方式后,下面我们将进一步探讨Condition背后的实现机制

### Condition的实现原理
Condition的具体实现类是AQS的内部类`ConditionObject`,前面我们分析过AQS中存在两种队列,一种是`同步队列`,一种是`等待队列`,而等待队列就相对于Condition而言的。注意在使用Condition前必须获得锁,同时在Condition的等待队列上的结点与前面同步队列的结点是同一个类即Node,其结点的waitStatus的值为CONDITION。在实现类ConditionObject中有两个结点分别是firstWaiter和lastWaiter,firstWaiter代表等待队列第一个等待结点,lastWaiter代表等待队列最后一个等待结点,如下
```java
 public class ConditionObject implements Condition, java.io.Serializable {
    //等待队列第一个等待结点
    private transient Node firstWaiter;
    //等待队列最后一个等待结点
    private transient Node lastWaiter;
    //省略其他代码.......
}
```

每个Condition都对应着一个等待队列,也就是说如果一个锁上创建了多个Condition对象,那么也就存在多个等待队列。等待队列是一个FIFO的队列,在队列中每一个节点都包含了一个线程的引用,而该线程就是Condition对象上等待的线程。

当一个线程调用了await()相关的方法,那么该线程将会释放锁,并构建一个Node节点封装当前线程的相关信息加入到等待队列中进行等待,直到被唤醒、中断、超时才从队列中移出。Condition中的等待队列模型如下

![](../img/Java多线程/condition中的等待队列模型.png)

正如图所示,Node节点的数据结构,在等待队列中使用的变量与同步队列是不同的,Condtion中等待队列的结点只有直接指向的后继结点并没有指明前驱结点,而且使用的变量是nextWaiter而不是next,这点我们在前面分析结点Node的数据结构时讲过。

`firstWaiter`指向等待队列的头结点,`lastWaiter`指向等待队列的尾结点,等待队列中结点的状态只有两种即`CANCELLED`和`CONDITION`,前者表示线程已结束需要从等待队列中移除,后者表示条件结点等待被唤醒。

再次强调每个Codition对象对于一个等待队列,也就是说AQS中只能存在`一个同步队列`,但可拥有`多个等待队列`。下面从代码层面看看被调用await()方法(其他await()实现原理类似)的线程是如何加入等待队列的,而又是如何从等待队列中被唤醒的

```java
public final void await() throws InterruptedException {
      //判断线程是否被中断
      if (Thread.interrupted())
          throw new InterruptedException();
      //创建新结点加入等待队列并返回
      Node node = addConditionWaiter();
      //释放当前线程锁即释放同步状态
      int savedState = fullyRelease(node);
      int interruptMode = 0;
      //判断结点是否同步队列(SyncQueue)中,即是否被唤醒
      while (!isOnSyncQueue(node)) {
          //挂起线程
          LockSupport.park(this);
          //判断是否被中断唤醒,如果是退出循环。
          if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
              break;
      }
      //被唤醒后执行自旋操作争取获得锁,同时判断线程是否被中断
      if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
          interruptMode = REINTERRUPT;
       // clean up if cancelled
      if (node.nextWaiter != null) 
          //清理等待队列中不为CONDITION状态的结点
          unlinkCancelledWaiters();
      if (interruptMode != 0)
          reportInterruptAfterWait(interruptMode);
  }
```
执行addConditionWaiter()添加到等待队列。

```java
 private Node addConditionWaiter() {
    Node t = lastWaiter;
      // 判断是否为结束状态的结点并移除
      if (t != null && t.waitStatus != Node.CONDITION) {
          unlinkCancelledWaiters();
          t = lastWaiter;
      }
      //创建新结点状态为CONDITION
      Node node = new Node(Thread.currentThread(), Node.CONDITION);
      //加入等待队列
      if (t == null)
          firstWaiter = node;
      else
          t.nextWaiter = node;
      lastWaiter = node;
      return node;
}
```

await()方法主要做了3件事

- 一是调用addConditionWaiter()方法将当前线程封装成node结点加入等待队列
- 二是调用fullyRelease(node)方法释放同步状态并唤醒后继结点的线程。
- 三是调用isOnSyncQueue(node)方法判断结点是否在同步队列中

注意是个while循环,如果同步队列中没有该结点就直接挂起该线程,需要明白的是如果线程被唤醒后就调用`acquireQueued(node, savedState)`执行自旋操作争取锁,即当前线程结点从等待队列转移到同步队列并开始努力获取锁。

接着看看唤醒操作singal()方法
```java
 public final void signal() {
     //判断是否持有独占锁,如果不是抛出异常
   if (!isHeldExclusively())
          throw new IllegalMonitorStateException();
      Node first = firstWaiter;
      //唤醒等待队列第一个结点的线程
      if (first != null)
          doSignal(first);
 }
```

这里signal()方法做了两件事,一是判断当前线程是否持有独占锁,没有就抛出异常,从这点也可以看出只有独占模式先采用等待队列,而共享模式下是没有等待队列的,也就没法使用Condition。二是唤醒等待队列的第一个结点,即执行doSignal(first)

```java
 private void doSignal(Node first) {
    do {
        //移除条件等待队列中的第一个结点,
        //如果后继结点为null,那么说没有其他结点将尾结点也设置为null
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    //如果被通知节点没有进入到同步队列并且条件等待队列还有不为空的节点,则继续循环通知后续结点
    } while (!transferForSignal(first) &&
        (first = firstWaiter) != null);
}

//transferForSignal方法
final boolean transferForSignal(Node node) {
    //尝试设置唤醒结点的waitStatus为0,即初始化状态
    //如果设置失败,说明当期结点node的waitStatus已不为
    //CONDITION状态,那么只能是结束状态了,因此返回false
    //返回doSignal()方法中继续唤醒其他结点的线程,注意这里并
    //不涉及并发问题,所以CAS操作失败只可能是预期值不为CONDITION,
    //而不是多线程设置导致预期值变化,毕竟操作该方法的线程是持有锁的。
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
         return false;

        //加入同步队列并返回前驱结点p
        Node p = enq(node);
        int ws = p.waitStatus;
        //判断前驱结点是否为结束结点(CANCELLED=1)或者在设置
        //前驱节点状态为Node.SIGNAL状态失败时,唤醒被通知节点代表的线程
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            //唤醒node结点的线程
            LockSupport.unpark(node.thread);
        return true;
    }
```

注释说得很明白了,这里我们简单整体说明一下,doSignal(first)方法中做了两件事

- 从条件等待队列移除被唤醒的节点,然后重新维护条件等待队列的firstWaiter和lastWaiter的指向。
- 二是将从等待队列移除的结点加入同步队列(在transferForSignal()方法中完成的),如果进入到同步队列失败并且条件等待队列还有不为空的节点,则继续循环唤醒后续其他结点的线程。

到此整个signal()的唤醒过程就很清晰了,即signal()被调用后,先判断当前线程是否持有独占锁,如果有,那么唤醒当前Condition对象中等待队列的第一个结点的线程,并从等待队列中移除该结点,移动到同步队列中,

如果加入同步队列失败,那么继续循环唤醒等待队列中的其他结点的线程,如果成功加入同步队列,那么如果其前驱结点是否已结束或者设置前驱节点状态为Node.SIGNAL状态失败,则通过LockSupport.unpark()唤醒被通知节点代表的线程,到此signal()任务完成,

注意被唤醒后的线程,将从前面的await()方法中的while循环中退出,因为此时该线程的结点已在同步队列中,那么while (!isOnSyncQueue(node))将不在符合循环条件,进而调用AQS的acquireQueued()方法加入获取同步状态的竞争中,这就是等待唤醒机制的整个流程实现原理,

流程如下图所示(注意无论是同步队列还是等待队列使用的Node数据结构都是同一个,不过是使用的内部变量不同罢了)

![](../img/Java多线程/等待唤醒机制的流程实现原理.png)

## 同步工具类
### CountDownLatch
用来控制一个线程等待多个线程。维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒
```java
public class MyCountDownLatch {

    public static void main(String[] args) throws InterruptedException {
        int totalThread = 10;
        CountDownLatch countDownLatch = new CountDownLatch(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++) {
            executorService.execute(()->{
                System.out.println("run...");
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        System.out.println("end...");
        executorService.shutdown();
    }
}
```
```text
run...
run...
run...
run...
run...
run...
run...
run...
run...
run...
end...
```
#### 源码分析
CountDownLatch也是依赖于AQS的state来实现

```java
 public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

Sync(int count) {
    setState(count);
}

protected final void setState(int newState) {
    state = newState;
}
```

countDown()
```java
public void countDown() {
    sync.releaseShared(1);
}

public final boolean releaseShared(int arg) {
    //只有当 state 减为 0 的时候,tryReleaseShared 才返回 true
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
```
```java
//自旋-1
protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}
```
```java
//调用这个方法的时候,state == 0,那么就调用下面的方法进行唤醒阻塞队列中的线程
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}
```
await()
```java
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
    
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
}
//tryAcquireShared()的作用是尝试获取共享锁。如果"锁计数器=0",即锁是可获取状态,则返回1;否则,锁是不可获取状态,则返回-1
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}
```
```java
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 创建"当前线程"的Node节点,且Node中记录的锁是"共享锁"类型;并将该节点添加到CLH队列末尾。
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            // 获取上一个节点。
            // 如果上一节点是CLH队列的表头,则"尝试获取共享锁"。
            final Node p = node.predecessor();
            if (p == head) {
                //一直判断state是否为0
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
        // (上一节点不是CLH队列的表头) 当前线程一直等待,直到获取到共享锁。
        // 如果线程在等待过程中被中断过,则再次中断该线程(还原之前的中断状态)。
        if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
```
await()方法是怎么“阻塞”当前线程的,已经非常明白了。其实说白了,就是当你调用了countDownLatch.await()方法后,你当前线程就会进入了一个死循环当中,在这个死循环里面,会不断的进行判断,通过调用tryAcquireShared方法,不断判断我们上面说的那个计数器,看看它的值是否为0了(为0的时候,其实就是我们调用了足够多次数的countDownLatch.countDown()方法的时候),如果是为0的话,tryAcquireShared就会返回1,然后跳出了循环,也就不再“阻塞”当前线程了。

### CyclicBarrier
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行
```java
public class MyCyclicBarrier {

    public static void main(String[] args) {
        int totalThread = 10;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++) {
            executorService.execute(()->{
                System.out.println("before...");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println("after...");
            });
        }
        executorService.shutdown();
    }
}
```
```text
before...
before...
before...
before...
before...
before...
before...
before...
before...
before...
after...
after...
after...
after...
after...
after...
after...
after...
after...
after...
```
#### 源码分析
```java
public CyclicBarrier(int parties) {
    this(parties, null);
}

public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) 
        throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}
```
await()

调用await方法的线程告诉CyclicBarrier自己已经到达同步点,然后当前线程被阻塞。直到parties个参与线程调用了await方法
```java
public int await() throws InterruptedException, BrokenBarrierException {
    try {
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe);
    }
}
```
```java
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {
    final ReentrantLock lock = this.lock;
    // 抢锁
    lock.lock();
    try {
        // 获取 Generation
        final Generation g = generation;

        // 如果这代损坏了,抛出异常
        if (g.broken)
            throw new BrokenBarrierException();

        // 当前线程被中断过,
        if (Thread.interrupted()) {
            // 将损坏状态设置为true
            // 并通知其他阻塞在此栅栏上的线程
            breakBarrier();
            throw new InterruptedException();
        }

        // 线程数减 1
        int index = --count;
        // index = 0 表示公共屏障点被触发
        if (index == 0) {
            boolean ranAction = false;
            try {
                // 执行栅栏任务
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                // 开始执行
                ranAction = true;
                // 唤醒之前等待的线程
                nextGeneration();
                return 0;
            } finally {
                // 如果执行栅栏任务的时候失败了,就将损坏状态设置为true
                if (!ranAction)
                    breakBarrier();
            }
        }

        // loop until tripped, broken, interrupted, or timed out
        for (;;) {
            try {
                // 没有时间限制,则无期限等待
                if (!timed)
                    trip.await();
                // 有时间限制,则等待一定时间
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                // 当前代没有损坏
                if (g == generation && ! g.broken) {
                    // 让栅栏失效
                    breakBarrier();
                    throw ie;
                } else {
                   // 上面条件不满足,说明这个线程不是这代的
                   // 就不会影响当前这代栅栏的执行,所以,就打个中断标记 Thread.currentThread().interrupt();
                }
            }

            // 当有任何一个线程中断了,就会调用breakBarrier方法
            // 就会唤醒其他的线程,其他线程醒来后,也要抛出异常
            if (g.broken)
                throw new BrokenBarrierException();

            // g != generation表示正常换代了,返回当前线程所在栅栏的下标
            // 如果 g == generation,说明还没有换代,那为什么会醒了?
            // 因为一个线程可以使用多个栅栏,当别的栅栏唤醒了这个线程,就会走到这里,所以需要判断是否是当前代。
            // 正是因为这个原因,才需要generation来保证正确。
            if (g != generation)
                return index;

            // 如果有时间限制,且时间小于等于0,销毁栅栏并抛出异常
            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}
```
### Semaphore
Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
```java
public class MySemaphore {

    public static void main(String[] args) {
        //最大并发量为3
        int clientCount= 3;
        //一共有10个线程去竞争
        int totalRequestCount = 10;
        Semaphore semaphore = new Semaphore(clientCount);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalRequestCount; i++) {
            executorService.execute(()->{
                try {
                    semaphore.acquire();
                    System.out.println("当前线程:"+Thread.currentThread().getName()+"获取了一个许可,还剩:"+semaphore.availablePermits());
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();
                    System.out.println("当前线程:"+Thread.currentThread().getName()+"释放了一个许可,还剩:"+semaphore.availablePermits());
                }
            });
        }
        executorService.shutdown();
    }
}
```
```text
当前线程:pool-1-thread-1获取了一个许可,还剩:1
当前线程:pool-1-thread-3获取了一个许可,还剩:0
当前线程:pool-1-thread-2获取了一个许可,还剩:1
当前线程:pool-1-thread-3释放了一个许可,还剩:3
当前线程:pool-1-thread-4获取了一个许可,还剩:2
当前线程:pool-1-thread-2释放了一个许可,还剩:3
当前线程:pool-1-thread-6获取了一个许可,还剩:0
当前线程:pool-1-thread-1释放了一个许可,还剩:3
当前线程:pool-1-thread-5获取了一个许可,还剩:1
当前线程:pool-1-thread-6释放了一个许可,还剩:3
当前线程:pool-1-thread-8获取了一个许可,还剩:1
当前线程:pool-1-thread-7获取了一个许可,还剩:2
当前线程:pool-1-thread-4释放了一个许可,还剩:3
当前线程:pool-1-thread-5释放了一个许可,还剩:3
当前线程:pool-1-thread-9获取了一个许可,还剩:0
当前线程:pool-1-thread-7释放了一个许可,还剩:3
当前线程:pool-1-thread-10获取了一个许可,还剩:2
当前线程:pool-1-thread-9释放了一个许可,还剩:3
当前线程:pool-1-thread-8释放了一个许可,还剩:3
当前线程:pool-1-thread-10释放了一个许可,还剩:3
```
#### 源码分析
底层还是依赖AQS,基本的源码看上文就不详细解析了,初始化如下默认使用非公平锁构造并设置state的值
```java
Semaphore semaphore = new Semaphore(2);

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

```
```java
Sync(int permits) {
    setState(permits);
}

protected final void setState(int newState) {
    state = newState;
}
```

**acquire()**

```java
public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //如果返回值 < 0 表示获取状态失败,需要被加入同步队列等待
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
}

protected int tryAcquireShared(int acquires) {
        return nonfairTryAcquireShared(acquires);
}

final int nonfairTryAcquireShared(int acquires) {
        //循环获取可用的state值,用总的-1,并CAS修改值
        for (;;) {
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
}
```
失败则封装节点加入等待队列竞争锁
```java
private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            //获得当前节点pre节点
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            //重组双向链表,清空无效节点,挂起当前线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
```
release()
```java
public void release() {
    sync.releaseShared(1);
}
```
```java
public final boolean releaseShared(int arg) {
    //释放共享锁
    if (tryReleaseShared(arg)) {
        //唤醒所有共享节点线程
        doReleaseShared();
        return true;
    }
    return false;
}
```
```java
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {//是否需要唤醒后继节点
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))//修改状态为初始0
                    continue;
                unparkSuccessor(h);//唤醒h.nex节点线程
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE));
        }
        if (h == head)                   // loop if head changed
            break;
    }
}
```
# 参考文章
- https://blog.csdn.net/javazejian/article/details/75043422
- https://www.jianshu.com/p/3fe5e41dc46f
- https://blog.csdn.net/BThinker/article/details/104417813

================================================
FILE: Java-多线程/CAS.md
================================================

# CAS(Compare-and-Swap)
## 原理
- 实际上是使用了unsafe的CAS操作
- 用期望值去与内存地址的值判断是否相等,相等则用更新值替换,否则CAS等待
## 参数
compareAndSwapInt
- this 对象
- Offset 内存地址
- expect 旧的期望值
- update 新的值


## CAS的问题
CAS操作是一种无锁算法,相对于使用锁机制,它能够更好地利用多核处理器的优势,提高并发性能。但是,CAS操作也存在一些问题,包括:

- `ABA问题`:在CAS操作中,如果内存位置的当前值与预期原值相等,就会执行修改操作。但是,如果在修改之前,有其他线程对内存位置的值进行了两次修改,从而又恢复为了原来的值,此时CAS操作可能会认为没有被修改过,从而执行了修改操作。这种情况被称为ABA问题。

- `自旋次数问题`:如果CAS操作失败,需要重试,这时会进入自旋状态,不断地进行CAS操作,直到操作成功为止。如果自旋次数太多,会浪费大量的CPU资源,降低系统的性能。

- `只能保证单个变量的原子操作`:CAS操作只能保证单个变量的原子操作,对于多个变量的操作,需要加锁才能保证原子性。

针对上述问题,可以采取一些措施来解决。例如,

- 针对ABA问题,可以使用带有版本号的CAS操作,从而保证在执行修改操作前,内存位置的值没有被修改过;
- 针对自旋次数问题,可以设置一个自旋次数的上限,超过上限时放弃自旋,转为使用锁机制;
- 针对多个变量的原子操作,可以使用分段锁等方式来保证原子性。

================================================
FILE: Java-多线程/Java对象头.md
================================================

# Java对象头
## 对象在内存中的布局
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下
![](../img/Java多线程/对象在内存中的布局.png)
- 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
- 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度

### Mark Word(标记字段)
#### 默认存储结构
![](../img/Java多线程/MarkWord默认储存结构.png)
- 锁状态
- 哈希码(HashCode)
- GC分代年龄
- 是否为偏向锁
- 锁标志位
#### 变化的结构
![](../img/Java多线程/MarkWord变化的储存结构.png)
- 锁状态标志
  - 轻量级锁
  - 重量级锁
    - 重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址
    - 在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的
      ![](../img/Java多线程/ObjectMonitor.png)
      1. _WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),
      2. _owner指向持有ObjectMonitor对象的线程
      3. 当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合
      4. 当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1
      5. 同时该线程进入 WaitSe t集合中等待被唤醒
      6. 若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)
- GC标记
- 偏向锁
### Class Pointer(类型指针)

================================================
FILE: Java-多线程/ThreadLocal.md
================================================

# ThreadLocal
## 原理
- 线程局部变量
- 只在当前线程拥有,绝对的线程安全
- 一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map

## ThreadLocalMap
- ThreadLocalMap 解决 hash 冲突的方式采用的是线性探测法,如果发生冲突会继续寻找下一个空的位置
- 内存泄漏
  - Entry为弱引用实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
  - 所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value
  - 一定要remove

## 源码分析
```java
public class ThreadLocal<T> {
    private final int threadLocalHashCode = nextHashCode();
    private static AtomicInteger nextHashCode = new AtomicInteger();
    
    private static final int HASH_INCREMENT = 0x61c88647;
    
    private static int nextHashCode() {
      return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
          /** The value associated with this ThreadLocal. */
          Object value;
          //Entry 数组,然后 Entry 里面弱引用了 ThreadLocal 作为 Key。
          Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
          }
        }
        private Entry[] table;
    }
}
```

### get
先获取当前线程,然后获取当前线程中的ThreadLocalMap,然后以当前的ThreadLocal为key,到ThreadLocalMap中获取value
```java
public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

//获取当前线程中的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
    //ThreadLocalMap是存在于Thread类里的
    return t.threadLocals;
}
```
通过取模获取数组下标,如果没有冲突直接返回数据,否则同样出现遍历的情况
```java
private Entry getEntry(ThreadLocal<?> key) {
    //用key的hash值与数组长度 得到index
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    //线性探测,如果值为空找下一个
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}
```
```java
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            //或主动清理无用的内存,但是如果清理的太多,自然慢了
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
```
### set
首先获取当前线程,然后获取当前线程中存储的threadLocals变量,此变量其实就是ThreadLocalMap,最后看此ThreadLocalMap是否为空,为空就创建一个新的Map,不为空则以当前的ThreadLocal为key,存储当前value
```java
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
```
ThreadLocalMap内部使用一个数组来保存数据,类似HashMap;每个ThreadLocal在初始化的时候会分配一个threadLocalHashCode,然后和数组的长度进行取模操作,所
Download .txt
gitextract_u9dhcn2c/

├── .gitignore
├── 23-Java知识点梳理.xmind
├── Go/
│   └── go.md
├── Java-JVM/
│   ├── JVM的启动过程.md
│   ├── JVM调优.md
│   ├── Java即时编译.md
│   ├── Java对象头.md
│   ├── 内存分配与回收策略.md
│   ├── 内存结构.md
│   ├── 垃圾回收.md
│   └── 类加载机制.md
├── Java-基础/
│   ├── JavaIO.md
│   ├── Java关键字.md
│   ├── Java类型.md
│   ├── Java长期支持版本.md
│   ├── Object.md
│   ├── String.md
│   ├── 反射.md
│   ├── 容器-collection.md
│   ├── 容器-map.md
│   ├── 异常.md
│   ├── 数组.md
│   ├── 泛型.md
│   ├── 继承.md
│   ├── 虚拟线程.md
│   └── 虚拟线程2.md
├── Java-多线程/
│   ├── AQS.md
│   ├── CAS.md
│   ├── Java对象头.md
│   ├── ThreadLocal.md
│   ├── volatile.md
│   ├── 线程.md
│   ├── 线程池.md
│   └── 锁机制.md
├── LICENSE
├── Linux/
│   └── linux.md
├── Mybatis/
│   └── mybatis.md
├── Netty/
│   └── netty.md
├── README.md
├── Redis/
│   ├── Redis.md
│   └── Redis常见面试题.md
├── Spring/
│   ├── Spring.md
│   ├── SpringBoot.md
│   └── SpringMVC.md
├── SpringCloud/
│   └── springcloud.md
├── TODO.md
├── 分布式相关/
│   ├── ApacheSeata.md
│   ├── BASE.md
│   ├── CAP.md
│   ├── 一致性算法.md
│   ├── 分布式ID.md
│   ├── 分布式事务.md
│   └── 分布式锁.md
├── 商城类问题/
│   └── 商城类问题.md
├── 场景设计/
│   └── 场景设计.md
├── 大数据/
│   ├── Flink/
│   │   └── Flink.md
│   ├── Hadoop/
│   │   ├── Hadoop.md
│   │   └── hadoop面试问题.md
│   ├── Hive/
│   │   └── Hive.md
│   └── Spark/
│       └── Spark.md
├── 容器技术/
│   ├── docker.md
│   └── k8s.md
├── 性能测试/
│   └── docker安装配置influxdb-v2+grafana+jemter.md
├── 数据库/
│   ├── Elasticsearch.md
│   ├── Hbase.md
│   ├── MongoDB.md
│   ├── MySQL.md
│   ├── Nebula.md
│   ├── SQL面试题.md
│   └── count.md
├── 数据结构和算法/
│   ├── 01-最长回文子串.md
│   ├── LFU.md
│   ├── LRU.md
│   ├── hotcode.md
│   ├── leetcodetop150.md
│   ├── 二叉树.md
│   ├── 二叉树遍历.md
│   ├── 加减乘除.md
│   └── 排序算法.md
├── 智力题/
│   └── 智力题.md
├── 架构/
│   ├── DDD领域驱动设计.md
│   ├── 系统设计.md
│   └── 计算和储存分离.md
├── 消息队列/
│   ├── Kafka.md
│   ├── Pulsar.md
│   ├── RabbitMQ.md
│   ├── Redis.md
│   ├── RocketMQ.md
│   ├── Zookeeper.md
│   └── mq常见面试题.md
├── 职业规划和学习习惯/
│   └── 职业规划和学习习惯.md
├── 计算机网络/
│   ├── CDN.md
│   ├── HTTP.md
│   ├── HTTP面试题.md
│   ├── IP报文.md
│   ├── JWT.md
│   ├── TCP_IP.md
│   ├── TCP报文.md
│   ├── TLS.md
│   ├── UDP报文.md
│   ├── cookie和session.md
│   ├── 网络协议分层.md
│   ├── 网络攻击行为.md
│   └── 跨域.md
├── 设计模式/
│   ├── 单例模式.md
│   ├── 工厂模式.md
│   ├── 策略模式.md
│   ├── 装饰者模式.md
│   ├── 设计模式面试题.md
│   └── 责任链模式.md
└── 面试解答/
    ├── HR会问什么.md
    ├── 面试解答2021-06.md
    ├── 面试解答2021-07.md
    ├── 面试解答2021-09.md
    └── 面试解答2021-10.md
Condensed preview — 115 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,632K chars).
[
  {
    "path": ".gitignore",
    "chars": 54,
    "preview": "# Created by .ignore support plugin (hsz.mobi)\n\n.idea\n"
  },
  {
    "path": "Go/go.md",
    "chars": 50,
    "preview": "\n# Go和Java的区别 \n\n\n# go和Java的线程模型区别\n\n# go和Java的GC区别\n"
  },
  {
    "path": "Java-JVM/JVM的启动过程.md",
    "chars": 1547,
    "preview": "\n* [JVM的启动过程](#jvm的启动过程)\n    * [1、JVM的装入环境和配置](#1jvm的装入环境和配置)\n    * [2、装载JVM](#2装载jvm)\n    * [3、初始化JVM,获得本地调用接口](#3初始化jv"
  },
  {
    "path": "Java-JVM/JVM调优.md",
    "chars": 2368,
    "preview": "\n* [原则](#原则)\n* [jvm调优](#jvm调优)\n  * [JVM调优目标](#jvm调优目标)\n  * [举例](#举例)\n  * [JVM调优的步骤](#jvm调优的步骤)\n  * [JVM参数解析及调优](#jvm参数解析"
  },
  {
    "path": "Java-JVM/Java即时编译.md",
    "chars": 12078,
    "preview": "# 什么是\n- 常见的编译型语言如C++,通常会把代码直接编译成CPU所能理解的机器码来运行。而Java为了实现“一次编译,处处运行”的特性,把编译的过程分成两部分,首先它会先由javac编译成通用的中间形式——字节码,然后再由解释器逐条将"
  },
  {
    "path": "Java-JVM/Java对象头.md",
    "chars": 4726,
    "preview": "# Java对象头\n\n由于Java面向对象的思想,在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。\n\n# 对象头形式\n\nJVM中对象头的方式有以下两种(以32"
  },
  {
    "path": "Java-JVM/内存分配与回收策略.md",
    "chars": 1279,
    "preview": "\n* [对象优先在 Eden 分配](#对象优先在-eden-分配)\n* [大对象直接进入老年代](#大对象直接进入老年代)\n* [长期存活的对象进入老年代](#长期存活的对象进入老年代)\n* [动态对象年龄判定](#动态对象年龄判定)\n*"
  },
  {
    "path": "Java-JVM/内存结构.md",
    "chars": 4194,
    "preview": "# 内存结构\n## JDK1.7和1.8对比\n<img src=\"../img/jvm/java7内存结构.png\" alt=\"java7内存结构\"  width=\"50%\" /><img src=\"../img/jvm/java8内存结构"
  },
  {
    "path": "Java-JVM/垃圾回收.md",
    "chars": 24814,
    "preview": "# 判断一个对象能否被回收\n## 引用计数法\n- 为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。\n- 在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法"
  },
  {
    "path": "Java-JVM/类加载机制.md",
    "chars": 5247,
    "preview": "# 类加载机制\n### 有哪些类加载器\n![](../img/jvm/类加载器.png)\n#### 1.引导类加载器 `bootstrap classloader`\n启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的"
  },
  {
    "path": "Java-基础/JavaIO.md",
    "chars": 12014,
    "preview": "* [文件io](#文件io)\n  * [磁盘操作](#磁盘操作)\n  * [字节流](#字节流)\n  * [字符流](#字符流)\n* [序列化](#序列化)\n  * [什么是序列化](#什么是序列化)\n  * [序列化的实现](#序列化的"
  },
  {
    "path": "Java-基础/Java关键字.md",
    "chars": 3373,
    "preview": "* [final](#final)\n  * [修饰变量](#修饰变量)\n  * [修饰方法](#修饰方法)\n  * [修饰类](#修饰类)\n  * [底层原理](#底层原理)\n  * [final关键字的好处](#final关键字的好处)\n"
  },
  {
    "path": "Java-基础/Java类型.md",
    "chars": 1350,
    "preview": "* [基本类型](#基本类型)\n  * [float或者double怎么判断=0?](#float或者double怎么判断0)\n* [包装类型](#包装类型)\n  * [包装类型的equals](#包装类型的equals)\n\n# 基本类型\n"
  },
  {
    "path": "Java-基础/Java长期支持版本.md",
    "chars": 4688,
    "preview": "* [Java8](#java8)\n  * [Lambda表达式](#lambda表达式)\n  * [函数式接口](#函数式接口)\n  * [默认方法](#默认方法)\n  * [方法引用](#方法引用)\n  * [Stream API](#"
  },
  {
    "path": "Java-基础/Object.md",
    "chars": 1400,
    "preview": "\n* [equals](#equals)\n* [hashcode](#hashcode)\n* [toString](#tostring)\n* [clone](#clone)\n* [wait](#wait)\n* [notify](#notif"
  },
  {
    "path": "Java-基础/String.md",
    "chars": 3065,
    "preview": "\n* [final](#final)\n  * [不可变的原因](#不可变的原因)\n* [储存数据](#储存数据)\n* [string pool](#string-pool)\n  * [new String(\"abc\")会创建几个对象](#n"
  },
  {
    "path": "Java-基础/反射.md",
    "chars": 2694,
    "preview": "\n* [什么是反射](#什么是反射)\n* [java.lang.reflect](#javalangreflect)\n* [反射的优缺点](#反射的优缺点)\n    * [优点](#优点)\n        * [可扩展性](#可扩展性)\n "
  },
  {
    "path": "Java-基础/容器-collection.md",
    "chars": 4489,
    "preview": "\n* [Collection](#collection)\n* [List](#list)\n* [常用的子类](#常用的子类)\n  * [Vector](#vector)\n  * [LinkedList](#linkedlist)\n  * ["
  },
  {
    "path": "Java-基础/容器-map.md",
    "chars": 34031,
    "preview": "# Map\n![](../img/基础/Java容器类关系图.png)\n## HashMap\n- 存储的结构是key-value键值对,不像Collection是单列集合\n- 阅读Map前最好知道什么是散列表和红黑树\n## 特点\n- k和v"
  },
  {
    "path": "Java-基础/异常.md",
    "chars": 4896,
    "preview": "\n* [Throwable](#throwable)\n  * [Error](#error)\n  * [Exception](#exception)\n* [try catch finally机制](#try-catch-finally机制)"
  },
  {
    "path": "Java-基础/数组.md",
    "chars": 467,
    "preview": "\n* [初始化](#初始化)\n  * [静态初始化](#静态初始化)\n  * [动态初始化](#动态初始化)\n* [Java 数组和内存](#java-数组和内存)\n* [参考文章](#参考文章)\n\n\n# 初始化\n## 静态初始化\nint["
  },
  {
    "path": "Java-基础/泛型.md",
    "chars": 810,
    "preview": "\n* [什么是泛型?为什么要使用泛型?](#什么是泛型为什么要使用泛型)\n* [泛型的使用](#泛型的使用)\n* [泛型上下边界](#泛型上下边界)\n    * [上边界](#上边界)\n    * [下边界](#下边界)\n\n## java "
  },
  {
    "path": "Java-基础/继承.md",
    "chars": 1602,
    "preview": "\n* [访问修饰符](#访问修饰符)\n* [抽象类与接口](#抽象类与接口)\n  * [抽象类](#抽象类)\n  * [接口](#接口)\n* [super](#super)\n* [重写与重载](#重写与重载)\n  * [重写(Overrid"
  },
  {
    "path": "Java-基础/虚拟线程.md",
    "chars": 17153,
    "preview": "# 虚拟线程\n\n## 背景\n2022-09-20,JDK 19 发布了GA版本,备受瞩目的协程功能也算尘埃落地,不过,此次 GA版本并不是以协程来命名,而是使用了 Virtual\n\nThread(虚拟线程),并且是 preview预览版本。"
  },
  {
    "path": "Java-基础/虚拟线程2.md",
    "chars": 3962,
    "preview": "# jdk21虚拟线程\njdk21已经正式发布了虚拟线程\n\n虚拟线程和系统线程的绑定关系\n![](../img/Java基础/虚拟线程和系统线程.png)\n\n## 虚拟线程实现原理\n虚拟线程是一种轻量级(用户模式)线程,这种线程是由Java"
  },
  {
    "path": "Java-多线程/AQS.md",
    "chars": 39204,
    "preview": "# AQS(AbstractQueuedSynchronizer)\n## 工作原理概要\nAbstractQueuedSynchronizer又称为队列同步器(后面简称AQS),它是用来构建锁或其他同步组件的基础框架,内部通过一个int类型的"
  },
  {
    "path": "Java-多线程/CAS.md",
    "chars": 679,
    "preview": "\n# CAS(Compare-and-Swap)\n## 原理\n- 实际上是使用了unsafe的CAS操作\n- 用期望值去与内存地址的值判断是否相等,相等则用更新值替换,否则CAS等待\n## 参数\ncompareAndSwapInt\n- th"
  },
  {
    "path": "Java-多线程/Java对象头.md",
    "chars": 1194,
    "preview": "\n# Java对象头\n## 对象在内存中的布局\n在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下\n![](../img/Java多线程/对象在内存中的布局.png)\n- 实例变量:存放类的属性数据信息,包括父类的"
  },
  {
    "path": "Java-多线程/ThreadLocal.md",
    "chars": 16440,
    "preview": "\n# ThreadLocal\n## 原理\n- 线程局部变量\n- 只在当前线程拥有,绝对的线程安全\n- 一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map\n\n## ThreadLo"
  },
  {
    "path": "Java-多线程/volatile.md",
    "chars": 5549,
    "preview": "\n# 前提了解\n## 机器内存模型\n\n![](../img/Java多线程/机器内存模型.png)\n\n由于CPU和内存的速度差异(内存的处理速度远低于CPU)所以引入了CPU缓存,一般都有几级缓存,处理数据时先从CPU缓存读到寄存器,如果没"
  },
  {
    "path": "Java-多线程/线程.md",
    "chars": 21303,
    "preview": "# 线程的生命状态\n![](../img/Java多线程/线程的生命状态.png)\n### 新建new\n创建后尚未启动。\n### 可运行runnable\n- 可能正在运行,也可能正在等待 CPU 时间片。\n- 包含了操作系统线程状态中的 R"
  },
  {
    "path": "Java-多线程/线程池.md",
    "chars": 9953,
    "preview": "# 创建线程池的方式\n### 1、Executors\n#### newCachedThreadPool\n创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程\n`Executors.newCachedT"
  },
  {
    "path": "Java-多线程/锁机制.md",
    "chars": 14406,
    "preview": "# Synchronized\n### 原理\n### Synchronized的语义底层是通过一个monitor的对象来完成\n### 同步一个代码块\n- 它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步\n- 同步代码块原理"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "Linux/linux.md",
    "chars": 24520,
    "preview": "# Linux\n\n## 文件和目录的操作\n\n- `ls` 显示文件和目录列表\n- `cd` 切换目录\n- `pwd` 显示当前工作目录\n- `mkdir` 创建目录\n- `rmdir` 删除空目录\n- `touch` 生成一个空文件或更改文"
  },
  {
    "path": "Mybatis/mybatis.md",
    "chars": 13431,
    "preview": "\n* [mybatis](#mybatis)\n    * [什么是mybatis](#什么是mybatis)\n    * [JDBC执行六步骤](#jdbc执行六步骤)\n    * [mybatis执行8步骤](#mybatis执行8步骤)"
  },
  {
    "path": "Netty/netty.md",
    "chars": 17668,
    "preview": "\n* [netty](#netty)\n    * [重要的组件](#重要的组件)\n        * [Channel](#channel)\n        * [ChannelFuture](#channelfuture)\n       "
  },
  {
    "path": "README.md",
    "chars": 40970,
    "preview": "<h1 align=\"center\">JavaDeveloperBrain</h1>\n\n<div align=\"center\">\n\n[comment]: <> ([![GitHub issues]&#40;https://img.shiel"
  },
  {
    "path": "Redis/Redis.md",
    "chars": 32503,
    "preview": "# redis\n## 特点\n- Key-Value健值类型存储\n- 支持数据可靠存储及落地\n- 单进程单线程高性能服务器\n- 单机qps(每秒查询率)可以达到10w.\n- 适合小数据量高速读写访问\n## Redis为什么这么快\n- 完全基于"
  },
  {
    "path": "Redis/Redis常见面试题.md",
    "chars": 1455,
    "preview": "## 储存结构和使用场景\n- `string` 字符串、数字,一些简单的k-v数据\n- `list` 数组、集合,简单消息队列(缺点很多,比如不能重复消费,不能多订阅,无\"消息持久化\",无确认机制)\n- `hash` 储存对象\n- `set"
  },
  {
    "path": "Spring/Spring.md",
    "chars": 27250,
    "preview": "# spring\n\n## 架构图\n\n<img src=\"../img/spring/spring架构图.png\" width=\"50%\" />\n\n## 模块\n\n### Core Container\n\n核心容器(Core Container)"
  },
  {
    "path": "Spring/SpringBoot.md",
    "chars": 12905,
    "preview": "# springboot\n## springboot启动流程\n### 启动类上注解:@SpringBootApplication\n#### @SpringBootConfiguration\n根据Javadoc可知,该注解作用就是将当前的类作"
  },
  {
    "path": "Spring/SpringMVC.md",
    "chars": 1869,
    "preview": "\n* [springMVC](#springmvc)\n   * [流程](#流程)\n   * [执行流程](#执行流程)\n\n\n# springMVC\n## 流程\n![](../img/spring/springmvc流程.png)\n## 执"
  },
  {
    "path": "SpringCloud/springcloud.md",
    "chars": 39356,
    "preview": "* [springcloud](#springcloud)\n    * [服务注册与发现](#服务注册与发现)\n        * [eureka](#eureka)\n        * [consul](#consul)\n    * [服"
  },
  {
    "path": "TODO.md",
    "chars": 61,
    "preview": "# TODO list\n\n- 面试题:三个线程交替打印ABC\n- clickhouse\n- java stream流的原理"
  },
  {
    "path": "分布式相关/ApacheSeata.md",
    "chars": 8677,
    "preview": "* [Apache Seata](#apache-seata)\n  * [Seata 是什么?](#seata-是什么)\n  * [AT 模式](#at-模式)\n  * [TCC 模式](#tcc-模式)\n  * [Saga 模式](#sa"
  },
  {
    "path": "分布式相关/BASE.md",
    "chars": 723,
    "preview": "\n* [BASE](#base)\n  * [基本可以  Basically Available](#基本可以--basically-available)\n  * [软状态  Soft-state](#软状态--soft-state)\n  *"
  },
  {
    "path": "分布式相关/CAP.md",
    "chars": 1365,
    "preview": "\n* [CAP理论](#cap理论)\n  * [一致性 Consistency](#一致性-consistency)\n  * [可用性 Availability](#可用性-availability)\n  * [分区容错性 Partitio"
  },
  {
    "path": "分布式相关/一致性算法.md",
    "chars": 2226,
    "preview": "\n* [一致性算法](#一致性算法)\n    * [Paxos](#paxos)\n    * [Raft](#raft)\n\n\n# 一致性算法\n## Paxos\n## Raft\n- 对Paxos的简化实现版本\n\nRaft是一个用户管理日志一致"
  },
  {
    "path": "分布式相关/分布式ID.md",
    "chars": 1870,
    "preview": "* [分布式唯一ID设计](#分布式唯一id设计)\n  * [UUID](#uuid)\n  * [多台MySQL服务器](#多台mysql服务器)\n  * [Twitter Snowflake](#twitter-snowflake)\n  "
  },
  {
    "path": "分布式相关/分布式事务.md",
    "chars": 2938,
    "preview": "\n* [分布式事务](#分布式事务)\n    * [两阶段提交](#两阶段提交)\n        * [原理](#原理)\n        * [大致的流程](#大致的流程)\n        * [问题](#问题)\n    * [TCC(Tr"
  },
  {
    "path": "分布式相关/分布式锁.md",
    "chars": 9695,
    "preview": "\n* [分布式锁](#分布式锁)\n  * [基于数据库](#基于数据库)\n    * [怎么实现](#怎么实现)\n      * [创建一张锁表](#创建一张锁表)\n      * [添加锁](#添加锁)\n      * [释放锁](#释放"
  },
  {
    "path": "商城类问题/商城类问题.md",
    "chars": 12155,
    "preview": "\n* [商城类问题](#商城类问题)\n  * [秒杀](#秒杀)\n    * [项目亮点](#项目亮点)\n    * [秒杀流程](#秒杀流程)\n    * [一. 环境的搭建](#一-环境的搭建)\n    * [二. 登录功能的实现](#"
  },
  {
    "path": "场景设计/场景设计.md",
    "chars": 41016,
    "preview": "\n* [场景设计](#场景设计)\n    * [有A、B两个大文件,每个文件几十G,而内存只有4G,其中A文件存放学号+姓名,而B文件存放学号+分数,要求生成文件C,存放姓名和分数。怎么实现?](#有ab两个大文件每个文件几十g而内存只有4"
  },
  {
    "path": "大数据/Flink/Flink.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "大数据/Hadoop/Hadoop.md",
    "chars": 8732,
    "preview": "# hadoop\n\n## Hadoop分布式文件系统:架构和设计\nHadoop分布式文件系统(HDFS)被设计成适合运行在通用硬件(commodity hardware)上的分布式文件系统。它和现有的分布式文件系统有很多共同点。但同时,它和"
  },
  {
    "path": "大数据/Hadoop/hadoop面试问题.md",
    "chars": 22579,
    "preview": "\n# 什么是Hadoop?它解决了什么问题?\nHadoop是一个开源的分布式计算框架,设计用于存储和处理大规模数据集。它通过将数据分布在多个廉价计算节点上并行处理来解决传统单机系统无法处理的大数据问题。Hadoop的核心优势在于它的扩展性和"
  },
  {
    "path": "大数据/Hive/Hive.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "大数据/Spark/Spark.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "容器技术/docker.md",
    "chars": 5829,
    "preview": "* [docker](#docker)\n  * [Docker简介](#docker简介)\n    * [是什么](#是什么)\n    * [能干嘛](#能干嘛)\n  * [Docker常用命令](#docker常用命令)\n    * [帮"
  },
  {
    "path": "容器技术/k8s.md",
    "chars": 4120,
    "preview": "\n![](../img/容器技术/k8s/k8s图标.png)\n\n# 简介\n\nKubernetes 是 Google 团队发起的开源项目,它的目标是管理跨多个主机的容器,提供基本的部署,维护以及应用伸缩,主要实现语言为 Go 语言。Kube"
  },
  {
    "path": "性能测试/docker安装配置influxdb-v2+grafana+jemter.md",
    "chars": 5595,
    "preview": "# 1、安装docker\n\nhttps://www.docker.com/get-started\n\n![](../img/性能测试/安装docker.png)\n\n\n# 2、安装配置influxdb\n\nhttps://docs.influxd"
  },
  {
    "path": "数据库/Elasticsearch.md",
    "chars": 12254,
    "preview": "\n* [Elasticsearch](#elasticsearch)\n  * [es的特点](#es的特点)\n  * [应用场景](#应用场景)\n* [Elasticsearch基本概念](#elasticsearch基本概念)\n  * ["
  },
  {
    "path": "数据库/Hbase.md",
    "chars": 11714,
    "preview": "\n* [HBASE](#hbase)\n    * [什么是?](#什么是)\n    * [列式存储](#列式存储)\n        * [储存图](#储存图)\n        * [Row Key](#row-key)\n        * "
  },
  {
    "path": "数据库/MongoDB.md",
    "chars": 7431,
    "preview": "\n* [MongoDB](#mongodb)\n* [特点](#特点)\n* [关键组件](#关键组件)\n    * [_id](#_id)\n    * [集合](#集合)\n    * [游标](#游标)\n    * [数据库](#数据库)\n "
  },
  {
    "path": "数据库/MySQL.md",
    "chars": 32155,
    "preview": "# MySQL\n## 架构\n\n<img src=\"../img/数据库/MySQL/MySQL架构.png\" width=\"50%\" />\n\n### 客户端\n各种语言都提供了连接mysql数据库的方法,比如jdbc、php、go等,可根据选"
  },
  {
    "path": "数据库/Nebula.md",
    "chars": 20935,
    "preview": "* [什么是Nebula Graph](#什么是nebula-graph)\n  * [什么是图数据库](#什么是图数据库)\n  * [Nebula Graph 的优势](#nebula-graph-的优势)\n    * [开源](#开源)\n"
  },
  {
    "path": "数据库/SQL面试题.md",
    "chars": 3365,
    "preview": "## 联合索引\n对列col1、列col2和列col3建一个联合索引\n\n联合索引 test_col1_col2_col3 实际建立了(col1)、(col1,col2)、(col,col2,col3)三个索引。\n```sql\nSELECT *"
  },
  {
    "path": "数据库/count.md",
    "chars": 1620,
    "preview": "\n* [分页count(*)的优化](#分页count的优化)\n    * [场景](#场景)\n    * [count(*)的工作原理](#count的工作原理)\n    * [解决方案](#解决方案)\n        * [show t"
  },
  {
    "path": "数据结构和算法/01-最长回文子串.md",
    "chars": 731,
    "preview": "# 子序列和子串\n\n现在有 abcd\n\nab bc cd 子串(连续字符)\nabd 子序列(非连续字符)\n\n双指针算法\n\n- 相向型双指针\n- 同向\n- 背向\n\n```text\nfor 起点 O(n)\n    for 终点 O(n)\n   "
  },
  {
    "path": "数据结构和算法/LFU.md",
    "chars": 5832,
    "preview": "# LFU\n请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。\n\n实现 LFUCache 类:\n\nLFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象\nint get(int key) -"
  },
  {
    "path": "数据结构和算法/LRU.md",
    "chars": 3296,
    "preview": "\n# LRU\n\n运用你所掌握的数据结构,设计和实现一个LRU (最近最少使用) 缓存机制 。\n实现 LRUCache 类:\n\nLRUCache(int capacity) 以正整数作为容量capacity 初始化 LRU 缓存\nint ge"
  },
  {
    "path": "数据结构和算法/hotcode.md",
    "chars": 175407,
    "preview": "# 常考的算法题\n\n## 3. 无重复字符的最长子串\n\nhttps://leetcode.cn/problems/longest-substring-without-repeating-characters/description/?env"
  },
  {
    "path": "数据结构和算法/leetcodetop150.md",
    "chars": 241735,
    "preview": "# 数组 / 字符串\n\n## 88. 合并两个有序数组\n\nhttps://leetcode.cn/problems/merge-sorted-array/description/?envType=study-plan-v2&envId=to"
  },
  {
    "path": "数据结构和算法/二叉树.md",
    "chars": 1542,
    "preview": "\n* [二叉树](#二叉树)\n    * [满二叉树(Full Binary Tree)](#满二叉树full-binary-tree)\n    * [完全二叉树(Complete Binary Tree)](#完全二叉树complete-"
  },
  {
    "path": "数据结构和算法/二叉树遍历.md",
    "chars": 123,
    "preview": "https://leetcode-cn.com/problems/binary-tree-preorder-traversal/solution/leetcodesuan-fa-xiu-lian-dong-hua-yan-shi-xbian"
  },
  {
    "path": "数据结构和算法/加减乘除.md",
    "chars": 4788,
    "preview": "* [加](#加)\n    * [字符串相加](#字符串相加)\n* [减](#减)\n* [乘](#乘)\n    * [字符串相乘](#字符串相乘)\n* [除](#除)\n    * [两数相除](#两数相除)\n\n# 加\n## 字符串相加\n给定"
  },
  {
    "path": "数据结构和算法/排序算法.md",
    "chars": 14382,
    "preview": "\n* [排序算法](#排序算法)\n  * [冒泡排序](#冒泡排序)\n  * [选择排序](#选择排序)\n  * [插入排序](#插入排序)\n  * [希尔排序](#希尔排序)\n  * [归并排序<g-emoji class=\"g-emoj"
  },
  {
    "path": "智力题/智力题.md",
    "chars": 2684,
    "preview": "# 智力题\n## 100只试管里有一只是有毒的,现在有10个小白鼠,如何最快速地判断出那只试管有毒\n- 100试管分为10组每组10只,1个小白鼠对应一组\n  - 每组混在一起\n- 死的那组10只,分5-5两组\n  - 9只\n- 5-5死一"
  },
  {
    "path": "架构/DDD领域驱动设计.md",
    "chars": 24390,
    "preview": "\n* [DDD(Domain Driven Design)领域驱动设计](#ddddomain-driven-design领域驱动设计)\n  * [软件架构模式的演进](#软件架构模式的演进)\n  * [微服务设计和拆分的困境](#微服务设"
  },
  {
    "path": "架构/系统设计.md",
    "chars": 3312,
    "preview": "\n# MVC设计\nMVC(Model-View-Controller)是一种软件设计模式,将应用程序划分为三个主要组成部分,分别是模型、视图和控制器。下面是每个组件的详细介绍:\n\n1. 模型(Model):模型是应用程序中数据和业务逻辑的表"
  },
  {
    "path": "架构/计算和储存分离.md",
    "chars": 2079,
    "preview": "# 计算和储存分离架构\n\n## 为什么说存储和计算分离的架构才是未来\nhttps://juicefs.com/blog/cn/posts/why-disaggregated-compute-and-storage-is-future/\n\n这"
  },
  {
    "path": "消息队列/Kafka.md",
    "chars": 11774,
    "preview": "# Kafka\n## 架构图\n![](../img/消息队列/kafka/架构图.png)\n- Producer : 发布消息的客户端\n- Broker:一个从生产者接受并存储消息的客户端\n- Consumer : 消费者从 Broker "
  },
  {
    "path": "消息队列/Pulsar.md",
    "chars": 16359,
    "preview": "\n\n* [Pulsar](#pulsar)\n    * [pulsar的优势](#pulsar的优势)\n* [Apache Pulsar 架构](#apache-pulsar-架构)\n    * [Topic 与分区](#topic-与分区"
  },
  {
    "path": "消息队列/RabbitMQ.md",
    "chars": 12395,
    "preview": "\n* [RabbitMQ](#rabbitmq)\n    * [概念介绍](#概念介绍)\n    * [架构图](#架构图)\n    * [exchange类型](#exchange类型)\n        * [Direct](#direc"
  },
  {
    "path": "消息队列/Redis.md",
    "chars": 732,
    "preview": "\n* [Redis](#redis)\n  * [Redis实现mq主要是依赖数据结构list](#redis实现mq主要是依赖数据结构list)\n  * [不足](#不足)\n\n\n# Redis\n## Redis实现mq主要是依赖数据结构li"
  },
  {
    "path": "消息队列/RocketMQ.md",
    "chars": 5447,
    "preview": "\n\n* [RocketMQ](#rocketmq)\n  * [架构图](#架构图)\n* [组件](#组件)\n  * [NameServer](#nameserver)\n  * [Broker](#broker)\n  * [Producer]"
  },
  {
    "path": "消息队列/Zookeeper.md",
    "chars": 12875,
    "preview": "\n* [Zookeeper](#zookeeper)\n* [概念](#概念)\n* [用zookeeper可以干嘛](#用zookeeper可以干嘛)\n* [数据结构](#数据结构)\n    * [ZNode](#znode)\n       "
  },
  {
    "path": "消息队列/mq常见面试题.md",
    "chars": 6462,
    "preview": "\n* [常见面试题](#常见面试题)\n    * [什么是消息队列](#什么是消息队列)\n    * [为什么要使用消息队列](#为什么要使用消息队列)\n    * [如何保证消息队列高可用](#如何保证消息队列高可用)\n    * [如何"
  },
  {
    "path": "职业规划和学习习惯/职业规划和学习习惯.md",
    "chars": 506,
    "preview": "\n* [职业规划和学习习惯](#职业规划和学习习惯)\n    * [项目遇到的问题](#项目遇到的问题)\n    * [职业规划](#职业规划)\n    * [平时规划](#平时规划)\n        * [看什么书](#看什么书)\n   "
  },
  {
    "path": "计算机网络/CDN.md",
    "chars": 626,
    "preview": "\n* [<a href=\"#\">什么是CDN</a>](#什么是cdn)\n* [<a href=\"#\">好处</a>](#好处)\n\n\n# [什么是CDN](#)\n- 内容分发网络(CDN)是指一组分布在不同地理位置的服务器,协同工作以提供互"
  },
  {
    "path": "计算机网络/HTTP.md",
    "chars": 7431,
    "preview": "\n# 特点\n\n无状态\n\n# 方法\n\n## get\n\n获取资源 \n\n## head\n\n获取报文首部\n\n## post\n\n传输实体主体\n\n## put\n\n- 上传文件\n\n- 修改资源完全替换\n  \n  ## patch\n  \n  修改部分资源\n"
  },
  {
    "path": "计算机网络/HTTP面试题.md",
    "chars": 9507,
    "preview": "# 在浏览器中输入url地址显示主页的过程\n\n- DNS解析\n  \n  - 域名到ip的映射\n  - 根域名 顶级域名 权威DNS服务器\n\n- TCP连接\n\n- 发送HTTP请求\n\n- 服务器处理请求并返回HTTP报文\n\n- 浏览器解析渲染"
  },
  {
    "path": "计算机网络/IP报文.md",
    "chars": 1898,
    "preview": "\n* [<a href=\"#\">图</a>](#图)\n* [<a href=\"#\">组成</a>](#组成)\n    * [<a href=\"#\">首部</a>](#首部)\n        * [<a href=\"#\">版本号</a>](#"
  },
  {
    "path": "计算机网络/JWT.md",
    "chars": 1340,
    "preview": "\n* [<a href=\"#\">json web token</a>](#json-web-token)\n* [<a href=\"#\">格式</a>](#格式)\n  * [<a href=\"#\">header</a>](#header)\n "
  },
  {
    "path": "计算机网络/TCP_IP.md",
    "chars": 8359,
    "preview": "\n* [UDP 和 TCP 的特点](#udp-和-tcp-的特点)\n  * [UDP](#udp)\n  * [TCP](#tcp)\n* [三次握手](#三次握手)\n  * [流程](#流程)\n  * [为什么需要三次握手](#为什么需要三"
  },
  {
    "path": "计算机网络/TCP报文.md",
    "chars": 2566,
    "preview": "\n* [<a href=\"#\">图</a>](#图)\n* [<a href=\"#\">组成</a>](#组成)\n  * [<a href=\"#\">首部</a>](#首部)\n    * [<a href=\"#\">源端口</a>](#源端口)\n "
  },
  {
    "path": "计算机网络/TLS.md",
    "chars": 2663,
    "preview": "# TLS的建立流程\nHTTPS协议其实就是HTTP over TSL,TSL(Transport Layer Security) 传输层安全协议是https协议的核心。\n\nTSL可以理解为SSL (Secure Socket Layer)"
  },
  {
    "path": "计算机网络/UDP报文.md",
    "chars": 705,
    "preview": "\n* [<a href=\"#\">图</a>](#图)\n* [<a href=\"#\">组成</a>](#组成)\n    * [<a href=\"#\">首部</a>](#首部)\n        * [<a href=\"#\">伪首部</a>](#"
  },
  {
    "path": "计算机网络/cookie和session.md",
    "chars": 871,
    "preview": "# [Cookie](#)\n## [Cookie是什么?](#)\nhttp协议里的cookie包含web cookie和浏览器cookie,他是服务器发送到web浏览器的一小块数据,服务器发送到浏览器的cookie,浏览器会进行存储,并与下"
  },
  {
    "path": "计算机网络/网络协议分层.md",
    "chars": 1875,
    "preview": "\n# OSI 7层(基本只是拿来作比较)\n- 应用层\n- 表示层\n- 会话层\n- 传输层\n- 网络层\n- 数据链路层\n- 物理层\n\n虽然划分的很细每层都有详细的分工,但是在实际实现中TCP/IP 5(4)层得到了大多数人的认可,于是7层基本"
  },
  {
    "path": "计算机网络/网络攻击行为.md",
    "chars": 6964,
    "preview": "\n* [CSRF攻击](#csrf攻击)\n  * [什么是CSRF攻击](#什么是csrf攻击)\n  * [场景](#场景)\n  * [怎么预防](#怎么预防)\n    * [阻止不明外域的访问](#阻止不明外域的访问)\n    * [提交"
  },
  {
    "path": "计算机网络/跨域.md",
    "chars": 1140,
    "preview": "\n* [<a href=\"#\">什么是跨域?</a>](#什么是跨域)\n* [<a href=\"#\">同源策略</a>](#同源策略)\n* [<a href=\"#\">解决方案</a>](#解决方案)\n  * [<a href=\"#\">JSO"
  },
  {
    "path": "设计模式/单例模式.md",
    "chars": 1687,
    "preview": "\n* [单例模式](#单例模式)\n    * [懒汉式单例](#懒汉式单例)\n    * [饿汉式单例](#饿汉式单例)\n    * [双检锁/双重校验锁(DCL,即 double-checked locking)](#双检锁双重校验锁dc"
  },
  {
    "path": "设计模式/工厂模式.md",
    "chars": 1848,
    "preview": "# 工程模式\n意图:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。\n\n## 实现\n\n接口\n```java\npublic interface Shape {\n   void draw();\n}"
  },
  {
    "path": "设计模式/策略模式.md",
    "chars": 1817,
    "preview": "\n* [策略模式](#策略模式)\n* [实现](#实现)\n    * [创建一个接口](#创建一个接口)\n    * [创建实现接口的实体类](#创建实现接口的实体类)\n    * [创建Context类](#创建context类)\n   "
  },
  {
    "path": "设计模式/装饰者模式.md",
    "chars": 2524,
    "preview": "\n* [装饰器模式(装饰者模式)](#装饰器模式装饰者模式)\n    * [创建一个接口](#创建一个接口)\n    * [创建实现接口的实体类](#创建实现接口的实体类)\n    * [创建实现了 Shape 接口的抽象装饰类](#创建实"
  },
  {
    "path": "设计模式/设计模式面试题.md",
    "chars": 65,
    "preview": "\n## 观察者模式和发布订阅模式的区别\n//TODO\n\nhttps://zhuanlan.zhihu.com/p/51357583"
  },
  {
    "path": "设计模式/责任链模式.md",
    "chars": 2444,
    "preview": "责任链模式是一种行为设计模式,使得多个处理者能够在处理请求时形成一个链条。每个处理者都有机会处理请求,但如果当前处理者无法处理,则将请求传递给链中的下一个处理者。\n\n### 责任链模式的结构\n\n1. **Handler(处理者)**:定义处"
  },
  {
    "path": "面试解答/HR会问什么.md",
    "chars": 4701,
    "preview": "\n* [问题](#问题)\n    * [请简单介绍一下你自己?](#请简单介绍一下你自己)\n    * [你希望通过这份工作获得什么?](#你希望通过这份工作获得什么)\n    * [你个人短期和长期目标分别是什么?](#你个人短期和长期目"
  },
  {
    "path": "面试解答/面试解答2021-06.md",
    "chars": 75932,
    "preview": "# 点击[:rocket::rocket::rocket:]可以看到知识点在哪\n\n---\n\n# 字节腾讯三轮社招面a经(附个人回答)\n\n> 作者: 洗脸高手\n链接:https://www.nowcoder.com/discuss/65959"
  },
  {
    "path": "面试解答/面试解答2021-07.md",
    "chars": 75086,
    "preview": "# 点击[:rocket::rocket::rocket:]可以看到知识点在哪\n\n---\n\n# 微店面经(已OC)\n\n> 作者:二本菜鸡不进大厂不改名\n链接:https://www.nowcoder.com/discuss/677437?s"
  },
  {
    "path": "面试解答/面试解答2021-09.md",
    "chars": 48399,
    "preview": "# 点击[:rocket::rocket::rocket:]可以看到知识点在哪\n\n---\n\n* [9月28美团java后端二面总结](#9月28美团java后端二面总结)\n  * [mysql:](#mysql)\n    * [1.从联合索"
  },
  {
    "path": "面试解答/面试解答2021-10.md",
    "chars": 5204,
    "preview": "# 点击[:rocket::rocket::rocket:]可以看到知识点在哪\n\n---\n\n* [面试复盘 | 字节番茄小说二面(已通过)](#面试复盘--字节番茄小说二面已通过)\n  * [1. 自我介绍](#1-自我介绍)\n  * [2"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the Swayingleaves/JavaDeveloperBrain GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 115 files (1.4 MB), approximately 689.6k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!