阿里巴巴面试题总结以及答案
一、数据结构
1、集合框架有哪些,hashMap是如何实现的,如何解决冲突,如何扩容
答:Java的集合类有两个父接口:Collection接口和Map接口。
Collection接口的主要直接子接口:List接口、Set接口。
Map接口的主要实现类:HashMap、Hashtable、TreeMap等。
List接口的主要实现类:ArrayList、Vector、LinkedList等。
Set接口的主要实现类:HashSet、TreeSet、LinkedHashSet等。
其中Hashmap在jdk7以及之前,底层使用数组+链表来实现的;
其中的实现过程:
- new HashMap()之后,并不会直接创建数组,而是等put数据时才会创建数组。
- put数据时,如果是第一次向这个集合里put数据,会先创建一个长度为16的一维数组(Node[] table),然后存储数据。在存储数据时,会先调用Key所在类的hashCode方法来计算此key的哈希值,在将哈希值进行处理计算后,得到该数据在数组table上的位置。
- 根据位置来分情况是否存储:
- 情况一:此时这里为空,就直接put数据。
- 情况二:位置不为空,说明此位置上已经有一个或者多个数据了(多个数据是以链表的形式存储),那么将put数据的key的哈希值与已有数据的key的哈希值依次进行比较,如果是不同的,就存储put的数据,将put的数据放在此位置上,原有数据以链表的形式存储。
- 情况三:此位置不为空,并且在情况二的基础上,比较是相同的,那么就会调用equals()方法进行比较(注意:此时的equals是重写过的,比较的是值),如果不同,那么就存储put,数据存储同情况二。
- 情况四:若在情况三的基础上是相同的,那么就用此时put数据的value替换原来位置上的value
扩容:当存储的数据超过临界值,并且要存放数据的位置为非空的,就扩容,扩容为原来容量的2倍。
注意:临界值 = 当前容量 * 填充因子 (填充因子一般为0.75)
而在jdk1.8以及之后的版本,其底层使用数组+链表+红黑树来实现的;
实现过程:基本上等同于1.7过程的1 2 3,其不同点在于:
- jdk7是将新数据放在数组位置上,原有数据以链表的形式存储。
- jdk8是原有数据位置不变,而新数据以链表形式存储在最后
扩容:如果使用的是默认的初始容量(16),每次扩充,就会变为原来的2倍。如果使用的是自己指定的容量初始值,那么就会将这个容量扩充为2的幂次方大小。
注意:jdk8在当数组的某一索引位置上的以链表形式存储的数据大于8个,且当时数组长度大于64时,此索引位置上所有数据改为红黑树存储,这样可以减少查找的时间。
解决冲突: 散列表要解决的一个问题就是散列值的冲突问题,通常是两种方法:链表法和开放地址法。链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。而HashMap采用的链表法的方式,链表是单向链表(其过程就是和上面的情况是相同的。
注意:当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。
其中重要的几点:
- List接口下的主要实现类都是有序的,数据可重复的。
- Map接口下的主要实现类都是键不可重复,值可重复的。
- Set接口下的主要实现类都是无序的,且不可重复的。
2、Set是如何实现的
HashSet
- 底层其实是基于HashMap实现的,大部分的方法都是直接调用HashMao中的方法,少部分的方法是自己实现的。
- 其实现了Set接口,只存储对象。
- 其是使用成员对象来计算哈希值。
- 其速度比不上HashMap(原因在第一点就可说明)
- HashSet中不允许有重复元素,这是因为HashSet是基于HashMap实现的,HashSet中的元素都存放在HashMap的key上面,而value中的值都是统一的一个固定对象private static final Object PRESENT = new Object();
- add方法调用了HashMap的Put方法,先判断是否存在,存在则更新,不存在则直接插入。
3、ConcurrentHashMap、Hashtable分别是如何实现的,ConcurrentHashMap在1.8以后有什么不同
实现方式:concurrentHashMap在jdk1.7之前使用分段数组 + 链表实现的,1.8之后是用数组+链表+红黑树实现,红黑树可以保证查找效率。而Hashtable底层是用数组+链表实现的。
具体区别:
- 底层区别:1.7:数组(Segment) + 数组(HashEntry) + 链表(HashEntry节点)
底层一个Segments数组,存储一个Segments对象,一个Segments中储存一个Entry数组,存储的每个Entry对象又是一个链表头结点。- 1.7
分段锁 对整个桶数组进行了分割分段(Segment)(继承了ReentrantLock),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
1.8
使用的是优化的synchronized 关键字同步代码块 和 cas操作了维护并发。
4、Comparable接口和Comparator接口有什么区别?用了哪些设计模式
- Comparable和Comparator都是用来实现集合中元素的比较、排序的。
- Comparable是在类内部定义的方法实现的排序,位于java.lang下。
- Comparator是在类外部实现的排序,位于java.util下。
- 实现Comparable接口需要覆盖compareTo方法,实现Comparator接口需要覆盖compare方法。
- Comparable接口将比较代码嵌入需要进行比较的类的自身代码中,而Comparator接口在一个独立的类中实现比较。
- Comparable接口强制进行自然排序,而Comparator接口不强制进行自然排序,可以指定排序顺序。
- Comparable是自已完成比较(Collections.sort(list)),Comparator是外部程序实现比较(Collections.sort(list, new CatComparator()))。
- Comparator体现了一种策略模式(strategy design pattern),就是不改变对象自身,而用一个策略对象(strategy object)来改变它的行为。
5、List如何一边遍历一边删除元素?为什么ArrayList中承载元素的elementData成员变量用了transient修饰符
解决方案:
- 使用Iterator的remove()方法:
List<String> platformList = new ArrayList<>(); platformList.add("博客园"); platformList.add("CSDN"); platformList.add("掘金"); Iterator<String> iterator = platformList.iterator(); while (iterator.hasNext()) { String platform = iterator.next(); if (platform.equals("博客园")) { iterator.remove(); } } System.out.println(platformList);
- 使用for循环的正序遍历
List<String> platformList = new ArrayList<>(); platformList.add("博客园"); platformList.add("CSDN"); platformList.add("掘金"); for (int i = 0; i < platformList.size(); i++) { String item = platformList.get(i); if (item.equals("博客园")) { platformList.remove(i); i = i - 1; //要注意整理进行修改指标 } } System.out.println(platformList);
3.使用for循环倒序遍历:
List<String> platformList = new ArrayList<>(); platformList.add("博客园"); platformList.add("CSDN"); platformList.add("掘金"); for (int i = platformList.size() - 1; i >= 0; i--) { String item = platformList.get(i); if (item.equals("掘金")) { platformList.remove(i); } } System.out.println(platformList);
- java8之后,新添方法 removelf 一行就可以解决问题:list.removeIf(item -> item.equals(“str”));
答:
transient 用来表示一个域不是该对象序行化的一部分,当一个对象被序行化的时候,transient修饰的变量的值是不包括在序行化的表示中的。
但是ArrayList在序列化的时候会调用writeObject,直接将size和element写入ObjectOutputStream;反序列化时调用readObject,从ObjectInputStream获取size和element,再恢复到elementData。
其中就没有使用elementData,不用的原因在于:elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
6、集合的fail-fast是怎样的
概念:快速失败是Java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast。
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候,线程2 修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就可能会抛出 ConcurrentModificationException异常,从而产生fast-fail快速失败。
而迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedModCount值,是的话就返回遍历;否则抛出异常,终止遍历。
可以看下ArrayList中的源码:
那么如何解决这种问题?
- 在遍历过程中,所有涉及到改变modCount值的地方全部加上synchronized。
- 使用 JUC 中的线程安全类来替代,比如使用 CopyOnWriteArrayList(写时复制) 来替代 ArrayList ,使用ConcurrentHashMap 来替代 HashMap 。
7、列举 B+树、B树、avl树、红黑树,他们的实际应用场景。
红黑树,AVL树简单来说都是用来搜索的。
AVL树:平衡二叉树,一般是用平衡因子差值决定并通过旋转来实现,左右子树树高差不超过1,那么和红黑树比较它是严格的平衡二叉树,平衡条件非常严格(树高差只有1),只要插入或删除不满足上面的条件就要通过旋转来保持平衡。由于旋转是非常耗费时间的。我们可以推出AVL树适合用于插入删除次数比较少,但查找多的情况。
红黑树:平衡二叉树,通过对任何一条从根到叶子的简单路径上各个节点的颜色进行约束,确保没有一条路径会比其他路径长2倍,因而是近似平衡的。所以相对于严格要求平衡的AVL树来说,它的旋转保持平衡次数较少。用于搜索时,插入删除次数多的情况下我们就用红黑树来取代AVL。(注意:现在部分场景使用跳表来替换红黑树)
B树,B+树:它们特点是一样的,是多路查找树,一般用于数据库系统中,为什么,因为它们分支多层数少呗,都知道磁盘IO是非常耗时的,而像大量数据存储在磁盘中所以我们要有效的减少磁盘IO次数避免磁盘频繁的查找。
B+树是B树的变种树,有n棵子树的节点中含有n个关键字,每个关键字不保存数据,只用来索引,数据都保存在叶子节点。是为文件系统而生的。
二、java基础
1、什么是双亲委派模型,为什么这样做,tomcat的类加载器是如何划分的
工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器(注意,上图已经指出,这里的父子关系是组合关系而非继承关系)去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。
好处:
对于任何一个类,都需要这个类本身和加载它的类加载器一同来确定其在虚拟机的唯一性,每一个类加载器都拥有一个独立的类名称空间。因此使用双亲委派模型来组织类加载器之间的关系,主要体现两个好处:
1.类伴随它的类加载器一起具备了一种带有优先级的层次关系,确保了在各种加载环境的加载顺序。
2.保证了运行的安全性,防止不可信类扮演可信任的类
Tomcat的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类(Object,String等),各个web应用自己的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给commonClassLoader走双亲委托。
对于JVM来说:
因此,按照这个过程可以想到,如果同样在CLASSPATH指定的目录中和自己工作目录中存放相同的class,会优先加载CLASSPATH目录中的文件。
图中的加载器:
- commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
- catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
- sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
- WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;
流程:
2、异常都有哪些分类,如何处理
处理方式:
- 遇到问题不进行具体处理,而是继续抛给调用者 (throw和throws)
- 针对性处理方式:捕获异常(try-catch)
注意:
RuntimeException:在定义方法时不需要声明会抛出RuntimeException, 在调用这个方法时不需要捕获这个RuntimeException;总之,未检查异常不需要try…catch…或throws 机制去处理 。
CheckedException:定义方法时必须声明所有可能会抛出的exception; 在调用这个方法时,必须捕获它的checked exception,不然就得把它的exception传递下去
总之,一个方法必须声明所有的可能抛出的已检查异常;未检查异常要么不可控制(Error),要么应该避免(RuntimeException)。如果方法没有声明所有的可能发生的已检查异常,编译器就会给出错误信息
3、volatile有什么作用,应用场景是怎样的
- Volatile 变量具有
synchronized
的可见性特性,但是不具备原子性。这就是说线程能够自动发现 volatile 变量的最新值。- 禁止指令重排(保证执行顺序的正确性)
- 要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
使用场景:
- 实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
- 在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。(单例模式的双重检查锁定)。
- 使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用和收集程序的统计信息供使用。
- 如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。
4、jdk提供了哪些线程池,是如何调度的,会有哪些问题
- 固定线程数的线程池(newFixedThreadPool)
FixedThreadPool 是通过 java.util.concurrent.Executors 创建的 ThreadPoolExecutor 实例。这个实例会复用 固定数量的线程处理一个共享的无边界队列 。任何时间点,最多有 nThreads 个线程会处于活动状态执行任务。如果当所有线程都是活动时,有多的任务被提交过来,那么它会一致在队列中等待直到有线程可用。如果任何线程在执行过程中因为错误而中止,新的线程会替代它的位置来执行后续的任务。所有线程都会一致存于线程池中,直到显式的执行 ExecutorService.shutdown() 关闭。由于阻塞队列使用了LinkedBlockingQueue,是一个无界队列,因此永远不可能拒绝任务。LinkedBlockingQueue在入队列和出队列时使用的是不同的Lock,意味着他们之间不存在互斥关系,在多CPU情况下,他们能正在在同一时刻既消费,又生产,真正做到并行。因此这种线程池不会拒绝任务,而且不会开辟新的线程,也不会因为线程的长时间不使用而销毁线程。这是典型的生产者----消费者问题,这种线程池适合用在稳定且固定的并发场景,比如服务器
- 缓存的线程池(newCachedThreadPool)
核心池大小为0,线程池最大线程数目为最大整型,这意味着所有的任务一提交就会加入到阻塞队列中。当线程池中的线程60s没有执行任务就终止,阻塞队列为SynchronousQueue。SynchronousQueue的take操作需要put操作等待,put操作需要take操作等待,否则会阻塞(线程池的阻塞队列不能存储,所以当目前线程处理忙碌状态时,所以开辟新的线程来处理请求),线程进入wait set。
总结下来:①这是一个可以无限扩大的线程池;②**适合处理执行时间比较小的任务;**③线程空闲时间超过60s就会被杀死,所以长时间处于空闲状态的时候,这种线程池几乎不占用资源;④阻塞队列没有存储空间,只要请求到来,就必须找到一条空闲线程去处理这个请求,找不到则在线程池新开辟一条线程。如果主线程提交任务的速度远远大于CachedThreadPool的处理速度,则CachedThreadPool会不断地创建新线程来执行任务,这样有可能会导致系统耗尽CPU和内存资源,所以在使用该线程池是,一定要注意控制并发的任务数,否则创建大量的线程可能导致严重的性能问题。
- 单个线程的线程池(newSingleThreadExecutor)
SingleThreadExecutor是使用单个worker线程的Executor,作为单一worker线程的线程池,SingleThreadExecutor把corePool和maximumPoolSize均被设置为1,和FixedThreadPool一样使用的是无界队列LinkedBlockingQueue,所以带来的影响和FixedThreadPool一样。对于newSingleThreadExecutor()来说,也是当线程运行时抛出异常的时候会有新的线程加入线程池替他完成接下来的任务。
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行,所以这个比较适合那些需要按序执行任务的场景。比如:一些不太重要的收尾,日志等工作可以放到单线程的线程中去执行。日志记录一般情况会比较慢(数据量大一般可能不写入数据库),顺序执行会拖慢整个接口,堆积更多请求,还可能会对数据库造成影响(事务在开启中),所以日志记录完全可以扔到单线程的线程中去,一条条的处理,也可以认为是一个单消费者的生产者消费者模式。
4.固定个数的线程池(newScheduledThreadPool)
相比于第一个固定个数的线程池强大在
①可以执行延时任务
②也可以执行带有返回值的任务
5、AQS的原理是怎样的,ReentrantLock 和synchronized比有什么优势
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。 AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
AQS 维护了一个 volatile int state(代表共享资源)和一个 FIFO 线程等待队列(多线程争用资源被阻塞时会进入此队列) 来完成获取资源线程的排队工作。 AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
两者的区别:
- ReentrantLock 显示的获得、释放锁, synchronized 隐式获得释放锁
- ReentrantLock 相比 synchronized 的优势是可响应、可轮回、 可中断(Lock 可以让等待锁的线程响应中断)、公
平锁、多个锁, synchronized 等待的线程会一直等待下去,不能够响应中断- ReentrantLock 是 API 级别的,使用的同步非阻塞,采用的是乐观并发策略具体是使用的 Unsafe.park()实现的,
synchronized 是 JVM 级别的是同步阻塞,使用的是悲观并发策略,具体是 sync 操作 MarkWord 实现的- ReentrantLock 可以实现公平锁 Lock lock=new ReentrantLock(true);//true 公平锁 false 非公平锁
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁
- ReentrantLock 通过 Condition 可以绑定多个条件
- synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象, 因此使用 Lock 时需要在 finally 块中释放锁。
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
- Lock 可以提高多个线程进行读操作的效率,既就是实现读写锁等。
6、锁升级机制是怎样的
2.1、无锁到偏向锁:
我们知道,Synchronized修饰的方法被调用前,其对象初始状态是处于无锁状态的,其锁标记位为01,此时当线程a调用此方法时,会通过CAS自旋,替换mark words。
偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;
如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置;
2.2、偏向锁到偏向锁:
由于偏向锁线程1获取锁后,不会主动修改对象头,所以哪怕此线程1实际已消亡,之前加锁对象的对象头还是保持偏向锁状态。这个时候线程2想要进入同步方法,他会去查看线程1是否还存活,如果已经消亡,则把对锁定对象的对象头恢复成无锁,然后重复无锁->偏向锁的过程。
同时如果线程1未消亡,但是其栈帧信息中不在需要此持有这个锁对象,也会进行一次偏向锁->偏向锁的过程。
2.3、偏向锁到轻量级锁:
2.2的情况中,如果线程2需要进入同步方法,线程1还持有这个对象,那么就会进入偏向锁->轻量级锁的过程。此时线程2进行cas替换失败,会修改对象头,升级为轻量级锁,同时开启自旋,重复尝试替换。
2.4、轻量级锁到重量级锁:
轻量级锁替换失败到达一定次数(默认为10)后,轻量级锁升级为重量级锁。
需要注意,如果线程2自旋期间,有线程3也需要访问同步方法,则立刻由轻量级锁膨胀为重量级锁,在java1.6中,引入了自适应自旋锁,自适应意味着自旋 的次数不是固定不变的,而是根据前一次在同一个锁上自 旋的时间以及锁的拥有者的状态来决定。 如果在同一个锁对象上,自旋等待 刚刚成功获得过锁,并 且持有锁的线程正在运行中,那么虚拟机就会认为这次自 旋也是很有可能再次成功,进而它将允许自旋等待持续相 对更长的时间。
7、BIO、NIO、AIO 分别是怎样的
- BIO:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
- NIO:同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
- AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理.AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
IO和NIO的区别
一. IO是面向流的,NIO是面向缓冲区的。
二. IO的各种流是阻塞的,NIO是非阻塞模式。
三. Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
8、jvm内存模型是怎样的,有哪些垃圾回收算法
标记存活对象:
- 引用计数法:给一个对象添加一个引用计数器,当一个地方引用它时,计算器+1,不引用的时候-1,当引用计数器为0时说明该对象可回收。但是一旦出现互相引用的情况,就会出现无法回收的现象。所以JVM采用的是可达性分析算法。
- 可达性分析算法:首先会标记所有GC root能够直接关联的对象。GC root能够直接关联的对象包含:Java虚拟机栈中引用的对象,本地方法栈中引用的对象,方法区中静态变量引用的对象和常量池中引用的对象。然后,再进行GC root 的tracing,标记GC root间接关联的对象。
垃圾回收算法主要有三种:标记清除、标记整理、标记复制
- 标记清除:标记存活的对象,然后将垃圾对象直接清除,优点是清除效率高,缺点是内存碎片多
- 标记整理:标记存活的对象,然后将存活的地方移动到一个连续的区域,将该区域外的对象全部清除。缺点是需要移动对象,清除效率比标记清除低。优点是不会产生内存碎片。
- 标记复制:将内存区域分成两个部分,标记存活的对象,将存活的对象复制到另外一个区域,然后将本区域全部清除。缺点是空间利用率不高
总结:
JVM垃圾回收机制采用的分代回收,新生代的垃圾回收采用的是标记复制算法,老年代的垃圾回收采用的是标记清除或者标记整理算法。
大多数情况下,对象在新生代Eden区上进行分配,大对象则直接分配到老年代。当Eden区空间不够时,则需要发起Minor GC清理垃圾对象。当对象经过Minor GC依然存活,将移动到Survivor中,年龄+1,增加到一定年龄则移动到老年代中。如果在survivor区中相同年龄的所有对象大小大于survivor空间的一半,则大于或等于该年龄的对象直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。当老年代空间不足时会进行Full GC。
9、如何判断一个对象是否存活
方法:其实就是8题中的标记算法
注意:
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中。并在稍后由一个虚拟机自动建立的,低优先级的Finalizer线程去执行它。这里所谓“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果有一个对象在finalize()方法中执行缓慢,或者发生死循环,将可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象这个时候,未被重新引用,那它基本上就真的被回收了。回收方法区
Java虚拟机规范中确实说过可以不要求虚拟机在方法区中实现垃圾回收,而且在方法区中进行垃圾回收的“性价比”一般比较低,方法区的垃圾收集主要回收两部分内容:废弃的常量和无用的类。
废弃的常量,以常量池中字面量的回收为例,假如一个字符串“abc”已经进入常量池中,但是当前系统已经没有任何一个String对象叫做“abc”的,也没有任何其他地方引用这个字面量,这个“abc”常量就会被清理出常量池。
判断一个无用的类需要同时满足下面3个条件才能算是“无用的类”
- 该类的所有实例都已经被回收
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象已经没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
10、CMS的优点是什么,缺点是什么,和G1有什么区别
CMS收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,CMS收集器是基于“”标记–清除”(Mark-Sweep)算法实现的,整个过程分为四个步骤:
1、初始标记 (Stop the World事件 CPU停顿, 很短)
初始标记仅标记一下GC Roots能直接关联到的对象,速度很快。
在Java语言中,可作为GC Roots的对象包括4种情况:
a) 虚拟机栈中引用的对象(栈帧中的本地变量表);
b) 方法区中类静态属性引用的对象;
c) 方法区中常量引用的对象;
d) 本地方法栈中JNI(Native方法)引用的对象。
2、 重新标记 (Stop the World事件 CPU停顿,比初始标记稍微长,远比并发标记短)
修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记时间短。
3、并发标记 (收集垃圾跟用户线程一起执行)
初始标记和重新标记任然需要“stop the world”,并发标记过程就是进行GC Roots Tracing的过程。
4、并发清理 -清除算法
整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS是一款优秀的收集器,它有优点,但也有明显的缺点:
优点:
并发收集,低停顿
理由:由于在整个过程和中最耗时的并发标记和 并发清除过程收集器程序都可以和用户线程一起工作,所以总体来说,Cms收集器的内存回收过程是与用户线程一起并发执行的。
缺点:
1、CMS收集器对CPU资源非常敏感
在并发阶段,虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢,总吞吐量会降低,为了解决这种情况,虚拟机提供了一种“增量式并发收集器” 的CMS收集器变种, 就是在并发标记和并发清除的时候让GC线程和用户线程交替运行,尽量减少GC 线程独占资源的时间,这样整个垃圾收集的过程会变长,但是对用户程序的影响会减少。(效果不明显,不推荐)
2、 CMS处理器无法处理浮动垃圾
CMS在并发清理阶段线程还在运行, 伴随着程序的运行自然也会产生新的垃圾,这一部分垃圾产生在标记过程之后,CMS无法再当次过程中处理,所以只有等到下次gc时候在清理掉,这一部分垃圾就称作“浮动垃圾” 。
3、CMS是基于“标记–清除”算法实现的,所以在收集结束的时候会有大量的空间碎片产生。
空间碎片太多的时候,将会给大对象的分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象的,只能提前触发 full gc。
为了解决这个问题,CMS提供了一个开关参数,用于在CMS顶不住要进行full gc的时候开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片没有了,但是停顿的时间变长了。
G1收集器
G1(Garbage First)是一款面向服务端应用的垃圾收集器。
G1运作步骤:
1、初始标记(stop the world事件 CPU停顿只处理垃圾);
2、并发标记(与用户线程并发执行);
3、最终标记(stop the world事件 ,CPU停顿处理垃圾);
4、筛选回收(stop the world事件 根据用户期望的GC停顿时间回收)
注意:CMS 在这一步不需要stop the world
与其他GC收集器相比,G1具备如下特点:
1、并行于并发。
G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
2、分代收集。
虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。
3、空间整合。
与CMS的“标记–清理”算法不同**,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的**。
4、可预测的停顿。
这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。
上面几个步骤的运作过程和CMS有很多相似之处:
初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象。这一阶段需要停顿线程,但是耗时很短。
并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段时耗时较长,但可与用户程序并发执行。
而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,需要把Remembered Set Logs的数据合并到Remembered Set Logs里面,这一阶段需要停顿线程,但是可并行执行。
最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
CMS收集器和G1收集器的区别
区别一: 使用范围不一样
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用区别二: STW的时间
CMS收集器以最小的停顿时间为目标的收集器。
G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)
区别三: 垃圾碎片
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
区别四: 垃圾回收的过程不一样
三、计算机网络基础
1、TCP,UDP区别,TCP为什么是三次握手和四次挥手
参照博客1:https://www.pianshen.com/article/2872927776/
参照博客2::https://www.nowcoder.com/discuss/568071?channel=-1&source_id=profile_follow_post_nctrack
2、一次http请求的全部过程是怎样的
转载博客:https://ww.jianshu.com/p/2f6705a16a20
下面详细介绍一下每一步
1、TCP建立连接
HTTP协议是基于TCP协议来实现的,因此首先就是要通过TCP三次握手与服务器端建立连接,一般HTTP默认的端口号为80;
2、浏览器发送请求命令
在与服务器建立连接后,Web浏览器会想服务器发送请求命令
3、浏览器发送请求头消息
在浏览器发送请求命令后,还会发送一些其它信息,最后以一行空白内容告知服务器已经完成头信息的发送;
4、服务器应答
在收到浏览器发送的请求后,服务器会对其进行回应,应答的第一部分是协议的版本号和应答状态码;
5、服务器回应头信息
与浏览器端同理,服务器端也会将自身的信息发送一份至浏览器
6、服务器发送数据
在完成所有应答后,会以Content-Type应答头信息所描述的格式发送用户所需求的数据信息
7、断开TCP连接
在完成此次数据通信后,服务器会通过TCP四次挥手主动断开连接。但若此次连接为长连接,那么浏览器或服务器的头信息会加入keep-alive的信息,会保持此连接状态,在有其它数据发送时,可以节省建立连接的时间;
3、TCP的流量控制、拥塞控制是如何实现的
参考博客:https://blog.csdn.net/yechaodechuntian/article/details/25429143
4、TCP的粘包拆包是怎么回事
参考博客:https://www.cnblogs.com/panchanggui/p/9518735.html
5、http和https有什么区别,http1.0和1.1的区别,get和post的区别
参考博客:https://www.nowcoder.com/discuss/613239?channel=-1&source_id=profile_follow_post_nctrack
四、框架和原理
1、Aop、IOC的原理,解决了什么问题
1、Spring IOC
IoC(Inverse of Control:控制反转)是⼀种设计思想,就是 将原本在程序中⼿动创建对象的控制
权,交由Spring框架来管理。 IoC 在其他语⾔中也有应⽤,并⾮ Spring 特有。IoC 容器是 Spring⽤来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注⼊。这样可以很⼤程度上简化应⽤的开发,把应⽤从复杂的依赖关系中解放出来。 IoC 容器就像是⼀个⼯⼚⼀样,当我们需要创建⼀个对象的时候,只需要配置好配置⽂件/注解即可,完全不⽤考虑对象是如何被创建出来的。
2、Spring AOP,动态代理
AOP(Aspect-Oriented Programming:⾯向切⾯编程)能够将那些与业务⽆关,却为业务模块所共同调⽤
的逻辑或责任(例如事务处理、⽇志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模
块间的耦合度,并有利于未来的可拓展性和可维护性。Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接⼝,那么Spring AOP会使⽤JDK
Proxy,去创建代理对象,⽽对于没有实现接⼝的对象,就⽆法使⽤ JDK Proxy 去进⾏代理了,这时候
Spring AOP会使⽤Cglib ,这时候Spring AOP会使⽤ Cglib ⽣成⼀个被代理对象的⼦类来作为代理。更多详情参考博客:https://blog.csdn.net/singwhatiwanna/article/details/106184348
2、spring是如何解决循环依赖的
参考博客:https://blog.csdn.net/qq_36381855/article/details/79752689
参考博客:https://www.jianshu.com/p/8bb67ca11831
3、Mysql的四大特性和四大隔离级别
参考博客:https://www.cnblogs.com/lmj612/p/10579475.html
四大特性:原子性、一致性、隔离性(脏读 幻读 可重复读)、永久性
隔离级别:
- Serializable (串行化):可避免脏读、不可重复读、幻读的发生
- Repeatable read (可重复读):可避免脏读、不可重复读的发生
- Read committed (读已提交):可避免脏读的发生
- Read uncommitted (读未提交):最低级别,任何情况都无法保证
以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,执行效率就越低。
像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待)
所以平时选用何种隔离级别应该根据实际情况。
4、Mysql底层数据结构有哪些,为什么要用这样的结构
简单来说就是围绕 B-树、B+树、innodb引擎、索引优化…太多了(自己得熟悉)
5、Redis的持久化机制是怎样的
简单概述:RDB和AOF
参考博客:http://blog.sina.com.cn/s/blog_e529ce510102xcg4.html
6、消息中间件有哪些,各有怎样的特性,如何实现这些特性的?
参考博客:https://blog.csdn.net/wqc19920906/article/details/82193316/
本人只粗略了解过rabbitmq,也只能围绕它来说了。
7、mvcc机制是怎样的
参考博客:https://www.cnblogs.com/luchangyou/p/11321607.html
1.1 什么是MVCC
MVCC(Multiversion concurrency control )是一种多版本并发控制机制。
1.2 MVCC是为了解决什么问题?
并发访问(读或写)数据库时,对正在事务内处理的数据做多版本的管理。以达到用来避免写操作的堵塞,从而引发读操作的并发问题。
大家都应该知道,锁机制可以控制并发操作,但是其系统开销较大,而MVCC可以在大多数情况下代替行级锁,使用MVCC,能降低其系统开销。1.3 MVCC实现
MVCC是通过保存数据在某个时间点的快照来实现的。不同存储引擎的MVCC实现是不同的,典型的有乐观并发控制和悲观并发控制。当我们创建表完成后,mysql会自动为每个表添加 数据版本号(最后更新数据的事务id)db_trx_id 删除版本号 db_roll_pt (数据删除的事务id) 事务id由mysql数据库自动生成,且递增。
8、Redis缓存雪崩、击穿、穿透分别是哪些场景,如何应对
参考博客:https://www.nowcoder.com/discuss/472041?channel=-1&source_id=profile_follow_post_nctrack
缓存雪崩和缓存穿透,以及解决方法
【1】缓存雪崩:
指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
1)缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2)一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
3)给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存。
【2】缓存穿透:
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
1)接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
2)从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击;
3)采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。
【3】缓存击穿:
这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库
解决方案:
1)设置热点数据永远不过期
2)加互斥锁,互斥锁
9、如何保证Redis和数据库的一致性
方式一:
读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
方式二:
先更新数据库,假如读缓存失败,先读数据库,再回写缓存的方式实现
10、spring boot 如何做到开箱即用的
参考博客:https://blog.csdn.net/m0_48246547/article/details/107644349
五、项目(因人而异,自己总结)
1、项目的价值
2、项目解决了什么问题
3、项目的痛点、难点、亮点。(多个角度用户场景、运维监控、故障排查、业务增长)
4、项目取得了什么成果
5、项目的瓶颈和优化点。
六、面试过程中的重点
1、对java &框架&中间件等技术原理的掌握。
2、面试沟通过程中,思路和逻辑是否非常清晰,能否根据自己的知识体系举一反三。
3、对自己项目是否有充分的挖掘和认知。
据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库
解决方案:
1)设置热点数据永远不过期
2)加互斥锁,互斥锁
9、如何保证Redis和数据库的一致性
方式一:
读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
方式二:
先更新数据库,假如读缓存失败,先读数据库,再回写缓存的方式实现
10、spring boot 如何做到开箱即用的
参考博客:https://blog.csdn.net/m0_48246547/article/details/107644349