面试题总结

1.什么情况下会发生栈内存溢出。

  • 栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用类型
    如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常,方法递归调用产生这种结果。
    如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出一个OutOfMemory 异常。(线程启动过多)
    参数 -Xss 去调整JVM栈的大小

2.详解JVM内存模型。

image.png
  • 程序计数器(线程私有)

程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。
我们知道对于一个处理器(如果是多核cpu那就是一核),在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。
注意:如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域

  • Java栈(虚拟机栈 线程私有)

同计数器也为线程私有,生命周期与相同,就是我们平时说的栈,栈描述的是Java
方法执行的内存模型。每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。

  • 堆(线程共享)

对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以
细分为: 新生代( Eden 区 、 From Survivor 区 和 To Survivor 区 )和老年代。

  • 本地方法区(私有)

方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆.
用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。

3.springmvc工作流程

  • springmvc请求过程
  • 组件说明
    1.DispatcherServlet:前端控制器。用户请求到达前端控制器,它就相当于mvc模式中的c,dispatcherServlet是整个流程控制的中心,由它调用其它组件处理用户的请求,dispatcherServlet的存在降低了组件之间的耦合性,系统扩展性提高。由框架实现
    2.HandlerMapping:处理器映射器。HandlerMapping负责根据用户请求的url找到Handler即处理器,springmvc提供了不同的映射器实现不同的映射方式,根据一定的规则去查找,例如:xml配置方式,实现接口方式,注解方式等。由框架实现
    3.Handler:处理器。Handler 是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下Handler对具体的用户请求进行处理。由于Handler涉及到具体的用户业务请求,所以一般情况需要程序员根据业务需求开发Handler。
    4.HandlAdapter:处理器适配器。通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。由框架实现。
    5.ModelAndView是springmvc的封装对象,将model和view封装在一起。
    6.ViewResolver:视图解析器。ViewResolver负责将处理结果生成View视图,ViewResolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。
    7View:是springmvc的封装对象,是一个接口, springmvc框架提供了很多的View视图类型,包括:jspview,pdfview,jstlView、freemarkerView、pdfView等。一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户,需要由程序员根据业务需求开发具体的页面。
  • 执行流程
    SpringMVC执行流程:
    1.用户发送请求至前端控制器DispatcherServlet
    2.DispatcherServlet收到请求调用处理器映射器HandlerMapping。
    3.处理器映射器根据请求url找到具体的处理器,生成处理器执行链HandlerExecutionChain(包括处理器对象和处理器拦截器)一并返回给DispatcherServlet。
    4.DispatcherServlet根据处理器Handler获取处理器适配器HandlerAdapter执行HandlerAdapter处理一系列的操作,如:参数封装,数据格式转换,数据验证等操作
    5.执行处理器Handler(Controller,也叫页面控制器)。
    6.Handler执行完成返回ModelAndView
    7.HandlerAdapter将Handler执行结果ModelAndView返回到DispatcherServlet
    8.DispatcherServlet将ModelAndView传给ViewReslover视图解析器
    9.ViewReslover解析后返回具体View
    10.DispatcherServlet对View进行渲染视图(即将模型数据model填充至视图中)。
    11.DispatcherServlet响应用户。

4.int i = 1 存放在哪里?

全局变量i,他是存放在java堆中。因为它不是静态的变量,不会独立于类的实例而存在,而该类实例化之后,放在堆中,当然也包含了它的属性i。
如果在方法中定义了int i = 0;则在局部变量表创建了两个对象:引用i和0。 这两个对象都是线程私有(安全)的。 比如定义了int[] is = new int[10]. 定义了两个对象,一个是is引用,放在局部变量表中,一个是长度为10的数组,放在堆中,这个数组,只能通过is来访问,方法结束后出栈,is被销毁,根据java的根搜索算法,判断数组不可达,就将它销毁了。
成员变量 int a = 1; a 存放在方法区,1存放在堆内存,a指向该内存
局部变量 int a = 1;a 存放在方法区, 1存放在栈内存,a指向该变量

5. springboot注入配置文件有哪几种方式

@ConfigurationProperties(prefix = "person") 注入yml文件
@Value 需要使用spel语法
@PropertySource 仅支持.properties文件不支持yml文件

6.给你一个ThreadPoolExcuter 现在我们给他一些构造参数,核心线程数10 ,最大线程数100,队列长度1024,过期时间5秒,还有拒绝策略。现在我开2000个任务,他会怎么执行

答:当线程数量小于核心线程数量,线程池会创建线程来执行任务,如果超过了核心线程数量,线程池会把任务放入队列,如果队列满了,线程池会继续创建线程直到线程数量=最大线程数量。如果此时还有任务要执行,线程池会执行拒绝策略。线程空闲时间超过5秒的时候,他会被销毁.
线程池的拒绝策略
(1)ThreadPoolExecutor.AbortPolicy 丢弃任务,并抛出RejectedExecutionException 异常。
(2)ThreadPoolExecutor.CallerRunsPolicy:该任务被线程池拒绝,由调用 execute方法的线程执行该任务。
(3)ThreadPoolExecutor.DiscardOldestPolicy : 抛弃队列最前面的任务,然后重新尝试执行任务。
(4)ThreadPoolExecutor.DiscardPolicy,丢弃任务,不过也不抛出异常。

7.maven整个生命周期

验证(validate)
编译源码(compile)
编译测试源码(test-compile)
单元测试(test)
打包(package)
安装至本地仓库(install)
复制到远程仓库(deploy)

8. 解决maven jar冲突问题

  • 1.MAVEN项目运行中如果报如下错误:
    Caused by:java.lang.NoSuchMethodError
    Caused by: java.lang.ClassNotFoundException
  • 2.jar包冲突原理
    A->B->C->D1(log 15.0):A中包含对B的依赖,B中包含对C的依赖,C中包含对D1的依赖,假设是D1是日志jar包,version为15.0
    E->F->D2(log 16.0):E中包含对F的依赖,F包含对D2的依赖,假设是D2是同一个日志jar包,version为16.0

