本文主要是介绍Java面经—远景智能,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 01、自我介绍一下吧
- 02、介绍一下简历中的实习经历吧
- 03、追问:MQTT协议说一下
- 04、说一下MySQL中的索引吧
- 05、索引的种类有哪些?
- 06、聚集索引和非聚集索引的区别?
- 07、为什么 MySQL 的索引要使用 B+树而不是其它树形结构?比如 B 树?
- 08、MySQL的四大特性
- 09、MySQL中的事务的隔离级别,MySQL中默认级别
- 10、 事务的实现原理(MySQL中的日志)
- 11、MySQL如何实现事务隔离的
- 12、MySQL中的锁
- 13、LinkedList、ArrayList的区别,分别适用于什么场景?
- 14、ConcurrentHashMap知道吗,JDK1.7和JDK1.8的区别
- 15、ConcurrentHashMap是线程安全的吗?如何实现的?
- 16、HashMap是线程安全的吗?并发情况下会出现什么问题?
- 17、讲一下HashMap中的put方法过程?
- 18、Volitile关键字知道吗?说一说?
- 19、Synchronize关键字知道吗?ReentrentLock知道吗?它俩的区别?
- 20、Java中的线程池了解吗?说一说它的参数有几个?拒绝策略也说一下?
- 21、用过SpringMVC框架吗,说一说其中的主要组件,调用流程?
- 22、用过Spring框架吗,它的两大特性是什么?具体的说一说实现原理
- 23、动态代理的方式有哪些?有什么不同点?
- 24、Spring框架如何能解决循环依赖的问题吗?是不是所有的循环依赖都能解决?
- 25、说一下Mybatis加载的流程是什么,以及MyBatis 的一、二级缓存?
- 26、你知道哪些设计模式,说一下?
- 27、JVM知道吗?说一下JVM的内存模型
- 28、说一下双亲委派机制?
- 29、说一下你知道的垃圾回收器,和垃圾回收算法?
- 新生代垃圾回收器
- Serial 垃圾回收器
- ParNew 垃圾回收器
- ParallelGC 回收器
- 老年代垃圾回收器
- SerialOld 垃圾回收器
- ParallelOldGC 回收器
- CMS 回收器
- G1 回收器
- 总结
01、自我介绍一下吧
面试官你好,我叫xxx,本科毕业于xxxx,所学专业是xxx,研究生就读于xxx,所学专业是xxx,研究方向是xxx,在去面八月份到今年的四月份期间有一段实习,实习单位是xxx,做的课题是xxx,所做工作是辅助该系统的研发,自己也做过一些小的项目,所用技术栈有Spring,SpringBoot,SpringMVC,MyBatis,
02、介绍一下简历中的实习经历吧
此处省略一万字。。。。。
03、追问:MQTT协议说一下
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(Publish/Subscribe)模式的轻量级通讯协议,该协议构建于TCP/IP协议上,是一个应用层协议。
MQTT最大的优点在于可以,以极少的代码和有限的带宽,为远程设备提供实时可靠的消息服务。MQTT在物联网、小型设备、移动设备等方面应用广泛。
当然,在物联网开发中,MQTT不是唯一的选择,与MQTT互相竞争的协议有XMPP和CoAP协议等
TCP和UDP位于传输层,应用层常见的协议有HTTP、FTP、SSH等。MQTT协议运行于TCP之上,属于应用层协议,因此只要是支持TCP/IP协议栈的地方,都可以使用MQTT。
每个MQTT消息的消息头都包含一个固定的报头,有些消息还会携带一个可变报文头和一个负荷,具体消息格式如下:
固定报文头 | 可变报文头 | 负荷
固定报头包含两个字节:
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Byte 1 | MQTT | Control | Packet | Type | Flags specific | to each | MQTT Control | Packet type |
Byte 2 |
-
首字节:高四位用于确定
报文类型
(总共可以表示16种协议类型,其中0000和1111是保留字段),低四位是某些报文类型的控制字段(实际上只有少数报文类型有控制位)。 -
第二个字节:开始是表示剩余长度的字段,这个字节如果
最高位为1
,则表示后续还有字节存在,一起用来表示消息的长度
,最多允许四个节。消息长度=可变报头+消息内容(负荷)。不包括固定头部(首字节和和表示消息长度的字节),如果消息长度为5(只需要一个字节就可以表示消息长度,一个字节最多可表示的长度为128),整条消息长度为7(首字节+1个表示消息长度字节+5=7)。
注意:固定头部最多为5个字节,其中从第二到第五字节表示消息长度
当从第二到第五字节中的值为:11111111 11111111 11111111 01111111 ,表示最大长度
最大长度计算:
127+127*128+127*128*128+127*128*128*128=268435455
,268435455/1024/1024=256 MB1 MB=1024 kb=1024*1024 bit
-
消息长度最长为:256 MB
可变报文头:
- 可变报文头主要包含协议名、协议版本、连接标志(Connect Flags)、心跳间隔时间(Keep Alive timer)、连接返回码(Connect Return Code)、主题名(Topic Name)等。
有效负荷(Payload):
-
Payload直译为负荷,可能让人摸不着头脑,实际上可以理解为
消息主体
(body)或者消息内容。 -
当MQTT发送的消息类型是CONNECT(连接)、PUBLISH(发布)、SUBSCRIBE(订阅)、SUBACK(订阅确认)、UNSUBSCRIBE(取消订阅)时,则会带有
负荷
。
04、说一下MySQL中的索引吧
-
引入索引的目的是为了提高查询效率。
-
索引是
表的目录
,在查询数据之前先通过索引目录查找到相应的位置,以此快速定位要查询的数据。 -
对于索引,会保存在额外的文件中,索引是帮助MySQL高效获取数据的数据结构。
05、索引的种类有哪些?
1、从数据结构角度分:这里所描述的是索引存储时保存的形式
- BTree索引(B-Tree或B+Tree索引)
- Hash索引
- full-index全文索引
- R-Tree索引
2、从应用层次来分:普通索引,唯一索引,联合(复合)索引
普通索引也称为单值索引
,由于创建索引会占用内存空间,如果普通索引创建的很多,就会占用太多的内存,因此建议使用联合索引- 索引使用的数据结构是B+Tree
- 唯一索引推荐使用自增的主键,因为如果使用UUID的话是随机生成的数,那样索引数据结构维护起来就比使用自增的主键要麻烦,这个和索引的数据结构有关系,了解即可
- 联合索引,查找的时候按照创建的顺序进行查找(
最左匹配原则
),第一个值无法确定,就使用第二个,然后依次类推,如果第一个值就能确定那么后面就不在继续查找,直接返回。
3、从物理存储角度分:根据数据的物理顺序与键值的逻辑(索引)顺序关系:聚集索引,非聚集索引。
- 在解释一下聚集索引和非聚集索引吧!!
- 聚集索引:该
索引中索引的逻辑顺序决定了表中相应行的物理顺序
,因为索引使用B+Tree实现,也就是叶子节点中存放的就是
要查找的数据。 - 非聚集索引:该
索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同
,因为索引使用B+Tree实现,也就是叶子节点中存放的不是
要查找的数据,而是指向要查找数据的指针
,因为地址离散,就好比先给离散地址编号,先找到这个编号的地址,然后通过这个地址找到存储数据的数据页。
4、从逻辑角度分:平时讲的索引类型一般是指在应用层次的划分(应用层次和逻辑角度是一样的
)。
-
普通索引:即一个索引只包含单个列,一个表可以有多个单列索引
没有任何限制的索引
-
复合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并
-
唯一索引:索引列的值必须唯一,但允许有空值且只能有一个。
主键索引为特殊的唯一索引,不允许有空值
06、聚集索引和非聚集索引的区别?
-
1、聚集索引一个表只能有一个,而非聚集索引一个表可以存在多个。
-
2、聚集索引存储记录是物理上连续存在,而非聚集索引是逻辑上的连续,物理存储并不连续。
-
3、聚集索引:聚集索引是一种索引组织形式,索引的逻辑顺序决定了表中相应行的物理顺序;
-
4、非聚集索引:该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同;非聚集索引则就是普通索引了,仅仅只是对数据列创建相应的索引,
不影响整个表的物理存储顺序
.
07、为什么 MySQL 的索引要使用 B+树而不是其它树形结构?比如 B 树?
-
B-tree:因为B树不管叶子节点还是非叶子节点,都会保存数据,而每一个页的存储空间是有限的,如果数据较大时将会致在
非叶子节点中能保存的指针数量变少
(有些资料也称为扇出),指针少的情况下要保存大量数据,只能增加树的高度,导致IO操作变多,查询性能变低; -
Hash:虽然可以快速定位,但是没有顺序,IO复杂度高。
-
二叉树:普通的二叉树,树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且IO代价高。
-
红黑树:树的高度随着数据量增加而增加,IO代价高。
-
B+Tree:B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的
叶子节点
上,而非叶子节点上只存储key值信息(也就是指针
),这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度,IO操作少,查询效率就会变高。
08、MySQL的四大特性
先说一下事务:
-
事务就是一组原子性的SQL执行单元,如果数据库引擎能够成功地对数据库应用该组査询的全部语句,那么就执行该组SQL,如果其中有任何一条语句因为崩溃或其他原因无法执行,那么所有的语句都不会执行。
-
总结就是:要么全部执行成功(commit),要么全部执行失败(rollback)。
事务的四大特性:ACID
-
原子性(Atomicity):单个事务,为一个不可分割的最小工作单元,整个事务中的所有操作要么全部commit成功,要么全部失败rollback,对于一个事务来说,不可能只执行其中的一部分SQL操作,这就是事务的原子性。
-
一致性(Consistency):数据库总是从一个一致性的状态转换到另外一个一致性的状态,也就是如果事务失败就会回到之前的状态,前面执行的操作全部回滚。
-
隔离性(Isolation):通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。
-
持久性(Durability):一旦事务提交,则其所做的修改就会永久保存到数据库中,此时即使系统崩溃,修改的数据也不会丢失。
09、MySQL中的事务的隔离级别,MySQL中默认级别
先说一下高并发下事务存在的问题:
脏读
:脏读是指在一个事务处理过程中读取到了另外一个未提交
事务中的数据。不可重复读
:不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。虚读(幻读)
:幻读发生在当两个完全相同的查询执行时,第二次查询所返回的结果集跟第一个查询不相同。比如两个事务操作,A 事务查询状态为 1 的记录时,这时 B 事务插入
了一条状态为 1 的记录,A 事务再次查询返回的结果不一样。
事务的隔离级别:
- Serializable(串行化):可避免脏读、不可重复读、幻读。(就是串行化读数据)
- Repeatable read(可重复读):可避免脏读、不可重复读的发生。
- Read committed(读已提交):可避免脏读的发生。
- Read uncommitted(读未提交):最低级别,任何情况都无法保证。
MySQL中默认级别:
- Repeatable read (可重复读)
在 Oracle 数据库中只有两种级别:
- Serializable (串行化)
- Read committed (读已提交),默认的为 Read committed 级别
10、 事务的实现原理(MySQL中的日志)
下面这张图是MySQL更新数据的基础流程,其中包括redo log
、bin log
、undo log
三种日志间的大致关系,很重要!!!
-
事务是基于
重做日志
(redo log)和回滚日志
(undo log)实现的。 -
每提交一个事务必须
先将
该事务的所有操作写入重做日志
文件进行持久化,如果需要回滚则根据 undo log 的反向语句进行逻辑操作,比如 insert 一条记录就 delete 一条记录,undo log 主要实现数据库的一致性 -
数据库就可以通过
重做日志
来保证事务的原子性和持久性。如果在事务中出现SQL语句错误,就执行
回滚
,使用undo日志如果在事务中没有出现SQL语句错误,而是因为数据宕机等因素,重启数据库之后,会使用redo日志进行
前滚
-
bin log(归档日志)(了解即可)
bin log
是一种数据库Server层(和什么引擎无关),以二进制形式存储在磁盘中的逻辑日志。- 默认情况下是关闭的。
bin log
记录了数据库所有DDL
和DML
操作(不包含SELECT
和SHOW
等命令,因为这类操作对数据本身并没有修改)。- 它不会像
redo log
那样循环写擦除之前的记录,而是会一直记录日志。一个bin log
日志文件默认最大容量1G
(也可以通过max_binlog_size
参数修改),单个日志超过最大值,则会新创建一个文件继续写。 - 一般来说开启
bin log
都会给日志文件设置过期时间(expire_logs_days
参数,默认永久保存),要不然日志的体量会非常庞大。
11、MySQL如何实现事务隔离的
-
读未提交和串行化基本上是不需要考虑的隔离级别,前者不加锁限制,后者相当于单线程执行,效率太差。
-
MySQL 在可重复读级别解决了幻读问题,是通过行锁和间隙锁的组合 Next-Key 锁实现的。
12、MySQL中的锁
先说一下为什么要加锁:
- 数据库是一个多用户使用的共享资源,当多个用户并发的存取数据时,在数据库中会产生多个事务同时的存取同一数据库的情况,若对并发操作不加以控制,就可能会读取和存储不正确的数据,破坏数据库的一致性。
数据库中锁的分类
- 按
锁的粒度
划分:表级锁、行级锁、页级锁 - 按
锁级别
划分:共享锁、排它锁、意向锁 - 按
是否独占
划分:读锁(共享锁),写锁(排它锁) - 而
表级锁/行级锁/页面锁
可以使用读锁或者写锁来实现
表级锁
:引擎 MyISAM
,直接锁定整张表,如果是表共享读锁
(Table Read Lock),则其他线程可以对表进行读操作(它不会阻塞其他用户对同一表的读
请求,但会阻塞对同一表的写操作),如果是表独占写锁
(Table Write Lock),其它进程既不能对表进行读操作也无法对表进行写操作(它会阻塞其他线程对同一表的读和写
请求)。
MyISAM存储引擎支持:
表共享的读锁
和表独占的写锁
。
MyISAM在执行查询语句(Select)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户控制,是MySQL SERVER端自动完成的。
页级锁
:引擎 BDB
,表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录
行级锁
:引擎 INNODB
, 仅对指定的记录进行加锁,这样其它进程还是可以对同一个表中的其它记录进行操作,InnoDB也支持两种形式的行级锁,一种是共享行级锁(读锁),一种是排他行级锁(写锁)。
InnoDB行锁是通过给索引上的索引项加锁来实现的,而不是给表的行记录加锁实现的,这就意味着,只有通过索引条件检索数据,InnoDB才使用行级锁,
否则InnoDB将使用表锁
(因为没有索引嘛,存储引擎只能给所有的行都加锁,和表锁一样,把记录返回给MySQL Server,它会筛选出符合条件的行进行加锁,其余的行就会释放锁)!
上述三种锁的特性可大致归纳如下:
- 1) 表级锁:开销小,加锁快;不会出现死锁,
锁定粒度大
,发生锁冲突的概率最高,并发度最低。 - 2) 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
- 3) 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
13、LinkedList、ArrayList的区别,分别适用于什么场景?
ArrayList:底层数据结构实现是动态数组
,数组因为地址连续,查询效率会比较高,但是插入和删除时,Arraylist要移动数据,所以插入和删除效率比较低。
LinkedList:底层数据结构实现是链表,地址是任意的,所以在开辟内存空间时不需要等一个连续的地址,对于插入(新增)和删除操作效率比较高,因为LinkedList要移动指针,所以查询操作性能比较低。
适用场景:查询多使用ArrayList,增加删除多使用LinkedList
14、ConcurrentHashMap知道吗,JDK1.7和JDK1.8的区别
jdk 1.7底层结构是:数组(Segment)+ 链表(HashEntry节点)组成,采用头插法。
-
使用分段锁技术,将整个数据结构分段(默认为16段)进行存储,将数据分成一段一段的存储,然后给
每一段数据配一把锁
,当一个线程占用锁访问其中一个段数据的时候,其他段的数据还能被其他线程访问,能够实现真正的并发访问。 -
ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。
jdk1.8底层结构是:Node数组+链表 / 红黑树 其实就是JDK 1.8中的HashMap结构,采用尾插法。
- 线程安全是如何实现的那?
put()
的时候采用了 CAS + synchronized 保证线程安全get()
就还是那样,读不影响线程安全,所以变化不大。
15、ConcurrentHashMap是线程安全的吗?如何实现的?
是的,通过加锁,JDK1.7中采用分段锁,JDK1.8中在put方法中采用CAS + synchronized 保证线程安全。
16、HashMap是线程安全的吗?并发情况下会出现什么问题?
先说一下HashMap的安全实现:HashTable、ConcurrentHashMap
- HashTable:锁住整张表,虽然线程安全,但是效率太低。
- ConcurrentHashMap:JDK1.8中,put方法中使用CAS+Synchronize关键字,来实现并发线程安全。
JDK1.7中的HashMap中的问题:
-
1、因为头插法,HashMap并发环境下扩容可能会产生环形链表,导致在调用get方法的时候出现死循环,爆内存。
-
2、多线程put时可能导致元素丢失。
JDK1.8中的HashMap中的问题:
-
多线程put时可能导致元素丢失。
多线程put的时候如果一个线程在将要存值的时候,让出了CPU,就可能会导致数据丢失。
17、讲一下HashMap中的put方法过程?
1、对key求哈希值然后计算下标
2、如果没有哈希碰撞则直接放入槽中
3、如果碰撞了以链表的形式链接到后面
4、如果链表长度超过阈值(默认阈值是8),就把链表转成红黑树
5、如果节点已存在就替换旧值
6、如果槽满了(容量*加载因子),就需要resize
18、Volitile关键字知道吗?说一说?
Volatile 是 Java 虚拟机提供轻量级的同步机制,用于修饰变量,可以看做程度较轻的 synchronized
:
- 1、保证可见性 :线程间修改可见,但是解决不了ABA问题
- 2、不保证原子性 :
- 3、禁止指令重排
volatile和synchronized的区别:
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化(禁止了指令重排);synchronized标记的变量可以被编译器优化
19、Synchronize关键字知道吗?ReentrentLock知道吗?它俩的区别?
在说这个两者的区别之前先说一下,一些概念性的东西:
-
悲观锁:当线程去操作数据的时候,总认为别的线程会去修改数据,所以它每次拿数据的时候总会先上锁,别的线程去拿数据的时候就会阻塞,比如synchronized
-
乐观锁:每次去拿数据的时候都认为别人不会修改,更新数据的时候
才会
判断是别人是否抢先更新了数据,通过版本来判断
,如果数据被修改了就拒绝更新。
注意:拿数据的时候不加锁而是设置一个版本号,更新数据的时候去判断这个版本号,然后决定是否更新。 -
自旋锁:当尝试给资源加锁却被其他线程抢先锁定时,不是阻塞等待而是循环,然后再次尝试获取锁
在锁常被短暂持有的场景下,线程阻塞挂起导致CPU上下文频繁切换,这可用自旋锁解决;但自旋期间它占用CPU空转,
因此不适用长时间持有锁的场景
Synhronized 和 ReenTrantLock 两者都是可重入锁。
- 同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
Synchronized关键字和ReentrantLock的区别。
1、synchronized是关键字,ReentrantLock是类;
- ReentrantLock可以获取锁的时候进行设置,避免死锁;
- ReentrantLock可以获取各种锁的信息;
- ReentrantLock可以灵活实现多路通知;
2、底层实现不一样:
- synchronized它是java语言的关键字,需要jvm实现
- ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成
- 也就是说synchronized隐式获得释放锁,ReentrantLock 显示的获得、释放锁
3、synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock 在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock 时需要在 finally块中释放锁。(这要看开发人员马虎不了)
4、synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,默认采用的是乐观并发策略。
5、ReentrantLock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断,通过ReentrantLock可以知道有没有成功获取锁,而synchronized却无法办到,最重要的是ReentrantLock可以控制公平性,而synchronized只能是非公平锁。
20、Java中的线程池了解吗?说一说它的参数有几个?拒绝策略也说一下?
创建线程池可以使用Executors
类中的静态方法来创建线程池,但是不推荐,因为底层还是调用ThreadPoolExecutor类来创建线程池
创建线程池推荐使用ThreadPoolExecutor类,该类的构造方法中有七大参数,用来设置线程池的属性:
public ThreadPoolExecutor(int corePoolSize,//核心线程池大小int maximumPoolSize,//最大核心线程池大小long keepAliveTime,//存活时间,没人调用时的存活时间,过期自动释放TimeUnit unit,//超时单位BlockingQueue<Runnable> workQueue,//阻塞对列ThreadFactory threadFactory, //创建线程的工厂,用来创建线程,一般不动RejectedExecutionHandler defaultHandler)//拒绝策略{this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,threadFactory, defaultHandler);}
拒绝策略有四种:
-
RejectedExecutionHandler
接口的四个实现类,也是ThreadPoolExecutor类
中的内部子类(使用默认的拒绝策略即可)默认拒绝策略:AbortPolicy())
不同拒绝策略的区别:
-
new ThreadPoolExecutor.
AbortPolicy()
:队列满了,再创建线程,不处理这个创建过程,抛出异常最大承载线程个数:最大核心线程池+阻塞对列
-
new ThreadPoolExecutor.
CallerRunsPolicy()
:哪来的去哪里 -
new ThreadPoolExecutor.
DiscardPolicy()
:队列满了,丢掉任务,不会抛出异常 -
new ThreadPoolExecutor.
DiscardOldestPolicy()
:队列满了,尝试去和最早的竞争,也不会抛出异常!
21、用过SpringMVC框架吗,说一说其中的主要组件,调用流程?
核心组件
-
① 前端控制器(DispatcherServlet):主要用于接收客户端发送的 HTTP 请求、响应结果给客户端。
-
② 处理器映射器(HandlerMapping):根据请求的 URL 来定位到对应的处理器(Handler)。
-
③ 处理器适配器(HandlerAdapter):在编写处理器(Handler)的时候要按照处理器适配器(HandlerAdapter) 要求的规则去写,通过适配器可以正确的去执行 Handler。
-
④ 处理器(Handler):就是我们经常写的 Controller 层代码,例如:UserController。
-
⑤ 视图解析器(ViewResolver):进行视图的解析,将 ModelAndView 对象解析成真正的视图(View)对象返回给前端控制器。
-
⑥ 视图(View):View 是一个接口, 它的实现类支持不同的视图类型(JSP,FreeMarker,Thymleaf 等)。
Springmvc执行流程:
- ① 首先,用户发送 HTTP 请求给 SpringMVC 前端控制器。
- ② 前端控制器收到请求后,调用处理器映射器,根据请求 URL 去定位到具体的处理器 Handler,并将该处理器对象返回给 DispatcherServlet 。
- ③ 接下来,DispatcherServlet 调用 HandlerAdapter 处理器适配器,通过处理器适配器调用对应的 Handler 处理器处理请求,并向前端控制器返回一个 ModelAndView 对象。
- ④ 然后,DispatcherServlet 将 ModelAndView 对象交给 ViewResoler 视图解析器去处理,并返回指定的视图 View 给前端控制器。
- ⑤ DispatcherServlet 对 View 进行渲染(即将模型数据填充至视图中)。View 是一个接口, 它的实现类支持不同的视图类型(JSP,FreeMarker,Thymleaf 等)。
- ⑥ DispatcherServlet 将页面响应给用户。
22、用过Spring框架吗,它的两大特性是什么?具体的说一说实现原理
Spring框架的两大特性:IOC(控制反转)和AOP(面向切面编程)
IOC:控制反转,另外一种说法叫DI(Dependency Injection),即依赖注入
它并不是一种技术实现,而是一种设计思想。
在任何一个有实际开发意义的程序项目中,我们会使用很多类来描述它们特有的功能,并且通过类与类之间的相互协作来完成特定的业务逻辑。
这个时候,每个类都需要负责管理与自己有交互的类的引用和依赖,代码将会变的异常难以维护和极度的高耦合。
而IOC的出现正是用来解决这个问题,我们通过IOC将这些相互依赖对象的创建、协调工作交给Spring容器去处理,每个对象只需要关注其自身的业务逻辑关系就可以了。
在这样的角度上来看,获得依赖的对象的方式,进行了反转,变成了由spring容器控制对象如何获取外部资源(包括其他对象和文件资料等等)。
AOP:面向切面编程
开发的系统一般都是由许多不同的组件所组成的,每一个组件各负责一块特定功能,但是有些组件除了实现自身核心功能之外,还经常承担着额外的职责,例如:打印日志、事务管理等。
对于这些这些交叉业务逻辑(日志、事务管理),我们封装成一个切面,然后织入到目标组件中去。
- 简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用
动态代理
的技术,在不修改源码的基础上,对我们的已有方法进行增强。
23、动态代理的方式有哪些?有什么不同点?
动态代理的分类:
-
基于接口的动态代理:
JDK动态代理
,被代理类至少实现一个接口,如果没有则不能使用。/*涉及的类:Proxy提供者:JDK官方如何创建代理对象:使用Proxy类中的newProxyInstance方法 */public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)throws IllegalArgumentException{.....}
-
基于子类的动态代理:
CGLIB动态代理
,被代理类不能是最终类(也就是没有被final修饰的类)/*涉及的类:Enhancer提供者:第三方cglib库如何创建代理对象:使用Enhancer类中的create方法*/ public static Object create(Class type, Callback callback) {Enhancer e = new Enhancer();e.setSuperclass(type);e.setCallback(callback);return e.create();}
24、Spring框架如何能解决循环依赖的问题吗?是不是所有的循环依赖都能解决?
先说一下创建对象:
- 一个完整的对象包含两部分:
当前对象实例化
和对象属性实例化
- 如果A类中包含B类类型的属性,B类中包含A类类型的属性,就会发生循环依赖的问题。
再说一下循环依赖的类型,根据注入的时机可以分为两种:
-
构造器循环依赖:依赖的对象是通过构造方法传入的,在实例化bean的时候发生。
-
赋值属性循环依赖:依赖的对象
成员变量类型
,对象已经实例化,在对对象属性
初始化的时候发生(初始化会对属性赋值)。 -
构造器循环依赖:本质上是无解的,所以Spring也是不支持构造器循环依赖的,当发现存在构造器循环依赖时,会直接抛出
BeanCurrentlyInCreationException
异常。 -
赋值属性循环依赖:Spring只支持bean在
单例模式
下的循环依赖,其它模式下的循环依赖Spring也是会抛出BeanCurrentlyInCreationException
异常的。 -
Spring通过对还在创建过程中的单例bean,进行缓存并提前暴露该单例,使得其他实例可以提前引用到该单例bean。
再说一下Spring为什么只支持单例模式下的bean的赋值情况下的循环依赖?
- 在prototype的模式下的bean,使用了一个ThreadLocal类型的变量(prototypesCurrentlyInCreation)来记录当前线程正在创建中的bean,这个变量在AbtractBeanFactory类里。
- 在创建前用beanName记录bean,在创建完成后删除bean。
- 在prototypesCurrentlyInCreation里采用了一个Set对象来存储正在创建中的bean,我们都知道Set是不允许存在重复对象的,这样就能保证同一个bean在一个线程中只能有一个正在创建。
再说一下Spring中的三级缓存:
-
所谓三级缓存,其实就是
org.springframework.beans.factory
包下DefaultSingletonBeanRegistry
类中的三个成员属性:// Spring一级缓存:用来保存实例化、初始化都完成的对象 // Key:beanName // Value: Bean实例 private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);// Spring二级缓存:用来保存实例化完成,但是未初始化完成的对象 // Key:beanName // Value: Bean实例 // 和一级缓存一样也是保存BeanName和创建bean实例之间的关系, // 与singletonObjects不同之处在于,当一个单例bean被放在里面后, // 那么bean还在创建过程中,就可以通过getBean方法获取到了,其目的是用来循环检测引用!(后面会分析) private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);// Spring三级缓存:用来保存一个对象工厂,提供一个匿名内部类,用于创建二级缓存中的对象 // Key:beanName // Value: Bean的工厂 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
-
如图所示,除了三级缓存是一个HashMap,其他两个都是ConcurrentHashMap
-
Spring之所以引入三级缓存,目的就是为了解决循环依赖问题
1、创建对象A,实例化的时候把A对象工厂放入三级缓存 2、A注入属性时,发现依赖 B,转而去实例化 B 3、同样创建对象 B,注入属性时发现依赖 A,依次从一级到三级缓存查询 A,从三级缓存通过对象工厂拿到 A,把 A 放入二级缓存,同时删除三级缓存中的 A,此时,B 已经实例化并且初始化完成,把 B 放入一级缓存。 4、接着继续创建 A,顺利从一级缓存拿到实例化且初始化完成的 B 对象,A 对象创建也完成,删除二级缓存中的 A,同时把 A 放入一级缓存。 5、最后,一级缓存中保存着实例化、初始化都完成的A、B对象。
从上面5步骤的分析可以看出,三级缓存解决循环依赖是通过把对象实例化和对象属性初始化的流程分开了。
如果依赖的对象是通过
构造方法
传入的,在实例化bean的时候发生就没法分离这个操作,因为在构造器注入
,对象实例化和对象属性初始化是一起进行的,因此在构造器中注入依赖
的话是无法解决循环依赖问题的。使用三级缓存而非二级缓存并不是因为只有三级缓存才能解决循环引用问题,其实二级缓存同样也能很好解决循环引用问题。
使用三级而非二级缓存并非出于IOC的考虑,而是出于AOP的考虑,即若使用二级缓存,在AOP情形下,往二级缓存中放一个普通的Bean对象,BeanPostProcessor去生成代理对象之后,覆盖掉二级缓存中的普通Bean对象,那么多线程环境下可能取到的对象就不一致了。
一句话总结就是,在 AOP 代理增强 Bean 后,会对早期对象造成覆盖,如果多线程情况下可能造成取到的对象不一致~
-
除了上面三个Map集合,还有另一个集合这里也说一下:
// 用来保存当前所有已注册的Bean private final Set<String> registeredSingletons = new LinkedHashSet<>(256);
在Spring中,对象的实例化
是通过反射
实现的,而对象的属性
则是在对象实例化之后
通过一定的方式设置的:
- 也就是,先实例化对象,在实例化对象中的属性
Spring实例化bean是通过ApplicationContext.getBean()
方法来进行的:
-
如果要获取的对象依赖了另一个对象,那么其首先会创建当前对象,然后通过递归的调用ApplicationContext.getBean()方法来获取所依赖的对象,最后将获取到的对象注入到当前对象中。
-
首先Spring尝试通过
ApplicationContext.getBean()
方法获取A对象的实例,由于Spring容器中还没有A对象实例,因而其会创建一个A对象。 -
然后发现其依赖了B对象,因而会尝试递归的通过
ApplicationContext.getBean()
方法获取B对象的实例,由于Spring容器中此时也没有B对象的实例,因而也会先创建一个B对象的实例。 -
注意:此时A对象和B对象都已经创建了,并且保存在Spring容器中了,只不过A对象的属性b和B对象的属性a都还没有注入进去。
-
在前面Spring创建B对象之后,Spring发现B对象依赖了属性A,因而此时还是会尝试递归的调用ApplicationContext.getBean()方法获取A对象的实例,因为Spring中已经有一个A对象的实例,虽然只是半成品(其属性b还未初始化),但其也还是目标bean,因而会将该A对象的实例返回。
-
此时,B对象的属性a就设置进去了,然后还是ApplicationContext.getBean()方法
递归的返回
,也就是将B对象的实例返回,此时就会将该实例设置到A对象的属性b中。这个时候,注意A对象的属性b和B对象的属性a都已经设置了目标对象的实例了。 -
问题:前面在为对象B设置属性a的时候,这个A类型属性还是个
半成品
,但是需要注意的是,这个A是一个引用,其本质上还是最开始就实例化的A对象,而在上面这个递归过程的最后,Spring将获取到的B对象实例设置到了A对象的属性b中了,这里的A对象其实和前面设置到实例B中的半成品A对象是同一个对象,其引用地址是同一个,这里为A对象的b属性设置了值,其实也就是为那个半成品的a属性设置了值。
-
图中getBean()表示调用Spring的ApplicationContext.getBean()方法,而该方法中的参数,则表示我们要尝试获取的目标对象。
-
图中的黑色箭头表示一开始的方法调用走向,走到最后,返回了Spring中缓存的A对象之后,表示递归调用返回了,此时使用绿色的箭头表示。
-
从图中我们可以很清楚的看到,B对象的a属性是在第三步中注入的半成品A对象,而A对象的b属性是在第二步中注入的成品B对象,此时半成品的A对象也就变成了成品的A对象,因为其属性已经设置完成了。
25、说一下Mybatis加载的流程是什么,以及MyBatis 的一、二级缓存?
Mybatis加载的流程?
- MyBatis 是以一个 SqlSessionFactory 实例为核心,SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。
- SqlSessionFactoryBuilder 可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。
- SqlSessionFactory 实例工厂可以生产 SqlSession ,它里面提供了在数据库执行 SQL 命令所需的所有方法。
具体流程:
-
①
加载配置文件
:需要加载的配置文件包括全局配置文件(mybatis-config.xml)和 SQL(Mapper.xml) 映射文件,其中全局配置文件配置了Mybatis 的运行环境信息(数据源、事务等),SQL映射文件中配置了与 SQL 执行相关的信息。 -
②
创建会话工厂
:MyBatis通过读取配置文件的信息来构造出会话工厂(SqlSessionFactory),即通过SqlSessionFactoryBuilder 构建 SqlSessionFactory。 -
③
创建会话
:拥有了会话工厂,MyBatis就可以通过它来创建会话对象(SqlSession)。会话对象是一个接口,该接口中包含了对数据库操作的增删改查方法。 -
④
创建执行器
:因为会话对象本身不能直接操作数据库,所以它使用了一个叫做数据库执行器(Executor)的接口来帮它执行操作。 -
⑤
封装SQL对象
:执行器(Executor)将待处理的SQL信息封装到一个对象中(MappedStatement),该对象包括SQL语句、输入参数映射信息(Java简单类型、HashMap或POJO)和输出结果映射信息(Java简单类型、HashMap 或 POJO)。 -
⑥
操作数据库
:拥有了执行器和SQL信息封装对象就使用它们访问数据库了,最后再返回操作结果,结束流程。
MyBatis 的一、二级缓存
一级缓存
:作用域是 SqlSession,同一个 SqlSession 中执行相同的 SQL 查询(相同的SQL和参数),第一次会去查询数据库并写在缓存中,第二次会直接从缓存中取。- 一级缓存是基于 PerpetualCache 的 HashMap 本地缓存,默认打开一级缓存。
- 失效策略:当执行 SQL 时候两次查询中间发生了增删改的操作,即 insert、update、delete 等操作 commit 后会清空该 SqlSession 缓存。
二级缓存
:作用域是 NameSpace 级别,多个 SqlSession 去操作同一个 NameSpace 下的 Mapper 文件的 sql 语句,多个 SqlSession可以共用二级缓存,如果两个 Mapper 的 NameSpace 相同,(即使是两个 Mapper,那么这两个 Mapper 中执行 sql 查询到的数据也将存在相同的二级缓存区域中)- 二级缓存也是基于 PerpetualCache 的 HashMap 本地缓存,
可自定义存储源,如 Ehcache/Redis等
。默认是没有开启二级缓存。 - 操作流程:第一次调用某个 NameSpace 下的 SQL 去查询信息,查询到的信息会存放该 Mapper对应的二级缓存区域。第二次调用同个 NameSpace 下的 Mapper 映射文件中的相同的 sql 去查询信息时,会去对应的二级缓存内取结果。
- 失效策略:执行同个 NameSpace 下的 Mapepr 映射文件中增删改 sql,并执行了commit 操作,会清空该二级缓存。
- 注意:实现二级缓存的时候,MyBatis 建议返回的 POJO 是可序列化的, 也就是建议实现 Serializable 接口。
- 二级缓存也是基于 PerpetualCache 的 HashMap 本地缓存,
26、你知道哪些设计模式,说一下?
工厂模式,单例模式,代理模式
单例模式分为两种:饿汉式
单例模式和懒汉式
单例模式:
单例模式的构造器都是私有的,通过一个静态方法来获取类的实例,饿汉式这个实例已经创建好等待着被获取,懒汉式是什么时候获取什么时候创建
。
饿汉式单例模式
-
饿汉式特点:
- 1.饿汉式在类加载的时候就实例化,并且创建单例对象
- 2.饿汉式
线程安全
(在线程还没出现之前就已经实例化了,因此饿汉式线程一定是安全的) - 3.饿汉式没有加任何的锁,因此执行效率比较高
- 4.饿汉式在类加载的时候就初始化,不管你是否使用,它都实例化了,所以会占据空间,浪费内存
//饿汉式单例 public class HungryDemo {//无参构造private HungryDemo(){};/** private 为了对外无法访问* static 为了在类加载的时候加载* final 为了只创建一次*///不管用不用已经创建出对象了private static final HungryDemo HUNGRY_DEMO = new HungryDemo();//通过这个静态方法获取实例public static HungryDemo getInstance(){return HUNGRY_DEMO;} }
懒汉式单例模式
-
懒汉式特点:
- 1.懒汉式默认不会实例化,外部什么时候调用什么时候new
- 2.懒汉式
线程不安全
( 因为懒汉式加载是在使用时才会去new 实例的,那么你去new的时候是一个动态的过程,是放到方法中实现的,如果这个时候有多个线程访问这个实例,这个时候实例还不存在,还在new,就会进入到方法中, 有多少线程就会new出多少个实例,一个方法只能return一个实例,那最终return出哪个呢?是不是会覆盖很多new的实例? - 3.懒汉式一般使用都会加同步锁,效率比饿汉式差
- 4.懒汉式什么时候需要什么时候实例化,相对来说不浪费内存
//懒汉式单例 public class LazyDemo {//构造方法private LazyDemo(){};private static LazyDemo lazyDemo=null;//什么时候调用什么时候实例化/* * 双重判断检测:* 如果一个线程在第一次判断之后让出线程,第二个线程获得执行权,并且顺利拿到锁,执行完毕会创建出一个实例,* 这时候第一个线程获得执行权,如果不进行二次判断,第二个线程也会创建出一个实例,这样就违背了单例模式的意义*/public static LazyDemo getInstance(){//第一层判断if (lazyDemo==null){synchronized (LazyDemo.class) {//在这个位置加锁比直接在静态方法上加锁效率高一点// 第二层判断if(lazyDemo==null) {lazyDemo = new LazyDemo();}}}return lazyDemo;} }
27、JVM知道吗?说一下JVM的内存模型
先说一下JVM:
-
JVM分为四大块:类加载子系统,运行时数据区,执行引擎,本地方法接口
-
JVM内存模型一般指运行时数据区,也称为JMM
JVM内存模行:
- JMM分为5大块:方法区,堆区,java虚拟机栈,本地方法栈,程序计数器
- 线程私有的是:java虚拟机栈,本地方法栈,程序计数器
- 线程共有的是:方法区,堆区
堆区细分:(垃圾回收器回收最频繁的一个区域)
- 新生代,老年代,默认比例为:
1 : 2
- 新生代有分为:伊甸园区,幸存者1区(From区),幸存者2区(To区),默认比例为:
8:1:1
Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项-Xmx和-Xms来进行设置。-Xms:用于表示堆区的起始内存,等价于-xx:InitialHeapSize-Xmx:则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
一旦堆区中的内存大小超过-Xmx所指定的最大内存时,将会抛出outofMemoryError异常。通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。配置新生代与老年代在堆结构的占比:默认:-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3可以修改:-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5配置新生代中Eden空间和另外两个survivor空间的占比:默认:-XX:SurvivorRatio=8,表示在Eden空间占8,survivor0空间占1,survivor1空间占1可以修改:-XX:SurvivorRatio=4,表示在Eden空间占4,survivor0空间占1,survivor1空间占1如果默认不是8:1:1,我们可以再VM options中关闭自适应内存分配策略:-XX:-UseAdaptiveSizePolicy
-
注意:JDK1.7之前是永久代,JDK1.8开始改为元空间(元空间不在堆区,分配在方法区)
-
堆区的垃圾回收:Minor GC,Major GC,Full Gc
- Minor GC:新生代的GC
- Major GC:老年代的GC
- Full GC:整堆收集,收集整个`Java堆`和`方法区`的垃圾收集
- 垃圾收集是JVM调优的一个环节,我们需要尽量的避免垃圾回收,因为在垃圾回收的过程中,容易出现STW(Stop the world)的问题
- 而 Major GC 和 Full GC出现STW的时间,是Minor GC的10倍以上
- JVM在进行GC时,并非每次都对上面三个内存区域 (新生代,老年代,方法区【永久带或者元空间】) 一起回收的,大部分时候回收的都是指新生代。
方法区:
- 可以看作是一块独立于Java堆的内存空间。
- 它用于存储已被虚拟机加载的
类型信息
、常量
、静态变量
、即时编译器编译后的代码缓存
等。 - 字节码文件就是首先被加载到方法区
程序计数器:
-
作用:PC寄存器用来存储
指向下一条指令的地址
,也即将要执行的指令代码地址
。由执行引擎读取下一条指令。 -
特点:PC寄存器中既没有GC也没有OOM
java虚拟机栈:早期也叫Java栈
。
-
虚拟机栈内部保存的是一个个的栈帧(Stack Frame)
-
线程中正在执行的每个方法都有各自对应一个栈帧(Stack Frame)。
-
栈帧是一个内存区块
,是一个数据集
,维系着方法执行过程中的各种数据信息。 -
栈帧的内部结构
- 局部变量表(Local Variables)
- 操作数栈(operand Stack)(或表达式栈)
- 动态链接(DynamicLinking)(或指向运行时常量池的方法引用)
- 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
- 一些附加信息
栈中可能出现的异常:StackoverflowError 和outofMemoryErrorJava 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackoverflowError 异常。如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 outofMemoryError 异常。
本地方法栈:
- Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
- 本地方法栈,也是线程私有的。
- 允许被实
现成固定
或者是可动态扩展的内存大小
。(内存溢出方面和java虚拟机栈是相同的) - 本地方法是使用C语言实现的。
小结:栈,堆,方法区的交互关系
- Person(类):
存放在元空间,也可以说方法区
- person(实例变量):存放在Java虚拟机栈的局部变量表中
- new Person()(实例):存放在Java堆中
28、说一下双亲委派机制?
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
工作原理:
- 1.如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 2.如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 3.如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
作用:
- 避免类的重复加载,如果在双亲委派的时候被加载就不会在反向委派,避免重复加载
- 保护程序安全,防止核心API被随意篡改,例如下面加载自定义的java.lang.String 时就会报错。
- 自定义类:java.lang.String
- 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
加载器的类型:
-
启动类加载器,自定义类加载器
-
常用的自定义类加载器:拓展类加载器,应用类加载器(系统类加载器)
29、说一下你知道的垃圾回收器,和垃圾回收算法?
先说一下垃圾回收算法吧:
-
要进行垃圾收集,首先要知道那些是垃圾:
标记
-
对垃圾进行回收:
清除
-
标记阶段的算法有:引用计数算法(目前已经淘汰,因为无法解决循环引用的问题),可达性分析算法(目前正在使用)
-
清除阶段的算法有:标记-清除算法,复制算法,标记-压缩算法(等同于标记-清除算法执行完成后,再进行一次内存碎片整理)
标记-清除和标记压缩二者的本质差异在于
标记-清除
算法是一种非移动式的回收算法,标记-压缩
是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。
在说一下分代收集算法,增量收集算法,分区算法吧:
-
对于前面提到的三种清除算法,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。
-
而为了尽量兼顾这三个指标(速度,空间开销,移动对象),
标记-压缩
算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法
多了一个标记的阶段,比标记-清除
多了一个整理内存的阶段。 -
所以说没有最好的算法,只有更合适的算法,前面所有这些算法中,并没有一种算法可以
完全替代
其他算法,它们都具有自己独特的优势和特点。分代收集算法
应运而生。分代收集算法是基于这样一个事实:- 不同的对象的生命周期是不一样的。- 因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。- 一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。- 目前几乎所有的GC都是采用分代收集(Generational Collecting) 算法执行垃圾回收的。在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。- 年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。这种情况复制算法的回收整理,速度是最快的。- 老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。这种复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-压缩的混合实现
最经典的垃圾回收器有七款,如下图:
新生代可配置的回收器:Serial、ParNew、Parallel Scavenge
老年代配置的回收器:CMS、Serial Old、Parallel Old
新生代和老年代区域的回收器之间进行连线,说明他们之间可以搭配使用。
新生代垃圾回收器
Serial 垃圾回收器
-
Serial收集器是最基本的、发展历史最悠久的收集器。俗称为:
串行回收器
,采用复制算法
进行垃圾回收 -
特点
- 串行回收器是指使用单线程进行垃圾回收的回收器,每次回收时,串行回收器只有一个工作线程。
- 对于并行能力较弱的单CPU计算机来说,串行回收器的专注性和独占性往往有更好的性能表现。
- 它存在Stop The World问题,也就是垃圾回收时要停止程序的运行。
- 使用
-XX:+UseSerialGC
参数可以设置新生代使用这个串行回收器
ParNew 垃圾回收器
-
ParNew其实就是Serial的
多线程
版本,除了使用多线程之外,其余参数和Serial一模一样。俗称:并行垃圾回收器
,采用复制算法
进行垃圾回收 -
特点
- ParNew
默认
开启的线程数与CPU数量相同,在CPU核数很多的机器上,可以通过参数-XX:ParallelGCThreads
来设置线程数。 - 它是目前新生代首选的垃圾回收器,因为除了Serial之外,它是唯一一个能与老年代CMS配合工作的。
- 它同样存在Stop The World问题
- 使用
-XX:+UseParNewGC
参数可以设置新生代使用这个并行回收器
- ParNew
ParallelGC 回收器
-
ParallelGC使用复制算法回收垃圾,也是多线程的。
-
特点
- 就是非常关注系统的吞吐量,
吞吐量
=代码运行时间
/(代码运行时间
+垃圾收集时间
) -XX:MaxGCPauseMillis
:设置最大垃圾收集停顿时间,可用把虚拟机在GC停顿的时间控制在MaxGCPauseMillis范围内,如果希望减少GC停顿时间可以将MaxGCPauseMillis设置的很小,但是会导致GC频繁
,从而增加了GC的总时间
,降低
了吞吐量
,所以需要根据实际情况设置该值。-XX:GCTimeRatio
:设置吞吐量大小,它是一个0到100之间的整数,默认情况下他的取值是99
,那么系统将花费不超过1/(1+n)
的时间用于垃圾回收,也就是1/(1+99)=1%
的时间。- 另外还可以指定
-XX:+UseAdaptiveSizePolicy
打开自适应模式,在这种模式下,新生代的大小、eden、from/to的比例,以及晋升老年代的对象年龄参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。 - 使用-
XX:+UseParallelGC
参数可以设置新生代使用这个并行回收器
- 就是非常关注系统的吞吐量,
老年代垃圾回收器
SerialOld 垃圾回收器
-
SerialOld是Serial回收器的
老年代
回收器版本,它同样是一个单线程
回收器。 -
用途
-
一个是在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用,
-
另一个就是作为CMS收集器的后备预案,如果CMS出现Concurrent Mode Failure,则SerialOld将作为后备收集器。
-
-
使用算法
:标记 - 整理算法
ParallelOldGC 回收器
-
老年代
ParallelOldGC
回收器也是一种多线程的回收器,和新生代的ParallelGC回收器一样,也是一种关注吞吐量的回收器,他使用了标记-压缩算法
进行实现。 -
-XX:+UseParallelOldGc
进行设置老年代使用该回收器 -
-XX:+ParallelGCThreads
也可以设置垃圾收集时的线程数量。
CMS 回收器
- CMS全称为:Concurrent Mark Sweep意为
并发标记清除
,他使用的是标记-清除算法
,主要关注系统停顿时间。 - 使用
-XX:+UseConcMarkSweepGC
进行设置老年代使用该回收器。 - 使用
-XX:ConcGCThreads
设置并发线程数量。 - 特点
- CMS并不是独占的回收器,也就说CMS回收的过程中,应用程序仍然在不停的工作,又会有新的垃圾不断的产生,所以在使用CMS的过程中应该确保应用程序的内存足够可用。
- CMS不会等到应用程序
饱和
的时候才去回收垃圾,而是在某一阀值的时候开始回收,回收阀值可用指定的参数进行配置:-XX:CMSInitiatingoccupancyFraction
来指定,默认为68
,也就是说当老年代的空间使用率
达到68%
的时候,会执行
CMS回收。 - 如果内存使用率增长的很快,在CMS执行的过程中,已经出现了内存不足的情况,此时CMS回收就会失败,虚拟机将启动老年代
串行
回收器;SerialOldGC
进行垃圾回收,这会导致应用程序中断,直到垃圾回收完成后才会正常工作。 - 这个过程GC的停顿时间可能较长,所以
-XX:CMSInitiatingoccupancyFraction
的设置要根据实际的情况。标记-清除算法有个缺点就是存在内存碎片
的问题,那么CMS有个参数设置-XX:+UseCMSCompactAtFullCollecion
可以使CMS回收完成之后进行一次碎片整理
。 -XX:CMSFullGCsBeforeCompaction
参数可以设置进行多少次CMS回收之后,对内存进行一次压缩
。
- 但是CMS并不完美,它有以下缺点:
- 由于并发进行,CMS在收集与应用线程会同时会增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间。
- 标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数-XX:CMSFullGCsBeForeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC。
G1 回收器
-
开启选项:-XX:+UseG1GC
-
之前介绍的几组垃圾收集器组合,都有几个共同点:
- 新生代、老年代是独立且连续的内存块;
- 新生代收集使用单eden、双survivor进行复制算法;
- 老年代收集必须扫描整个老年代区域;
- 都是以尽可能少而快地执行GC为设计原则。
-
G1垃圾收集器也是以关注延迟为目标、服务器端应用的垃圾收集器,被HotSpot团队寄予取代CMS的使命,也是一个非常具有调优潜力的垃圾收集器。虽然G1也有类似CMS的收集动作:初始标记、并发标记、重新标记、清除、转移回收,并且也以一个串行收集器做担保机制,但单纯地以类似前三种的过程描述显得并不是很妥当。事实上,G1收集与以上三组收集器有很大不同:
- G1的设计原则是"首先收集尽可能多的垃圾(Garbage - First)"。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部- 采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时- 间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;
- G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进- 行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天- 然就是一种压缩方案(局部压缩);
- G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的- survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不- 同代之间前后切换;
- G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次- 收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合- 收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿
总结
串行收集器组合 :Serial + Serial Old
并行收集器组合 :Parallel Scavenge + Parallel Old
并发标记清除收集器组合 :ParNew + CMS + Serial Old
这篇关于Java面经—远景智能的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!