Java 基础篇

让程序性能优异的并发利器

线程池

创建参数对工作机制对影响

线程池构造函数:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
参数含义:

  • corePoolSize
    线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;
    如果当前线程数=corePoolSize,继续提交的任务会被保存到阻塞队列中,等待被执行;
    如果执行了线程池的 prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
  • maximumPoolSize
    线程池允许的最大线程数。
    如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于 maximumPoolSize;
  • keepAliveTime
    非核心线程空闲时的存活时间,当没有任务执行时,非核心线程继续存活的时间。
    默认情况下,该参数只在线程数大于corePoolSize时才有用。
  • TimeUnit
    keepAliveTIme的时间单位。
  • workQueue
    workQueue必须是BlockingQueue阻塞队列。
    当线程池中的线程数超过他的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能。

workQueue
用于保持等待执行任务的的任务阻塞队列,尽量使用有界队列,因为无界队列会对线程池有影响:
1、当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因为线程池中的线程数不会超过corePoolSize;
2、使用无界队列时,maximumPoolSize和keepAliveTime将是无效参数
3、使用无界queue可能会耗尽系统资源,有界队列则有助于防止资源耗尽,同时即使使用有界队列,也要控制队列的大小在一个合适的范围。
所以一般使用 ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue,PriorityBlockingQueue

  • threadFactory
    创建线程的工厂。
    通过自定义的线程工厂,可以给每个新建的线程设置一个具有识别度的线程名,当然还可以更加自由的对线程做更多的设置,比如设置所有的线程为守护线程。
    Exexutors 静态工厂里默认的threadFactory,线程的命名规则是“pool-数字-thread-数字”
  • RejectedExecutionHandler
    线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,即当前线程数已经达到最大线程数,如果继续提交任务,必须采取一种策略处理该任务。

线程池提供了4中策略:
1、AbortPolicy:直接抛出异常,默认策略
2、CallerRunsPolicy: 用调用者所在的线程来执行任务
3、DiscardOlderestPolicy: 丢弃阻塞队列中靠最前的任务,并执行当前任务
4、DiscardPolicy: 直接丢弃任务
也可以根据应用场景实现 RejectedExecutionHandler接口,自定义饱和策略,如记录日志活持久化存储不能处理的任务。

合理配置线程池

首先分析任务特性:
* 任务的性质:CPU密集型,IO密集型和混合型任务
* 任务的优先级:高,中,低
* 任务的执行时间:长,中,短
* 任务的依赖性:是否依赖其他系统资源,如数据库链接
性质不同的任务可以用不同规模的线程池分开处理

CPU 密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。
IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如 2*Ncpu。
混合型的任务,如果可以拆分,将其拆分为一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。
可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

概述 ConcurrentHashMap

基本概述

ConcurrentHahsMap 是线程安全的Map,1.7 和 1.8 中实现方式不同

  • 1.7
    采用分段锁的机制,实现并发的更新操作,底层采用数组+链表的存储结构,包括两个核心静态内部类 Segment 和 HashEntry。
    1、segment继承ReentrantLock(重入锁)用来充当锁的角色,每个Segemnt 对象守护每个散列映射表的若干个锁。
    2、HashEntry 用来风中映射表的键值对
    3、每个桶是由若干个HashEntry 对象链接起来的链表
  • 1.8
    采用 Node+CAS+Synchronized 来保证并发安全。取消类Segment,直接用table数组存储键值对;当HashEntry对象组成对链表长度超过 TREEIFY_ THRESHOLD 时,链表转换为红黑树,提升性能。底层变更为数组 + 链表 + 红黑树。
    1、重要对常量:
    private transient volatile int sizeCtl;
    当为负数时,-1表示正在初始化,-N表示 N -1 个线程正在进行扩容;
    当为 0 时,表示 table 还没有初始化;
    当为其他正数时,表示初始化或者下一洗进行扩容的大小。
    2、 数据结构:
    Node时存储结构的基本单元,实现了Map中的Entry接口,用于存储数据;
    TreeNode继承Node,但是数据结构换成来二叉树结构,是红黑树的存储结构,用于红黑树中存储数据。
    3、存储对象时(put() 方法)
    1、如果没有初始化,就调用initTable()方法来进行初始化;
    2、如果没有 hash 冲突就直接 CAS 无锁插入;
    3、如果需要扩容,就先进行扩容;
    4、如果哦存在hash冲突,就枷锁来保证线程安全,两种情况:一种时链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入;
    5、如果该链表的数量大雨阈值8,就要先转换成红黑树的结构
    6、如果添加成功就调用 addCount()方法统计size,并坚持是否需要扩容。
    4、扩容方法 transfer()
    默认容量为16,扩容是,容量变为原来的两倍
    helpTransfer(): 调用多个工作线程一起帮助进行扩容,效率更高
    5、获取对象时(get()方法)
    1、计算hash值,定位到该table索引位置,如果是首节点符合就返回;
    2、如果遇到扩容时,会标记正在扩容结点 ForwardingNode.find() 方法,查找该结点,匹配就返回;
    3、以上都不符合的话,就往下遍历结点,匹配就返回,否则最后就返回 null