9. 死锁产生原因如何防止死锁

Java发生死锁的根本原因是:
死锁是因为多线程访问共享资源,由于访问的顺序不当所造成的,在申请锁时发生了交叉闭环申请。即线程在获得了锁A并且没有释放的情况下去申请锁B,这时,另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环。
防止死锁:
1)尽量使用tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
2)尽量使用java.util.concurrent(jdk 1.5以上)包的并发类代替手写控制并发,比较常用的是ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等等,实际应用中java.util.concurrent.atomic十分有用,简单方便且效率比使用Lock更高
3)尽量降低锁的使用粒度,尽量不要几个功能用同一把锁
4)尽量减少同步的代码块

10.java中同步和并发的概念

  • 1.同步的概念
    synchronize从英译过来是"是同时发生"。
    但其真正的含义确实截然相反的。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。线程同步的目的就是让各个线程去排队使用资源,而不是让线程同时去使用资源。
  • 并发的概念

11.线程安全和线程不安全的集合

1.线程安全集合
Vector:就比Arraylist多了个同步化机制(线程安全)。
Hashtable:就比Hashmap多了个线程安全。
ConcurrentHashMap:是一种高效但是线程安全的集合。
Stack:栈,也是线程安全的,继承于Vector。
2.线程不安全集合
ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的。值得注意的是:为了保证集合是线程安全的,相应的效率也比较低;线程不安全的集合效率相对会高一些。

12.innodb索引结构为什么是树结构,不是hash结构。

hash索引,时间复杂度为O(1),平衡二叉树的时间复杂度为O(lg(n))。但是由于sql查询数据,很多都是范围查询,而树是有序的,hash是无序的,hash定位不到范围数据,所以索引结构是树,而不用hash结构。
此外,支持hash索引的引擎有:


image.png

innodb自适应hash索引,并不是和普通b+索引一样,我们手动指定哪一行创建还是不创建,而是innodb引擎会监控二级索引的访问频率,如果频率太高,则自动创建这个二级索引的hash索引,便于下次查找这个数据的时候,不需要通过b+树定位这个索引,而是直接hash找到这个索引。
当然,可以关闭innodb_adaptive_hash_index 这个变量来关闭自适应hash索引。

13.mysql的索引结果为什么是B+树

b+树和b树的区别是
b+树 非叶子节点不存储记录,记录都是存储在叶子节点上。2、叶子节点之间有链表关联。
这样,1、在范围查找的时候,直接找到最大,最小的值,然后进行链表遍历就能找到所有数据,不需要再进行树遍历。2、非叶子节点存储pk,叶子节点存储记录,记录之间存储的会更加紧密,遍历pk的时候,读取一页数据进内存,这页数据都是pk,而没有实际的记录,所以查找会更快。

14.jvm优化

1)年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(命名为A和B)。当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,如果对象仍然存活,则复制到B Suvivor Space,如果B Suvivor Space已经满,则复制到Old Gen。同时,在扫描Suvivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个持久化对象,则将其移到Old Gen。扫描完毕后,JVM将Eden Space和A Suvivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和B Suvivor Space。这么做主要是为了减少内存碎片的产生。
我们可以看到:Young Gen垃圾回收时,采用将存活对象复制到到空的Suvivor Space的方式来确保尽量不存在内存碎片,采用空间换时间的方式来加速内存中不再被持有的对象尽快能够得到回收。
2)年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。
3)持久代(Perm Gen):持久代主要存放类定义、字节码和常量等很少会变更的信息。
分区的目的:新生区由于对象产生的比较多并且大都是朝生夕灭的,所以直接采用复制算法。而养老区生命力很强,则采用标记-清理算法,针对不同情况使用不同算法。
非heap区域中Perm Gen中放着类、方法的定义,JVM Stack区域放着方法参数、局域变量等的引用,方法执行顺序按照栈的先入后出方式。

15.分代收集算法

  • 1.GC的分类
    Minor GC 采用复制算法
    Full GC 标记清除算法
  • 2.年轻代:尽可能快速地收集掉那些生命周期短的对象
    Eden区
    两个Survivor区


    image.png

    -> 3.年轻代垃圾回收过程演示
    (1)假设Eden区可存放四个对象,Survivor区可以存放3个对象


    image.png

    (2)假设Eden区被挤满,则触发一次Minor GC,此时如果有对象存活,则复制一份到Survivor区域中(此时S0我们成为from区域,S1为to区域)
    image.png

    (3)清理所有使用过的Eden区,并设置存活的对象年龄为1
    image.png

    (4)假设Eden区再次被填满,又会触发一次Minor GC。会将Eden区存活的对象与S0中的对象拷贝到S1中,同时对这些对象的年龄分别+1。此时S0从from区变成to区,S1从to区变成from区


    image.png

    (5)拷贝完成后Eden与S0均会被清空,这样便完成第二次Minor GC
    image.png

    (6)假设Eden区又满了,此时触发第三次Minor GC
    image.png

    会将Eden区存活的对象与S1中的对象拷贝到S0中,同时对这些对象的年龄分别+1
    image.png

    再次清空Eden与S1中的 对象
    image.png

    (7)周而复始,熬过一次Minor GC,年龄加一,默认年龄到达15时进入老年代。
  • 4.对象如何晋升老年代
    经历一定的Minor GC次数依然存活的对象
    Survivor区中存放不下的对象
    新生成的大对象(-XX:+PretenuerSizeThreshold)
  • 5.触发Full GC的条件
    老年代空间不足
    永久代空间不足
    CMS GC时出现promotion fail,concurrent mode failure
    Minor GC晋升到老年代的平均大小大于老年代的剩余空间
    调用System.gc()
    使用RMI来进行RPC或管理的JDK应用,每小时执行一次Full GC

16.springboot注解自动配置原理

