面经资料整理01
- 面试问题来源
- Java相关
- 1. 讲一下Java数据类型,浮点型为什么有精度损失?
- 2. CopyOnWriteArrayListt与ArrayList区别?
- 3. LinkedList与ArrayList区别讲一下?
- 4. HashMap底层原理?
- 5. 讲一下HashMap与ConcurrentHashMap区别,使用的时候有没有什么问题?
- 6. ConcurrentHashMap怎么保证size变量的线程安全?
- 7. 线程池用过吗, 怎么使用,如果要自定义一个线程池,需要哪些核心参数?
- 8. ReentrantLock公平锁与非公平锁怎么实现的,为什么默认非公平锁?
- 9. 讲一下知道的JVM命令?
- 10. 常见的垃圾回收器?
- 11. 垃圾回收机制?
- 12. hashmap hashtable对比?
- 13. hashmap put过程?
- 14. hashmap怎么变成线程安全?
- 15. CountDownLatch简述
- 16.什么是逃逸分析?
- 操作系统
- 算法
面试问题来源
本文章内的题目来自牛客网上的陌陌面经, 主要涉及Java方面的面试题。
Java相关
1. 讲一下Java数据类型,浮点型为什么有精度损失?
java八种基本类型:
char、byte、boolean、int、short、long、float、double
浮点型为什么有精度损失?
参考文章
浮点运算很少是精确的,只要超过精度能表示的范围就会产生误差。往往产生误差不是因为数的大小,而是因为数据的精度,因此,产生的结果接近但不等于想要的结果。可以考率采用一些替代方法,如String结合BigDecimal或者通过使用long类型来转换。
2. CopyOnWriteArrayListt与ArrayList区别?
CopyOnWriteArrayList是ArrayList的一个线程安全的变体,其中所有可变操作(add、set等等)都是通过对底层数组进行一次新的复制来实现的。相比较于ArrayList它的写操作要慢一些,因为他需要实例的快照。
CopyOnWriteArrayList写操作需要大面积复制数组,所有写操作时性能很差,但由于读操作的对象和写操作不是同一个对象,读之间也不需要加锁,读和写之间也不需要加锁,读操作很快很安全。
3. LinkedList与ArrayList区别讲一下?
4. HashMap底层原理?
在JDK1.6, JDK1.7中, HashMap采用位桶+链表实现,即使用链表处理冲突, 同一hash值的链表都存储在一个链表里。当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值查找的效率较低。 在JDK1.8中, HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值8时,将链表转换为红黑树,这样会大大减少查找时间。
HashMap实现原理简述:
首先有一个每个元素都是链表(可能表述不准确)的数组,当添加一个元素(key-value)时,就首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,同一各链表上的Hash值是相同的,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。
为什么使用加载因子(默认0.75), 为什么需要扩容?
因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,因为链表的长度很大(当然最新版本使用了红黑树后会改进很多),扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率
HashMap数据结构是以空间换时间,所以填充比不需要太大,填充比过小又会导致空间浪费。如果考虑点为内存,填充比可以稍大,如果注重查找性能,填充比可以稍小。
HashMap的put(key, value)过程?
键值对put(key, value)的过程简述:
1.判断键值对数组tab[]是否为空,否则以默认大小resize();
2.根据键值key计算hash值得到插入的数组索引i, 如果tab[i] == null, 直接新建结点添加,否则转入3;
3. 判断当前数组中处理hash冲突的方式为链表还是红黑树(check第一个结点类型即可), 分别处理。
5. 讲一下HashMap与ConcurrentHashMap区别,使用的时候有没有什么问题?
ConcurrentHashMap(JDK 1.7)要点简述:
- 底层采用分段的数组+链表实现,线程安全
- 通过把整个Map分为N个Segment, 可以提供相同的线程安全,但是效率提升N倍,默认提升16倍,(读操不加锁,由于HashEntry的value变量是volatile的,也能保证读取到最新的值。)
- HashTable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
- 有些方法需要跨段,比如size()和containsValue(), 它们可能需要锁定整张表而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
- 扩容: 段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测不需要扩容,有效避免无效扩容
锁分段技术: 首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。HashMap中采用的锁机制是一次锁住整张hash, 从而同一个时刻只能有一个线程对其操作;而ConcurrentHashMap中则是一次锁住一个桶。
ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作,只锁住当前需要用到的桶,这样原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能显而易见得到提升。
HashMap使用过程中遇到的问题:
HashMap的迭代器(iterotor)是fail-fast迭代器, 而HashTable的enumerator迭代器不是fail-fast, 当有其它线程改变了HashMap的结构时(增加或者移除元素), 将会抛出ConcurrentModificationException。
6. ConcurrentHashMap怎么保证size变量的线程安全?
JDK1.7的size()方法:
JDK1.7中的ConcurrentHashMap的size方法,计算size的时候会先不加锁获取一次数据长度,然后再获取一次,最多三次,然后比较前后两次的值,如果相同的话说面不存在竞争的编辑操作,就直接把值返回就可以了。
但是如果前后获取的值不一样,那么会将每个Segment都加上锁,然后计算ConcurrentHashMap的size值。
JDK1.8的size()方法:
baseCount volidate变量 + CAS操作
7. 线程池用过吗, 怎么使用,如果要自定义一个线程池,需要哪些核心参数?
Java线程池使用方式:
1.使用Executors提供的静态方法能满足要求,就尽量使用它提供的三个方法,因为自己手动配置ThreadPoolExecutor的参数有点麻烦,要根据实际任务的类型和数量来进行配置。如果Executors达不到要求,可以自己继承ThreadPoolExecutor类进行重写。
Excutors提供如下五种线程池:
newSIngleThreadExecutor: 一个单线程的线程池,可以用于需要保证顺序执行的场景,并且只有一个线程在执行。
newFixedThreadPool: 一个固定大小的线程池,可以用于已知并发压力的情况下,对线程数做限制。
newCachedThreadPool: 一个可以无限扩大的线程池,比较适合处理执行时间比较小的任务。
newScheduledThreadPool: 可以延时启动,定时启动的线程池,适合需要多个后台线程执行周期性任务的场景。
newWorkStealingPool: 一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行。
自定义线程池需要的核心参数:
corePoolSize: 核心池的大小
maximumPoolSize: 最大线程数,线程池能创建的最大线程数量
keepAliveTime: 在线程数量超过corePoolSize后,多余空闲线程的最大存活时间
unit: 时间单位
workQueue: 存放来不及处理的任务的队列,是一个BlockingQueue。
threadFactory: 生产线程的工厂类,可以定义线程名,优先级等。
构造函数的定义:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) ;
8. ReentrantLock公平锁与非公平锁怎么实现的,为什么默认非公平锁?
为什么非公平锁性能高于公平锁性能?
在恢复一个被挂起的线程与该线程真正运行之间存在严重的延迟。
假设线程A持有一个锁,并且线程B请求这个锁。由于锁被A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此B会再次尝试获取这个锁。与此同时,如果线程C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样就是一种双赢的局面:B获得锁的时刻并没有推迟,C更早的获得了锁,并且吞吐量也提高了。
当持有锁的时间相对较长或者请求锁的平均时间间隔较长,应该使用公平锁。在这些情况下,插队带来的吞吐量提升(当锁处于可用状态时,线程却还处于被唤醒的过程中)可能不会出现。
9. 讲一下知道的JVM命令?
10. 常见的垃圾回收器?
Serial收集器(新生代):
Serial以串行的方式执行,它是单线程的收集器,只会使用一个线程进行垃圾收集工作,GC线程工作时,其它所有线程都将停止工作。它使用复制算法收集新生代来及。
优点: 简单高效,在单个CPU环境下,由于没有线程交互的开销,拥有最高的单线程收集效率,它是Client场景下的默认新生代收集器。
显示的使用该垃圾收集器作为新生代收集器:
-XX:+UseSerialGC
ParNew 收集器(新生代)
ParNew是Serial收集器的多线程版本,ParNew在单核环境下是不如Serial的,在多核的条件下才有优势,使用复制算法收集新生代垃圾。
Server场景下默认的新生代收集器,主要是因为除了Serial收集器,只有他能与CMS收集器配合使用。
优点: 多核环境下收集效率高于Serial收集器
显示的使用该垃圾收集器作为新生代收集器:
-XX:+UserParNewGC
Parallel Scavenge收集器(新生代)
相比于Serial、ParNew收集器等的目标是尽可能缩短用户线程的停顿时间,它的目标是提高吞吐量(吞吐量=运行用户程序的时间/(运行用户程序的时间 + 垃圾收集的时间))。
停顿的时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,适合在后台运算不需要太多交互的任务。
Parallel Scavenge使用复制算法收集新生代垃圾。
显示的使用该垃圾收集器作为新生代收集器:
-XX:+UseParellelGC
Serial Old收集器(老年代)
Serial收集器的老年代版本, Client场景下默认的老年代垃圾收集器
使用标记-整理算法收集老年代垃圾
显示的使用该垃圾收集器作为老生代收集器:
-XX:+UseSerialOldGC
Parellel Old收集器(老年代)
Parellel Scavenge 收集器的老年代版本。
在注重吞吐量的场景下,可以采用Parallel Scanvenge + Parallel Old的组合
使用标记-整理收集老年代垃圾
显示的使用该垃圾收集器作为老生代收集器:
-XX:+UseParallelOldGC
CMS垃圾收集器
CMS(Concurrent Mark Sweep),收集器几乎占据着 JVM 老年代收集器的半壁江山,它划时代的意义就在于垃圾回收线程几乎能做到与用户线程同时工作。
使用标记-清除算法收集老年代垃圾。
工作流程主要有如下 4 个步骤:
- 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿(Stop-the-world)
- 并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿
- 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿(Stop-the-world)
- 并发清除: 清理垃圾,不需要停顿
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
但 CMS 收集器也有如下缺点:
- 吞吐量低
- 无法处理浮动垃圾
- 标记-清除算法带来的内存空间碎片问题
显示的使用该垃圾收集器作为老生代收集器:
-XX:+UseConcMarkSweepGC
*G1垃圾收集器
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
使用复制 + 标记 - 整理算法收集新生代和老年代垃圾。
使用复制 + 标记 - 整理算法收集新生代和老年代垃圾。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
显示的使用该垃圾收集器作为老生代收集器:
-XX:+UseG1GC
11. 垃圾回收机制?
垃圾回收(Garbage Collection GC), 顾问思义就是释放垃圾占用空间,防止内存泄漏。有效的使用可以使用的内存, 对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
垃圾判断算法
引用计数法: 给每个对象加一个计数器,当有地方引用该对象时计数器加1,当引用失效时,计数器减1。用对象计数器是否为0来判断对象是否回收,缺点为:无法解决循环引用问题。
可达性分析: 通过GC ROOT 的对象作为搜索起始点,通过引用向下搜索,所走过的路径称为引用链。通过对象是否有到达引用链的路径来判断对象是否可被回收(可作为GC ROOT)的对象: 虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区常量引用的对象,本地方法栈中JNI引用对象。
垃圾回收算法
以下为几种常见的垃圾回收算法
标记-清除算法:
标记清除算法(Mark-Sweep) 是最基础的一种垃圾回收算法,它分为2部分,先把内存区域中的这些对象进行标记,哪些属于可回收标记出来,然后把这些垃圾拎出来清理掉。它存在一个很大的问题,那就是内存碎片。
复制算法
复制算法(Copying)是在标记清除算法基础上演化而来,解决标记清除算法的内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
标记-整理算法
标记-整理算法标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,再清掉端边界以外的内存区域。
标记整理算法解决了内存碎片的问题,也规避了复制算法只能利用一般一半内存区域的弊端。标记整理算法对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制差很多。一般把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
分代收集算法
分代收集算法严格来说并不是一种思想或理论,而是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳,根据对象存活周期的不同将内存划分为几块。
- 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
- 在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理算法或者标记-整理算法来进行回收。
内存区域与回收策略
Survivor区域存在的意义,:
Survivor的存在意义就是减少被送到老年代的对象,进而减少 Major GC的发生。 Survivor的预筛选保证,只有经历16次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。
12. hashmap hashtable对比?
Hashtable数据结构:
Hashtable已经被弃用, 性能比较低
1.Hashtable中key-value的映射,key和value都是不允许为null的,如果为null, 会出现空指针异常抛出;
2. Hashtable在计算结点元素在哈希表的位置使用的算法稍有区别,它有它的好处,但和HashMap的算法比起来性能低一些;
3. Hashtable的扩容是原来容量的二倍加1(2n+1), 源码参考: int newCapacity = (oldCapacity << 1) + 1;
HashMap与Hashtable的区别:
1.HashMap继承AbstractMap类,而HashTable继承自Dictionary类, 都实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口;
2.HashMap多出elments()和contains()方法;
3.HashTable的方法几乎都是被synchronized关键字修饰,是线程安全的,HashMap不是线程安全的;
4.初始容量大小和每次扩容大小的不同
Hashtable默认初始大小为11, 扩容容量变为2n+1。HashMap默认初始大小为16,之后每次扩充容量为原来2倍;
5.计算hash值的方法不同
Hashtable直接使用对象的hashCide,hashCode是根据对象的地址或者字符串或者数字计算出来的int类型的数值,然后使用除留余数来获取最终位置,除法运算比较耗时。
HashMap为了提高计算效率,将哈希表的大小固定为2的幂,在取模运算时,不需要做触发,只需做位运算,位运算比除法效率高。
13. hashmap put过程?
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
14. hashmap怎么变成线程安全?
Collections.synchronizedMap(new HashMap<>());
15. CountDownLatch简述
16.什么是逃逸分析?
操作系统
1. linux日志查看异常
2. git回滚命令
/var/log/message 系统启动后的信息和错误日志,是Red Hat Linux中最常用的日志之一
/var/log/secure 与安全相关的日志信息
/var/log/maillog 与邮件相关的日志信息
/var/log/cron 与定时任务相关的日志信息
/var/log/spooler 与UUCP和news设备相关的日志信息
/var/log/boot.log 守护进程启动和停止相关的日志消息
cat /etc/issue
3. unix与linux的用户态和内核态的区别?
怎样去理解Linux用户态和内核态?
内核态:
它是一种特殊的软件程序, 他能控制计算机的硬件资源,例如协调CPU资源, 分配内存资源,并且提供稳定的环境供应用程序运行。
用户态:
用户态就是供应用程序运行的空间,为了使应用程序访问到内核管理的资源如CPU, 内存,I/O。内核必须提供一组通用的访问接口, 这些接口叫做系统调用。
从用户态到内核态切换可以通过三种方式:
1.系统调用。系统调用本身就是中断,但是软件中断,跟硬中断不同。
2.异常:如果当前进程运行在用户态,如果这时候发生了异常事件,就会触发切换。例如:缺页异常。
3.外设中断: 当外设完成用户的请求时,会向CPU发送中断信号。
4. 讲一下软链接、硬链接?
硬链接:
使有链接关系的两个文件共享同样的内容,拥有同样innode, 缺陷是只能创建指向文件的硬链接,不能指向目录。
软链接:
有链接关系的两个文件,它们的inode是不同的,它可以指向目录,软链接真正像我们在windows下的快捷方式。
5. 线程、进程的区别?
(1) 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
(2) 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行,另外进程可以并行运行而线程不能做到
(3) 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.
(4) 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
算法
1. java实现一下jvm堆溢出、栈溢出
堆溢出:
public class HeapOverflow {
public static void main(String[] args) {
List<Object> listObj = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Byte[] bytes = new Byte[1*1024*1024];
listObj.add(bytes);
}
System.out.println("添加success");
}
}
VM参数配置:
-Xms1m -Xmx10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
栈溢出
public class StackOverflow {
private static int count;
public static void count() {
try {
count++;
count();
} catch (Throwable e) {
System.out.println("最大深度:" + count);
e.printStackTrace();
}
}
public static void main(String[] args) {
count();
}
}
控制台输出:
2. 前序遍历(非递归)
public class Main {
public static void main(String[] args) {
// 构建简单树
TreeNode<Integer> root = new TreeNode<Integer>(1);
TreeNode<Integer> l = new TreeNode<>(2);
TreeNode<Integer> r = new TreeNode<>(4);
TreeNode<Integer> ll = new TreeNode<>(5);
TreeNode<Integer> lr = new TreeNode<>(3);
root.left = l;
root.right = r;
l.left = ll;
l.right = lr;
preOrder(root);
}
// 前序遍历
public static void preOrder(TreeNode<Integer> root) {
Stack<TreeNode> stack = new Stack<>();
// 插入头结点
stack.push(root);
while (stack.size() != 0) {
TreeNode temp = stack.pop();
System.out.println(temp.val);
if (temp.right != null) {
stack.push(temp.right);
}
if (temp.left != null) {
stack.push(temp.left);
}
}
}
public static class TreeNode<T> {
T val;
TreeNode<T> left;
TreeNode<T> right;
public TreeNode(T val) {
super();
this.val = val;
}
}
}