为什么hashmap1.8 不直接使用红黑树而还要保留链表

因为插入时红黑树需要进行左旋,右旋操作,而单链表不需要,在数量较少时,红黑树并没有表现出比链表更好的查询效率,而且在占用空间上,红黑树的节点比链表的节点更大,时链表的两倍。

为什么大于8个的时候才转换红黑树

1、 按照JDK源码的解释:
TreeNodes占用空间是普通Nodes的两倍,所以只有当bin包含足够多的节点时才会转成TreeNodes,而是否足够多就是由 TREEIFY_THRESHOLD的值决定的。当bin中节点数变少时,又会转成普通的bin。TREEIFY_THRESHOLD的值是这个空间和时间的权衡。
当hashCode离散性很好的时候,树形bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。
但是在随机hahsCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。
不过理想情况下随机 hashCode 算法下所以bin中节点的分布频率会遵循泊松分布,一个bin中链表长度达到8个元素的概率为 0.00000006,几乎是不可能时间。所以,之所以选择8,不是拍拍屁股决定的,而是根据概率统计决定的。
2、网上的说法:
红黑树的平均查找长度是 log(n), 如果长度为8,平均查找长度为 log(8)=3,链表的平均查找长度为n/2,当长度为8是,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果小于等于6,6/2=3,而log(6)=2.6, 虽然速度也很快,但是转化为树结构和生成树的时间并不会太短。

概述volatile

volatile 关键字的主要作用:
多线程主要围绕可见性和原子性两个特性而展开,使用volatile 关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据。但是volatile不能保证操作的原子性,对任意单个volatile变量的读/写具有原子性,但类似++这种复合操作不具有原子性。

代码底层在执行时为了获取更好的性能会对指令进行重排序,多线程下可能会出行一些意想不到的问题。使用volatile则会禁止重排序,但是会降低代码的执行效率。
同时在内存语义上,当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存,当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
在java中,对与volatile修饰的变量,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序问题、强制刷新和读取。
在具体实现上,volatile关键字修饰的变量会存在一个“lock:”的前缀。它不是一种内存屏障,但是它能完成类似内存屏障的功能,lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。
同时该指令会将当前处理器缓存行的数据直接写回到系统内存中,且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效。

概述AQS

AQS是用来构建锁或者其他同步组件的基础框架,比如ReentrantLock、ReentrantReadWriteLock 和 CountDownLatch 就是基于AQS实现的。
它使用了一个int成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。踏实CLH队列锁的一种变体实现。它可以实现2种同步方式:独占式,共享式。
AQS的主要使用方式式继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,同步器的设计基于模版方法模式,所以如果要实现我们自己的同步工具类就需要覆盖其中几个可以重写的方法:tryAcquire,tryReleaseShared 等。
这样设计的目的是同步组件(比如锁)是面向使用者的,它定义来使用者于同步组件交互的接口(比如可以允许两个线程并行访问),隐藏来实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。这样可以很好的隔离使用者和实现者所需要关注的领域。
在内部,AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。该队列由一个一个的Node节点组成,每个Node节点维护一个prev引用和next引用,分别指向自己的前驱和后继节点,构成一个双端双向链表。
同时与Condition相关的等待队列,节点类型也是Node,构成一个单向链表。

synchronized 的实现原理

synchronized 在JVM里的实现都是基于进入和退出的Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的 MonitorEnter 和 MonitorExit 指令来实现。
对同步块,MonitorEnter指令插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor到所有权,即尝试获得该对象的锁,而monitorExit指令则插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit。
对同步方法,从同步方法反编译对结果来看,方法对同步并没有通过指令monitorEnter和monitoerExit来实现,相对于普通方法,其常量池中多来 ACC_SYNCHRONIZED 标示符。
JVM就是根据该标示符来实现方法的同步的: 当方法被调用时,调用指令将会坚持方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后在释放monitor。在方法执行期间,其他任何线程都无法在获得同一个monitor对象。
synchronized使用的锁是存放在Java对象头里面,具体位置是对象头里面的MarkWord,MarkWord 里默认数据是存储对象的HashCode 等信息,但是会随着对象的运行改变而发生变化,不同的锁状态对应着不同的记录存储方式。在具体优化上,从1.6开始引入了偏向锁、自旋锁等机制提升性能。

什么是CAS操作,缺点是什么?

CAS的基本思路是:如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事,但是要返回原值是多少。每个CAS操作过程都包含三个运算符:一个内存地址V, 一个期望的值A 和 一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。

CAS 缺点:
  1. ABA问题:
    一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,最终又变回A,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但可能存在潜在但问题。从Java1.5开始,JDK的atomic包里提供了一个类的AtomicStampedReference来解决ABA问题。

  2. 循环时间长,开销大:
    对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

  3. 只能保证一个共享变量的原子操作:
    当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时间就可以用锁。