@SpringBootApplication 这个注解是个组合注解,其中包含了@EnableAutoConfiguration自动注解类,这个类使用@Import方法注入AutoConfigurationImportSelector.class对象


image.png

image.png

这个类有个方法叫做selectImports,他通过SpringFactoriesLoader.loadFactoryNames先拿到这个自动注解的包名,之后按照这个包名保存到一个map中,


image.png

image.png

读取配置文件META-INF/spring.factories 其中配置了默认加载的类的列表

17.目前mysql有哪几种索引

1.普通索引
是最基本的索引,它没有任何限制。它有以下几种创建方式:
(1)直接创建索引
CREATE INDEX index_name ON table(column(length))
(2)修改表结构的方式添加索引
ALTER TABLE table_name ADD INDEX index_name ON (column(length))
(3)创建表的时候同时创建索引
CREATE TABLE table ( id int(11) NOT NULL AUTO_INCREMENT , title char(255) CHARACTER NOT NULL , content text CHARACTER NULL , time int(10) NULL DEFAULT NULL , PRIMARY KEY (id), INDEX index_name (title(length)) )
2.唯一索引
与前面的普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。它有以下几种创建方式:
(1)创建唯一索引
CREATE UNIQUE INDEX indexName ON table(column(length))
(2)修改表结构
ALTER TABLE table_name ADD UNIQUE indexName ON (column(length))
(3)创建表的时候直接指定
CREATE TABLE table ( id int(11) NOT NULL AUTO_INCREMENT , title char(255) CHARACTER NOT NULL , content text CHARACTER NULL , time int(10) NULL DEFAULT NULL , UNIQUE indexName (title(length)) );
(4)删除索引
DROP INDEX index_name ON table
3.主键索引
是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引:
CREATE TABLE table ( id int(11) NOT NULL AUTO_INCREMENT , title char(255) NOT NULL , PRIMARY KEY (id) );
4.组合索引
指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合
ALTER TABLE table ADD INDEX name_city_age (name,city,age);
5.全文索引
主要用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的where语句的参数匹配。fulltext索引配合match against操作使用,而不是一般的where语句加like。它可以在create table,alter table ,create index使用,不过目前只有char、varchar,text 列上可以创建全文索引。值得一提的是,在数据量较大时候,现将数据放入一个没有全局索引的表中,然后再用CREATE index创建fulltext索引,要比先为一张表建立fulltext然后再将数据写入的速度快很多。
(1)创建表的适合添加全文索引
CREATE TABLE table ( id int(11) NOT NULL AUTO_INCREMENT , title char(255) CHARACTER NOT NULL , content text CHARACTER NULL , time int(10) NULL DEFAULT NULL , PRIMARY KEY (id), FULLTEXT (content) );
(2)修改表结构添加全文索引
ALTER TABLE article ADD FULLTEXT index_content(content)
(3)直接创建索引
CREATE FULLTEXT INDEX index_content ON article(content)

18.mysql的MyISAM和innodb 区别

MySQL默认采用的是MyISAM。
MyISAM不支持事务,而InnoDB支持。InnoDB的AUTOCOMMIT默认是打开的,即每条SQL语句会默认被封装成一个事务,自动提交,这样会影响速度,所以最好是把多条SQL语句显示放在begin和commit之间,组成一个事务去提交。
InnoDB支持数据行锁定,MyISAM不支持行锁定,只支持锁定整个表。即 MyISAM同一个表上的读锁和写锁是互斥的,MyISAM并发读写时如果等待队列中既有读请求又有写请求,默认写请求的优先级高,即使读请求先到,所以 MyISAM不适合于有大量查询和修改并存的情况,那样查询进程会长时间阻塞。因为MyISAM是锁表,所以某项读操作比较耗时会使其他写进程饿死。
InnoDB支持外键,MyISAM不支持。
InnoDB的主键范围更大,最大是MyISAM的2倍。
InnoDB不支持全文索引,而MyISAM支持。全文索引是指对char、 varchar和text中的每个词(停用词除外)建立倒排序索引。MyISAM的全文索引其实没啥用,因为它不支持中文分词,必须由使用者分词后加入空 格再写到数据表里,而且少于4个汉字的词会和停用词一样被忽略掉。
MyISAM支持GIS数据,InnoDB不支持。即MyISAM支持以下空间数据对象:Point,Line,Polygon,Surface等。
没有where的count()使用MyISAM要比InnoDB快得多。因 为MyISAM内置了一个计数器,count()时它直接从计数器中读,而InnoDB必须扫描全表。所以在InnoDB上执行count()时一般 要伴随where,且where中要包含主键以外的索引列。为什么这里特别强调“主键以外”?因为InnoDB中primary index是和raw data存放在一起的,而secondary index则是单独存放,然后有个指针指向primary key。所以只是count()的话使用secondary index扫描更快,而primary key则主要在扫描索引同时要返回raw data时的作用较大。

19.rocketmq架构原理

image.png
  • Broker
    broker主要用于producer和consumer接收和发送消息
    broker会定时向nameserver提交自己的信息
    是消息中间件的消息存储、转发服务器
    每个Broker节点,在启动时,都会遍历NameServer列表,与每个NameServer建立长连接,注册自己的信息,之后定时上报
  • Nameserver
    理解成zookeeper的效果,只是他没用zk,而是自己写了个nameserver来替代zk
    底层由netty实现,提供了路由管理、服务注册、服务发现的功能,是一个无状态节点
    nameserver是服务发现者,集群中各个角色(producer、broker、consumer等)都需要定时向nameserver上报自己的状态,以便互相发现彼此,超时不上报的话,nameserver会把它从列表中剔除nameserver可以部署多个,当多个nameserver存在的时候,其他角色同时向他们上报信息,以保证高可用,NameServer集群间互不通信,没有主备的概念nameserver内存式存储,nameserver中的broker、topic等信息默认不会持久化,所以他是无状态节点
  • Producer
    消息的生产者随机选择其中一个NameServer节点建立长连接,获得Topic路由信息(包括topic下的queue,这些queue分布在哪些broker上等等)接下来向提供topic服务的master建立长连接(因为rocketmq只有master才能写消息),且定时向master发送心跳
  • Consumer
    消息的消费者通过NameServer集群获得Topic的路由信息,连接到对应的Broker上消费消息由于Master和Slave都可以读取消息,因此Consumer会与Master和Slave都建立连接进行消费消息
  • 核心流程
    Broker都注册到Nameserver上
    Producer发消息的时候会从Nameserver上获取发消息的topic信息
    Producer向提供服务的所有master建立长连接,且定时向master发送心跳
    Consumer通过NameServer集群获得Topic的路由信息
    Consumer会与所有的Master和所有的Slave都建立连接进行监听新消息

