“北科Java面试宝典(211最详细讲解)“(上)
简介:
“北科Java面试宝典(211最详细讲解)“
北科Java面试宝典
一、Java基础面试题【24道】
- 聊一聊Java平台的理解!
Java 本身是一种面向对象的语言,最显著的特性有两个方面,一是所谓的“书写一次,到处运行”,能够
非常容易地获得跨平台能力;另外就是垃圾收集,Java 通过垃圾收集器回收分配内存,大部分情况下,
程序员不需要自己操心内存的分配和回收。 一次编译、到处运行”说的是Java语言跨平台的特性,Java的
跨平台特性与Java虚拟机的存在密不可分,可在不同的环境中运行。比如说Windows平台和Linux平台
都有相应的JDK,安装好JDK后也就有了Java语言的运行环境。其实Java语言本身与其他的编程语言没有
特别大的差异,并不是说Java语言可以跨平台,而是在不同的平台都有可以让Java语言运行的环境而
已,所以才有了Java一次编译,到处运行这样的效果。 Java具有三大特性: 封装,继承,多态.利用这三大特
性增加代码的复用率和灵活度.
- String、StringBuffer、StringBuilder的区别!
String的典型的不可变类型,内部使用final关键字修饰,所以每次进行字符串操作(拼接,截取等)都
会产生新的对象!在开发中,会使用到大量的字符串操作!所以字符串的临时对象,会对程序操作较大
的性能开销!产生了大量的内存浪费!
StringBuilder比StringBuffer速度快, StringBuffer比String速度快
StringBuffer线程安全
- int和Integer的区别!
int是基本数据类型,是Java的8个原始数据类型之一,直接存数值。
Integer是int对应的包装类,在拆箱和装箱中java 可以根据上下文,自动进行转换,极大地简化了相关
编程。
int是基本类型,Integer是对象,用一个引用指向这个对象。由于Integer是一个对象,在JVM中对象需
要一定的数据结构进行描述,相比int而言,其占用的内存更大一些。
- == 和 equals 的区别是什么?
是直接比较的两个对象的堆内存地址,如果相等,则说明这两个引用实际是指向同一个对象地址的。
因此基本数据类型和String常量是可以直接通过来直接比较的。
对于引用对象对比是否相等使用equals方法,equals方法比较的是对象中的值是否相等, 但是要从写
hashcode和equals
- 聊一聊JDK1.8版本的新特性!
拉姆达表达式
函数式接口,
方法引用
新的日期类LocalDate
引入了stream类,支持链式编程
- final关键字的作用?
final关键字可以用来修饰变量、方法和类。
被final修饰的类该类成为最终类,无法被继承。
被final修饰的方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。
final修饰类中的属性或者变量
无论属性是基本类型还是引用类型,final所起的作用都是变量里面存放的“值”不能变。
对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用
类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
- 什么是内存泄漏和内存溢出!
内存泄漏(memoryleak),是指应用程序在申请内存后,无法释放已经申请的内存空间,一次内存泄漏危
害可以忽略,但如果任其发展最终会导致内存溢出(outofmemory)。如读取 文件后流要进行及时的
关闭以及对数据库连接的释放。
内存溢出(outofmemory)是指应用程序在申请内存时,没有足够的内存空间供其使用。如我们在项目中
对于大批量数据的导入,采用分批量提交的方式。
- 抽象类和接口的区别!
抽象类 : 抽象类必须用 abstract 修饰,子类必须实现抽象类中的抽象方法,如果有未实现的,那么子类
也必须用 abstract 修饰。抽象类默认的权限修饰符为 public,可以定义为 public 或 procted,如果定
义为 private,那么子类则无法继承。抽象类不能创建对象
接口和抽象类的区别 : 抽象类只能继承一次,但是可以实现多个接口
接口和抽象类必须实现其中所有的方法,抽象类中如果有未实现的抽象方法,那么子类也需要定义为抽
象类。抽象类中可以有非抽象的方法
接口中的变量必须用 public static final 修饰,并且需要给出初始值。所以实现类不能重新定义,也不能
改变其值。
接口中的方法默认是 public abstract,也只能是这个类型。不能是 static,接口中的方法也不允许子类
覆写,抽象类中允许有static 的方法
- Error和Execption的区别?
Exception 和 Error 都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出
(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类。Exception 是程序正常运行中,可
以预料的意外情况,可能并且应该被捕获,进行相应处理。
Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM自身)处于
非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如
OutOfMemoryError 之类,都是 Error 的子类。
Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常
- 说一说常见的Execption和解决方案!
数组越界异常 : Java.lang.ArrayIndexOutofBoundsException
产生的原因:访问了不存在的索引
解决的办法:索引0到数组长度-1的范围内取值
空指针异常 : Java.lang.NullPointerException
产生的原因:对象没有创建就访问了元素或者方法或者属性
解决的办法: 先找出出现的所有引用类型,判断哪个对象是没有new的元素或者方法或者属性,如
果没有就创建该对象
没有这样的元素异常 : Java.util.NoSuchElementException
产生的原因:在迭代器迭代的时候没有下一个元素了
解决的办法:在迭代器之前做相对应得判断,如果没有元素了就不迭代输出了
并发修改异常 : Java.util.ConcurrentModificationException
产生的原因:在迭代器迭代的同时使用集合修改元素
解决的办法:使用普通for循环来遍历 , 使用toArray来遍历 , 使用ListIterator来遍历
类型转换异常 : Java.lang.ClassCastException
产生的原因:在向下转型的过程中,没有转换成真实的类型
解决的方法:在向下转型之前使用instanceof关键字对所有子类做逐一判断
算法出错异常 : Java.lang.ArithmeticException
产生的原因:除数不能为零
解决的办法:改变除数的结果再进行测试
没有序列化异常 : Java.io.NotSerialzableException
产生的原因:没有实现serializable接口
解决的办法:对需要的写入到文件的类实现serializable接口,表示允许该类的该类写入到文件
- 重写和重载区别
重写: 子类继承父类, 在子类中存在和父类中一模一样的方法, 从新编写方法中的实现业务代码.
重载: 在同一个类中存在多个方法名相同, 传参个数,顺序和类型不同, 返回值可以相同也可以不同的方法.
- 什么是反射, 反射的好处?
Java 反射,就是在运行状态中:
获取任意类的名称、package信息、所有属性、方法、注解、类型、类加载器等
获取任意对象的属性,并且能改变对象的属性
调用任意对象的方法
判断任意一个对象所属的类
实例化任意一个类的对象
Java 的动态就体现在这。通过反射我们可以实现动态装配,降低代码的耦合度;动态代理等。反射的过
度使用会严重消耗系统资源。
- 事务控制在哪一层? 可否控制在dao层? 为什么?
事务必须控制在service层, 不可以在dao层.
因为dao层需要按照单一职责原则设计, 一个类对应一张表, 类中最好都是单表增删改查, 增加代码复
用率而事务具有隔离性, service会调用多个dao方法组装业务, 如果事务控制在dao层就会被分隔成
多个事务, 无法整体控制
所以事务必须控制在service层, 保证一套业务操作要么全成功, 要么全失败.
- Cookie和session的区别和联系?
Cookie是客户端浏览器用来保存数据的一块空间, cookie没有session安全, cookie中保存的数据量
有大小限制
Session是服务器端用来保存数据的一块空间, session比较安全
Cookie和session联系:
当用户第一次发送请求访问网站的时候, cookie中没有信息, 这个时候服务器就会生成一个session
对象, 这个session对象有个唯一id叫做sessionID, 这个id会被保存到用户的浏览器cookie中.
当用户再次访问服务器的时候, cookie中就会有sessionID, 会被带到服务器, 服务器会根据这个id找
到对应的session对象使用.
如果服务器没有找到对应的session对象则会新生成一个session对象, 然后将新的session对象的id
再次写入到cookie中保存.
- —个线程两次调用start。方法会出现什么情况?
Java的线程是不允许启动两次的,第二次调用必然会抛出legalThreadStateException,这是一种运行时
异常,多次调用 start 被认为是编程错误。
- 谈谈final、finally、 finalize有什么不同!
final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 class 代表不可以继承扩展,final
的变量是不可以修改的,而 final 的方法也是不可以重写的(override)。
finally 则是 Java 保证重点代码一定要被执行的一种机制。我们可以使用 try-finally 或者 trycatch-finally
来进行类似关闭 JDBC 连接、保证 unlock 锁等动作。
finalize 是基础类 java.lang.Object 的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源
的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9 开始被标记为deprecated。
- 强引用、软引用、弱引用、幻象引用有什么区别!
所谓强引用(“Strong” Reference) :
就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收
集器不会碰这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域
或者显式地将相(强)引用赋值为 null,就是可以被垃圾收集的了,当然具体回收时机还是要看垃
圾收集策略。
软引用(SoftReference) :
是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,
才会去试图回收软引用指向的对象。JVM 会确保在抛出OutOfMemoryError 之前,清理软引用指
向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内
存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
弱引用(WeakReference):
并不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径。这就可以用来构建
一种没有特定约束的关系,比如,维护一种非强制性的映射关系,如果试图获取时对象还在,就使
用它,否则重现实例化。它同样是很多缓存实现的选择。
幻象引用 :
有时候也翻译成虚引用,你不能通过它访问对象。幻象引用仅仅是提供了一种确保对象被 finalize
以后,做某些事情的机制,比如,通常用来做所谓的 Post-Mortem 清理机制,我在专栏上一讲中
介绍的 Java 平台自身 Cleaner 机制等,也有人利用幻象引用监控对象的创建和销毁。
- 父子类静态代码块, 非静态代码块, 构造方法执行顺序
父类 - 静态代码块
子类 - 静态代码块
父类 - 非静态代码
父类 - 构造函数
子类 - 非静态代码
子类 - 构造函数
- 如何实现对象克隆
实现 Cloneable 接口并重写 Object 类中的 clone()方法;
实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆
- 什么是 java 序列化,如何实现 java 序列化?
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对
象进行读
写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的
问题。
序 列 化 的 实 现 : 将 需 要 被 序 列 化 的 类 实 现 Serializable 接 口
- 深拷贝和浅拷贝区别是什么?
浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值
都会随之变化,这就是浅拷贝
深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝
- jsp 和 servlet 有什么区别?
jsp经编译后就变成了Servlet.(JSP的本质就是Servlet,JVM只能识别Java的类,不能识别JSP的代码,
Web容器将JSP的代码编译成JVM能够识别的Java类)
jsp更擅长表现于页面显示,servlet更擅长于逻辑控制。
- 说一下 jsp 的 4 种作用域?
JSP中的四种作用域包括page、request、session和application:
page : 代表与一个页面相关的对象和属性。
request : 代表与Web客户机发出的一个请求相关的对象和属性。一个请求可能跨越多个页面,涉
及多个Web组件;需要在页面显示的临时数据可以置于此作用域。
session : 代表与某个用户与服务器建立的一次会话相关的对象和属性。跟某个用户相关的数据应该
放在用户自己的session中。
application : 代表与整个Web应用程序相关的对象和属性,它实质上是跨越整个Web应用程序,包
括多个页面、请求和会话的一个全局作用域。
- 请求转发(forward)和重定向(redirect)的区别?
请求转发forward比重定向redirect快
请求转发的url地址只能是本网站内部地址, 重定向的url地址可以是外部网站地址
请求转发浏览器url不发生变化, 重定向浏览器url地址发生改变.
请求转发request域中的数据可以带到转发后的方法中, 重定向request域中的数据无法带到重定向后的
方法中.
二、JVM虚拟机面试题【14道】
- JDK1.7、1.8的JVM内存模型,哪些区域可能发生OOM(out of memory)内存溢出!
Out of memory: 堆内存溢出
JDK 1.7:有永久代,但已经把字符串常量池、静态变量,存放在堆上。逐渐的减少永久代的使用。
JDK 1.8:无永久代,运行时常量池、类常量池,都保存在元空间。但字符串常量池仍然存放在堆上。
JVM 内存区域划分图 :
内存溢出通俗的讲就是内存不够用了,并且 GC 通过垃圾回收也无法提供更多的内存。实际上除了程序
计数器,其他区域都有可能发生 OOM, 简单总结如下:
【1】堆内存不足是最常见的 OOM 原因之一,抛出错误信息
java.lang.OutOfMemoryError:Java heap space
原因也不尽相同,可能是内存泄漏,也有可能是堆的大小设置不合理。
【2】对于虚拟机栈和本地方法栈,导致 OOM 一般为对方法自身不断的递归调用,且 没有结束点,导
致不断的压栈操作。类似这种情况,JVM 实际会抛出 StackOverFlowError , 但是如果 JVM 试图去拓展栈
空间的时候,就会抛出 OOM.
【3】对于老版的 JDK, 因为永久代大小是有限的,并且 JVM 对老年代的内存回收非常不积极,所以当我
们添加新的对象,老年代发生 OOM 的情况也非常常见。
【4】随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的 OOM 有所改 观,出现 OOM,
异常信息则变成了:“java.lang.OutOfMemoryError: Metaspace”。
- 请介绍类加载过程和双亲委派模型!
类加载过程 : 加载 -> 连接 -> 验证 -> 准备 -> 解析 -> 初始化
双亲委派模型: 先找父类加载器, 让父类加载器加载, 如果父类加载器无法加载就让子类加载器加载.这个
加载过程叫做双亲委派模型或者双亲委派机制.
- 谈一谈堆和栈的区别!
栈: 存储局部变量, 基本类型数据, 动态链接(堆中对象的内存地址)
堆: 凡是new出来的对象都在堆中, 也就是存储数组和对象,垃圾回收器不定期到堆中收取垃圾.
- 说一说jvm常见垃圾回收器和特点!
按照内存空间来划分,年轻代的垃圾回收器有:
Serial:是一个串行的垃圾回收器
ParNew:是Serial的并行版本
Parallel Scavenge:也是一个并行的垃圾回收器,区别在于它注重吞吐量
年轻代的三个垃圾回收器采用的垃圾回收算法都是复制算法,所以在进行垃圾回收时都会暂停所有的用
户线程;
然后是老年代的垃圾回收器:
Serial Old:是Serial的老年代版本,串行,采用的是标记-整理算法,同样会STW
Parallel Old:是Parallel Scavenge的老年代版本,并行,采用的是标记-整理算法,会STW,注重
吞吐量
CMS:注重低延迟,采用的是标记-清除算法,分为四个阶段:初始标记、并发标记、重新标记、
并发清除;在初始化和重新标记阶段中是并行的,会STW,其余两个阶段都是并发执行与用户线程
同时执行;由于采用标记清理算法,会产生空间碎片
最后是整堆收集器:G1收集器,G1收集器的特点有:能够独立管理整个堆空间、可利用多CPU、多核的
硬件优势缩短STW的时间、采用分代收集算法不会产生空间碎片
- Java创建对象的过程!
对象的创建过程一般是从new指令开始的,JVM首先会对符号引用进行解析,解析完毕后JVM会为对象在
堆中分配内存,之后,JVM会将该内存进行零值初始化。最后,JVM会调用对象的构造函数。此时,一般
会有一个引用指向这个对象,将对象的地址值赋值给变量。在Java对象初始化过程中,主要涉及三种执
行对象初始化的结构,分别是 实例变量初始化、实例代码块初始化 以及 构造函数初始化。
- Java中垃圾回收机制
垃圾回收机制用到finalize。当程序创建对象、数组等引用类型实体时,系统都会在堆内存中为之分
配一块内存区,对象就保存在这块内存中,当这块内存不再被任何引用变量引用时,这块内存就会
变成垃圾,等待垃圾回收机制进行回收。
分析对象是否为垃圾的方法 : 可达性分析法, 引用计数法两种
强制垃圾回收 : 当一个对象失去引用后,系统何时调用它的finalize ()方法对它进行资源清理,何时
它会变成不可达状态,系统何时回收它所占有的内存。对于系统程序完全透明。程序只能控制一个
对象任何不再被任何引用变量引用,绝不能控制它何时被回收。强制只是建议系统立即进行垃圾回
收,系统完全有可能并不立即进行垃圾回收,垃圾回收机制也不会对程序的建议置之不理:垃圾回收
机制会在收到通知后,尽快进行垃圾回收。
强制垃圾回收的两个方法 : 调用System类的gc()静态方法System.gc(), 调用Runtime对象的gc()实例
方法:Runtime.getRuntime().gc()方法
垃圾回收基本算法有四种 : 引用计数法, 标记清除法, 标记压缩法, 复制算法
垃圾回收复合算法 – 分代收集算法:
当前虚拟机的垃圾收集都采用分代收集算法,这种算法就是根据具体的情况选择具体的垃圾回收算
法。一般将java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集
算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的
复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对
它进行分配担保,所以我们必须选择“标记-清除”或“标记-压缩”算法进行垃圾收集。
- Java中垃圾回收算法
(1)标记-清除算法:使用可达性分析算法标记内存中的垃圾对象,并直接清理,容易造成空间碎片
(2)标记-压缩算法:在标记-清除算法的基础上,将存活的对象整理在一起保证空间的连续性
(3)复制算法:将内存空间划分成等大的两块区域,from和to只用其中的一块区域,收集垃圾时将存
活的对象复制到另一块区域中,并清空使用的区域;解决了空间碎片的问题,但是空间的利用率低
(4)复合算法 - 分代收集算法:是对前三种算法的整合,将内存空间分为老年代和新生代,新生代中存
储一些生命周期短的对象,使用复制算法;而老年代中对象的生命周期长,则使用标记-清除或标记-整
理算法。
- YoungGC和FullGC触发时机?
Young GC :Young GC其实一般就是在新生代的Eden区域满了之后就会触发,采用复制算法来回
收新生代的垃圾。
Full GC : 调用System.gc()时。系统建议执行Full GC,但是不必然执行
触发时机 :
老年代空间不足
方法区空间不足
进入老年代的平均大小大于老年代的可用内存
由Eden区,幸存者0区向幸存者1区复制时,对象大小大于1区可用内存,则把该对象转存到
老年代,且老年代的可用内存大小小于该对象大小。
注意:full GC是开发或调优中尽量要避免的,这样STW会短一些
- FullGC触发可能产生什么问题
fullgc的时候除gc线程外的所有用户线程处于暂停状态,也就是不会有响应了。一般fullgc速度很快,毫秒级
的,用户无感知。除非内存特别大上百G的,或者fullgc也无法收集到足够内存导致一直fullgc,应用的外在表
现就是程序卡死了.
- jvm调优工具和性能调优思路?
Jvm中调优使用的分析命令如下, 先试用下面命令分析jvm中的问题 :
Jps : 用于查询正在运行的JVM进程。 直接获取进程的pid
Jstat : 可以实时显示本地或远程JVM进程中装载,内存,垃圾信息,JIT编译等数据
Jinfo : 用于查询当前运行着的JVM属性和参数的值
Jmap : 用于显示当前java堆和永生代的详细信息
Jhat : 用于分析使用jmap生成的dump文件,是JDK自带的工具
Jstack: 用于生成当JVM所有线程快照,线程快照是虚拟机每一条线程正在执行的方法,目的是定位
线程出现长时间停顿的原因。
下面是JVM常见的调优参数, 经过上面命令分析定位问题后使用下面参数优化:
-Xmx 指定java程序的最大堆内存
-Xms 指定最小堆内存
-Xmn 设置年轻代大小 , 整个堆大小=年轻代大小+年老代大小。所以增大年轻代后,会减小年老代
大小。此值对系统影响较大,Sun官方推荐为整个堆的3/8
-Xss 指定线程的最大栈空间,此参数决定了java函数调用的深度,值越大说明调用深度越深,若值
太小则容易栈溢出错误(StackOverflowError)
-XX: PermSize 指定方法区(永久区)的初始值默认是物理内存的1/64。
-XX:MetaspaceSize指定元数据区大小, 在Java8中,永久区被移除,代之的是元数据区
-XX:NewRatio=n 年老代与年轻代的比值,-XX:NewRatio=2,表示年老代和年轻代的比值为2:1
-XX:SurvivorRatio=n Eden区与Survivor区的大小比值,-XX:SurvivorRatio=8表示Eden区与
Survivor区的大小比值是8:1:1,因为Survivor区有两个(from,to);
- Jvm内存模型?
注意 : 大多数面试官混淆了内存模型和内存结构的区别, 如果面试官这么问, 可以咨询下面试官是否问的
是jvm内存的结构都包含哪些东西, 还是问JMM(Java memory model)内存模型原理
Jvm内存结构由9块组成:
类加载器, 垃圾回收器, 执行引擎, 方法区, 堆, 栈, 直接内存, 本地方法栈, 程序计数器(pc寄存器)
JMM(java memory model)java内存模型:
JMM规定了所有变量(除了方法参数和本地变量,包括实例变量和静态变量)都放在主内存中。每
个线程都有自己的工作内存,工作内存保存了该线程使用的主内存的变量副本,所有的操作都在工
作内存中进行,线程不能直接操作主内存。线程之间通过将数据刷回主内存的方式进行通信。
JMM定义了原子性,可见性和有序性。
原子性:一个操作不可分割,不可中断,不可被其他线程干扰。JMM提供moniterenter和
monitereixt俩个字节码指令保证代码块的原子性
可见性:当一个变量被修改后,其他线程能够立即看到修改的结果
有序性:禁止指令重排序
- GC如何识别垃圾?
引用计数算法(已被淘汰的算法)
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就
减1;任何时刻计数器为0的对象就是不可能再被使用的。
可达性分析算法
目前主流的编程语言(java,C#等)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判
定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这
些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有
任何引用链相连, 就是从GC Roots到这个对象不可达时,则证明此对象是不可用的。
分配流程
- 详细介绍一下 CMS 垃圾回收器?
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾
回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。
在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。
CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候会产生大量的内存碎片,当剩余内存不能满足
程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃
圾清除,此时的性能将会被降低。
CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:
(1)初始标记
只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
(2)并发标记
进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
(3)重新标记
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,
仍然需要暂停所有的工作线程。
(4)并发清除
清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标
记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作, 所以总体上来看CMS 收集器的内存
回收和用户线程是一起并发地执行。
- Java创建对象的内存分配流程?
三、集合相关面试题【17道】
- 聊一聊Java中容器体系结构!
Collection:集合接口
List:是Collection的子接口,用来存储有序的数据集合
ArrayList:是List集合的实现类,底层采用动态数组进行存储数据,默认是空数组,如果存值则
扩容至10,如果不够则以1.5倍进行扩容。
LinkedList:是List集合的实现类,底层采用双向链表结构进行存储数据,增删数据比较方便,
速度快。
Vector :Vector类实现了可扩展的对象数组, 像数组一样,它包含可以使用整数索引访问的组
件。但是,Vector的大小可以根据需要增长或缩小,以适应在创建Vector之后添加和删除。
【注】Vector是同步的(线程安全)。 如果不需要线程安全的实现,建议使用ArrayList代替
Vector.
Set: 是Collection的子接口,用来存储无序的数据集合
HashSet:底层采用哈希表(HashMap)的方式存储数据,数据无序且唯一
TreeSet:采用有序二叉树进行存储数据,遵循了自然排序。
Map: 与Collection并列,用来存储具有映射关系(key-value)的数据
HashMap:哈希表结构存储数据,key值可以为null
TreeMap:红黑树算法的实现
HashTable:哈希表实现,key值不可以为null
Properties:HashTable的子类,键值对存储数据均为String类型,主要用来操作以.properties
结尾的配置文件。
【特点】存储数据是以key,value对进行存储,其中key值是以Set集合形式存储,唯一且不可重复。
value是以Collection形式存储,可以重复。
- List、Set和Map的区别,以及各自的优缺点
List :
可以允许重复的对象。可以插入多个null元素。是一个有序容器,保持了每个元素的插入顺
序,输出的顺序就是插入的顺序。
常用的实现类有 ArrayList、LinkedList 和 Vector。ArrayList 最为流行,它提供了使用索引的
随意访问,而 LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适。
Set :
不允许重复对象。无序容器,你无法保证每个元素的存储顺序,TreeSet通过 Comparator 或
者 Comparable 维护了一个排序顺序。只允许一个 null 元素。
Set 接口最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基于
HashMap 实现的 HashSet;TreeSet 还实现了 SortedSet 接口,因此 TreeSet 是一个根据其
compare() 和 compareTo() 的定义进行排序的有序容器。而且可以重复。
Map :
Map不是collection的子接口或者实现类。Map是一个接口。
Map 的 每个 Entry 都持有两个对象,也就是一个键一个值,Map 可能会持有相同的值对象但
键对象必须是唯一的。
TreeMap 也通过 Comparator 或者 Comparable 维护了一个排序顺序。
Map 里你可以拥有随意个 null 值但最多只能有一个 null 键。
Map 接口最流行的几个实现类是 HashMap、LinkedHashMap、Hashtable 和 TreeMap。
(HashMap、TreeMap最常用)
- ArrayList,LinkedList,Vector的区别!
ArrayList和Vector是基于数组实现的。 ArrayList线程不安全,速度快, Vector线程安全,速度慢, 因为
底层数组实现所以查询快, 修改快, 添加和删除慢,
LinkedList是基于双向链表实现的, 线程不安全, 添加, 删除快, 查询和修改慢.
ArrayList和Vector都是使用Object的数组形式来存储的,当向这两种类型中增加元素的时候,若容
量不够,需要进行扩容。ArrayList扩容后的容量是之前的1.5倍,然后把之前的数据拷贝到新建的
数组中去。而Vector默认情况下扩容后的容量是之前的2倍。
Vector可以设置容量增量,而ArrayList不可以。
- TreeSet如何保证对象的有序性!
TreeSet是有序不可重复集,具有以下特点:
数据会按自然排序(可以理解为从小到大排序)
不可存储null
数据不可重复
非线程安全
TreeSet底层依赖于TreeMap,TreeMap的数据结构是二叉树(红黑树),由底层数据结构决定了
TreeSet中元素有序且唯一。
我们使用TreeSet时,如果添加对象的话,我们就必须去实现Comparable接口,当然也可以实现
Comparator接口来保证对象的有序性。
- HashTable、HashMap、TreeMap的区别!
Hashtable、HashMap、TreeMap都实现了Map接口,使用键值对的形式存储数据和操作数据。
Hashtable是java早期提供的,方法是同步的(加了synchronized)。key和value都不能是null
值。
HashMap的方法不是同步的,支持key和value为null的情况。行为上基本和Hashtable一致。由于
Hashtable是同步的,性能开销比较大,一般不推荐使用Hashtable。通常会选择使用HashMap。
HashMap进行put和get操作,基本上可以达到常数时间的性能
TreeMap是基于红黑树的一种提供顺序访问的Map,和HashMap不同,它的get或put操作的时间
复杂度是O(log(n))。具体的顺序由指定的Comparator来决定,或者根据键key的具体顺序来决定。
关系图:
- HashMap的底层数据存储结构
JDK1.7采用 : 数组 + 链表结构
JDK1.8采用 : 数组 + 链表 + 红黑树
红黑树:jdk1.8最重要的就是引入了红黑树的设计,当hash表的单一链表长度超过 8 个的时候,链表结
构就会转为红黑树结构。
HashMap的底层数据存储结构为哈希表加红黑树。实现过程如下:
调用HashMap的无参构造方法,加载因子loadFactor赋值0.75,table数组是空。
当添加第一个元素时,创建长度16的数组,threshold=12。
当‘链表长度大于等于8时,并且数组长度大于等于64时,链表调整红黑树
当红黑树的节点个数小于6时,调整为链表
当HashMap的容量超出阈值时,扩容为原来大小的2倍,减少元素的移动,提高效率。
- HashMap的扩容机制
为什么会需要扩容:
当它在初始化时会创建一个容量为16的数组,当元素数量超过阈值(当前容量X加载因子(通
常为0.75)=扩容阈值)时,会将数组大小扩展为当前容量的两倍。
但是HashMap的容量是有上限的。如果hashmap的数组当前容量达到了1073741824,则该
数组不会再增长。且阈值将被设置为Integer.MAX_VALUE(2^31-1)即永远不会超出阈值。
Hashmap的扩容机制在JDK8中,容量变化通常有以下集中情况:
无参构造:实例化的hashmap默认内部数组是null,也就是无实例化。第一次调用put方法
时,会开始第一次初始化扩容,长度为16.
有参构造:用于指定容量。会根据指定的正整数找到不小于指定容量的2的幂数,将这个数设
置赋值给阈值。第一次调用put方法时,会将阈值赋值给容量,然后让 阈值=容量X负载因子。
因此并不是手动指定了容量就一定不会触发扩容,超过阈值后一样扩容。
如果不是第一次扩容,则容量变为原来的2倍,阈值也变为原来的2倍。但负载因子不变。
补充:
首次put时会以扩容的方式初始化,然后存入数据,之后判断是否需要扩容
如果不是首次put,则不咋需要初始化,会直接存入数据,然后判断是否需要扩容。
元素迁移:
在JDK8中,由于数组的容量是以2的幂次方扩容的,那么一个数组在扩容时,一个元素要么在
原位置要么在原长度+原位置的位置。
数组长度变为原来的2倍表现在二进制上多了一个高位参与数组下标确定。此时,一个元素通
过hash转换坐标的方法计算后会出现一个现象:
最高位是0,则坐标不变,最高位是1则坐标变为10000+原坐标,即原长度+原坐标。
- HashMap链表和红黑树转化时机!
HashMap中维护有一个Node类型的数组table,当新添加的元素的hash值(hashCode & (length - 1))所
指向的位置已有元素时就会被挂在该位置的最后面而形成链表。当在插入一个元素后某一位置的链表长
度大于等于树化的阈值(TREEIFY_THRESHOLD = 8)时,会先去检查table的size,如果此时table数组
的长度小于最小树化容量(MIN_TREEIFY_CAPACITY = 64),会先对数组进行扩容,扩容大小为原来的
二倍。如果此时数组的长度已经大于64,则会将链表转化为红黑树。在扩容时,如果发现发现某处已经
变为红黑树的树上的节点数量总和小于等于红黑树转化为链表的阈值(UNTREEIFY_THRESHOLD =
6),则会将此处的红黑树转化为链表。
简而言之,当链表长度超过8且哈希表容量大于64,会将链表转化为红黑树。而当红黑树的大小小于6,
则由树转化为链表。
- 什么是Hash碰撞以及常见的解决方案
Hash碰撞 : 对象Hash的前提是实现equals()和hashCode()两个方法,那么HashCode()的作用就是保证
对象返回唯一hash值,但当两个不同对象计算值一样时,这就发生了碰撞冲突。
解决方案:
开放地址法(再散列法)开放地执法有一个公式:Hi=(H(key)+di) MOD m i=1,2,…,k(k<=m-1)
其中,m为哈希表的表长。di 是产生冲突的时候的增量序列。如果di值可能为1,2,3,…m-1,称线性
探测再散列。
如果di取1,则每次冲突之后,向后移动1个位置.如果di取值可能为1,-1,2,-2,4,-4,9,-9,16,-16,…kk,-
kk(k<=m/2),称二次探测再散列。如果di取值可能为伪随机数列。称伪随机探测再散列。
再哈希法Rehash当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时。
缺点:计算时间增加。
比如上面第一次按照姓首字母进行哈希,如果产生冲突可以按照姓字母首字母第二位进行哈希,再
冲突,第三位,直到不冲突为止.这种方法不易产生聚集,但增加了计算时间。
链地址法(拉链法)将所有关键字为同义词的记录存储在同一线性链表中.基本思想:将所有哈希地
址为i的元素构成一个称为同义词
链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词
链中进行。链地址法适用于经常进行插入和删除的情况。
- HashMap在多线程操作中会出现什么问题!
会出现线程安全问题, 可以使用加锁解决, 但是速度慢, 所以建议使用juc包下的ConcurrentHashMap替代
HashMap, 速度快且线程安全.
- 如何保证集合是线程安全的?
第一: 使用JUC包下的ConcurrentHashMap, 线程安全, 并且速度也快
第二: 选用synchronize或者Lock加锁解决, 速度慢.
- 聊一聊JUC包下的并发工具
CountDownLatch 闭锁,使用时需要传入一个int类型的参数,一个线程等待一组线程执行完毕后再恢复
执行;await() 等待其他线程都执行完毕,通过计数器来判断等待的线程是否全部执行完毕。计数器:
countDown方法,被等待线程执行完毕后将计数器值-1,当CountDownLatch的值减为0时无法恢复,
这就是叫做闭锁的原因。
CyclicBarrier 循环栅栏, 一组线程 同时到达临界点后再同时恢复执行(先到达临界点的线程会阻塞,直到
所有线程都到达临界点),当多个线程同时到达临界点时,随机挑一个线程执行barrierAction后再同时恢
复执行。await():调用该方法时表示线程已经到达屏障,随即阻塞。模拟:多线程向磁盘写入数据,计
数器的值可以恢复。
SemaPhore-信号量:SemaPhore是synchronized或Lock的加强版,作用是控制线程的并发数量。作
用:用来控制同时访问特定资源的线程数量,通过协调保证合理的使用公共资源。acquire():尝试占用一
个信号量,失败的线程会阻塞,直达有新的信号量,再恢复执行release():释放一个信号量;acquire(n):
尝试占用n信号量,失败的线程会阻塞,直达有新的信号量,再恢复执行,release(n):释放n信号量。
Exchanger 线程交换器,Exchange类似于一个交换器,Exchange类允许在两个线程之间定义同步点。
当两个线程都到达同步点时,他们交换数据,因此第一个线程的数据进入到第二个线程中,第二个线程
的数据进入到第一个线程中。
- ConcurrentHashMap如何保证线程安全
底层采用CAS(内存比较交换技术) + volatile + synchronize实现, 初始化为无锁状态也就是使用CAS +
volatile解决安全问题, 但是会涉及ABA问题. 但是几率很小, 如果涉及ABA问题, 底层自动切换成使用
synchronize实现.
- ConcurrentHashMap在1.7和1.8的底层实现
JDK1.7底层采用 : 数组 + 链表,采用Segement保证安全
JDK1.8底层采用: 数据 + 链表 + 红黑树,采用CAS+synchronized代码块保证安全
- ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别
首先二者都是线程安全的得队列,都可以用于生产与消费模型的场景。
LinkedBlockingQueue是阻塞队列,其好处是:多线程操作共同的队列时不需要额外的同步,由于具有
插入与移除的双重阻塞功能,对插入与移除进行阻塞,队列会自动平衡负载,从而减少生产与消费的处
理速度差距。
由于LinkedBlockingQueue有阻塞功能,其阻塞是基于锁机制实现的,当有多个线程消费时候,队列为
空时消费线程被阻塞,有元素时需要再唤醒消费线程,队列元素可能时有时无,导致用户态与内核态切
换频繁,消耗系统资源。从此方面来讲,LinkedBlockingQueue更适用于多线程插入,单线程取出,即
多个生产者与单个消费者。
ConcurrentLinkedQueue非阻塞队列,采用 CAS+自旋操作,解决多线程之间的竞争,多写操作增加冲
突几率,增加自选次数,并不适合多写入的场景。当许多线程共享访问一个公共集合时,
ConcurrentLinkedQueue 是一个恰当的选择。从此方面来讲,ConcurrentLinkedQueue更适用于单线
程插入,多线程取出,即单个生产者与多个消费者。
总之,对于几个线程生产与几个线程消费,二者并没有严格的规定, 只有谁更适合。
- 说一下 HashSet 的实现原理
HashSet底层使用了哈希表来支持的,特点:存储快, 底层由HashMap实现
往HashSet添加元素的时候,HashSet会先调用元素的hashCode方法得到元素的哈希值 ,然后通过元
素 的哈希值经过移位等运算,就可以算出该元素在哈希表中的存储位置。
如果算出的元素存储的位置目前没有任何元素存储,那么该元素可以直接存储在该位置上
如果算出的元素的存储位置目前已经存在有其他的元素了,那么还会调用该元素的equals方法与该位置
的元素再比较一次,如果equals方法返回的是true,那么该位置上的元素视为重复元素,不允许添加,
如果返回的是false,则允许添加
- HashSet和TreeSet有什么区别?
HashSet是由一个hash表来实现的,因此,它的元素是无序的。add(),remove(),contains()方法的时
间复杂度是O(1)。
TreeSet是由一个树形的结构来实现的,它里面的元素是有序的。因此,add(),remove(),contains()方
法的时间复杂度是O(logn)。