性能等奠基之石,SQL优化

Mysql索引类型和区别

  • 普通索引:即一个索引只包含单个列,一个表可以有多个单列索引
  • 唯一索引:索引列的值必须唯一,但允许有空值
  • 复合索引:一个索引包含多个列
  • 聚集索引(聚蔟索引):innodb, 数据和索引放到一起
  • 非聚集索引:myisam,数据和索引文件分开存放

事务等四大特性

如果一个数据库声称支持事务但操作,那么该数据库必须具备以下四个特性:

  1. 原子性
    是指事务包含的操作要么全部成功,要么全部失败回滚。因此事务的操作如果成功就完全应用到数据库,如果操作失败则不能对数据库有任何影响。

  2. 一致性
    是指事务必须使数据库从一个一致性状态变化到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
    拿转账来说,假设用户A和用户B的钱加一起是200,无论A和B直接如何转账,转几次账,事务结束后两个用户的钱加一起还是200,这就是事务的一致性。

  3. 隔离性
    是当多个用户并发访问数据库时,比如操作同一张表是,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
    即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发的执行。

  4. 持久性
    是指一个事务一旦被提交列,那么对数据库中对数据对改变就是永久性对,即使是在数据库系统遇到故障对情况下也不会丢失提交对事务的操作。

事务的隔离级别

不考虑事务的隔离性会发生的问题
  • 脏读
    在一个事务处理过程里读取里了另一个未提交的事务中的数据。
    当一个事务正在多次修改某个数据,而在这个事务中这多次的修改还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。
  • 不可重复读
    是指在对于数据库中的某个数据,一个事务范围内多次查询却返回来不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
    程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他买单时(程序员事务开启),收费系统事先检测到他的卡里有3.6万,就在这个时候!!程序员的妻子要把钱全部转出充当家用,并提交。当收费系统准备扣款时,再检测卡里的金额,发现已经没钱了(第二次检测金额当然要等待妻子转出金额事务并提交完)。程序员就会很郁闷,明明卡里是有钱的…
    不可重复读和脏读的区别:
    • 脏读是某一税务读取了另一个事务未提交的脏数据
    • 不可重复读是读取了前一事务提交的数据
      在某些情况下,不可重复读并不是问题,比如给我们多次查询某个数据当然是以最后查询得到的结果为主。
  • 虚读(幻读)
    幻读是事务非独立执行时发生的一种现象。例如事务T1 对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是“1”,并提交给了数据库。而操作T1的用户如果在查看刚刚修改的数据,就会发现还有一行没有修改,其实这行是T2中添加的,就好像产生幻觉一样,这就是发生了幻读。
    幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体。

数据库提供的隔离级别

Read uncommitted(读未提交):
顾名思义,就是一个事务可以读取另一个未提交的事务的数据。最低级别,任何情况都无法保证。
Read committd (读已提交)
一个事务要等另一个事务提交后才能读取数据。可避免脏读等发生,但是无法避免不可重复读。
Repeatable read(可重复读)
就是在开始读取数据时,不再允许修改操作,可避免脏读、不可重复读的发生。但是无法避免幻读。
Serializable(串行化)
是最高的事务隔离级别,在该级别下,事务串行化执行,可避免脏读,不可重复读,幻读的发生。
就是以锁表的方式(类似于Java多线程中的锁)使其他的线程只能在锁外等待,这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。

以上四种隔离级别最高的是Serializable级别,最低的是 Read uncommitted 级别,当然级别越高,执行的效率越低。所以平时选用何种隔离级别应该根据实际情况。在MySql数据库中默认的隔离级别为Repeatable read(可重复读)。
在Mysql数据库中,支持上面四种隔离级别,默认的为 Repeatable read;而在Oracle 数据库中,只支持Serializable级别和Read committed这两种级别,默认为 Read committed 级别。

MySQL事务的实现原理

事务具有ACID四个特性。也就是:原子性,一致性,隔离性,持久性。ACD三个特性是通过 Redo log(重做日志)和 Undo log 实现的。而隔离性是通过锁来实现的。
重做日志(Redo log)用来实现事务的持久性,即D特性。它由两部分组成:

  1. 内存中的重做日志缓冲
  2. 重做日志文件
    在事务提交时,必须先将事务的所以日志写入到redo日志文件中,待事务的commit操作完成才算整个事务操作完成。

Undo log,它可以实现如下两个功能:

  1. 事务回滚
  2. 实现MVCC(多版本并发控制)
    undo log 可以认为当delete 一条记录时,undo log中会记录一条对应的 insert 记录,反之亦然,当update 一条记录时,它记录一条对应相反的update 记录。

SQL优化

常见步骤:

环境方面
  1. 尽可能的使用高速磁盘和大内存
  2. 服务器使用Linux,并且进行操作系统级别的调优,比如网络参数,避免使用swap交换区等等