20.堆积的消息会不会进死信队列?

不会,消息在消费失败后会进入重试队列(%RETRY%+ConsumerGroup),18次(默认18次,网上所有文章都说是16次,无一例外。但是我没搞懂为啥是16次,这不是18个时间吗 ?)才会进入死信队列(%DLQ%+ConsumerGroup)。

21.堆积时间过长消息超时了?

RocketMQ中的消息只会在commitLog被删除的时候才会消失,不会超时。也就是说未被消费的消息不会存在超时删除这情况。

22.分代收集算法

分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存
划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(Young
Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃
圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

23.Rocketmq每个Broker与Name Server集群中的所有节点建立长连接

1、这样可以使Name Server之间可以没有任何关联,因为它们绑定的Broker是一致的。
2、作为Producer或者Consumer可以绑定任何一个Name Server 因为它们都是一样的.
3.容错率比较高,就算某一台nameServer挂掉也不会影响其他服务

24.app微信登录图解

image.png

登录流程: 手机端单击微信,拉起微信授权,单击同意,微信会返回最终code值,微信返回第三方app,之后拿着code请求我们后端接口,根据code接口请求最终的用户信息包括openid,查询数据库是否是存在该用户,存在直接返回token信息.

25.pc微信登录

image.png

登录流程:pc单击登陆按钮跳转微信二维码,跳转二维码的时候需要携带appid信息,扫描二维码会请求微信后端,并回调第三方服务,第三方服务设置重定向并携带登录信息,前端拿到登录信息后进行登录.这个和app登录不相同的地方是, app登录的时候 code是app主动调用后端接口,而pc则是 微信回调传输的code值

25. 使用futureTask创建线程

import java.util.concurrent.Callable;
public class Task implements Callable{
    
    @Override
    public Integer call() throws Exception {
        System.out.println("子线程在进行计算");
        Thread.sleep(3000);
        int sum = 0;
        for(int i=0;i<100;i++)
            sum += i;
        return sum;
    }
}
package com.demo.test
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
public class CallableTest1 {
    public static void main(String[] args) {
        //第一种方式
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        FutureTask futureTask = new FutureTask(task);
        executor.submit(futureTask);
        executor.shutdown();
        //第二种方式,注意这种方式和第一种方式效果是类似的,只不过一个使用的是ExecutorService,一个使用的是Thread
//        Task task = new Task();
//        FutureTask futureTask = new FutureTask(task);
//        Thread thread = new Thread(futureTask);
//        thread.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        System.out.println("主线程在执行任务");
        try {
            if(futureTask.get()!=null){  
                System.out.println("task运行结果"+futureTask.get());
            }else{
                System.out.println("future.get()未获取到结果"); 
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("所有任务执行完毕"); }}

26.springboot加载bean的几种方式

  • 1.如果是复杂的依赖进去的jar环境可以使用spring.factories在中进行配置bean resources/META-INF/spring.factories 其中
    org.springframework.boot.autoconfigure.EnableAutoConfiguration top.huic.logrecord.plus.ui.LogRecordPlusUiStart
    代表自动配置的 key,即代表需要自动配置哪些类,\ 可以理解为一个换行符,则该行下面的每行当做一个参数
  • 2.@Configuration是声明为一个配置类@ComponentScan是设置自动扫描包,让Spring能够发现我们封装的组件的其他 Spring Bean,@Import直接写入包名也可直接注入

27. springcache缓存,本地的二级缓存 redis发布订阅模式整合

使用 RedisMessageListenerContainer 调用 addMessageListener添加监听topic,使用redistemplate提供的convertAndSend发送消息,这个发布订阅是广播模式,每个收到这个生产者消息的时候都会进行消费,清除本地缓存,自定义cache继AbstractValueAdaptingCache抽象类重写put方法,evict方法,clear,lookup查询方法添加上本地缓存同时在对应方法写上redistemplate的生产消息方法,创建cachemanager并实现cachemanager方法,覆盖getCache方法,改成自定义cache类,

28.zookeeper分布式锁原理

首先,Zookeeper的每一个节点,都是一个天然的顺序发号器。
在每一个节点下面创建子节点时,只要选择的创建类型是有序(EPHEMERAL_SEQUENTIAL 临时有序或者PERSISTENT_SEQUENTIAL 永久有序)类型,那么,新的子节点后面,会加上一个次序编号。这个次序编号,是上一个生成的次序编号加一比如,创建一个用于发号的节点“/test/lock”,然后以他为父亲节点,可以在这个父节点下面创建相同前缀的子节点,假定相同的前缀为“/test/lock/seq-”,在创建子节点时,同时指明是有序类型。如果是第一个创建的子节点,那么生成的子节点为/test/lock/seq-0000000000,下一个节点则为/test/lock/seq-0000000001,依次类推,等等。


image.png

其次,Zookeeper节点的递增性,可以规定节点编号最小的那个获得锁。
一个zookeeper分布式锁,首先需要创建一个父节点,尽量是持久节点(PERSISTENT类型),然后每个要获得锁的线程都会在这个节点下创建个临时顺序节点,由于序号的递增性,可以规定排号最小的那个获得锁。所以,每个线程在尝试占用锁之前,首先判断自己是排号是不是当前最小,如果是,则获取锁。

29.Mysql使用索引为什么会变快?

检索中主要耗时在于内存与磁盘的IO耗时,所以加速的关键在于减少IO的次数。


image.png

图中是一颗 b 树,每个磁盘块包含几个数据项和指针,
如磁盘块 1 包含数据项 17 和 35,包含指针 P1、P2、P3,
P1指向包含数据项小于17的磁盘块,P2指向数据项在17和35之间的磁盘块,P3 指向数据项大于35的磁盘块。真实的数据存在于叶子节点,即 3、5、9、10、13、15、28、29、36、60、75、79、90、99。非叶子节点只不存储真实的数据,只存储指引搜索方向的数据项,如 17、35 并不真实存在于数据表中。
【查找过程】 以查找数据项29为例
首先会把磁盘块 1 由磁盘加载到内存,此时发生一次 IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的 P2指针,相比磁盘的 IO,内存时间非常短可以忽略不计。通过磁盘块 1的 P2 指针的磁盘地址把磁盘块 3 由磁盘加载到内存,发生第二次 IO,29 在 26 和 30 之间,同理锁定磁盘块 3 的 P2 指针。通过指针加载磁盘块 8 到内存,发生第三次 IO,同时内存中做二分查找找到了数据项 29,结束查询,总计三次 IO。

30.mybatis是如何从xml文件查询到对应的文件的

31.redis分布式锁机制,如果该锁过期之后怎么办

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

// 最常见的使用方法
lock.lock();
lock.unlock()

// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
lock.unlock();

//公平锁
RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();

32.mysql的行级锁和表级锁

mysql行级锁注意几点:
1、行锁必须有索引才能实现,否则会自动锁全表,那么就不是行锁了。
2、两个事务不能锁同一个索引(不能有锁住有交集的行).
例如:
事务A先执行:select math from zje where math>60 for update;
事务B再执行:select math from zje where math<60 for update;
行级锁又分共享锁和排他锁。
名词解释:

  • 1.共享锁
    共享锁又叫做读锁,所有的事务只能对其进行读操作不能写操作,加上共享锁后在事务结束之前其他事务只能再加共享锁,除此之外其他任何类型的锁都不能再加了。
    用法:SELECT id FROM table WHERE id in(1,2) LOCK IN SHARE MODE 结果集的数据都会加共享锁
  • 2.排他锁
    名词解释:若某个事物对某一行加上了排他锁,只能这个事务对其进行读写,在此事务结束之前,其他事务不能对其进行加任何锁,其他进程可以读取,不能进行写操作,需等待其释放。
    用法:SELECT id FROM mk_user WHERE id=1 FOR UPDATE
    锁冲突:例如说事务A将某几行上锁后,事务B又对其上锁,锁不能共存否则会出现锁冲突。(但是共享锁可以共存,共享锁和排它锁不能共存,排它锁和排它锁也不可以)。
    死锁:例如说两个事务,事务A锁住了1-5行,同时事务B锁住了6-10行,此时事务A请求锁住6-10行,就会阻塞直到事务B施放6-10行的锁,而随后事务B又请求锁住1-5行,事务B也阻塞直到事务A释放1~5行的锁。死锁发生时,会产生Deadlock错误。
    3、insert ,delete , update在事务中都会自动默认加上排它锁(写)。
    会话1:select math from zje where math>60 for update;
    会话2:update zje set math=99 where math=68;阻塞...........
    如上,会话1先把zje表中math>60的行上排它锁。然后会话2试图把math=68的行进行修改,math=68处于math>60中,所以是已经被锁的,会话2进行操作时,就会阻塞,等待会话1把锁释放。当commit时或者程序结束时,会释放锁。

33.springboot如何更换tomcat

只需要修改tomcat即可实现替换tomcat


org.springframework.boot
spring-boot-starter-web


org.springframework.boot
spring-boot-starter-tomcat




org.springframework.boot
spring-boot-starter-jetty

34.java本地事务失效的原因

1.当前类没有交给spring进行管理
2.目标方式不是public,事务默认直接进行忽略


image.png

3.自己调用自己的方法的时候事务也是会失效的,其本质上是因为最终生成的代理类,最终调用还是原始的方法,

 
    private DmzService dmzService;
 
    public DmzServiceProxy(DmzService dmzService) {
        this.dmzService = dmzService;
    }
 
    public void saveAB(A a, B b) {
        dmzService.saveAB(a, b);
    }
 
    public void saveA(A a) {
        try {
            // 开启事务
            startTransaction();
            dmzService.saveA(a);
        } catch (Exception e) {
            // 出现异常回滚事务
            rollbackTransaction();
        }
        // 提交事务
        commitTransaction();
    }
 
    public void saveB(B b) {
        try {
            // 开启事务
            startTransaction();
            dmzService.saveB(b);
        } catch (Exception e) {
            // 出现异常回滚事务
            rollbackTransaction();
        }
        // 提交事务
        commitTransaction();
    }
}

35.mysql索引回表问题

mysql说起回表问题,要从从mysql的聚集索引和非聚集索引说起,聚集索引指的是以主键建立的索引中他是带有当前表的所有数据的,当然如果当前没有定义主键的话,他是按照当前表中具有唯一性的字段作为索引,去简历聚集索引,当前聚集索引的叶子结点存储的是当前的行记录, 而非聚集索引指的其他非主键去建立的索引 这种1索引是没有存储行记录的,
比如在name字段加上索引,然后按照name进行查询 select * 他会查询两边索引,先查询name索引树,name索引树上挂载的当前行记录的id,他拿到id需要去id索引树下进行查询,id索引树下挂载的是完整的行记录

36.mysql 索引最左原则原理

B+数

索引本质是一棵B+Tree,联合索引(col1, col2,col3)也是。其非叶子节点存储的是第一个关键字的索引,而叶节点存储的则是三个关键字col1、col2、col3三个关键字的数据,且按照col1、col2、col3的顺序进行排序。联合索引(年龄, 姓氏,名字),叶节点上data域存储的是三个关键字的数据。且是按照年龄、姓氏、名字的顺序排列的。而最左原则的原理就是,因为联合索引的B+Tree是按照第一个关键字进行索引排列的。

37.B+树和B树有什么区别

只有叶子节点存储data,叶子节点包含了这棵树的所有键值,叶子节点不存储指针。每个节点都存储key和data,所有节点组成这棵树,并且叶子节点指针为null。
B+树的叶子节点会用指针将数据串联起来,B树叶子结点是空的只有键值存储数据,B+树是叶子结点存储数据

38.hashMap 扩充过程会有什么安全隐患?

hashmpa默认长度是16,超过16的时候就需要进行扩充,当再扩充的时候,如果多个线程去操作他的话,就会出现循环链表,hashMpa扩充的机制,

39.count(*) 和 count(1)区别

count(字段),根绝字段判断为不为不空,根据字段定义,考虑要不要累加返回值,既然你引擎都返回值了,那我server层 “ +1 ”count(id),根据id主键取值,累加返回值,也是server层 “ +1 ”count(1),同样会遍历,但不取值,引擎告诉不为空那我就 “+1”count(),也不取值,而且人家还是经过优化的根据上面的推倒,搜主键肯定比搜正常字段快, 不取值的一定比取值的快(我就是查数统计一下,你给我这一行所有的值也没啥用啊), 优化过的比没优化过的快以下排行是按照效率,而不是时间count() > count(1) > count(id) > count(字段)

40.linux命令总结

查找文件内容
grep "search content" filename1

41.mybatis是如何将xml转换为sql的

42.sql执行顺序

from
join
on
where
group by(开始使用select中的别名,后面的语句中都可以使用)
avg,sum....
having
select
distinct
order by
limit
第一步:首先对from子句中的前两个表执行一个笛卡尔乘积,此时生成虚拟表 vt1(选择相对小的表做基础表)。
第二步:接下来便是应用on筛选器,on 中的逻辑表达式将应用到 vt1 中的各个行,筛选出满足on逻辑表达式的行,生成虚拟表 vt2 。
第三步:如果是outer join 那么这一步就将添加外部行,left outer jion 就把左表在第二步中过滤的添加进来,如果是right outer join 那么就将右表在第二步中过滤掉的行添加进来,这样生成虚拟表 vt3 。
第四步:如果 from 子句中的表数目多余两个表,那么就将vt3和第三个表连接从而计算笛卡尔乘积,生成虚拟表,该过程就是一个重复1-3的步骤,最终得到一个新的虚拟表 vt3。
第五步:应用where筛选器,对上一步生产的虚拟表引用where筛选器,生成虚拟表vt4,在这有个比较重要的细节不得不说一下,对于包含outer join子句的查询,就有一个让人感到困惑的问题,到底在on筛选器还是用where筛选器指定逻辑表达式呢?on和where的最大区别在于,如果在on应用逻辑表达式那么在第三步outer join中还可以把移除的行再次添加回来,而where的移除的最终的。举个简单的例子,有一个学生表(班级,姓名)和一个成绩表(姓名,成绩),我现在需要返回一个x班级的全体同学的成绩,但是这个班级有几个学生缺考,也就是说在成绩表中没有记录。为了得到我们预期的结果我们就需要在on子句指定学生和成绩表的关系(学生.姓名=成绩.姓名)那么我们是否发现在执行第二步的时候,对于没有参加考试的学生记录就不会出现在vt2中,因为他们被on的逻辑表达式过滤掉了,但是我们用left outer join就可以把左表(学生)中没有参加考试的学生找回来,因为我们想返回的是x班级的所有学生,如果在on中应用学生.班级='x'的话,left outer join会把x班级的所有学生记录找回(感谢网友康钦谋__康钦苗的指正),所以只能在where筛选器中应用学生.班级='x' 因为它的过滤是最终的。
第六步:group by 子句将中的唯一的值组合成为一组,得到虚拟表vt5。如果应用了group by,那么后面的所有步骤都只能得到的vt5的列或者是聚合函数(count、sum、avg等)。原因在于最终的结果集中只为每个组包含一行。这一点请牢记。
第七步:应用cube或者rollup选项,为vt5生成超组,生成vt6.
第八步:应用having筛选器,生成vt7。having筛选器是第一个也是为唯一一个应用到已分组数据的筛选器。
第九步:处理select子句。将vt7中的在select中出现的列筛选出来。生成vt8.
第十步:应用distinct子句,vt8中移除相同的行,生成vt9。事实上如果应用了group by子句那么distinct是多余的,原因同样在于,分组的时候是将列中唯一的值分成一组,同时只为每一组返回一行记录,那么所以的记录都将是不相同的。
第十一步:应用order by子句。按照order_by_condition排序vt9,此时返回的一个游标,而不是虚拟表。sql是基于集合的理论的,集合不会预先对他的行排序,它只是成员的逻辑集合,成员的顺序是无关紧要的。对表进行排序的查询可以返回一个对象,这个对象包含特定的物理顺序的逻辑组织。这个对象就叫游标。正因为返回值是游标,那么使用order by 子句查询不能应用于表表达式。排序是很需要成本的,除非你必须要排序,否则最好不要指定order by,最后,在这一步中是第一个也是唯一一个可以使用select列表中别名的步骤。
第十二步:应用top选项。此时才返回结果给请求者即用户。

from tb_Grade 
where 考生姓名 is not null 
group by 考生姓名 
having max(总成绩) > 600 
order by max总成绩 

(1)首先执行 FROM 子句, 从 tb_Grade 表组装数据源的数据
(2). 执行 WHERE 子句, 筛选 tb_Grade 表中所有数据不为 NULL 的数据
(3). 执行 GROUP BY 子句, 把 tb_Grade 表按 "学生姓名" 列进行分组(注:这一步开始才可以使用select中的别名,他返回的是一个游标,而不是一个表,所以在where中不可以使用select中的别名,而having却可以使用,感谢网友 zyt1369 提出这个问题)
(4). 计算 max() 聚集函数, 按 "总成绩" 求出总成绩中最大的一些数值
(5). 执行 HAVING 子句, 筛选课程的总成绩大于 600 分的.
(7). 执行 ORDER BY 子句, 把最后的结果按 "Max 成绩" 进行排序.

43.红黑树结构

image.png

1.每个节点只能是红色或者黑色。
2.根节点必须是黑色。
3.红色的节点,它的叶节点只能是黑色。
4.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

44.redis内存满的时候的缓存淘汰算法

FIFO

  1. FIFO(First in First out),先进先出。在FIFO Cache设计中,核心原则就是:如果一个数据最先进入缓存中,则应该最早淘汰掉。
    1、利用一个双向链表保存数据,
    2、当来了新的数据之后便添加到链表末尾,
    3、如果Cache存满数据,则把链表头部数据删除,
    4、然后把新的数据添加到链表末尾。
    5、在访问数据的时候,如果在Cache中存在该数据的话,则返回对应的value值;
    6、否则返回-1。如果想提高访问效率,可以利用hashmap来保存每个key在链表中对应的位置。
    FIFO是最简单的淘汰策略,遵循着先进先出的原则,这里简单提一下:


    image.png
  2. LRU(Least Recently Used)表示最近最少使用,该算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。LRU算法的常见实现方式为链表:新数据放在链表头部 ,链表中的数据被访问就移动到链头,链表满的时候从链表尾部移出数据。


    image.png

    3.LFU算法
    LFU(Least Frequently Used)表示最不经常使用,它是根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也高”。
    LFU算法反映了一个key的热度情况,不会因LRU算法的偶尔一次被访问被误认为是热点数据。LFU算法的常见实现方式为链表:新数据放在链表尾部 ,链表中的数据按照被访问次数降序排列,访问次数相同的按最近访问时间降序排列,链表满的时候从链表尾部移出数据。


    image.png

45.redis的过期删除key的策略

在Redis中过期的key不会立刻从内存中删除,而是会同时以下面两种策略进行删除:惰性删除:当key被访问时检查该key的过期时间,若已过期则删除;已过期未被访问的数据仍保持在内存中,消耗内存资源;
定期删除:每隔一段时间,随机检查设置了过期的key并删除已过期的key;维护定时器消耗CPU资源;
Redis每10秒进行一次过期扫描:
随机取20个设置了过期策略的key;
检查20个key中过期时间中已过期的key并删除;
如果有超过25%的key已过期则重复第一步;

46.AOF和RDB的过期删除策略

1.RDB对过期建的处理
在启动 Redis 服务器时,如果服务器开启了 RDB 功能,那么服务器将对 RDB 文件进行载入:
如果服务器以主服务器模式运行,那么在载入 RDB 文件时,程序会对文件中保存的键进行检查,未过期的键会被载入到数据库中,而过期键则会被忽略,所以过期键对载入RDB 文件的主服务器不会造成影响。
如果服务器以从服务器模式运行,那么在载入 RDB 文件时,文件中保存的所有键,不论是否过期,都会被载入到数据库中。不过,因为主从服务器在进行数据同步的时候,从服务器的数据库就会被清空,所以一般来讲,过期键对载入 RDB 文件的从服务器也不会造成影响。
2.AOF 持久化功能对过期键的处理
AOF文件的写入
当服务器以 AOF 持久化模式运行时, 如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么 AOF文件不会因为这个过期键而产生任何 影响。 当过期键被惰性删除或者定期删除之后,程序会向 AOF 文件追加(append)一条 DEL 命令,来显式地记录该键已被删除。
AOF文件载入
和生成 RDB 文件时类似,在执行 AOF 重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的 AOF 文件中。
过期删除操作统一在 master 实例中进行并向下传递,而不是各 salve 各自处理。这样一来便不会出现数据不一致的情形。
扩展
当 slave 连接到 master 后并不能立即清理已过期的 key(需要等待由master传递过来的DEL操作),slave 仍需对数据集中的过期状态进行管理维护以便于在 slave 被选为 master 时能像 master 一样独立的进行过期处理。

47.类加载机制

1.类加载的过程
我们编写的java文件都是保存着业务逻辑代码。java编译器将 .java 文件编译成扩展名为 .class 的文件。.class 文件中保存着java转换后,虚拟机将要执行的指令。当需要某个类的时候,java虚拟机会加载 .class 文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程被称为类的加载。

image.png

加载
类加载过程的一个阶段,ClassLoader通过一个类的完全限定名查找此类字节码文件,并利用字节码文件创建一个class对象。
验证
目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身的安全,主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证。
准备
为类变量(static修饰的字段变量)分配内存并且设置该类变量的初始值,(如static int i = 5 这里只是将 i 赋值为0,在初始化的阶段再把 i 赋值为5),这里不包含final修饰的static ,因为final在编译的时候就已经分配了。这里不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象分配到Java堆中。
解析
这里主要的任务是把常量池中的符号引用替换成直接引用
初始化
这里是类记载的最后阶段,如果该类具有父类就进行对父类进行初始化,执行其静态初始化器(静态代码块)和静态初始化成员变量。(前面已经对static 初始化了默认值,这里我们对它进行赋值,成员变量也将被初始化)
类记载器的任务是根据类的全限定名来读取此类的二进制字节流到 JVM 中,然后转换成一个与目标类对象的java.lang.Class 对象的实例,在java 虚拟机提供三种类加载器,引导类加载器,扩展类加载器,系统类加载器。
forName和loaderClass区别
Class.forName()得到的class是已经初始化完成的。

Classloader.loaderClass得到的class是还没有链接(验证,准备,解析三个过程被称为链接的。

jdk中的加载器
Bootstrp loader
Bootstrp加载器是用C++语言写的,它是在Java虚拟机启动后初始化的,它主要负责加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类。
ExtClassLoader
Bootstrp loader加载ExtClassLoader,并且将ExtClassLoader的父加载器设置Bootstrp loader.ExtClassLoader是用Java写的,具体来是sun.misc.Launcher ExtClassLoader,ExtClassLoader主要加载/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库。
AppClassLoader
Bootstrp loader加载完ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为 ExtClassLoader。AppClassLoader也是用Java写成的,它的实现类是 sun.misc.Launcher$AppClassLoader,另外我们知道ClassLoader中有个getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器。

48. 类加载顺序

1)加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
2)在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。
3)Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null。
垃圾回收器比较
垃圾回收算法性能:
吞吐量:运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。吞吐量越高,CPU利用越高效,则算法越好
最大暂停时间:因 GC 而暂停应用程序线程的最长时间。暂停时间越短,则算法越好

