- 普通集合
- Arrays.asList()返回的是视图(ArrayList内部类对象,只提供了替换数据的方法,其底层依旧是原数组数据)
- subList:返回的List是ArrayList中某段数据的一个视图,不可在使用时对原对 象进行操作,否则会出现CME异常
- HashMap
- 对于容量的初始化分配,在首次put操作时执行(lazy load)
ii. 并发下出现的死链问题:https://www.jianshu.com/p/619a8efcf589、https://juejin.im/post/5a255bbd6fb9a0450c493f4d
iii. JDK1.8版本中的HashMap在高并发情况下存在数据丢失问题
- 临时结点Node<K,V>可能由于并发问题被覆盖
- Entry链表转为了红黑树
- 对于容量的初始化分配,在首次put操作时执行(lazy load)
ii. 并发下出现的死链问题:https://www.jianshu.com/p/619a8efcf589、https://juejin.im/post/5a255bbd6fb9a0450c493f4d
iii. JDK1.8版本中的HashMap在高并发情况下存在数据丢失问题
- Arrays.asList()返回的是视图(ArrayList内部类对象,只提供了替换数据的方法,其底层依旧是原数组数据)
- 并发集合
- LongAdder与AtomicLong
- LongAdder(用空间换时间)
- 如何实现原子操作的:调用UnSafe.compareAndswapInt,底层指令确保操作执行原子性(比较更新操作)
iii. 原子数组(AtomicIntegerArray)
- 数组通过其构造器传入,然后AtomicIntegerArray会将数组进行一次copy操作,因此对原有的数组没有任何影响
- 存在ABA问题,如何解决:版本号机制
- ConcurrentLinkedQueue(优秀博文:https://juejin.im/entry/5b4dde4b5188251af6621e13)
- CopyOnWriteList
- Disruptor
- 使用环形数组结构,为了应对Java内存回收机制,采用数组而非链表(为什么);同时数组对处理器的缓存机制更加友好——元素位置定位
- 核心思想:CAS锁操作尽量代替原有的Lock操作
- 生产者
- 申请写入m个元素,若有m个元素可以写入,则返回最大序列号(判断是否会覆盖未读的元素)
- 遇到多个生产者重复写一个queue时,会为每个线程分配不同的一段数组空间进行操作
- 防止读到未写入的元素:新创建一个与ring buffer大小相同的buffer——avaliable buffer,某个位置写入成功,相应位置置位标记为写入成功,读取时遍历avaliable buffer
- 消费者等待策略
- ConcurrentHashMap
- 不同java版本的实现区别
- Java1.7版本中,采用segment分段锁技术,segment[]数组中每个segment对应一段table数据,每个segment可以看做一个锁,这个锁负责一部分Map数据的读写
- Java1.8版本中,直接用table数组的每个元素进行分段锁(个人觉得),通过提高锁的细粒度,更好的避免了并发冲突,cas乐观锁更新机制
- CAS:
- CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见
- CAS:
- 扩容的实现(将扩容任务分给多个线程去完成,每个线程认领各自的桶区间,对各自负责的桶区间进行resize操作——核心也是并发高效的原因之一)
- 计算每个线程可以处理的桶区间,默认16
- 让多个线程分摊Map的扩容操作,每个线程负责一部分(没有重叠部分,也就避免了资源的竞争问题)
- 实现代码,除以CPU核数是为了求出每个CPU处理的桶的个数,并让每个线程处理的桶的个数相同,避免出现转移任务不均匀的现象
- (if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE;)
- 新旧Table,正在进行resize时,原table对应的槽位会放置一个ForwardingNode节点,将find请求转发至新的table中;当别的线程发现槽位的是fwd类 的节点时,就自动跳过
- 对putVal可感知
else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else {…}
- 转发查询请求的代码
- 根据Node的hash值进行判断
- 如果对应的槽位存在实际值,则对该槽位进行加锁进行扩容操作,此时处理桶的行为是同步的
- 如果桶是链表的,则根据hash结果拆分成两个链表(高低位)
for (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) // 如果是0 ,那么创建低位节点 ln = new Node<K,V>(ph, pk, pv, ln); else // 1 则创建高位 hn = new Node<K,V>(ph, pk, pv, hn); }
- 然后通过CAS操作更新新表高低槽位,将新生成的两个链表放入,同时更新旧表中对应槽位的占位符为ForwardingNode节点
- 对putVal可感知
- 通过sizeCtl进行显示当前有多少个线程正在进行扩容操作(被volatile修饰)
- 优秀博客:https://juejin.im/post/5b00160151882565bd2582e0
- 有这么一个问题,ConcurrentHashMap,有三个线程,A先put触发了扩容,扩容时间很长,此时B也put会怎么样?此时C调用get方法会怎么样?C读取到的元素是旧桶中的元素还是新桶中的
- A先触发扩容,ConcurrentHashMap迁移是在锁定旧桶的前提下进行迁移的,并没有去锁定新桶。
- 在某个桶的迁移过程中,别的线程想要对该桶进行put操作怎么办?一旦某个桶在迁移过程中了,必然要获取该桶的锁,所以其他线程的put操作要被阻塞。因此B被阻塞。
- 某个桶已经迁移完成(其他桶还未完成),别的线程想要对该桶进行put操作怎么办?该线程会首先检查是否还有未分配的迁移任务,如果有则先去执行迁移任务,如果没有即全部任务已经分发出去了,那么此时该线程可以直接对新的桶进行插入操作(映射到的新桶必然已经完成了迁移,所以可以放心执行操作)
- A先触发扩容,ConcurrentHashMap迁移是在锁定旧桶的前提下进行迁移的,并没有去锁定新桶。
- 计算每个线程可以处理的桶区间,默认16
- 不同java版本的实现区别
- LongAdder与AtomicLong
- 字节流与字符流
- 字节流(InputStream、WriteStream)、字符流(Read、Write)
- 字节流向字符流编码的桥梁(InputStreamReader、InputStreamWriter)
- 本质上流动的都是字节,字符流只是进行了相应的编码操作
- 二者操作的基本单位不一样,字节流操作的是字节(byte=8bit),默认不使用缓冲区;而字符流操作的是Unicode码元,默认使用缓冲区
- 序列化
- Kryo:无需使用Class即可反序列化,可以在序列化时选择是否将Class信息一并序列化
- static、final、super、this关键字(this、super不能用在static方法中)以及泛型
- inal(变量、方法、类)
- 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
- 当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。
- 使用final方法的原因有两个。一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。
- static(变量、方法、类)
- 修饰成员变量和成员方法: 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:类名.静态变量名、类名.静态方法名()
- 静态代码块: 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次。静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问。
- 静态内部类(static修饰类的话只能修饰内部类): 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。
- 静态导包(用来导入类中的静态资源,1.5之后的新特性): 格式为:import static 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。
- this(引用类的当前实例——对象),并且在构造函数中,this会隐式的充当第一个参数传入
- super(用于从子类访问父类的变量和方法)
- 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, this和super是属于对象范畴的东西,而静态方法是属于类范畴的东西。
- 泛型
- (上界通配符,只能拿不能存,谁继承了T类型) 与 (下界通配符,只能存不能拿,谁是T类型的父类)以及 (某种特定类型的非原生List,既不能拿元素也不能放元素,但是可以通过迭代器获取存储的元素),List(持有任何Object类型的原生List)
- 异常体系
- 顶层都是Throwable,其中分为Exception(还可以挽救)以及Error(无法恢复)
- 受检异常与运行时异常:非RuntimeException的为受检异常
- throws用于方法签名中,throw相当于return,抛出一个异常
- 其中一个线程抛出OOM,其他线程会受影响吗
- ava的IO
- 常见的IO模型
- NIO(本质还是同步I/O,即只会通知你IO可读,在读的过程中,还是阻塞的,只不过实现了复用机制、事件机制,通过事件注册以及事件轮询)
- Reactor模式
- Proactor模式
- 两种模式的不同:Reactor框架中用户定义的操作是在实际操作之前调用的。比如你定义了操作是要向一个SOCKET写数据,那么当该SOCKET可以接收数据的时候,你的操作就会被调用;而Proactor框架中用户定义的操作是在实际操作之后调用的。比如你定义了一个操作要显示从SOCKET中读入的数据,那么当读操作完成以后,你的操作才会被调用。
- AIO(异步式I/O、还未实践过)
- 异步回调通知类,任务完成后带着结果回调complete方法,由线程池负责回调并驱动读写
- 新增的方法
- AsynchronousFileChannel: 用于文件异步读写;
- AsynchronousSocketChannel: 客户端异步socket;
- AsynchronousServerSocketChannel: 服务器异步socket。
- BIO
- java对象如何判断是否可以回收(注意,此处仅仅为判断对象是否可达,不一定判断对象是否可以回收)
- 对象引用计数器
- 方式:对象被引用时计数器加一、引用失效时计数器减一
- 优点:简单
- 缺点:面对两个对象相互循环引用,则无法回收,此时对象的引用计数器都为一(内存泄漏)
- 使用例子
- Netty自己的内存管理实现
- Redis的内存管理实现
- 根搜索算法
- 四种对象引用(如何通过软、弱引用提升JVM性能:https://www.javazhiyin.com/13546.html、http://blogxin.cn/2017/09/16/java-reference/、-XX:+PrintReferenceGC)
- 强引用
- 获取对象的方式为直接调用
- 如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误(仅抛出OOM的线程停止工作,其他线程照常执行)
- 软引用
- 获取对象的方式为get()
- 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存
- 使用价值:如果中间涉及大量的中间计算结果,由于 new Object() 是直接在堆上分配内存的,如果采用软引用的方式,可以尽快的解决内存不足的问题;可以认为是一个LRUCache缓存的实现
- 回收条件:SoftReference中有一个全局变量clock代表最后一次GC的时间点,有一个属性timestamp,每次访问SoftReference时,会将timestamp其设置为clock值。
- 根据clock-timestamp得知对象大概有多久没有被访问
- 内存空间的大小
- SoftRefLRUPolicyMSPerMB常量值(每1M空闲空间可保持的SoftReference对象生存的时长(单位ms))
- 判断是否保留软引用对象:clock - timestamp <= freespace * SoftRefLRUPolicyMSPerMB
- 弱引用
- 获取对象的方式为get()
- 只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;只能存活到下一次垃圾回收发生之前
- 使用价值(数据失效时自动更新对应的数据文件,无需人工进行代码干预)
- 虚引用
- 无法取得
- 虚引用主要用来跟踪对象被垃圾回收器回收的活动
- 虚引用必须和引用队列 (ReferenceQueue)联合使用
- 典型使用例子:com.mysql.jdbc.NetworkResources的ConnectionPhantomReference
- 强引用
- 对象生存 or 消亡
- 宣告一个对象的消亡,需要经历两次的标记过程
- 两次标记过程(两次标记走完后才可以确定对象是否可被回收)
- GC Roots的可达判断——>第一次标记并且进行一次筛选(判断对象是否有必要执行finalize方法,当对象无覆盖finalize或者已经调用过时,没必要执行finalize)——>需要执行finalize方法的对象入队列(F-Queue)——>finalizer线程执行(触发每个对象的finalize方法)——>如果重新与引用链上的对象建立关联随即逃脱消亡操作(examp:重写finalizer方法,将this对象泄露可以避免本次的对象消亡,但是仅一次)——>对对象进行第二次标记——>移除即将回收集合——>对象消亡
- 任何对象的finalize方法只会被系统调用一次
- 对象引用计数器
- java中的SPI
- 当接口属于调用方时,我们就将其称为spi,全称为:service provider interface
- 动态替换发现机制
- 服务发现核心类:java.util.ServiceLoader
- 服务提供者需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件
- 服务发现主要代码
ServiceLoader<ObjectSerializer> serializers = ServiceLoader.load(ObjectSerializer.class); final Optional<ObjectSerializer> serializer = StreamSupport.stream(serializers.spliterator(), false) .findFirst();
- 使用场景
- dubbo的服务扩展是采用spi的机制
- 数据库驱动的注册目前是采用spi机制
- Java虚拟机
- 虚拟机前导知识(虚拟机的实现分为Stack(栈)以及Register(寄存器)两种实现,Java的JVM属于栈虚拟机)
- 虚拟机结构图
- class文件
- 每个class文件的头四个字节成为魔数,用于确定这个文件是否可以被java虚拟机接受(在Travis-CI中曾经遇到JDK7环境编译JDK8的class文件,导致无法接受)
- 第5~8个字节是class版本号,第5、6个字节是次版本号,第7、8字节是主版本号
- 一切方法调用在Class中都只是符号引用,方法调用阶段唯一的任务就是确定方法调用的版本,如果方法在运行之前就有一个确定的版本,那么可以直接从符号引用转为直接引用
- 常量池
- 常量池容器计数器从1开始(由于常量池中元素的数量是不固定的)
- 存放字面常量(文本字符串以及final常量)以及符号引用(类和接口的全限定名、字段的名称和描述符、方法的名称和描述符)
- 字节码
- 创建一个对象的大致流程
- 类加载校验
- 执行 static 代码块
- 为对象分配堆内存
- 对成员变量进行初始化(对象的实例字段在可以不赋初始值就直接使用,而局部变量中如果不赋值就直接使用,因为没有这一步操作,不赋值是属于未定义的状态,编译器会直接报错)
- 调用初始化代码块
- 调用构造器函数(可见构造器函数在初始化代码块之后执行)
- new、dup、invokespecial中存在dup指令的原因,如果 new 指令执行完,那么就会出栈,如果不加一个dup指令就找不到对象实例了
- Java多态的基石——vtable
- ava虚拟机需要根据调用者的动态类型,来确定虚方法调用的目标方法,这个过程称之为动态绑定;相对于静态绑定的非虚方法调用来说,虚方法调用更加耗时
- Java虚拟机采用了空间换时间的方式来实现动态绑定,为每个类生成一张方法表,用以快速定位目标方法;方法表本质就是个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法
- ry-catch-finally实现的本质
- 本质采用了goto语句进行代码的跳转
- 当程序出现异常时,Java 虚拟机会从上至下遍历异常表中所有的条目。当触发异常的字节码索引值在某个异常条目的[from, to)范围内,则会判断抛出的异常与该条目想捕获的异常是否匹配。如果匹配,Java 虚拟机会将控制流跳转到 target 指向的字节码;如果不匹配则继续遍历异常表;如果遍历完所有的异常表,还未匹配到异常处理器,那么该异常将蔓延到调用方(caller)中重复上述的操作。最坏的情况下虚拟机需要遍历该线程 Java 栈上所有方法的异常表
- finally功能的实现(本质是将 finally 的函数进行 copy 到各个部分)
- 代码展示
- ava反射
- 在JDK中,对于Java的反射调用存在一个阈值,当调用反射的次数低于阈值时,直接使用Java原生的反射API(native)进行反射操作,如果超过阈值,则使用ASM字节码工具创建一个新的类实现新的反射调用机制
- 编译期
- 数据及控制流分析
- 检查程序局部变量在使用前是否有复值等等
- 数据及控制流分析
- 虚拟机类加载
- 类的生命周期:加载——>验证——>准备——>解析——>初始化——>使用——>卸载
- 子类引用父类的静态字段不会导致子类的初始化,只有直接定义这个字段的类才会被初始化;接口在初始化时并不要求父接口也要初始化,只有在使用到父接口时才会去初始化父接口
- 加载(用户可参与类加载的控制、仅在此阶段用户可以参与(字节码的的工作方式,相比java的proxy性能高,不需要设计反射,直接产生一个继承的class)
- 通过类的全限定名获取定义此类的二进制字节流(多种方式获取二进制字节流)
- 字节流所代表的静态数据结构转化为运行时数据结构
- 在java堆中为这个class生成java.lang.Class对象,作为方法区数据入口
- 加载时的验证流程:文件格式验证——元数据验证——字节码验证——符号引用验证
- 准备
- 正式为类变量分配内存并初始化(零值初始化)(类变量指的是被static修饰的变量,而不是指实例变量),但是如果是static final修饰的话,会直接初始化为所期望初始化的值
- 解析
- 符号引用:一组符号描述所引用的目标
- 常量池内的符号引用(引用目标不一定在内存中存在)替换为直接引用(引用目标必定在内存中存在)
- 符号引用替换为直接引用
- 类或接口的解析、字段的解析、类方法的解析、接口方法解析
- 初始化
- 主动引用会触发类的初始化
- 父类静态变量—>父类静态代码块—>子类静态变量—>子类静态代码块—>父类非静态变量(父类实例成员变量)—>父类构造函数—>子类非静态变量(子类实例成员变量)—>子类构造函数
- 类加载器
- 类+类加载器确定java类在虚拟机中的唯一性(可以根据此特性设计出一个可以加载两个版本的jar包)
- ar包依赖的隔离实现,可以使得在同一个App中同时存在两个Jar包,仅仅是版本不同而已
- 启动类加载器(Bootstrap Classloader:负责从classpath加载类,优先级最高,加载rt.jar)<= 扩展类加载器(Extension Classloader:负责加载扩展文件夹(jre/lib)中的类)<= 应用程序类加载器(Application Classloader:负责加载应用级classpath和环境变量指向的路径下的类)
- 比较两个类的前提是要在两个类位于同一classloader下,如果一个class文件被两个classloader所加载,那么所产生的类也是不同的
- 溯源委托类加载(优先级的层次关系、java推荐的机制、并不强制要求)
- 除了顶层类加载器,其余类加载器都有自己的父类加载器
- 流程:一个类加载器收到类加载的请求,不会自己尝试去加载这个类,而是把这个请求委派给父类加载器去完成,所有的类加载请求都应该传送到顶层类加载器完成,只有当父类反馈自己无法完成这个请求时,子类加载器才会自己去完成类的加载
- 好处:优先级的层次关系,通过这种层级关系可以避免类的重复加载(类的唯一性确定的机制)
- 可以不采用溯源委托加载:重写loadClass方法;如果不想破坏溯源委托机制,只需要重写findClass方法
- 线程上下文加载器(Thread Local ClassLoader)——使用场景:java的spi
- Object类在程序的各种类加载器环境中都是一个类——Bootstrap ClassLoader加载
- 类+类加载器确定java类在虚拟机中的唯一性(可以根据此特性设计出一个可以加载两个版本的jar包)
- JVM虚拟机调试
- Native Heap区被打散为sub-pools(为应用系统在多核心CPU和多Sockets环境中高伸缩性提供了一个动态内存分配的特性增强)
- ChunkPool:堆外内存申请,创建一个堆外内存池,降低内存的malloc/free的系统开销
- Java内存
- 内存模型
- Java对象在内存中的存储布局
- Java对象头:每个Java对象都有一个对象头,保存对象的系统信息,对象头存在一个称为Mark World的部分,他是锁实现的关键,存放着对象的hash值、对象的年龄、锁的指针信息;一个对象是否使用锁、占用哪个锁,都记录在这个信息里面了
- 实例数据:对象真正存储的有效信息
- 对齐填充:占位符的作用,仅仅为了将内存占用空间凑为8字节的整数倍
- Java虚拟机运行时数据区(通俗来说就是堆栈)
- 方法区(所有线程共享的区域、常量池(java8已被移动到堆中)、被虚拟机加载的类的信息、常量、静态变量、对象引用)、虚拟机栈(线程私有、执行java方法、StackOverFlow或者OOM,与栈内存申请有关)、本地方法栈(为Native方法服务,StackOverFlow或者OOM,与栈内存申请有关)、堆(所有线程共享的一块内存区域、存放对象实例、虚拟机启动时创建、垃圾收集器管理的主要区域、对堆内部的划分只是为了更好的回收内存与分配内存,如果分配对象时空间不足,OOM)、程序计数器(线程私有、指令跳转、如果执行native方法时为undefined,唯一不报OOM)、直接内存(Java直接操作堆外内存,如果使用不当会导致OOM)
- 堆、方法区和栈的关系
- 栈帧是方法运行时期的基础数据结构
- 栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,栈帧随着方法调用而创建,随着方法结束而销毁,栈帧的存储空间分配在 Java 虚拟机栈中,每个栈帧拥有自己的局部变量表(Local Variables)、操作数栈(Operand Stack) 和 指向运行时常量池的引用
- 每个栈帧内部都包含一组称为局部变量表(Local Variables)的变量列表,局部变量表的大小在编译期间就已经确定。Java 虚拟机使用局部变量表来完成方法调用时的参数传递,当一个方法被调用时,它的参数会被传递到从 0 开始的连续局部变量列表位置上。当一个实例方法(非静态方法)被调用时,第 0 个局部变量是调用这个实例方法的对象的引用(也就是我们所说的 this )
- 操作数栈
- 每个栈帧内部都包含了一个称为操作数栈的后进先出(LIFO)栈,栈的大小同样也是在编译期间确定。Java 虚拟机提供的一些字节码指令用来从局部变量表或者对象实例的字段中复制常量或者变量到操作数栈,也有一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用时,操作数栈也用来准备调用方法的参数和接收方法返回的结果。
- java堆
- 划分方式
- 新生代(eden空间、from survivor空间、to survivor空间)、老年代(对象存活周期长)
- 需要两个survivor空间的原因:降低老年代GC的频率、降低内存空间的碎片化(主要原因,新生代采用复制回收算法)
- 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC
- Survivor的存在意义,就是减少被送到老年代的对象
- 为什么新生代垃圾回收算法采用复制回收算法
- 简单、高效,并且新生代的对象大多都是朝生暮死,生命周期短,mirror gc频率高,因此对于垃圾回收算法的选择,越简单高效越好
- survivor空间的存在,from1与from2,使得可以使用复制回收算法
- 为什么老年代采用标记整理、标记清除算法
- 首先老年代的对象存活的时间都比较长,存活率基本都很高,同时需要预留较多得到内存,因此就不是适用复制回收算法
- 需要两个survivor空间的原因:降低老年代GC的频率、降低内存空间的碎片化(主要原因,新生代采用复制回收算法)
- 小trip:PermGen(永久代),在java1.8版本中,String常量池已经从方法区移到了堆中
- 新生代(eden空间、from survivor空间、to survivor空间)、老年代(对象存活周期长)
- java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可
- 划分方式
- 方法区(包括运行时常量池)
- 存储信息:类信息、常量、静态变量、编译器编译的代码
- 在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。
- java对象的访问
- 句柄访问方式:句柄池、稳定的句柄地址、开销多(多一次指针定位时间开销)
- 直接指针访问方式
- 无用类的三个必要条件(类可以被回收,不是一定被回收)
- 该类的所有实例已被回收
- 加载该类的classloader已被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用、无法通过反射访问该类方法
- 虚拟机会报错的异常
- 除了程序计数器外,java虚拟机运行时数据区都有可能出现OOM异常
- StackOverFlowError: 若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
- OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
- java垃圾收集算法
- 标记-清除法(可用于老年代)
- 复制算法(可用于新生代、内存被缩小为原来的一半)
- 标记-整理算法(压缩算法)(可用于老年代)
- 分代收集算法
- 根据对象的存活周期,选择上述合适的算法进行内存收集
- 如何加快新生代的垃圾回收
- 卡表:维护一个Card Table,比特位记录老年代是否持有某新生代的对象引用,可以避免扫描老年代对象
- 虚拟机为线程分配空间的注意点(相同的理念——redis的内存分配机制也是有类似的思想)
- 优先在一块叫做TLAB的区域,对于体积不大的对象,直接在此处分配,失去了在老年代分配对象的机会
- TLAB(Thread Local Allocation Buffer):线程本地分配缓存,线程专用的内存分配区域
- TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB,而在老TLAB里的对象还留在原地什么都不用管——它们无法感知自己是否是曾经从TLAB分配出来的,而只关心自己是在Eden里分配的
- 每次分配 TLAB 的大小不是固定的,而是每个线程根据该线程启动开始到现在的历史统计信息来自己单独调整的。如果一个线程上跑的代码的内存分配速率非常高,则该线程会选择使用更大的 TLAB 以达到均摊同步开销的效果,反之亦然;同时它还会统计浪费比例,并且将其放入计算新 TLAB 大小的考虑因素当中,把浪费比例控制在一定范围内
- 均摊对GC堆(Eden区)里共享的分配指针做更新而带来的同步开销
- 为了加速对象分配而产生的
- 本身的占用了Eden内存区域
- Java垃圾收集器(算法的实现)
- Serial收集器(可收集新生代)
- 单线程收集器、复制算法、进行垃圾收集时,必须暂停其他所有的工作环境线程直到垃圾收集结束
- ParNew收集器(复制算法)
- 可以看作是Serial收集器的多线程版本
- CMS收集器(可收集老年代)

- 初始标记:标记GC Roots能直接关联到的对象(存在停顿);并发标记:进行GC Roots Tracing的过程;重新标记(停顿):修正并发标记期间用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
- 并发收集器、标记-清除算法、让垃圾收集线程与用户工作线程同时工作
- 获取最短回收停顿时间为目标、重视响应速度
- 对CPU资源敏感、无法处理浮动垃圾(由于垃圾收集线程与用户工作线程并发执行的后果、可能导致Full GC动作)
- 可以设置参数——多少次CMS后执行一次内存压缩的操作避免内存碎片
- 由于在垃圾收集阶段,用户线程还需要运行,因此需求预留出足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎被完全填满了再收集,如果预留的空间无法满足需求,则会触发CMF
- Parallel Scavenge收集器(复制算法、吞吐量优先收集器)
- 使用复制算法、并行多线程、可控制的吞吐量
- 相比ParNew收集器的重要区别是存在自适应调节策略(不需要设定新生代大小、Eden与Survivor区的比例、晋升老年代的对象年龄)
- Serial Old收集器(可收集老年代)
- 单线程收集器、标记-整理算法
- Parallel Old收集器(可收集老年代)
- 多线程、标记-整理算法
- G1收集器
- Java中的stop the world(STW)
- 为了让垃圾回收器正常且高效的执行,大部分情况下会要求系统进入一个停顿的状态(终止所有应用线程的执行,才不会有新的垃圾;同时保证了系统在某一瞬间的一致性;使垃圾回收器更好的标记垃圾对象)
- 停止其他非垃圾回收线程的工作,直到完成垃圾回收;Java中的Stop-the-worldd是通过安全点机制来实现的
- Serial收集器(可收集新生代)
- Java对象内存分配策略
- 优先在新生代Eden区分配——>Minor GC(触发条件:当Eden区满时),频繁、速度快(因此垃圾回收算法的选择更适合选用标记-复制算法)
- 空间分配担保
- 发生mirror gc之前,会去判断老年代的最大可用连续空间是否大于新生代的所有对象总空间,条件成立的话,那么mirror gc发生可以确保是安全的;否则执行一次 or 多次的full gc
- 空间分配担保
- 大对象直接进入老年代(需要大量连续空间的java对象)
- 程序应避免朝生夕灭的大对象创建
- 长期存活的对象进入老年代
- 对象拥有对象年龄计数器:第一次Minor GC后Eden中的对象存活并且可以被Survivor容纳,对象移动到Survivor,对象年龄加一,在Survivor区中每经历一次Minor GC(频繁、速度快),对象年龄加一,一定程度后晋升至老年代
- 动态对象年龄判定
- 若Survivor空间中相同年龄的对象大小总和等于Survivor空间的一半,则对象年龄大于等于该年龄进入老年代
- 老年代的Full GC(触发条件:老年代空间已满、System.gc、方法区空间不足)
- 卡表技术
- 场景:老年代的对象可能引用新生代的对象,在标记存活对象的时候,需要扫描老年代的对象,如果该对象拥有对新生代对象的引用,那么这个引用也会被称为GC Roots——因此有做了一次全表扫描
- 解决方案:
- 将整个堆划分为一个个大小为512字节的卡,并维护一个卡表,用来存储每张卡的一个标识位,这个标识位代表对应的卡是否可能存有指向新生代对象的引用,如果存在,则代表这张卡是脏的;在进行Minor GC的时候,可以不用扫描整个老年代,而是在卡表中寻找脏卡,将脏卡中的对象加入到Minor GC的GC Roots中,完成所有的脏卡扫描后,Java虚拟机便会将所有的脏卡的标识位清零
- Minor GC伴随着存活对象的复制,而复制需要更新指向该对象的引用,因此,在更新引用的同时,又会设置引用所在的卡的标识位,为了确保每个可能指向新生代对象引用的卡都被标记为脏卡,那么Java虚拟机需要截获每个引用型实例变量的写操作,并作出对应的写标识位操作(即时编译器生成的机器代码中 ,需要插入额外的逻辑——写屏障,写屏障不会判断更新后的引用是否指向新生代中的对象,而是一律当做指向新生代对象的引用)
- 优先在新生代Eden区分配——>Minor GC(触发条件:当Eden区满时),频繁、速度快(因此垃圾回收算法的选择更适合选用标记-复制算法)
- JVM相关虚拟机参数
- 垃圾回收:-XX:(+PrintGCDetails、+PrintAHeapAtGC 打印GC前后的堆情况、+PrintGCTimeStamps 分析GC发生的时间)、-Xloggc:log/gc.log 设置GC打印到文件中
- 类的加载与卸载:-XX:+TraceClass[Loading/Unloading](特别是用于观察动态生成类的加载、卸载的过程)、-XX:PrintClassHistogram 查看系统类的分布情况 iii. 堆的配置参数:-Xms20m指定堆的大小、-Xmx指定堆的最大空间大小(注意,实际可用的空间大小与-Xmx参数配置的存在偏差)、-Xmn 设置新生代的大小、-XX:SurvivorRatio:设置新生代中eden空间和from/to空间的比例关系、-XX:NewRatio 设置新生代与老年代的比例
- 内存错误:-XX:+HeapDumpOnOutOfMemoryError 内存溢出时导出整个堆信息、-XX:HeapDumpPath 指定信息到处存放的路径
- 栈配置:-Xss 指定线程的栈大小
- 堆参数的配置
- 非堆内存参数设置
- 设置最大直接内存大小(Java堆外内存),如果超出设置,依旧会导致系统OOM,另外,该内存区域也存在垃圾回收,并且直接内存不适用于频繁申请空间的场景,更适合于申请次数较少,访问频繁的场景
- 虚拟机的工作模式
- Client、Server
- 查看Java进程:jps、查看虚拟机参数设置:jinfo、导出堆到文件:jmap(对象统计信息、(-heap)当前堆快照信息、查看finalizer队列中的垃圾对象)、jhat(自带的堆分析工具,对*.hprof文件进行分析,直接在http访问分析的结果) x. 查看虚拟机运行时命令:jstat(-gc pid 打印GC相关的堆信息)(-gccause pid 打印最近一次GC,以及导致GC的原因)
- 查看线程堆栈信息:jstack(jstack -l pid > a.txt)
- Java锁机制
- Java虚拟机在释放锁时,同样会强制刷新缓存,使得当前线程所修改的内存对其他线程可见
- Lock锁
- Lock锁与jvm的synchronized相比,Lock是可中断的锁、可超时获取锁、尝试非阻塞的获取锁,而synchornized是不可中断的
- 什么是可中断:如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁
- ReentrantLock
- 支持获取锁时是采用公平原则还是非公平原则
- 公平原则获取锁时,多了hasQueuedPredecessors()方法判断,判断当前节点是否有前驱节点,如果有,等待前驱节点获取并释放锁后才可以获取锁
- 支持锁的重入(锁被线程A所持有,当线程A再次进入时可以再次获取到锁、再次获取锁时只是更新同步状态值、对当前线程进行锁持有者的判断、假设线程A重复n次获取锁,那么最终释放锁时也需要释放n次)
- 对于独占锁,内部存在一个变量记录当前持有独占锁锁的线程
- 锁支持重入的原因:内部有与线程相关的计数器,获取一次锁,计数器加一;释放一次锁,计数器减一
- 支持获取锁时是采用公平原则还是非公平原则
- ReentrantReadWriteLock
- 定义了读锁与写锁两个方法
- 读锁之间不互斥,只要有写锁就会产生互斥,保证了写操作对读操作的可见性,适用于读多写少的应用场景
- 对int32进行按位划分,高16位为读锁状态,低16位为写锁状态,通过位运算进行判断
- 每个线程的各自获取读锁的次数信息保存在ThreadLocal上
- 写锁的降级(读优先于写、数据实时连续性:当获取到最新的数据时需要马上根据最新的数据进行处理)
- Condition(golang的cond以及lock)
- condition依赖于Lock对象、定义了等待/通知两种类型
- 每个condition对象包含一个队列(FIFO)、没有采用cas保证更新过程,因为已经采用了锁来保证了(依赖于Lock对象,在condition使用之前必须lock.lock()上锁)
- Lock锁与jvm的synchronized相比,Lock是可中断的锁、可超时获取锁、尝试非阻塞的获取锁,而synchornized是不可中断的
- Synchronized(偏向锁->轻量级锁->重量级锁(重量级锁涉及用户态与内核态的切换),锁的重量逐渐递增)
- 偏向锁与轻量级锁的图示
- 对象头信息
- 偏向锁
- 该类型的锁会偏向于第一个获得它的线程,当持有该偏向锁的线程进入同步块时,虚拟机可以不再进行任何同步操作
- 一旦有其他线程去尝试获取这个锁,偏向模式就宣告结束
- 不适合竞争激烈的场景,对象头会记录获得锁的线程信息
- 轻量级锁
- 使用时会先备份对象的原有的对象头信息,然后采用CAS操作将Basiclock的地址对对象头进行替换操作,如果替换成功则加锁成功,否则轻量级锁有可能膨胀为重量级锁;如果要判断某一线程是否持有对象的锁,仅需简单的判断对象头的指针是否在当前线程栈空间的地址范围内
- 自旋锁:执行一个空循环,在若干个空循环后线程如果可获得锁,则继续执行,否则线程将会被挂起
- 出现异常锁自动释放、可重入锁
- 静态synchronized锁的对应的class类
- 监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的
- synchronized 同步语句块的情况:synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令;会自动加上异常 try-catch -finally,在finally中加上锁的释放操作
- synchronized 修饰方法的的情况:synchronized 修饰的方法是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
- 修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
- 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁(因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁)
- 单例模式的实现
- 内部类(类加载以及初始化的机制)
public class SingletonIniti { private SingletonIniti() { } private static class SingletonHolder { private static final SingletonIniti INSTANCE = newSingletonIniti(); } public static SingletonIniti getInstance() { return SingletonHolder.INSTANCE; } } ``` - 双重校验锁 ```java public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { if (uniqueInstance == null) { synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } } ```
- java多线程
- 在HotSpot VM中,Java线程被一对一映射为本地操作系统线程
- 变量值在线程间传递需要通过主内存来完成(java内存模型的要求)
- 核心——AQS(https://juejin.im/post/5a4a4530518825697078553e)

- 构建锁以及同步器的框架(本质就是一个队列——CLH)
- 原始的CLH采用locked自旋,而ASQ中的CLH在每个node里面使用一个状态字段来控制阻塞,而不是自旋
- 为了可以处理timeout和cancel操作,每个node维护一个指向前驱的指针。如果一个node的前驱被cancel,这个node可以前向移动使用前驱的状态字段
- 中断补偿的原因:由于Lock设计成了可以响应中断的获取锁,因此通过当前获取锁的方式是否支持中断响应来决定是否进行相应的中断补偿
- 共享资源采用volatile修饰(线程间的可见性);对于资源的占用方式采用独占或者共享两种模式
- 核心思想:被请求的共享资源空闲,将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态
- 线程节点Node中存储着当前线程的等待状态信息:CANCELLED 等待超时/被中断、SIGNAL 待唤醒状态、CONDITION 处于等待队列中、PROPAGATE 共享模式有关
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补偿
- 核心对象——Node
- acquireQueued(Node,int):用于线程资源申请
- Volatile(解决数据的可见性,对volatile的读写是原子性的,但是不保证复合操作是原子性)
- 多线程访问volatile关键字不会发生阻塞
- 由于内存写操作同时会无效化其他处理器所持有的、指向同一内存地址的缓存行,因此可以认为其他处理器能够立即见到该volatile字段的最新 值。
- 保证变量对所有线程的可见性、新值对于其他线程是立即可知的(其他线程去读时将被强制要求去读主内存中的变量值,线程拷贝的值被过期)
- 写一个volatile变量时,JVM会把线程对应的本地内存中的共享变量刷新到主内存
- 读一个volatile变量时,JVM会把线程对应的本地内存置为无效,使得线程必须从主内存中读取共享变量最新的值
- 如果volatile修饰的是数组,那么仅仅保证对象获取数组的地址具有可见性,数组内的元素不具有可见性
- 会限制指令重排序
- 典型示例
- 单例模式中,java对象的创建分为三步骤:1.为对象分配内存空间——>2.初始化对象——>3.将对象指向分配的内存地址(2、3步骤存在指令重排序)
- 典型示例
- CountDownLatch
- 类似thread的join方法,可以设置等待的n个线程或者n个步骤(可用于控制多个线程同时运行)
- countDown():消费一个cnt,await():等待cnt==0
- 只允许使用一次
- CyclicBarrier(同步屏障)
- 线程执行await方法告知已到达屏障,当规定的n个线程到达屏障时,屏障解放,不阻塞线程
- 可调用rest方法进行重置重复使用
- Semaphore(信号量)
- 控制同时访问资源的线程数量(类似于流量控制)
- acquire获取资源、release方法释放资源
- Yeild使用
- 让出当前CPU资源,让其他线程来竞争;当大量的线程执行yeild时,导致大量的线程在竞争资源,因此会导致CPU利用率高达100%
- 线程
- 如果直接调用run方法则不是异步执行,而是又回到了最初的顺序执行;只有调用Thread的start方法才是开启另一个线程去执行
- 线程优先级具有继承性(A线程启动B线程,B线程优先级与A线程一样)
- 非线程安全存在于实例变量中,方法内部的私有变量则不存在线程安全的问题(线程的方法栈)
- 守护线程(Daemo),为其他线程提供便利服务(JVM中的GC线程就是一个守护线程),当一个Java程序只存在守护线程时,程序随即退出;
- Thread.interrupt
- 如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
- Leader-Follower线程模型
- 在Leader-follower线程模型一开始会创建一个线程池,并且会选取一个线程作为leader线程,leader线程负责监听网络请求,其它线程为follower处于waiting状态,当leader线程接受到一个请求后,会释放自己作为leader的权利,然后从follower线程中选择一个线程进行激活,然后激活的线程被选择为新的leader线程作为服务监听,然后老的leader则负责处理自己接受到的请求(现在老的leader线程状态变为了processing),处理完成后,状态从processing转换为。Follower
- 这种模式下接受请求和进行处理使用的是同一个线程,这避免了线程上下文切换和线程通讯数据拷贝
- ScheduledThreadPoolExecutor中的DelayedWorkQueue的实现中采用了此线程模式
- 线程状态
- ThreadLocal
- 创建只能被同一个线程读写的变量(每个线程拥有一个自己的共享变量、每个线程绑定自己的值、存放每个线程的私有数据)
- ThreadLocal的特殊性保证了其能够满足事务的实现、保证当前线程操作的都是同一个Connection
- 内部数据key-value形式的存放由ThreadLocalMap对象实现
- Map的key是每个线程引用的ThreadLocal对象
- ThreadLocalMap的内部有一个Entry对象(继承了WeakReference)
- ThreadLocalMap的key是LocalThread对象本身,value则是要存储的对象,ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value
- 副作用
- 存在脏数据以及内存泄露(常见于线程池中的线程使用ThreadLocal)
- 脏数据:thread的复用,可能导致线程读取到上一个线程缓存的信息
- 内存泄露:由于thread持有threadlocal引用,因此触发弱引用机制回收就显的不现实
- 解决方案:根据业务场景在适合的地方执行remove方法,进行清除数据
- 存在脏数据以及内存泄露(常见于线程池中的线程使用ThreadLocal)
- 应用场景:多源数据库读写的切换,为了确保每个线程连接的数据库源不被外部所影响,用ThreadLoacl保存该线程所连接的数据库源标识信息
- InheritableThreadLocal(父线程传递本地变量到子线程)
- 开源项目:https://github.com/alibaba/transmittable-thread-local
- Thread维护了两个变量:ThreadLocal以及InheritableThreadLocal
- 在线程的构造函数中有一个init(…)函数,会获取创建该线程的父线程信息,进行父线程信息同步给子线程;但是在线程池模式下,存在线程复用的情况,那么这个时候就无法再次执行init函数将父线程的信息赋值给子线程
- 线程间通信
- Object类方法的notify、notifyAll、wait方法(wait()/notify()时,必须拥有该对象的同步锁)
- notify:通知一个在对象上等待的线程,使其从wait方法返回(前提是该线程获取了该对象的锁)
- notifyAll:通知所有在对象上等待的线程
- wait:使线程进入waiting状态
- 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
- 管道输入、输出流
- 字节管道PipedOutputStream、PipedInputStream,字符管道PipedReader、PipedWriter
- 输入、输出管道需要进行连接(out.connect(in))
- Thread.join
- 等待join的线程终止后从join返回
- 其内部实现依旧是依靠Object类的等待/通知机制实现,线程终止时会调用notifyAll
-
public final synchronized void join()throws InterruptedException { 条件不满足,继续等待 while(isAlive()){ wait(0) } 条件符合,返回返回 }
- Object类方法的notify、notifyAll、wait方法(wait()/notify()时,必须拥有该对象的同步锁)
- 死锁
- 当使用Future模式时,可能存在自己把自己挂起导致线程死锁的问题
- 线程池
- java线程池会将守护线程转为用户线程进行运行
- 线程池参数
- corePoolSize(核心线程数量)
- runnableTaskQueue(任务队列、当线程数达到核心线程数量时,任务进入队列中等待调度执行,如果队列是有界的,则当队列有界时,判断当前的最大线程数量与当前线程数量的关系决定是否可以创建新的线程执行任务)
- ArrayBlockingQueue:必须要有初始队列大小
- LinkedBlockingQueue
- SynchronousQueue:不是一个真正的队列,而是一种在线程之间移交的机制。要将一个元素放入SynchronousQueue中, 必须有另一个线程正在等待接受这个元素. 如果没有线程等待,并且线程池的当前大小小于最大值,那么ThreadPoolExecutor将创建 一个线程, 否则根据饱和策略,这个任务将被拒绝
- PriorityBlockingQueue:优先级队列,有界队列,内部通过比较器实现
- DelayedWorkQueue:延迟的工作队列,无界队列
- maximumPoolSize(线程池最大数量,当任务队列满时,判断线程数量是否到达此值,创建临时线程跑任务)
- ThreadFactory(设置创建线程的工厂、当任务抛出异常时,线程相当于停止——即worker消亡)
- RejectedExecutionHandler(饱和策略、直接抛出异常、只用调用者所在的线程执行任务、丢弃队列里最近一个任务并执行当前任务、抛弃任务)
- keeyAliveTime(线程活动保持时间,终止前多余的空闲线程等待新任务的最长时间)
- TimeUnit(线程活动保持时间单位)
- 线程池的处理流程
- 任务提交方式
- execute:用于提交不需要返回值的任务
- submit:用于提交需要返回值的任务(feature对象)
- Executor框架(异步任务框架)
- 两级调度模型:任务通过Executor框架映射到java线程,java线程通过操作系统映射到硬件处理器、Executor框架(用户级调度器)控制上层的调度、下层的调度由操作系统内核控制,下层的调度不受应用程序的控制
- 几种实现
- FixedThreadPool:固定线程数目
- SingleThreadExecutor:单个线程,适用于顺序执行各个任务
- CacheThreadPool:大小无界的线程池、执行短期异步任务的小程序
- ScheduledThreadPoolExecutor:若干线程的周期任务
- SingleThreadScheduledExecutor:单线程的周期任务
- CompletionService:带有完成任务队列的任务提交池,能够获取完成的任务(内部存在这样的一个已完成任务队列)
- Future
- 链式问题
- CompletableFuture能够较好的解决future之间的存在数据联系时的链式调用
- 链式问题
- ForkJoin框架(适用于将一个任务变为并行的数个小任务)
- 大任务分成数个小任务,结果集合并
- golang版本的实现:https://github.com/chuntaojun/go-fork-join
- Java中的安全模型
- 本地代码默认为可信任的(可访问一切本地资源),而远程代码为不可信任的(安全依赖于沙箱机制,将代码限定于JVM的特定运行访问内)
- 增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制
- 引入了域 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。存在于不同域中的类文件就具有了当前域的全部权限
- 访问控制上下文的继承问题。当一个线程创建另一个新线程时,会同时创建新的堆栈。如果创建新线程时没有保留当前的安全上下文,也就是线程相关的安全信息,则新线程调用 AccessController.checkPermission 检验权限时,安全访问控制机制只会根据新线程的上下文来决定安全性问题,而不会考虑其父线程的相应权限
- Spring
- 事务管理(TransactionAspectSupport类)
- 使用了大量的ThreadLocal类进行保存当前线程的信息,当事务切换时,本质上就是获取一个新的数据库连接然后把事务同步管理器中的 ThreadLocal 变量替换掉
- 优秀博客:https://huzb.me/2019/03/28/Spring-AOP%E6%BA%90%E7%A0%81%E6%B5%85%E6%9E%90%E2%80%94%E2%80%94%E4%BA%8B%E5%8A%A1%E7%9A%84%E5%AE%9E%E7%8E%B0/
- readOnly:设置当前事务是否为只读事务
- rollbackFor:设置需要回滚的异常类型
- propagation:设置事务传播行为
- 支持当前事务,如果当前没有事务,则新建一个事务
- 支持当前事务,如果当前没有事务,则以非事务方式运行
- 支持当前事务,如果当前没有事务,则抛出异常
- 新建事务,如果存在当前事务,则当前事务挂起
- 非事务执行,如果当前存在事务,则当前事务挂起
- 非事务执行,如果当前存在事务,则抛出异常
- 当前存在事务,则在嵌套事务内执行:嵌套事务的本质是对外部事物做一次save point机制,内部事务的回滚都是回滚到保存点
- isolation:设置底层数据库的事务隔离级别
- timeout:设置事务超时秒数
- 执行带有事务注解的方法
- spring之所以能够接管数据库的事务管理,是因为提供了接口:org.springframework.transaction.PlatformTransactionManager ,继承该接口,各个平台实现自己的事务管理
- SpringMVC路由<=>方法(DispatcherHandler)
- HttpWebHandlerAdaper、SimpleHandlerAdapter、ExceptionHandlingWebHandler、WebHandlerDecorator
- 流程
- 接收到Http请求时进入HttpWebHandlerAdaper
- 将请求包装为ServerWebExchange:ServerWebExchange exchange = createExchange(request, response);
- 将ServerWebExchange送入到WebHandlerDecorator
- 接着送入ExceptionHandlingWebHandler
- 进入ReactorHttpHandlerAdapter
- 注册(将几个Handler的实现进行注册到Spring中)
- 进行URL与handler的匹配(lookupHandlerMethod)
- SpringBoot相关
- @SpringBootApplication
- 由三大注解的组合:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan
- @SpringBootConfiguration:将当前类标注为配置类,并且将当前类里以@Bean注解标记的方法实例注入到Spring容器中
- @EnableAutoConfiguration:启动自动配置功能,将所有符合条件的@Configuration配置都加载到当前的IOC容器中(通过spring.factories文件进行自动配置)
- 借助 AutoConfigurationImportSelector ,通过实现selectImports()导出Configuration,依赖于SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader())读取classpath下的spring.factories文件来导出所有的类
- @ComponentScan:扫描包下的所有类查看是否被标注了特定的注解,进而进行Bean的生成以及相应的IOC
- 获取当前main方法所在的类
- new RuntimeException().getStackTrace()获取StackTraceElements[]数组,然后遍历比较方法名,通过StackTraceElement.getClassName获取当前main方法的类名,然后利用反射获取main方法所在类的Class对象
- 启动流程
- 获取并创建SpringApplicationRunListener并且由其通知starting——>创建参数,配置Environment——>SpringApplicationRunListener通知environmentPrepared——>创建ApplicationContext——>初始化ApplicationContext(设置application-context类型)、设置Environment、加载相关配置——>SpringApplicationRunListener通知contextPrepared、contextLoaded(告知Spring应用使用的Application已经装填完毕)——>refresh ApplicationContext——>SpringApplicationRunListener通知started——>完成最终程序启动
- 代码
- 由三大注解的组合:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan
- @SpringBootApplication
- IOC(控制反转)
- Bean管理
- ApplicationContext容器管理bean
- java的代理类机制实现(Proxy)
- CGLib的动态字节码库代理实现(相比java的代理机制实现更为强大,不需要实现接口,相当于生成一个新的类,将类的字节码装入虚拟机而不需要通过反射) iv. Spring Aware:使得Bean意识到Spring容器的存在,使得调用Spring所提供的资源
- BeanFactory与ApplicationContext的联系以及区别
- 区别
- BeanFactory是延迟加载,使用到才会去创建Bean,ApplicationContext会在初始化的时候就加载并且检查
- 联系
- ApplicationContext继承BeanFactory,并且提供了更多面向应用的功能,面向的是Spring的开发者;BeanFactory是Spring的基础设施,更多的是面向Spring
- 区别
- ApplicationContextInitialize执行
- ConfigurationClassPostProcessor执行(优先执行)
- postProcessBeanDefinitionRegistry(如果存在Aware,则优先执行Aware回调,然后再执行)
- postProcessBeanFactory
- Bean的注入
- 注册与解析BeanDefintion
- 注册和解析BeanDefinition,发生在AnnotationConfigApplicationContext#register流程中,其方法内部使用了AnnotatedBeanDefinitionReader#register来实现BeanDefinition的解析和注册;而且在实例化AnnotatedBeanDefinitionReader后,立即向container注册了多个BeanPostProcessor的BeanDefinition
- 准备BeanFactory
- 在AnnotationConfigApplicationContext内部,组合了DefaultListableBeanFactory。在prepareBeanFactory(beanFactory)方法的调用过程中,向beanfactory注入了环境变量、环境属性等。而且注入了多个BeanPostProcessor
- 调用BeanFactoryProcessor(此时所有的BeanDefinition都已加载完毕)
- 此时container已经注册了一系列的BeanFactoryPostProcessor、BeanPostProcessor和应用层相关的bean的BeanDefinition。由于此时所有的bean(包括BeanFactoryPostProcessor、BeanPostProcessor已经应用层的bean)都是以BeanDefinition存在于container中,并未实例化
- 注册BeanPostProcessor
- 对注册到BeanFactory中的BeanPostProcessor进行实例化,添加到BeanFactory中的BeanPostProcessor处理队列中
- 真正实例化和初始化Bean
- 与加载所有已注册的所有Bean
- BeanFactory中,对一个Bean调用getBean方法才会进行Bean的加载,而ApplicationContext则是直接触发其内部的BeanFactory加载所有定义好的Bean;在加载bean的过程中,涉及三个步骤
- 实例化、填充属性、初始化
- 注册与解析BeanDefintion
- 解决类与类之间的依赖关系、将类的管理移交给Spring框架
- 依赖于Java的反射机制来实现
- BeanDefinition(容器实现依赖反转功能的核心数据结构)
- Bean的三级缓存结构(解决循环依赖问题:AbstractBeanFactory.doGetBean)
- 博客地址:https://segmentfault.com/a/1190000015221968
- 缓存结构定义于DefaultSingletonBeanRegistry类中
- singletonFactories:单例对象工厂cache(存放 bean 工厂对象,用于解决循环依赖、三级缓存)
- earlySingletonObjects:提前曝光的单例对象的cache(此缓存是由于解决循环依赖的重要部分)、正在构建但是构建未完成的对象、构造函数已经执行(属性尚未填充、被称为早期引用)
- singletonObjects:单例对象的cache(一级缓存、由于存在并发情况,采用ConcurrentHashMap,完全初始化好的Java对象)
- 三级缓存获取到对象后,从三级对象移除(因为ObjectFactory调用getObject()会创建bean)并放入二级缓存中
- Bean的生命周期
- Bean容器找到配置文件中 Spring Bean 的定义。
- Bean容器利用Java Reflection API创建一个Bean的实例。
- 如果涉及到一些属性值 利用set方法设置一些属性值。
- 如果Bean实现了BeanNameAware接口,调用setBeanName()方法,传入Bean的名字。
- 如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。
- 如果Bean实现了BeanFactoryAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。
- 与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。
- 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执行postProcessBeforeInitialization()方法。
- 如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。
- 如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。
- 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执行postProcessAfterInitialization()方法。
- 当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy()方法。
- 当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。
- Bean管理
- AOP(切面编程)
- Application事件和监听器(可以在容器加载过程中执行自己的业务代码)
- 运行开始(除监听器注册和初始化以外)=>ApplicationStartedEvent
- Environment将被用于已知的上下文,但在上下文被创建前 =>ApplicationEnvironmentPreparedEvent
- refresh之前,bean定义已被加载后 =>ApplicationPreparedEvent
- refresh之后,相关回调处理完 =>ApplicationReadyEvent
- Spring Cloud如何实现配置刷新
- 事务管理(TransactionAspectSupport类)
- Mybatis
- SqlSessionFactoryBuilder
- 唯一的作用就是创建SqlSessionFactory
- 生命周期仅仅局限于方法内部
- SqlSessionFactory
- 具有两个默认实现、创建SqlSession
- 通过文件流获取.xml的配置文件,将配置文件信息缓存到Configuration对象,然后创建SqlSessionFactory对象
- SqlSession
- SQLMapper
- 框架生成MapperMethod对象
- 执行一条条sql语句
- 仅仅是一个接口(java中的动态代理需要接口对象进行实现),不包含具体的逻辑实现
- 由java自带的动态代理 or CGLib字节码库创建出的代理类去具体实现每个接口的方法
- 映射器内部组成(核心)
- MappedStatement:保存映射器的节点
- SqlSource:根据参数以及规则组装sql
- BoundSql:建立SQL和参数的地方
- parameterMappings:是一个List对象,用于描述参数的具体信息,可以结合PreparedStatement找到parameterObject设置参数
- parameterObject:参数本身、如果是多个参数会自动专为Map<String,Object>对象(如果没有@Param注解时String值为“1”或者“param1”),如果存在@Param注解时String的值为@Param的值
- sql:书写在映射器的sql语句
- Executor执行器调度StatementHandler、ParameterHandler、ResultHandler
- 通过Configure对象创建,同时需要事务对象final Executor executor = configuration.newExecutor(tx, execType);
- Executor:真正执行java和数据库交互的东西(三种执行器:SIMPLE、REUSE、BATCH)
- StatementHandler:专门处理数据库会话,真实对象为RoutingStatementHandler(而其下又分为三种Handler:SimpleStatementHandler、PreparedStatementHandler、CallableStatementHandler),根据上下文进行创建期望的Handler
- ParameterHandler:参数处理器,对预编译的sql语句进行参数设置
- ResultHandler:结果集处理器
- 查询操作大致流程
- instantiateStatement(connection)进行sql预编译,设置超时时间、获取的最大行数
- parameterize(statement)设置sql参数——>调用ParameterHandler进行参数设置
- 执行sql语句
- 级联关系
- 一对一·:association(父方设置该属性、同时设置select语句查询)
- 一对多:collection(在一的一方设置该属性)
- 鉴别器:discriminator(根据实际情况,例如person对象有男女区分、类似于Java中的switch语句)
- 缓存
- 系统缓存(一级缓存、二级缓存)
- 同一个Mapper+同参数+同sql——>SqlSession第一次执行后将其放入缓存中
- 一级缓存是SqlSession级别的,SqlSession之间缓存不共享;二级缓存在SqlSessionFactory层面共享,返回的POJO必须是可序列化的
- 默认使用LRU(最近最少使用)算法回收
- 系统缓存(一级缓存、二级缓存)
- 动态SQL语句
- swith-case-default的解决方案——>choose-when-otherwise
- foreach:
- 遍历集合,支持数组、List、Set
- 参数:collections(集合的参数名称)、item(循环中当前的元素)、index(位置下标)、open和close(以什么符号将元素包装起来)、separator(各个元素的间隔符)
- bind:自定义一个上下文变量,更多的是sql的参数连接操作
- 延迟加载问题(对返回类的进行动态代理,拦截相应的方法)
- 与Spring的结合
- 实现javax.sql.DataSource接口
- SqlSessionFactoryBuilder
- Apache HttpClient
- 设计模式:责任链模式,继承ClientExecChain,根据各自的作用,将所有实现链起来,将请求发送到职责链上即可
- rewriteRequestURI(request, route):如果设置了HttpRoute,会自动开启URI的重写,HttpRoute对象是immutable的
- 终止请求调用abort()方法
- HttpEntity的不同实现决定了是否可以复用内容:ByteArrayEntity、StringEntity、FileEntity为可重复获得使用
- ByteArrayEntity其内部的getContent方法的实现采用调用—创建一个新的InputStream对象,将数据copy到新的InputStream对象中,实现重复使用
- HttpContext
- Http请求的上下文,发起Http请求时带上HttpConext将共享保存的信息(类似于Session机制)
- MainClientExec(The last request executor in the HTTP request execution chain)
- 大致请求流程
- http请求链接的建立(socket链接建立);消息的发送执行者(路由的选择、可能的重定向、消息的鉴权、链接的分配回收、链接相关设置[链接是否保持、相应的encoding、保持连接的时间设置、重试规则的设置、相关代理路由的设置])
- inal(变量、方法、类)













































