目录
1.你觉得 Java 的好处是什么?
2.如何设计一个 HashMap?
3.并发类库提供的线程池实现有哪些?
1.newFixedThreadPool
2.newWorkStealingPool
3.newSingleThreadExecutor
4.newCachedThreadPool
5. newScheduledThreadPool
4.让你设计一个线程池如何设计?
5. GC 如何调优?
6.动态代理是什么?
7.JDK 动态代理与 CGLIB 区别?
8.注解是什么原理?
9.反射是什么?
10.能说下类加载过程吗?
11.什么是双亲委派?
13.用过哪些 JDK 提供的工具?
14.接口和抽象类有什么区别?
15.什么是序列化?什么是反序列化?
16.什么是不可变类?
17.Java 按值传递还是按引用传递?
18.泛型有什么用?泛型擦除是什么?
19.说说强、软、弱、虚引用?
21.Exception 和 Error 的区别知道吗?
22.什么是内部类,有什么用?
23.说说 Java 的集合类吧?
24.同步、异步、阻塞、非阻塞 IO 的区别?
25.String,Stringbuffer,StringBuilder的区别?
26.final 和可以保证可见性吗?
1.你觉得 Java 的好处是什么?
我觉得可以从跨平台、垃圾回收、生态三个方面来阐述。
首先 Java 是跨平台的,不同平台执行的机器码不一样,Java 加了一层中间层 JVM ,可以做到一次编写多平台运行,即 「Write once,Run anywhere」。
编译执行过程是先把 Java 源代码编译成字节码,字节码再由 JVM 解释或 JIT 编译执行,JIT 编译时需要预热的,提供了 AOT(Ahead-of-Time Compilation),可以直接把字节码转成机器码,(解释执行比编译执行效率差)
Java 还提供垃圾自动回收功能,虽说手动管理内存意味着自由、精细化地掌控,但是很容易出错。在内存较充裕的当下,将内存的管理交给 GC 来做,减轻了程序员编程的负担,提升了开发效率,更加划算!
2.如何设计一个 HashMap?
可以从 HashMap 的一些关键点入手,hash函数、如何处理冲突、如何扩容。
1.先说下你对 HashMap 的理解。
HashMap 无非就是一个存储
基本原理:就是将 key 经过 hash 函数进行散列得到散列值,然后通过散列值对数组取模找到对应的 index 。hash 函数很关键,不仅运算要快,还需要分布均匀,减少 hash 碰撞。
输入值是无限的,而数组的大小是有限的所以肯定会有碰撞,因此可以采用拉链法来处理冲突。为了避免恶意的 hash 攻击,当拉链超过一定长度之后可以转为红黑树结构。
超过一定的结点还是需要扩容的,不然碰撞就太严重了。
而普通的扩容会导致某次 put 延时较大,特别是 HashMap 存储的数据比较多的时候,所以可以考虑和 redis 那样搞两个 table 延迟移动,一次可以只移动一部分。
使用之前预估准数据大小,避免频繁的扩容。
3.并发类库提供的线程池实现有哪些?
线程池有几个关键的配置:核心线程数、最大线程数、空闲存活时间、工作队列、拒绝策略。
- 默认情况下线程不会预创建,一般来任务之后才会创建线程(设置prestartAllCoreThreads可以预创建核心线程)。
- 当核心线程满了之后不会新建线程,而是把任务堆积到工作队列中。
- 如果工作队列放不下了,然后才会新增线程,直至达到最大线程数。
- 如果工作队列满了,然后也已经达到最大线程数了,这时候来任务会执行拒绝策略。
- 如果线程空闲时间超过空闲存活时间,并且线程线程数是大于核心线程数的则会销毁线程,直到线程数等于核心线程数(设置allowCoreThreadTimeOut 可以回收核心线程)。
实现指的就是 Executors 的 5 个静态工厂方法:
- newFixedThreadPool
- newWorkStealingPool
- newSingleThreadExecutor
- newCachedThreadPool
- newScheduledThreadPool
1.newFixedThreadPool
特点:核心线程数和最大线程数是一致的, keepAliveTime 的时间是 0 ,队列是无界队列。
可能出现 OOM 的现象,因为队列是无界的,所以任务可能挤爆内存。
2.newWorkStealingPool
特性:Stealing,会窃取任务。
每个线程都有自己的双端队列,当自己队列的任务处理完毕之后,会去别的线程的任务队列尾部拿任务来执行,加快任务的执行速率。
3.newSingleThreadExecutor
一个线程池就一个线程,一个人一座城,配备的也是无界队列。
它的特性就是能保证任务是按顺序执行的。
4.newCachedThreadPool
核心线程数是 0 ,最大线程数看作无限,任务队列是没有存储空间的
cached 意思就是会缓存之前执行过的线程,缓存时间是 60 秒
适合用在短时间内有大量短任务的场景。如果暂无可用线程,那么来个任务就会新启一个线程去执行这个任务,快速响应任务。
5. newScheduledThreadPool
定时执行任务,重点就是那个延时队列。
Java 的几个定时任务调度相关的:Timer、DelayQueue 和 ScheduledThreadPool
4.让你设计一个线程池如何设计?
线程池就是存储线程的一个容器,池内保存之前建立过的线程来重复执行任务,减少创建和销毁线程的开销,提高任务的响应速度,并便于线程的管理。
设计一个线程池考虑:池内工作 : 线程的管理、任务编排执行、线程池超负荷处理方案、监控。
初始化线程数、核心线程数、最大线程池都暴露出来可配置,包括超过核心线程数的线程空闲消亡配置。
任务的存储结构可配置,可以是无界队列也可以是有界队列,也可以根据配置分多个队列来分配不同优先级的任务,也可以采用 stealing 的机制来提高线程的利用率。
我这个不是标准答案,仅供参考。
5. GC 如何调优?
GC 调优的核心思路就是尽可能的使对象在年轻代被回收,减少对象进入老年代。
具体调优还是得看场景根据 GC 日志具体分析,常见的需要关注的指标是 Young GC 和 Full GC 触发频率、原因、晋升的速率 、老年代内存占用量等等。
可能是晋升年龄设置的太小,也有可能分析日志之后发现是内存泄漏、或者有第三方类库调用了 System.gc等等。
6.动态代理是什么?
动态代理就是一个代理机制,动态是相对于静态来说的。动态代理有 JDK 动态代理 和 CGLIB。
代理可以看作是调用目标的一个包装,通常用来在调用真实的目标之前进行一些逻辑处理,消除一些重复的代码。
静态代理指的是我们预先编码好一个代理类,而动态代理指的是运行时生成代理类。
动态更加方便,可以指定一系列目标来动态生成代理类(AOP),而不像静态代理需要为每个目标类写对应的代理类。
7.JDK 动态代理与 CGLIB 区别?
JDK 动态代理是基于接口的,要求代理类一定是有定义接口的。
CGLIB 基于ASM字节码生成工具,它是通过继承的方式来实现代理类, final 方法。
JDK 动态代理实现原理:
- 首先通过实现 InvocationHandler 接口得到一个切面类。
- 然后利用 Proxy 根据目标类的类加载器、接口和切面类得到一个代理类。
- 代理类的逻辑就是把所有接口方法的调用转发到切面类的 invoke() 方法上,然后根据反射调用目标类的方法。
CGLIB和 JDK 动态代理的实现逻辑是一致,只是实现方式不同。
Enhancer en = new Enhancer();
//2.设置父类,也就是代理目标类,上面提到了它是通过生成子类的方式
en.setSuperclass(target.getClass());
//3.设置回调函数,这个this其实就是代理逻辑实现类,也就是切面,可以理解为JDK 动态代理的handler
en.setCallback(this);
//4.创建代理对象,也就是目标类的子类了。
return en.create();
8.注解是什么原理?
注解其实就是一个标记,可以标记在类上、方法上、属性上等,标记自身也可以设置一些值。
有了标记之后,就可以在解析的时候得到这个标记,然后做一些特别的处理,这就是注解的用处。
注解生命周期有三大类:
- RetentionPolicy.SOURCE:给编译器用的,不会写入 class 文件
- RetentionPolicy.CLASS:会写入 class 文件,在类加载阶段丢弃,也就是运行的时候就没这个信息了
- RetentionPolicy.RUNTIME:会写入 class 文件,永久保存,可以通过反射获取注解信息
9.反射是什么?
反射就是Java提供的能在运行期可以得到对象信息的能力,包括属性、方法、注解等,也可以调用其方法。
一般的编码不会用到反射,在框架上用的较多,因为很多场景需要很灵活,所以不确定目标对象的类型,届时只能通过反射动态获取对象信息。
10.能说下类加载过程吗?
类加载顾名思义就是把类加载到 JVM 中,而输入一段二进制流到内存,之后经过一番解析、处理转化成可用的 class 类,这就是类加载要做的事情。
二进制流可以来源于 class 文件,或者通过字节码工具生成的字节码或者来自于网络都行,只要符合格式的二进制流,JVM 来者不拒。
类加载流程分为加载、连接、初始化三个阶段,连接拆分为:验证、准备、解析三个阶段。
可以分为 5 个阶段:
- 加载:将二进制流搞到内存中来,生成一个 Class 类。
- 验证:主要是验证加载进来的二进制流是否符合一定格式,是否规范,是否符合当前 JVM 版本等等之类的验证。
- 准备:为静态变量(类变量)赋初始值,也即为它们在方法区划分内存空间。这里注意是静态变量,并且是初始值,比如 int 的初始值是 0。
- 解析:将常量池的符号引用转化成直接引用。
解析就是把 A(符号引用) 替换成小明(直接引用)。符号引用就是一个字面量,没有什么实质性的意义,只是一个代表。直接引用指的是一个真实引用,在内存中可以通过这个引用查找到目标。 - 初始化:这时候就执行一些静态代码块,为静态变量赋值,这里的赋值才是代码里面的赋值,准备阶段只是设置初始值占个坑。
11.什么是双亲委派?
双亲委派的意思是:
一个类加载器需要加载类,它会把这个类加载请求委派给父类加载器去完成,如果父类还有父类则接着委托,每一层都是如此。
一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。
双亲其实就指的是父类,没有mother。
双亲委派模型不是一种强制性约束,也就是你不这么做也不会报错怎样的,它是一种JAVA设计者推荐使用类加载器的方式。
13.用过哪些 JDK 提供的工具?
- jps:虚拟机进程状况工具
- jstat:虚拟机统计信息监视工具
- jmap:Java内存映像工具
- jhat:虚拟机堆转储快照分析工具
- jstack:Java堆栈跟踪工具
- jinfo:Java配置信息工具
- VisualVM:图形化工具,可以得到虚拟机运行时的一些信息:内存分析、CPU 分析等等,在 jdk9 开始不再默认打包进 jdk 中。
14.接口和抽象类有什么区别?
接口:只能包含抽象方法,不能包含成员变量,当 has a 的情况下用接口。
接口是对行为的抽象,类似于条约。在 Java 中接口可以多实现,从 has a 角度来说接口先行,也就是先约定接口,再实现。
抽象类: 可以包含成员变量和一般方法和抽象方法,当 is a 并且主要用于代码复用的场景下使用抽象类继承的方式,子类必须实现抽象类中的抽象方法。
在 Java 中只支持单继承。从 is a 角度来看一般都是先写,然后发现代码能复用,然后抽象一个抽象类。
15.什么是序列化?什么是反序列化?
序列化其实就是将对象转化成可传输的字节序列格式,以便于存储和传输。
反序列化就是将字节序列格式转换成对象的过程。
16.什么是不可变类?
不可变类指的是无法修改对象的值,String 就是典型的不可变类,当你创建一个 String 对象之后,这个对象就无法被修改。
才会有不要在字符串拼接频繁的场景不要使用 + 来拼接,因为这样会频繁的创建对象。
不可变类安全,因为知晓这个对象不可能会被修改,因此可以放心大胆的用,在多线程环境下也是线程安全的。
如何实现一个不可变类?
String 类用 final 修饰,表示无法被继承。
String 本质是一个 char 数组,然后用 final 修饰,不过 final 限制不了数组内部的数据,所以这还不够。
所以 value 是用 private 修饰的,并且没有暴露出 set 方法,这样外部其实就接触不到 value 所以无法修改。
17.Java 按值传递还是按引用传递?
Java 只有按值传递,不论是基本类型还是引用类型。
JVM 内存有划分为栈和堆,局部变量和方法参数是在栈上分配的,基本类型和引用类型都占 4 个字节,当然 long 和 double 占 8 个字节。
18.泛型有什么用?泛型擦除是什么?
泛型可以把类型当作参数一样传递,使得像一些集合类可以明确存储的对象类型,不用显示地强制转化(在没泛型之前只能是Object,然后强转)。
并且在编译期能识别类型,类型错误则会提醒,增加程序的健壮性和可读性。
泛型擦除指的指参数类型其实在编译之后就被抹去了,也就是生成的 class 文件是没有泛型信息的,所以称之为擦除。
19.说说强、软、弱、虚引用?
Java 根据其生命周期的长短将引用类型又分为强引用、软引用、弱引用、幻象引用。
- 强引用:就是我们平时 new 一个对象的引用。当 JVM 的内存空间不足时,宁愿抛出 OutOfMemoryError 使得程序异常终止,也不愿意回收具有强引用的存活着的对象。
- 软引用:生命周期比强引用短,当 JVM 认为内存空间不足时,会试图回收软引用指向的对象,也就是说在 JVM 抛出 OutOfMemoryError 之前,会去清理软引用对象,适合用在内存敏感的场景。
- 弱引用:比软引用还短,在 GC 的时候,不管内存空间足不足都会回收这个对象,ThreadLocal中的 key 就用到了弱引用,适合用在内存敏感的场景。 -虚引用:也称幻象引用,之所以这样叫是因为虚引用的 get 永远都是 null ,称为get 了个寂寞,所以叫虚。
21.Exception 和 Error 的区别知道吗?
Exception 是程序正常运行过程中可以预料到的意外情况,应该被开发者捕获并且进行相应的处理。
Error 是指在正常情况下不太可能出现的情况,绝大部分的 Error 都会导致程序处于不正常、不可恢复的状态,也就是挂了。
Exception和Error都是继承了Throwable类,在Java代码中只有继承了Throwable类的实例才可以被throw或者被catch。
22.什么是内部类,有什么用?
内部类顾名思义就是定义在一个类的内部的类,按位置分:在成员变量的位置定义,则是成员内部类,在方法内定义,则是局部内部类。
如果用 static 修饰则为静态内部类,还有匿名内部类。
一般而言只会用成员内部类、静态内部类和匿名内部类。
在 JVM 中是没有内部类的概念的。
23.说说 Java 的集合类吧?
常用的集合有 List、Set、Map、Queue 等。
List 常见实现类有 ArrayList 和 LinkedList。
- ArrayList 基于动态数组实现,支持下标随机访问,对删除不友好。
- LinkedList 基于双向链表实现,不支持随机访问,只能顺序遍历,但是支持O(1)插入和删除元素。
Set 常见实现类有:HashSet、TreeSet、LinkedHashSet。
- HashSet 其实就是 HashMap 包了层马甲,支持 O(1)查询,无序。
- TreeSet 基于红黑树实现,支持范围查询,不过基于红黑树的查找时间复杂度是O(lgn),有序。
- LinkedHashSet,比 HashSet 多了个双向链表,通过链表保证有序。
Map 常见实现类有:HashMap、TreeMap、LinkedHashMap
- HashMap:基于哈希表实现,支持 O(1) 查询,无序。
- TreeMap:基于红黑树实现,O(lgn)查询,有序。
- LinkedHashMap:同样也是多了双向链表,支持有序,可以很好的支持 lru 的实现。
24.同步、异步、阻塞、非阻塞 IO 的区别?
Linux 系统分了:用户态和内核态。
磁盘(网卡)读数据的两个步骤:
- 数据从存储设备拷贝到内核缓存
- 数据再从内核缓存拷贝到用户空间
- 同步I/O:指的是线程需要等待 2 执行完毕。
- 异步I/O:指的是线程不需要等待 2 执行。
- 阻塞I/O:指的是步骤 1 会阻塞,即线程需要阻塞等待 1 执行完毕。
- 非阻塞I/O:指的是步骤 1 不会被阻塞,不需要阻塞等待。
阻塞和非阻塞:一般用在底层系统调用身上,阻塞指的是线程未满足条件会被阻塞,进入 sleep 状态,即时间片还未到就让出 CPU,非阻塞则是计算未满足条件也直接返回。
25.String,Stringbuffer,StringBuilder的区别?
String 是 Java 中基础且重要的类,并且 String 也是 Immutable 类的典型实现,被声明为 final class,除了 hash 这个属性其它属性都声明为 final。
不可变性,所以例如拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。
StringBuffer 就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类,提供 append 和 add 方法,可以将字符串添加到已有序列的末尾或指定位置。
它的本质是一个线程安全的可修改的字符序列,把所有修改数据的方法都加上了 synchronized。但是保证了线程安全是需要性能的代价的。
StringBuffer 和 StringBuilder 二者都继承了 AbstractStringBuilder ,底层都是利用可修改的 char 数组(JDK 9 以后是 byte 数组)。
26.final 和可以保证可见性吗?
不可以, final 无法保证可见性!
- 在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。 初次读一个包含
- final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序。