49.hashMap初始化容量建议值

image.png

50.hashMap扩容机制

JDK7的扩容机制相对简单,有以下特性:

  • 空参数的构造函数:以默认容量、默认负载因子、默认阈值初始化数组。内部数组是空数>组
  • 有参构造函数:根据参数确定容量、负载因子、阈值等。
  • 第一次put时会初始化数组,其容量变为不小于指定容量的2的幂数。然后根据负载因>子确定阈值。
  • 如果不是第一次扩容,则新容量=旧容量2, 新阈值=新容量负载因子
    JDK8的扩容机制
    JDK8的扩容做了许多调整。
    HashMap的容量变化通常存在以下几种情况:
  1. 空参数的构造函数:实例化的HashMap默认内部数组是null,即没有实例化。第一次调用put方法时,则会开始第一次初始化扩容,长度为16。
  2. 有参构造函数:用于指定容量。会根据指定的正整数找到不小于指定容量的2的幂数,将这个数设置赋值给阈值(threshold)。第一次调用put方法时,会将阈值赋值给容量,然后让 阈值 = 容量乘于负载因子
    。(因此并不是我们手动指定了容量就一定不会触发扩容,超过阈值后一样会扩容!!)
  3. 如果不是第一次扩容,则容量变为原来的2倍,阈值也变为原来的2倍。(容量和阈值都变为原来的2倍时,负载因子还是不变)
    4.讨论当前索引扩充之后如何重新计算当前的hashcode问题
    我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
    image.png

    总结因为在hash中存储的值的是 拿着hashCode与当前hashMap的长度进行&运算得到的最终结果,也就是如果当计算的hashCode值比当前hashMap数组大小 小的情况下其对应的扩充之后hashMap索引值是不会变的,我们都知道容量乘于2的时候实际上相当于将当前容器容量左移1位,但是左移的话,二进制就会出现0位,但是他是按照 2 * oldCap - 1,减1操作就会把0全部置换为1,我们知道hashMap初始容量都是2的幂数, 因此对应的二进制只有开头有一个1后边全是0.因此减去1之后就是全部是1了,当前old跟hashCode进行计算的时候如果hashCode大小没有old大的话,也就是再参与位运算的时候他比old短,那就意味着;不论old扩充几倍,跟hashCode参与运算的都是那几个数字因此,大小不变,还有一种情况就是如果hashCode比oldCap大的话情况就不同了,这种情况下就要查看hashCode的计算中新增的那一位是0还是1如果是0的话还是不变,如果是1的话就需要变化