SQL相关
  1. 先找到慢查询日志,就是查询慢的日志,是指mysql记录所有执行超过long_query_time 参数设定的时间阈值的SQL语句的日志。该日志能为SQL语句的优化带来很好的帮助。默认情况下,慢查询日志是关闭的,要使用慢查询日志功能,首先需要开启慢查询日志功能。

    • slow_query_log 启动/停止慢查询
    • slow_query_log_file 指定慢查询日志的存储路径及文件(默认和数据文件放在一起)
    • long_query_time 指定记录慢查询日志SQL执行时间的阈值(单位:秒,默认10秒)
    • log_queries_not_using_indexed 是否记录未使用索引的SQL
    • log_output 日志存放的地方【table】,【file】,【file,table】
  2. 分析慢查询日志。慢查询的日志记录非常多,要从里面找寻一条慢查询的日志并不少很容易的事情,一般需要一些辅助工具才能快速定位需要优化的SQL语句,比如 Mysqldumpslow

  3. SQL本身优化,比如少用子查询,in查询改关联查询,不实用外键与级联等

  4. 反范式设计,字段允许适当冗余,选择合适的字段存储长度等

  5. 使用执行计划分析SQL语句,使用EXPLAN关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。分析你的查询语句或是表结构的性能平静下来,至少可以知道:

* 表的读取顺序
* 数据读取操作的操作类型
* 哪些索引可以使用
* 哪些索引被实际使用
* 表之间的引用
* 每张表有多少行被优化器查询
比如,执行计划中的type显示的是访问类型,是较为重要的一个指标,结果值从最好到最坏依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > rang > index > ALL
一般来说,得保证查询至少达到 rang 级别,要求能达到 ref。

优化10大策略

尽量全值匹配
当建立了索引列后,能在where条件中使用索引的尽量使用。
最佳左前缀法则
如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。
不在索引列上做任何操作
不在索引列上做任何操作(计算,函数,手动/自动的类型转换),会导致索引失效而转向全表扫描
范围条件放最后
中间有范围查询回导致后面的索引列全部失效
覆盖索引尽量用
尽量使用覆盖索引(指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取),减少select *;
不等于要慎用
mysql 在使用不等于(!= 或 <>)的时候无法使用索引,会导致全表扫描,如果一定要使用不等于,请使用覆盖索引。
Null/Not 有影响
使用is null 或 is not null 会导致索引失效
解决方式:覆盖索引
Like 查询要当心
like 以通配符开头('%abc..'),mysql 索引会失效,变成全表扫描
解决方式:覆盖索引
字符类型加引号
    字符串不佳单引号导致索引失效,变成全表扫描
    解决方式:加引号
OR 改 UNION 效率高
解决方式:如果一定要用OR,那么使用覆盖索引

JVM

JVM 内存区域

JVM在执行Java 程序的过程中会把它管理的内存分为若干个不同的区域,这些组成部分有些是线程私有的,有些则是线程共享的。
线程私有的:程序计数器,虚拟机栈,本地方法栈
线程共享的:方法区,堆
  • 程序计数器
    较小的内存空间,当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响,此内存区域是唯一一个不会出现 OutOfMemoryError 请求的区域。
  • 虚拟机栈
    每个线程私有的,线程在运行时,在执行每个方法的时候都会打包成一个栈帧,存储了局部变量表,操作树栈,动态链接,方法出口等信息,然后放入栈。每个时刻正在执行的当前方法就是虚拟机栈顶的栈帧。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程。
  • 本地方法栈
    各虚拟机自由实现,本地方法栈 native 方法调用 JNI 到了底层的 C/C++(c/c++ 可以出发汇编语言,然后驱动硬件)
  • 方法区/永久代
    用于存储已经被虚拟机加载的类信息,常量(“zdy”,"124"等),静态变量(static变量)等数据,比如类信息就包括类的完整有效名,返回值类型,修饰符(public,private。。。),变量名,方法名,方法代码,这个类型直接父类的完整有效名(除非这个类型是 interface 或者 java.lang.Object,两种情况下都没有父类),类的直接接口的一个有序裂变等等。

  • 几乎索引对象都分配在这里,也是垃圾回收发生的主要区域

JVM垃圾回收器