51.HashMap中的hash算法

首先要明白一个概念,HashMap中定位到桶的位置 是根据Key的hash值与数组的长度取模来计算的。具体的细节我就不说了,默认认为大家都懂这一点。取模可以改为:hashCode & (length - 1),这里的数组长度最好是2的幂数,这样能减少hashcode碰撞概率

52.cms算法

初始标记(STW initial mark)
并发标记(Concurrent marking)
并发预清理(Concurrent precleaning)
重新标记(STW remark)
并发清理(Concurrent sweeping)
并发重置(Concurrent reset)
初始标记 :在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的”根对象”开始,只扫描到能够和”根对象”直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。
并发标记 :这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。
并发预清理 :并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段”重新标记”的工作,因为下一个阶段会Stop The World。
重新标记 :这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从”跟对象”开始向下追溯,并处理对象关联。
并发清理 :清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。


image.png

3.CMS缺点
CMS回收器采用的基础算法是Mark-Sweep。所有CMS不会整理、压缩堆空间。这样就会有一个问题:经过CMS收集的堆会产生空间碎片。 CMS不对堆空间整理压缩节约了垃圾回收的停顿时间,但也带来的堆空间的浪费。为了解决堆空间浪费问题,CMS回收器不再采用简单的指针指向一块可用堆空 间来为下次对象分配使用。而是把一些未分配的空间汇总成一个列表,当JVM分配对象空间的时候,会搜索这个列表找到足够大的空间来hold住这个对象。
需要更多的CPU资源。从上面的图可以看到,为了让应用程序不停顿,CMS线程和应用程序线程并发执行,这样就需要有更多的CPU,单纯靠线程切 换是不靠谱的。并且,重新标记阶段,为空保证STW快速完成,也要用到更多的甚至所有的CPU资源。当然,多核多CPU也是未来的趋势!
CMS的另一个缺点是它需要更大的堆空间。因为CMS标记阶段应用程序的线程还是在执行的,那么就会有堆空间继续分配的情况,为了保证在CMS回 收完堆之前还有空间分配给正在运行的应用程序,必须预留一部分空间。也就是说,CMS不会在老年代满的时候才开始收集。相反,它会尝试更早的开始收集,已 避免上面提到的情况:在回收完成之前,堆没有足够空间分配!默认当老年代使用68%的时候,CMS就开始行动了。 – XX:CMSInitiatingOccupancyFraction =n 来设置这个阀值。

你可能感兴趣的:(面试题总结)