JVM中是通过可达性分析算法判断对象是否可回收的。
这个算法的基本思想就是通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象不可用的。
在垃圾回收上,又几种常见的算法:

  1. 标记-清除算法
    标记-清除算法分为“标记”和“清除”阶段:首先标记出所以需要回收的对象,在标记完成后统一回收所以被标记的对象。但是会带来两个明显的问题:

    1. 效率问题
    2. 空间问题(标记清除后会产生大量不连续的碎片)
  2. 复制算法
    将内存氛围大小相同的两块,每次使用其中的一块。当一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

  3. 标记-整理算法
    根据老年代代特点设计的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所以存活的对象向一段移动,然后直接清理掉端边界以外的内存。
    根据对象的生命周期,将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
    比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
    在具体的垃圾算法的实现上又几种垃圾回收器。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-th1e58cY-1591856868610)(file:///Users/yangmingyue/Documents/Gridea/post-images/1590464776107.jpg)]

Serial/Serial Old
最古老的,单线程,独占式,成熟,适合CPU 服务器
-XX:+UseSerialGC 新生代和老年代都用串行收集器
-XX:+UseParNewGC 新生代使用ParNew, 老年代使用 Serial Old
-XX:+UseParallelGC 新生代使用ParallerGC,老年代使用Serial Old

ParNew
和Serial 基本没有区别,唯一区别:多线程,多CPU多,停顿时间比Serial少
-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old
除了性能原因外,主要是因为除了 Serial 收集器,只有他能与CMS收集器配合工作。

Parallen Scavenge (ParallerGC) /Parallel Old
关注吞吐量的垃圾收集器,高吞吐量则可以高效的利用CPU事件,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
所谓吞吐量就是CPU用于运行用户代码的事件与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行哦用户代码时间 + 垃圾收集时间),虚拟机总共运行了 100 分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
-XX:+UseParalleOldGC 则会开启这一对组合,同时Parallel Scavenge还有一个自适应调整策略,就不需要手工指定新生代的大小(-Xmn),Eden与Survivor区的比例(-XX:SurvivorRatio),晋升老年代对象年龄(-XX:LPretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量。通过打开-XX:+UseAdaptiveSizePolicy, 只需要把基本你的内存数据设置好(如-Xmx 设置最大堆),然后使用 MaxGCPauseMillis 参数(更关注最大停顿时间)或 GCTimeRatio参数(更关注吞吐量)给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。

Concurrent Mark Sweep(CMS)
收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
-XX:+UseConcMarkSweepGC, 一般新生代使用ParNew,老年代的用CMS,并发收集失败,转为SerialOld.
从名字(包含“Mark Sweep”)可以看出,CMS收集器是基于“标记-清楚”算法实现的,它的运作过程相对于前面集中收集器来说更复杂一些。
整个过程分为4个步骤:

  • 初始标记:纠结是标记一下 GC Roots 能直接关联到的对象,速度最快,需要停顿(STW–stop the world)
  • 并发标记:从GC Roots 开始对堆中对象进行可达性分析,找到存活对象,它在整个回收过程中耗时最长,不需要停顿。
  • 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记变动的那一本部分对象的标记记录,需要停顿。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
  • 并发清除:不需要停顿。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SEcXeOIX-1591856868611)(file:///Users/yangmingyue/Documents/Gridea/post-images/1590475711018.jpg)]

优点:
由于整个过程中耗时最长的并发标记和并发清除过程周几去线程都可以和用户线程一起工作,所以,总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
缺点:

  • CPU资源敏感:因为并发阶段多线程占用CPU资源,如果CPU资源不足,效率会明显降低。
  • 浮动垃圾:由于CMS并发清理阶段 用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在档次收集中处理掉他们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
    由于浮动垃圾的存在,因为需要预留出一部分内存,意味着CMS收集不能像其他收集器那样等待老年代快满
    的时候再回收。
    再1.6的版本中,老年代空间使用率阈值(92%)
    如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Filure,这时虚拟机将临时启用 Serial Old来代替CMS。
  • 产生空间碎片:标记-清除算法会导致产生不联系的空间碎片,CMS只会删除无用的对象,不会对内存做压缩,会造成内存碎片。

G1垃圾回收器
主要是用在大内存和多处理器数量的服务器上。jdk9 中将G1 变成默认的垃圾收集器。
G1中重要参数:
-XX:_UseG1GC 使用G1垃圾回收器
-XX:MaxGCPauseMillis=200 设置GC的最大暂停时间为200ms

内部布局改变
G1把堆划分为多个大小相等的区域(Region),每个Region大小为2的倍数,范围在1MB-32MB之间,可能为1,2,4,8,16,32MB。所有的Region有一样的大小,JVM生命周期内不会改变。整个堆被划分为2048左右个Region。新生代和老年代不再物理隔离。Region可以说是G1回收器一次回收的最小单元。
算法:标记-整理(old,humongous) 和 复制回收算法(survivor)。

Stop The World 现象
Stop The World机制,简称STW,主要指执行垃圾收集算法时,Java应用程序的其他所有除了垃圾回收线程之外的线程都被挂起。
此时,系统只能允许GC线程进行运行,其他线程则会全部暂停,等待GC线程执行完才能再次运行。这些工作都是由虚拟机在后台自动发起和自动完成的,是在用户不可见的情况下把用户正常工作的线程全部停下,这对于很多应用程序,尤其是那些对于实时性要求很高的程序来说是难以接受的。我们GC调优的目标就是尽可能的减少STW的时间和次数。

JVM中存在哪些引用

强引用

大部分引用都是强引用,这是最普遍的引用类型。
A a = new A();
如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出 OutOfMemoryError 错误,是程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

软引用

如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的告诉缓存。
软引用可以和一个引用队列(ReferenceQuence)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

弱引用

可有可无。弱引用与软引用的区别:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了至于有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

虚引用

顾名思义,就是形同虚设,徐银银并不会决定对象的声明周期。如果哦一个对象仅持有虚引用,那么它就和没有任何引用一样,在任务时候都可能被来及回收。
虚引用主要用来跟踪对象被垃圾回收的活动。

在程序设计中除了强引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出等问题的产生。

类加载机制

类从被加载到虚拟机内存中开始,到卸载出内存位置,它的整个生命周期包括:加载,验证,准备,解析,初始化,使用和卸载 7个阶段。其中验证,准备,解析 3各部分统称为连接(Linking)

加载

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

验证

连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。但从哪个整体来看,验证阶段大致上会完成4个阶段的检验动作:文件格式验证,元数据验证,字节码验证,符号引用验证。

准备

正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念。首先,这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:
public staic int value = 123;
那变量初始阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何Java方法,而把value 赋值为123的putstatic指令是程序被编译后,存放于类构造器() 方法中,所以把 value 赋值为 123 的动作将在后面的初始化阶段才会执行。
假设类变量value的定义为:
public static final int value = 123
编译时javac将会为value生成 ConstantValue属性,在准备阶段虚拟机就跟根据 ConstantValue 的设置将value赋值为123.

解析

是虚拟机将常连池内的符号引用替换为直接引用的过程。
符合引用以一组符号来描述所引用的目标,符号可以说任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用于与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会像他。如果有了直接引用,那引用的目标必定已经在内存中存在。

初始化

虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加重,验证,准备自然需要在此之前开始):

  1. 遇到new,getstatic,putstatic或invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最长久的Java代码场景是:使用new 关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用 java.lang.reflect 包的方法对类的进行反射调用的时候,如果类还没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic,RED_putstatic,REF_invokestatic的方法局部,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

初始化也是类加载的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
从另一个角度表达:初始化阶段是执行类构造器() 方法的过程。() 方法是由编译期自动收集类中的所有类变量的赋值动作和静态语句块(statiic{}) 中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的。
() 方法对于类或接口并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 () 方法。
虚拟机会保证一个类的 () 方法在多线程环境中被正确的加锁,同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待直到活动线程执行() 方法完毕。如果在一个类的() 方法中有韩式很长的操作,就可能造成多个进程阻塞。所以类的初始化是线程安全的,项目中可以利用这点。

双亲委派模型

对任意一个类,都需要由加载它的类加载器和这个类本身一起确立其在Java虚拟机中的唯一性。
从Java虚拟机的角度来讲,只存在两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader)
    这个类加载器使用C++语言实现,是虚拟机的一部分。这个类将负责将存放在<JAVA_HOME>/lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载带虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替就可以。

  • 其他类加载器
    这些类加载器都由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类 java.lang.ClassLoader

    • 扩展类加载器 (Extension ClassLoader)
      这个加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载<JAVA_HOME>/lib/ext 目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所以类型,开发者可以直接使用扩展类加载器。
    • 应用程序类加载器 (Application ClassLoader)
      这个类加载器由 sun.misc.Launcher$AppClassLoader 实现。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以一般也称它为系统类加载器。他负责加载用户类路径(ClassPath) 上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义的类加载器,一般情况下这个就是程序中默认的类加载器。

我们的应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自定义的类加载器。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance) 的关系来实现,而是都使用组合(omposition) 关系来复用父加载器的代码。

使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java类随着它的类加载器一起将具备了一种带有优先级的层次关系。例如类 java.lang.Object, 它存放在rt.jar 之中,无论哪一个类加载器要加载这个类,最终都会委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各类加载器环境中都是同一个类。

ClassLoader 中的 loadClass 方法中的代码逻辑就是双亲委派模型:
在自定义ClassLoader的子类的时候,我们常见的会有两种做法,一种是重写 loadClass 方法,另一种是重写 findClass方法 。其实这两种方法本质上长不大,毕竟loadClass 也会调用findClass, 但是从逻辑上讲我们最好不要直接修改 loadClass 的内部逻辑。我建议的做法是只在 findClass 里findClass 里重写自定义类的加载方法。

loadClass这个方法是实现双亲委派模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题。因此我们最好是在双亲委派模型框架内进行小范围的改掉,不破坏原有的稳定结构。同时,也避免了自己重写 loadClass 方法的过程中鼻血重写双亲委托的重复代码,从代码的复用性来看,不直接修改这个方法始终是比较好的选择。

但是Tomcat 中没有完全遵守双亲委派模型。

双亲委派模型的破坏

双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类越由上层的加载器进行加载),基础类之所以称为“基础”,是因为他们总是作为被用户代码调用的API。如果基础类要调用用户的代码,那怎么办?

比如JDBC是原生的JDBC中Driver驱动本身只是一个接口,并没有具体实现,具体的实现由不同数据库类型去实现的。

例如,MySQL的mysql-connector.jar 中的Driver类具体实现的。原生的JDBC中的类是放在 rt.jar 包的,是由启动类加载器进行加载的,在JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector.jar 中的Driver类是由独立厂商实现并部署在应用程序的ClassPath下的,那启动类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由应用程序启动类去进行类加载。

于是,这个时候就引入线程上下文类加载器(Thread Context ClassLoader)。有了这个东西,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那么这个类加载器默认就是应用程序类加载器。

Java中所以设计SPI的加载动作基本上都是采用这种方式,例如 JNDI,JDBC,JCE,JAXB和JBI等。
双亲委派模型的“被破坏”是由于用户对程序动态性的追求而导致的,这里说说的“动态性”指的是当前一些非常“热门“的名词:代码热替换(HotSwap),模块热部署(HotDeplooyment)等等。

JVM常用工具

  • jps
    列出当前机器上正在运行的虚拟机进程,Jps从操作系统的临时目录上去找
    * -q: 仅仅线上进程
    * -m:输出主函数传入的参数,
    * -l:输出应用程序主类完整package名称或jar完整名称
    * -v:列出jvm参数,-Xms20m -Xmx20 是启动程序指定的JVM参数

  • jstat
    是用于见识虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载,内存,垃圾收集,JIT编译等运行数据,在没有GUI图像件,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。
    假设需要每 250 毫秒查询一次进程 13616 垃圾收集状况,一共查询 10 次,
    那命令应当是:jstat -gc 13616 250 10
    常用参数:

    • -class 类加载器
    • -compiler JIT
    • -gc GC堆状态
    • -gccapacity 各区大小
    • -gccause 最近一次GC统计和原因
    • -gcnew 新区统计
    • -gcnewcapacity 新区大小
    • -gcold 老区统计
    • -gcoldcapacity 老区大小
    • -gcpermcapacity 永久区大小
    • gcutil GC统计汇总
    • printcompilation HotSpot编译统计
  • jinfo
    查看和修改虚拟机参数

    • -sysprops 可以查看有 System.getProperties() 取得的参数
    • -flag 未被显示指定的参数的系统默认值
    • -flags 显示虚拟机的参数
  • jmap
    用于生产对转储快照(一般称为heapdump或dump文件)。jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列,java堆和永久带的详细信息,如空间使用率,当前用的是哪种收集器等。和info命令一样,jmap有不少功能在windows 平台下都是受限的,除了生成dump文件的-dump选项和用于查看每个类的实例,空间占用统计的 -histo选项在所有操作系统都提供之外,其余选项都只能在Linux、Solaris下使用。
    jmap -dump:live,format=b,file=heap.bin
    Sun JDK 提供jhat(JVM heap Analysis Tool)命令与jmap搭配使用,来分析jamp 生成的对转储快照。

  • jhat
    jhat dump 文件名
    屏幕显示 ”server is ready“的提示后,浏览器中访问 http://localhost:7000/ 就可以访问详情
    使用jhat可以在服务器上生成堆转储文件分析(一般不推荐,比较占用服务器资源)

  • jstack
    Strack Trace Java, 命令用于生产虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁,死循环,请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。
    在代码中可以用 java.lang.Thread 类的 getAllStrackTraces() 方法用于获取虚拟机中所有线程的 StackTraceElement 对象。使用这个方法可以通过简单的几行代码就完成 jstack 的大部分功能,在实际项目中不妨调用这个方法做一个管理员页面,可以随时使用浏览器来查看线程堆栈。

项目内存或者CPU占用过高如何排查

  1. 针对CPU的问题:

    1. 查看问题进程,得到进程PID: top -c
    2. 查看进程里的线程明细,并手动记下CPU异常的线程PID:top -p PID -H
    3. 使用jdk提供的jstack命令打印出项目堆栈:jstack pid > dump.log
  2. 针对内存的问题:

    1. 查看内存中的存活对象统计,找出业务相关的类名: jmap -histo:live PID > xxx.log
    2. 通过简单的捅进还是没办法定位问题的话,就输出内存明细来分析。这个命令会将内存里的所有信息都输出,输出的文件大小和内存大小基本一致。而且会导致应用暂时挂起,所有谨慎使用:jmap -dump:live,format-b,file=xxx.hprof PID
    3. 最后对dump出来的文件进行分析。文件大小不是很大的话,使用jdk自带的jhat命令即可:
      jhat -J -mx2G -port 7170
    4. dump 文件太大的话,可以使用 jprofiler 工具来分析。
  3. 需要分析GC的情况,可以使用一下命令:
    jstat -gc PID

框架源码,CRUD和高级程序员的分水岭

谈谈依赖注入和面向切面

谈谈你对Spring框架的理解,谈谈Spring中的IOC和AOP概念
Spring 框架是一个开源而轻量级的框架,是一个IOC和AOP的容器,spring的核心就是控制反转(IOC)和面向切量编程(AOP)

  • IOC
    面向对象编程中的一种设计原则,用来降低代码之间的耦合度,使整个程序体系结构更加灵活,与此同时将类的创建和依赖关系卸载配置文件里,由配置文件注入,达到松耦合的效果。与此同时IOC也称为DI(依赖注入),依赖注入是一种开发模式;依赖注入提倡使用接口编程;依赖注入使得可以开发各个组件,然后根据组件之间的依赖关系注入组装,
    所谓依赖,从程序的角度看,就是比如A要调用B的方法,那么A就依赖于B,返回A要用到B,则A依赖于B。所谓倒置,如果不倒置,因为A必须要用到B,所以要有B才可以调用B的方法。不倒置的话A就需要主动获取B的实例:B b = new B(); 这就是最简单的获取B实例的方法(各种设计模式也可以帮忙去获得B的实例,比如工厂,Locator等),然后就可以调用b对象了。所以不倒置,就需要A主动获取B,才能使用B。倒置的话,就是A要调用B的话,A并不需要主动获取B,而是由其他人自动将B送上门。
  • AOP
    面向切面编程将安全,事物等程序逻辑相对独立的功能抽取出来,利用Spring的配置文件将这些功能插进去,实现了按照切面编程,提高了复用性;最主要的作用: 可以再不锈钢源代码的情况下,给目标方法动态添加功能。
    面向切面编程的目标就是分离关注点。什么是关注点呢?就是你要做的事,就是关注点。
    AOP的好处就是你只需要干你的正事,其他事情交给别人帮你干。
    从Spring的角度来看,AOP最大的用途就在于提供了事务管理能力。事务管理就是一个关注点,你的正事是去访问数据库,而你不太想管事务,所以,Spring在你访问数据库之前,自动帮你开启事务,当你访问数据库结束后,自动帮你提交、回滚事务。

Spring优点

  • 低侵入式设计,独立于各种应用服务器
  • 依赖注入的特点将组件关系透明化,降低耦合度
  • 与第三方框架具有良好的整合效果

Spring框架中bean实例化的流程

Spring Bean的生命周期

Spring在Bean创建过程中是如何解决循环依赖的

循环依赖只会存在单例实例中,多例循环依赖直接报错。
A类实例化后,把实例放map中,A类中有一个B类属性,A类实例化要进行IOC依赖注入,这时候B类需要实例化,B类实例化跟A类一样,实例化后放入map容器中。B类中有一个A类属性,接着B类的IOC过程,又去实例化A类,这时候实例化A类过程中从map容器发现A类已经在容器中了,就直接返回了A的实例,依赖注入到B类中A属性中,B类IOC完成后,B实例化就完全完成了,就返回给A类的IOC过程。这就是循环依赖的解决。

AOP实现流程

  1. aop:config 自定义标签解析
  2. 自定义标签解析时会执行到aop入口类中
  3. Bean实例化过程中会执行到aop入口类中
  4. 在aop入口类中,判断当前正在实例化的类是否在pointcut中,pointcut可以理解为一个模糊匹配,是一个joinpoint的集合
  5. 如果当前正在实例化的类在pointcout中,则返回该bean的代理类,同时把所有配置的advice封装成 MethodInterceptor对象加入到容器中,封装成一个过滤器链
  6. 代理对象调用,jdk动态代理会调用invocationHandler中,cglib型代理调到 MethodInterceptor的callback类中,然后在 invoke 方法中执行过滤器链。

Spring框架中如何基于AOP实现事务管理

事务管理,是一个切面。在aop环节中,其他环节都一样,事务管理就是由Spring提供的advice,既是TransactionInterceptor,它一样的会在过滤器链中被执行到,这个TransactionInterceptor 过滤器类是通过解析 tx:advice 自定义标签得到的。

描述SpringMvc的整个访问或者调用流程

  1. 发起请求到前端控制器(DispatcherServlet)
  2. 前端控制器请求HandlerMapping查找Handler(可以根据xml配置,注解查找),处理器映射器 HandlerMapping 向前端控制器返回Handler
  3. 前端控制器调用处理器适配器去执行 Handler
  4. 处理器适配器去执行Handler
  5. Handler 执行完成给适配器返回 ModelAndView,处理器适配器向前端控制器返回 ModelAndView(ModelAndView 是 springmvc 框架的一个底层对象,包括 model 和 view)
  6. 前端控制器请求视图解析器去进行视图解析(根据逻辑视图名解析成真正的视图(jsp)),视图解析器向前端控制器返回 View
  7. 前端控制器进行视图渲染(视图渲染将模型数据填充到request域)
  8. 前端控制器向用户响应结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zy1vPyth-1591856868613)(file:///Users/yangmingyue/Documents/Gridea/post-images/1590658353054.jpg)]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杨愁心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值