Java最新面试题汇总

文章篇幅过长(目前有13万字),资料收集不容易呀,大家多点点赞啦!!!。文章会持续更新,文章有什么错误的地方,欢迎在评论区留言,我会第一时间处理。如果大家有新的面试题,也可以在评论区留言,合理的,我也会第一时间加上去。

目录

  • Java最新面试题汇总
    • 一、Java基础篇
      • 1.1 Java语言有哪些特点
      • 1.2 面向对象和面向过程的区别
      • 1.3 基本数据类型
      • 1.4 instanceof关键字的作用
      • 1.5 Java自动装箱与拆箱
      • 1.6 重载和重写的区别
      • 1.7 equals与==的区别
      • 1.8 String、StringBuffer、StringBuilder的区别
      • 1.9 ArrayList和LinkedList的区别
      • 1.10 HashMap和HashTable的区别
      • 1.11 Collection和Collections的区别
      • 1.12 Java的四种引用,强弱软虚
      • 1.13 泛型常用特点
      • 1.14 Java创建对象的几种方式
      • 1.15 深拷贝和浅拷贝
      • 1.16 final有哪些用法
      • 1.17 try catch finally,try里有return,finally还执行么?
      • 1.18 进程与线程的区别?
      • 1.19 Java序列化中如果有些字段不能序列化,如何处理?
      • 1.20 Java中IO流
      • 1.21 Java反射
      • 1.22 List、Set、Map三者的区别
      • 1.23 HashMap 中的 key 我们可以使用任何类作为 key 吗?
      • 1.24 HashMap 的长度为什么是 2 的 N 次方呢?
      • 1.25 HashMap 与 ConcurrentHashMap 的异同
      • 1.26 红黑树有哪些特征?
      • 1.27 面向对象的特征有哪些?
      • 1.28 访问修饰符
      • 1.29 float f=3.4;是否正确?
      • 1.30 Math.round(11.5) 等于多少?Math.round(-11.5)等于多少?
      • 1.31 switch 是否能作用在byte 上,是否能作用在long 上,是否能作用在String 上?
      • 1.32 用最有效率的方法计算2 乘以8?
      • 1.33 数组有没有length()方法?String 有没有length()方法?
      • 1.34 抽象类(abstract class)和接口(interface)有什么异同?
      • 1.35 线程有哪些状态?
      • 1.36 sleep() 和 wait() 有什么区别?
      • 1.37 notify()和 notifyAll()有什么区别?
      • 1.38 线程的 run() 和 start() 有什么区别?
      • 1.39 创建线程池有哪些方式?
      • 1.40 线程池中 submit() 和 execute() 方法有什么区别?
      • 1.41 Java死锁如何避免?
      • 1.42 ThreadLocal是什么?
      • 1.43 synchronized底层原理
      • 1.44 synchronized 和 volatile 的区别?
      • 1.45 synchronized 和 Lock 的区别?
      • 1.46 synchronized 和 ReentrantLock 的区别?
      • 1.47 什么是Callable和Future?
    • 二、JVM篇
      • 2.1 JVM知识点汇总
      • 2.2 JVM内存模型
      • 2.3 类的加载与卸载
      • 2.4 加载机制,双亲委派模型
      • 2.5 JVM垃圾判断算法
      • 2.6 JVM垃圾回收算法
      • 2.7 JVM垃圾回收器
      • 2.8 什么时候会触发FullGC?
      • 2.9 什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?
      • 2.10 说说对象分配规则
      • 2.11 对象一定分配在堆中吗?有没有了解逃逸分析技术?
      • 2.12 什么是Stop The World ? 什么是OopMap?什么是安全点?
      • 2.13 简述Java的对象结构
      • 2.14 什么是指针碰撞?
      • 2.15 什么是空闲列表?
      • 2.16 什么是TLAB?
      • 2.17 JVM里的有几种classloader,为什么会有多种?
    • 三、多线程与并发编程篇
      • 3.1 有三个线程T1,T2,T3,如何保证顺序执行?
      • 3.2 什么是线程安全
      • 3.3 说一下线程之间是如何通信的?
      • 3.4 CAS的原理呢?
      • 3.5 什么是AQS?
      • 3.6 了解Semaphore吗?
      • 3.7 什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?
      • 3.8 什么是多线程中的上下文切换?
      • 3.9 什么是Daemon线程?它有什么意义?
      • 3.10 乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
      • 3.11 Java 中用到的线程调度算法是什么?
      • 3.12 SynchronizedMap 和ConcurrentHashMap 有什么区别?
      • 3.13 CopyOnWriteArrayList 可以用于什么应用场景?
      • 3.14 你对线程优先级的理解是什么?
      • 3.15 什么是线程调度器(Thread Scheduler)和时间分片(TimeSlicing )?
      • 3.16 并发编程三要素
      • 3.17 线程池原理
    • 四、JMM(Java内存模型)篇
      • 4.1 JMM概念
      • 4.2 JSR-133(Java内存模型与线程规范)
      • 4.3 JMM内存模型
      • 4.4 JMM三大特性
      • 4.5 JMM数据原子操作
      • 4.6 缓存一致性协议(MESI)
      • 4.7 volatile缓存可见性
      • 4.8 指令重排序
      • 4.9 内存屏障
    • 五、Spring篇
      • 5.1 Spring是什么?
      • 5.2 Spring有哪些特点?
      • 5.3 谈谈你多AOP的理解
      • 5.4 谈谈你对IOC的理解
      • 5.5 依赖注入的三种方式
      • 5.6 AOP核心概念
      • 5.7 AOP两种代理方式
      • 5.8 @Autowired和@Resource的区别?
      • 5.9 Spring中bean的生命周期
      • 5.10 什么是MVC?
      • 5.11 SpringMVC执行流程
      • 5.12 Spring支持的几种bean的作用域
      • 5.13 Spring基于xml注入bean的几种方式?
      • 5.14 Spring用到了那些设计模式
      • 5.15 Spring 中的单例 Bean 是线程安全的么?
      • 5.16 Spring中什么时候@Transactional会失效
      • 5.17 Spring容器启动流程
      • 5.18 单例Bean和单例模式
      • 5.19 Spring中的事务是如何实现的
      • 5.20 Spring这么解决循环依赖的?
    • 六、Mybatis篇
      • 6.1 什么是Mybatis?
      • 6.2 MyBatis的优点和缺点
      • 6.3 #{}和${}的区别是什么?
      • 6.4 Mybatis是如何进行分页的?分页插件的原理是什么?
      • 6.5 Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?
      • 6.6 Xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签?
      • 6.7 Mybatis的一级、二级缓存
      • 6.8 通常一个Xml 映射文件,都会写一个Dao 接口与之对应,这个Dao 接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?
      • 6.9 Mybatis的缓存机制
      • 6.10 Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?
      • 6.11 MyBatis与Hibernate有哪些不同?
    • 七、SpringBoot篇
      • 7.1 SpringBoot优点
      • 7.2 Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?
      • 7.3 Spring Boot中的监视器是什么?
      • 7.4 如何使用Spring Boot实现异常处理?
      • 7.5 运行Spring Boot有哪几种方式?
      • 7.6 你如何理解 Spring Boot 中的 Starters?
      • 7.7 Spring Boot 的核心配置文件有哪几个?它们的区别是什么?
      • 7.8 SpringBoot自动配置原理
    • 八、MySQL篇
      • 8.1 概念
      • 8.2 基本数据类型
      • 8.3 innerjoin、leftjoin、rightjoin三者之间的区别
      • 8.4 SQL语句中where与having的区别
      • 8.5 char与varchar的区别
      • 8.6 数据库三大范式
      • 8.7 数据库事务是什么?
      • 8.8 事务的四大特性(ACID)
      • 8.9 事务并发问题
      • 8.10 什么是脏读、幻读和不可重复读?
      • 8.11 事务的隔离级别
      • 8.12 索引数据结构
      • 8.13 索引分类
      • 8.14 为什么建议InnoDB必须建主键?
      • 8.15 为什么推荐使用整型主键?
      • 8.16 Hash索引与B+Tree索引的区别
      • 8.17 为什么使用B+Tree?
      • 8.18 什么是最左匹配原则?
      • 8.19 索引下推
      • 8.20 like语句左边的'%'不会使用索引
      • 8.21 范围条件右边不会使用索引
      • 8.22 负向条件不会使用索引
      • 8.23 在索引列任何操作(函数,计算、表达式)会导致索引失效
      • 8.24 强制类型转换会导致索引失效
      • 8.25 组合索引要匹配最左前缀原则
      • 8.26 减少select*的使用
      • 8.27 优化Group by,使用where子句替换Having子句
      • 8.28 使用union all 替换 union
      • 8.29 优化深度分页的场景:利用延迟关联或者子查询
      • 8.30 锁分类
      • 8.31 表锁
      • 8.32 行锁
      • 8.33 页锁
      • 8.34 共享(读)锁(Share Lock)
      • 8.35 排他(写)锁(Exclusive Lock)
      • 8.36 意向锁(Intention Lock)
      • 8.37 乐观锁
      • 8.38 悲观锁
      • 8.39 MVCC
      • 8.40 隔离级别和锁关系
      • 8.41 日志分类
      • 8.42 重做日志(redo log)
      • 8.43 归档日志(binlog)
      • 8.44 重做日志(redo log)与归档日志(binlog)的区别
      • 8.45 两阶段提交
      • 8.44 MySQL数据库引擎有哪些?
      • 8.45 InnoDB与MyISAM的区别
    • 九、Redis篇
      • 9.1 什么是Redis?
      • 9.2 为什么要用缓存
      • 9.3 Redis优点
      • 9.4 Redis为何使用单线程
      • 9.5 为什么Redis 单线程模型效率也能那么高?
      • 9.6 Redis的线程模型
      • 9.7 Redis 的同步机制
      • 9.8 pipeline 有什么好处,为什么要用 pipeline?
      • 9.9 Redis单线程为什么这么快
      • 9.10 简述Redis事务实现
      • 9.11 Redis 持久化方式
      • 9.12 持久化有两种,那应该怎么选择呢?
      • 9.13 Redis的过期键的删除策略
      • 9.14 Redis 主从复制的核心原理
      • 9.15 Redis有哪些数据结构?分别有哪些典型的应用场景?
      • 9.16 Redis分布式锁底层是如何实现的?
      • 9.17 Redis集群策略
      • 9.18 缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级
      • 9.19 Redis 缓存刷新策略有哪些?
      • 9.20 什么是 bigkey?会存在什么影响?
      • 9.20 说说 Redis 哈希槽的概念?
      • 9.21 假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?
      • 9.22 如果有大量的 key 需要设置同一时间过期,一般需要注意什么?
      • 9.23 什么情况下可能会导致 Redis 阻塞?
      • 9.24 Redis 如何解决 key 冲突?
      • 9.25 Memcache与Redis的区别?
      • 9.26 为什么Redis的操作是原子性的,怎么保证原子性的?
    • 十、Nginx篇
      • 10.1 什么是Nginx,它有什么优势和功能?
      • 10.2 Nginx是如何处理一个HTTP请求的呢?
      • 10.3 Nginx服务器上的Master和Worker进程分别是什么?
      • 10.4 在Nginx中,如何使用未定义的服务器名称来阻止处理请求?
      • 10.5 worker 进程
      • 10.6 正向代理和反向代理
      • 10.7 Nginx应用场景
      • 10.8 Nginx限流怎么做的?
      • 10.9 漏桶流算法和令牌桶算法
      • 10.10 Nginx负载均衡策略
    • 十一、Tomcat篇
      • 11.1 Tomcat的缺省端口是多少,怎么修改?
      • 11.2 tomcat 有哪几种Connector 运行模式(优化)?
      • 11.3 Tomcat有几种部署方式?
      • 11.4 tomcat容器是如何创建servlet类实例?用到了什么原理?
      • 11.5 tomcat 如何优化?
      • 11.6 熟悉tomcat的哪些配置?
      • 11.7 Tomcat是什么?
      • 11.8 什么是Servlet呢?
      • 11.9 什么是Servlet规范?
      • 11.10 为什么我们将tomcat称为Web容器或者Servlet容器 ?
      • 11.11 tomcat是如何处理Http请求流程的?
    • 十二、Zookeeper篇
      • 12.1 Zookeeper 是什么?
      • 12.2 ZooKeeper 有哪些应用场景?
      • 12.3 说说Zookeeper的工作原理?
      • 12.4 Zookeeper 提供了什么?
      • 12.5 Zookeeper 文件系统
      • 12.6 Zookeeper 通知机制
      • 12.7 ZAB 协议?
      • 12.8 Zookeeper 对节点的 watch 监听通知是永久的吗?
      • 12.9 Zookeeper 集群中有哪些角色?
      • 12.10 Zookeeper 集群中Server有哪些工作状态?
      • 12.11 Zookeeper 集群中是怎样选举leader的?
      • 12.12 Zookeeper 是如何保证事务的顺序一致性的呢?
      • 12.13 ZooKeeper 集群中个服务器之间是怎样通信的?
      • 12.14 了解Zookeeper的系统架构吗?
      • 12.15 你知道Zookeeper中有哪些角色?
      • 12.16 熟悉Zookeeper节点ZNode和相关属性吗?
      • 12.17 为什么Zookeeper集群的数目,一般为奇数个?
      • 12.18 Zookeeper监听器原理
      • 12.19 说说Zookeeper中的ACL 权限控制机制
      • 12.20 Zookeeper 有哪几种几种部署模式?
      • 12.21 Zookeeper集群支持动态添加机器吗?
      • 12.22 ZAB 和 Paxos 算法的联系与区别?
      • 12.23 Zookeeper 宕机如何处理?
      • 12.24 描述一下 Zookeeper 的 session 管理的思想?
      • 12.25 Zookeeper 负载均衡和 Nginx 负载均衡有什么区别?
      • 12.26 Zookeeper 的序列化
      • 12.27 Zookeeper 持久化机制
      • 12.28 Zookeeper选举中投票信息的五元组是什么?
      • 12. 29 说说Zookeeper中的脑裂?
      • 12.30 Zookeeper脑裂是什么原因导致的?
      • 12.31 Zookeeper 是如何解决脑裂问题的?
      • 12.32 说说 Zookeeper 的 CAP 问题上做的取舍?
      • 12.33 watch 监听为什么是一次性的?
    • 十三、Dubbo篇
      • 13.1 Dubbo是什么?
      • 13.2 RPC是什么?
      • 13.3 Dubbo有哪些特性?
      • 13.4 Dubbo的负载均衡是怎么实现的?
      • 13.5 Dubbo服务超时怎么设置吧,有什么要注意的吗?
      • 13.6 Dubbo 服务请求流程?
      • 13.7 Dubbo 核心特性(官网)
      • 13.8 Dubbo 负载均衡策略
      • 13.9 Dubbo 容错策略
      • 13.10 Dubbo 动态代理策略有哪些?
      • 13.11 Zookeeper 和 Dubbo 的关系?
    • 十四、MQ篇
      • 14.1 RabbitMQ是什么?
      • 14.2 AMQP是什么?
      • 14.3 RabbitMQ如何保证高可用的?
      • 14.4 如何保证消息的可靠传输?如果消息丢了怎么办
      • 14.5 如何保证消息的顺序性
      • 14.6 如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?
      • 14.7 消息队列有哪些作用?
      • 14.8 死信队列是什么?延时队列是什么?
      • 14.9 如何保证消息的高效读写?
      • 14.10 消息队列如何保证消息可靠传输
      • 14.11 RabbitMQ死信队列、延时队列
      • 14.12 简述RabbitMQ的架构设计
      • 14.13 Kafka是什么?
      • 14.14 Kafka为什么吞吐量高?
      • 14.15 Kafka的Pull和Push分别有什么优缺点?
      • 14.16 Kafka中的ISR、AR又代表什么?ISR的伸缩又指什么?
      • 14.17 Kafka高效文件存储设计特点
      • 14.18 Kafka与传统消息系统之间有三个关键区别
      • 14.19 Kafka创建 Topic 时如何将分区放置到不同的 Broker 中
      • 14.20 Kafka的消费者如何消费数据
      • 14.21 Kafka消费者负载均衡策略
      • 14.22 kafaka⽣产数据时数据的分组策略
      • 14.23 Kafka中是怎么体现消息顺序性的?
      • 14.24 Kafka如何实现延迟队列?
      • 14.25 RocketMQ的实现原理
    • 十五、分布式与微服务篇
      • 15.1 什么是微服务
      • 15.2 为什么需要微服务?
      • 15.3 微服务与单体架构的区别
      • 15.4 微服务与SOA的区别
      • 15.5 微服务本质
      • 15.6 微服务设计原则
      • 15.7 微服务架构的常见概念
      • 15.8 什么是CAP理论
      • 15.9 什么是BASE理论
      • 15.10 数据一致性模型
      • 15.11 分布式ID是什么?有哪些解决方案?
      • 15.12 分布式锁的使用场景是什么?有哪些实现方案?
      • 15.13 什么是分布式事务?有哪些实现方案?
      • 15.14 简述paxos算法
      • 15.15 负载均衡算法有哪些
      • 15.16 如何实现接口的幂等性
      • 15.17 雪花算法原理
      • 15.18 如何解决不使用分区键的查询问题
      • 15.19 Spring Cloud Alibaba

Java最新面试题汇总

一、Java基础篇

1.1 Java语言有哪些特点

  1. 简单易学、有丰富的资源库
  2. 面向对象(Java最重要的特性,让程序耦合度更低,内聚性更高)
  3. 跨平台(JVM是Java跨平台使用的根本)
  4. 可靠安全,支持多线程

1.2 面向对象和面向过程的区别

面向过程: 面向过程是分析解决问题的步骤,用函数把这些步骤一步一步的实现,在使用的时候一一调用即可。性能较高,单片机、嵌入式开发等一般采用面向过程开发。

面向对象: 面向对象是把构成问题的事务分解成各个对象,建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事务在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。但性能上比面向过程要低。

1.3 基本数据类型

数据类型 大小(字节) 默认值
byte 1 0
short 2 0
int 4 0
long 8 0L
float 4 0.0f
double 8 0.0d
boolean (1位) false
char 2 \u000

1.4 instanceof关键字的作用

instanceof严格来说是Java中的双目运算符,用来测试一个对象是否为一个类的实例

boolean result = obj instanceof Class;

其中obj为一个对象,Class表示一个类或者一个接口,当obj为Class的对象,或者是其直接或间接子类,或者是接口实现类,结果result都为true,否则为false。

编译器会检查obj是否能转换成右边的class类型,如果不能转换直接报错。

int i = 0;
System.out.println(i instanceof Integer);//编译不通过 i必须是引用类型,不能是基本类型
System.out.println(i instanceof Object);//编译不通过

Integer integer = new Integer(1);
System.out.println(integer instanceof Integer);//true

//false ,在 JavaSE规范 中对 instanceof 运算符的规定就是:如果 obj 为 null,那么将返回 false。
System.out.println(null instanceof Object);

1.5 Java自动装箱与拆箱

装箱就是自动将基本数据类型转换为包装器类型,(int—>Integer)调用Integer.valueOf()方法

拆箱就是自动将包装器类型转换为基本数据类型,(Integer—>int)调用Integer.intValue()方法

1.6 重载和重写的区别

重写:

  1. 重写发生在父类与子类之间
  2. 方法名、参数列表、返回类型必须相同
  3. 访问修饰符限制必须大于被重写方法的访问修饰符
  4. 重写方法一定不能抛出新的检查异常,或者比重写方法申明更加宽泛的异常

重载: 在同一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同、参数顺序不同)则视为重载,与返回类型无关。

1.7 equals与==的区别

==: ==比较的是变量在内存中存放的对象内存地址,用来判断两个对象的内存地址是否相同。

equals: equals用来比较两个对象的内容是否相等,由于所有的类都是继承Object类,适用于所有对象,如果没有重写该方法的话,调用的是Object类中的方法,而Object类中的方法确是==判断的。

在阿里代码规范中只使用equals

1.8 String、StringBuffer、StringBuilder的区别

String是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码看是一个final修饰的字符数组,所引用的字符串不能改变,每次对String的操作都会生成新的String对象。

StringBuffer和StringBuilder他们两都继承了AbstractStringBuilder抽象类,从AbstractStringBuilder抽象类中我们可以看到它们底层都是可变字符数组。对字符串频繁操作建议使用StringBuffer和StringBuilder。StringBuffer对方法加了同步锁,所以是线程安全的。StringBuilder没有对方法加同步锁,所以是非线程安全的。

1.9 ArrayList和LinkedList的区别

Array(数组)是基于索引(index)的数据结构,它使用索引在数组搜索和读取是很快的。

Array获取数据的时间复杂度是O(1),但删除数据开销很大,因为需要重排数据中的所有数据。

LinkedList是一个双向链表,在添加和删除是比ArrayList性能更好,但在get和set弱于ArrayList。

1.10 HashMap和HashTable的区别

  1. 两者父类不同:HashMap是继承自AbstractMap类,而Hashtable是继承自Dictionary类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。
  2. 对null的支持不同:HashTable,key和value都不能为null。HashMap,key可以为null,但这样的key只能有一个,因为必须保证key的唯一性,可以有对个key对应的value为null。
  3. 安全性不同:HashMap是线程不安全的,HashTable是线程安全的,它的每个方法都加了synchronized关键字。
  4. 初始容量大小和每次扩容量大小不同,Hashtable 的初始长度是 11,之后每次扩充容量变为之前的2n+1(n 为上一次的长度)而 HashMap 的初始长度为 16,之后每次扩充变为原来的两倍。
  5. 计算hash值的方法不同

1.11 Collection和Collections的区别

Collection是集合类的上级接口,子接口有Set、List、LinkedList、ArrayList、Vector、Stack、set。

Collections是集合的工具类,它包含有各种有关集合操作的静态多态方法,用于实现对各种集合的搜索、排序、线程安全化等操作。

1.12 Java的四种引用,强弱软虚

强引用: 强引用是平常使用最多的引用,强引用在程序内存不足时也不会被回收,使用方式:

String str = new String("str");
System.out.println(str);

软引用: 软引用在程序内存不足时会被回收,使用方式:

// 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的,
// 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T
SoftReference<String> wrf = new SoftReference<String>(new String("str"));

弱引用: 弱引用就是JVM垃圾回收器发现它,就会被回收。

// str就是弱引用
WeakReference<String> wrf = new WeakReference<String>(str);

虚引用: 虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入 ReferenceQueue 中。注意哦,其它引用是被JVM回收后才被传入 ReferenceQueue 中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有 ReferenceQueue

PhantomReference<String> prf = new PhantomReference<String>(new String("str"),
new ReferenceQueue<>());

1.13 泛型常用特点

泛型是Java1.5之后的特性,《Java核心技术》中对泛型的定义:“泛型”意味着编写的代码可以被不同类型的对象所重用。

“泛型”,顾名思义,“泛指的类型”。我们提供泛指的概念,但具体执行的时候可以有具体的规则来约束,比如我们用的非常多的ArrayList就是个泛型类,ArrayList作为集合可以存放各种元素,如Integer, String,自定义的各种类型等,但在我们使用的时候通过具体的规则来约束,如我们可以约束集合中只存放Integer类型的元素。

1.14 Java创建对象的几种方式

  1. new方式创建对象
  2. 通过反射机制
  3. 通过clone机制
  4. 通过反序列化机制

1.15 深拷贝和浅拷贝

浅拷贝: 浅拷贝只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制引用地址所指向的对象,内部的类属性指向的是同一个对象。

深拷贝: 深拷贝会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行拷贝,内部的类属性指向的不是同一个对象。

1.16 final有哪些用法

  1. 被final修饰的类不能被继承
  2. 被final修饰的方法不能被重写
  3. 被final修饰的变量不可被改变
  4. 被final修饰的方法,JVM会尝试将其内联,提高运行效率
  5. 被final修饰的常量,在编译阶段会存入常量池

除此之外,编译器对final域要遵守的两个重排序规则更好:

在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

1.17 try catch finally,try里有return,finally还执行么?

执行,并且finally的执行早于try里面的return

  1. 不管有么有异常操作,finally里的代码都会执行
  2. 当try和catch中有return时,finally仍然会执行
  3. finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally执行前确定的
  4. finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值

1.18 进程与线程的区别?

进程: 进程是一个具有一定独立功能的程序在一个数据集合上依次动态执行的过程。进程一个正在执行的程序的实例,包括程序计数器、寄存器和程序变量的当前值。

进程的特征:

  1. 进程依赖于程序运行而存在,进程是动态的,程序是静态的
  2. 进程是操作系统进行资源分配和调度的一个独立单元(CPU除外,线程是处理器任务调度和执行的基本单元)
  3. 每个进程拥有独立的地址空间,地址空间包括代码区、数据区和堆栈区,进程之间的地址空间隔离的,互不影响

线程: 线程与进程相似,但线程是一个比进程更小的执行单元。一个进程在执行过程中可以产生多个线程。与进程不同的同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

进程与线程的区别总结:

  1. 本质区别: 进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
  2. 包含关系: 一个进程至少有一个线程,线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
  3. 资源开销: 每个进程都有独立的地址空间,进程之间的切换会有较大的开销;线程可以看做轻量级的进程,同一个进程内的线程共享进程的地址空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
  4. 影响关系: 一个进程崩溃后,在保护模式下其他进程不会被影响,但是一个线程崩溃可能导致整个进程被操作系统杀掉,所以多进程要比多线程健壮。

推荐下面的博客,讲得很透彻。
线程与进程,你真得理解了吗

1.19 Java序列化中如果有些字段不能序列化,如何处理?

对于不想进行序列化的变量,使用 transient 关键字修饰。

transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。

1.20 Java中IO流

Java中IO流分几种:

  1. 按照流方向分,可分为输入流和输出流
  2. 按照操作单元分,可分为字节流和字符流
  3. 按照流的角色分,可分为节点流和处理流

流分类结构图:
Java最新面试题汇总_第1张图片

1.21 Java反射

定义: 反射机制是在运行时,对于任意一个类,都能知道类的所有属性和方法;对于任意一个对象,都能调用它的任意一个方法。在Java中,只要给定类的名称,就可以通过反射机制来获取类的所有信息。这种动态获取的信息以及动态调用对象方法的功能称为Java的反射机制

反射的实现方式:

  1. Class.forName(“类的路径”)
  2. 类名.class
  3. 对象名.getClass()
  4. 基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象

反射机制的优缺点:
优点:
1)能够运行时动态获取类的实例,提高灵活性;
2)与动态编译结合
缺点:
1)使用反射性能较低,需要解析字节码,将内存中的对象进行解析。 解决方案: 1、通过setAccessible(true)关闭JDK的安全检查来提升反射速度; 2、多次创建一个类的实例时,有缓存会快很多 3、ReflectASM工具类,通过字节码生成的方式加快反射速度
2)相对不安全,破坏了封装性(因为通过反射可以获得私有方法和属性)

1.22 List、Set、Map三者的区别

List(对付顺序的好帮手): List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
Set(注重独一无二的性质): 不允许重复的集合。不会有多个元素引用相同的对象。
Map(用Key来搜索的专家): 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。

1.23 HashMap 中的 key 我们可以使用任何类作为 key 吗?

平时可能大家使用的最多的就是使用 String 作为 HashMap 的 key,但是现在我们想使用某个自定义类作为 HashMap 的 key,那就需要注意以下几点:

  1. 如果类重写了 equals 方法,它也应该重写 hashCode 方法。
  2. 类的所有实例需要遵循与 equals 和 hashCode 相关的规则。
  3. 如果一个类没有使用 equals,你不应该在 hashCode 中使用它。
  4. 咱们自定义 key 类的最佳实践是使之为不可变的,这样,hashCode 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode 和 equals 在未来不会改变,这样就会解决与可变相关的问题了。

1.24 HashMap 的长度为什么是 2 的 N 次方呢?

为了能让HashMap的存数据和取数据效率高,尽可能地减少hash值的碰撞,也就是说尽量把数据均匀分配,每个链表或者红黑树的长度相等。

我们首先可能会想到 % 取模的操作来实现。

取余(%)操作中如果除数是 2 的幂次,则等价于与其除数减一的与(&)操作(也就是说hash % length == hash &(length - 1) 的前提是 length 是 2 的 n 次方)。并且,采用二进制位操作 & ,相对于 % 能够提高运算效率。

1.25 HashMap 与 ConcurrentHashMap 的异同

  1. 都是 key-value 形式的存储数据;
  2. HashMap 是线程不安全的,ConcurrentHashMap 是 JUC 下的线程安全的;
  3. HashMap 底层数据结构是数组 + 链表(JDK 1.8 之前)。JDK 1.8 之后是数组 + 链表 + 红黑树。当链表中元素个数达到 8 的时候,链表的查询速度不如红黑树快,链表会转为红黑树,红黑树查询速度快;
  4. HashMap 初始数组大小为 16(默认),当出现扩容的时候,以 0.75 * 数组大小的方式进行扩容;
  5. ConcurrentHashMap 在 JDK 1.8 之前是采用分段锁来现实的 Segment + HashEntry,Segment 数组大小默认是 16,2 的 n 次方;JDK 1.8 之后,采用 Node + CAS + Synchronized来保证并发安全进行实现。

1.26 红黑树有哪些特征?

  1. 每个节点是黑色或红色
  2. 根节点是黑色
  3. 每个叶子节点都是黑色(指向空的叶子节点)
  4. 如果一个叶子节点是红色,那么其子节点必须都是黑色
  5. 从一个节点到该节点的子孙节点,每条路径上有相同数目的黑节点

1.27 面向对象的特征有哪些?

抽象: 抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。

继承: 继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段(如果不能理解请阅读阎宏博士的《Java 与模式》或《设计模式精解》中关于桥梁模式的部分)。

封装: 通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口(可以想想普通洗衣机和全自动洗衣机的差别,明显全自动洗衣机封装更好因此操作起来更简单;我们现在使用的智能手机也是封装得足够好的,因为几个按键就搞定了所有的事情)。

多态: 多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的服务,那么运行时多态性可以解释为:当A 系统访问B 系统提供的服务时,B系统有多种提供服务的方式,但一切对A 系统来说都是透明的(就像电动剃须刀是A 系统,它的供电系统是B 系统,B 系统可以使用电池供电或者用交流电,甚至还有可能是太阳能,A 系统只会通过B 类对象调用供电的方法,但并不知道供电系统的底层实现是什么,究竟通过何种方式获得了动力)。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:1). 方法重写(子类继承父类并重写父类中已有的或抽象的方法);2). 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。

1.28 访问修饰符

访问修饰符 本类 同包 子类 其他包
private
default
protected
public

1.29 float f=3.4;是否正确?

不正确。3.4 是双精度数,将双精度型(double) 赋值给浮点型(float)属于下转型( down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F;。

1.30 Math.round(11.5) 等于多少?Math.round(-11.5)等于多少?

Math.round(11.5)的返回值是12, Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加0.5 然后进行下取整。

1.31 switch 是否能作用在byte 上,是否能作用在long 上,是否能作用在String 上?

在Java 5 以前,switch(expr)中,expr 只能是byte、short、char、int。从Java5 开始, Java 中引入了枚举类型,expr 也可以是enum 类型,从Java 7 开始,expr 还可以是字符串( String), 但是长整型( long)在目前所有的版本中都是不可以的。

1.32 用最有效率的方法计算2 乘以8?

2 << 3(左移3 位相当于乘以2 的3 次方,右移3 位相当于除以2 的3 次方) 。

1.33 数组有没有length()方法?String 有没有length()方法?

数组没有length()方法,有length 的属性。String 有length()方法。JavaScript中, 获得字符串的长度是通过length 属性得到的, 这一点容易和Java 混淆。

1.34 抽象类(abstract class)和接口(interface)有什么异同?

抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现, 否则该类仍然需要被声明为抽象类。接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法全部都是抽象方法。抽象类中的成员可以是private、默认、protected、public 的,而接口中的成员全都是public 的。抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。有抽象方法的类必须被声明为抽象类, 而抽象类未必要有抽象方法。

1.35 线程有哪些状态?

创建状态: 在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。

就绪状态: 当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。

运行状态: 线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。

阻塞状态: 线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。

死亡状态: 如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪。

1.36 sleep() 和 wait() 有什么区别?

  1. 类的不同:sleep() 来自 Thread,wait() 来自 Object。
  2. 释放锁:sleep() 不释放锁;wait() 释放锁。
  3. 用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。

1.37 notify()和 notifyAll()有什么区别?

notifyAll()会唤醒所有的线程,notify()之后唤醒一个线程。notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。

1.38 线程的 run() 和 start() 有什么区别?

start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。

1.39 创建线程池有哪些方式?

new SingleThreadExecutor(): 它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。

new CachedThreadPool(): 它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列;

new FixedThreadPool(int nThreads): 重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads;

new SingleThreadScheduledExecutor(): 创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度;
new ScheduledThreadPool(int corePoolSize): 和newSingleThreadScheduledExecutor()类似,创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;

new WorkStealingPool(int parallelism): 这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;

new ThreadPoolExecutor():是最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装。

特别提醒:在阿里开放手册中(规约【强制),线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式。 原文下图:

Java最新面试题汇总_第2张图片

ThreadPoolExecutor的方式创建线程池代码示例:

	//核心线程数
    private static final int CORE_POOL_SIZE = 20;
    //最大线程数
    private static final int MAX_POOL_SIZE = 40;
    //队列数
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;

    public static void main (String [] args) {
        ThreadPoolExecutor executor = getThreadPoolExecutor();
        executor.execute(new Thread(()->{
            for (int i=0; i<10 ; i++){
                System.out.println(i);
            }
        }));
        executor.shutdown();
    }

    public static ThreadPoolExecutor getThreadPoolExecutor() {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }

1.40 线程池中 submit() 和 execute() 方法有什么区别?

execute():只能执行 Runnable 类型的任务。submit():可以执行 Runnable 和 Callable 类型的任务。

1.41 Java死锁如何避免?

死锁的原因:

  1. 一个资源每次只能被一个线程使用。
  2. 一个线程在阻塞等待某个资源时,不释放已占有资源。
  3. 一个线程已经获的资源,在未使用之前,不能强行剥夺。
  4. 若干线程形成头尾相接的循环等待资源关系。

当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。

如何避免死锁:

  1. 要注意加锁顺序,保证每个线程按同样的顺序加锁。
  2. 要注意加锁时间,可以设置一个超时时间。
  3. 要注意死锁检测,这时一种预防机制,确保在第一时间发现死锁并解决。

1.42 ThreadLocal是什么?

ThreadLocal就是为每个使用该变量的线程提供一个变量副本,每个线程都可以独立操作自己的变量副本,不会影响其它线程对应的副本。ThreadLocal 的经典使用场景是数据库连接和 session 管理等。

1.43 synchronized底层原理

synchronized底层使用了自旋锁、偏向锁、重量级锁、轻量级锁。

偏向锁: 在锁对象的对象头记录一下当前获取线程锁的线程id,该线程下次来获取就可以直接获取了。

轻量级锁: 由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,如果第二个线程来竞争锁,偏向锁会升级为轻量级锁,之所以叫轻量级锁,是为了与重量级锁区分开,轻量级锁底层是通过自旋来实现的,并不会阻塞线程。

重量级锁: 如果自旋次数过多任然没有获取到锁,则会升级到重量级锁,重量级锁会导致线程阻塞。

自旋锁: 自旋锁就是线程在获取锁的过程中不会阻塞线程,也就不会唤醒线程,阻塞或唤醒这两个步骤都需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,这个过程线程一直在运行中,相对而言没有使用过多操作系统资源,比较轻量。

1.44 synchronized 和 volatile 的区别?

  1. volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。
  2. volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
  3. volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

1.45 synchronized 和 Lock 的区别?

  1. synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
  2. synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
  3. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

1.46 synchronized 和 ReentrantLock 的区别?

  1. synchronized是一个关键字,ReentrantLock是一个类。
  2. synchronized会自动加锁和释放锁,ReentrantLock需要手动加锁和释放锁。
  3. synchronized的底层是JVM层面的锁,ReentrantLock是API层面的锁。
  4. synchronized是非公平锁,ReentrantLock可选择公平锁与非公平锁。
  5. synchronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过int类型state标识控制锁状态。
  6. synchronized底层有一个锁升级过程。

1.47 什么是Callable和Future?

Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。可以认为是带有回调的Runnable。

Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。

二、JVM篇

2.1 JVM知识点汇总

JVM是Java运行基础,面试时一定会遇到JVM的有关问题,内容相对集中,但对只是深度要求较高。其中内存模型,类加载制,GC是重点方面。性能调优部分更偏向应用,重点突出实践能力。编译器优化和执行模式部分偏向于理论基础,重点掌握知识点。需了解 内存模型各部分作用,保存哪些数据。类加载双亲委派加载机制,常用加载器分别加载哪种类型的类。GC分代回收的思想和依据以及不同垃圾回收算法的回收思路和适合场景。性能调优常有JVM优化参数作用,参数调优的依据,常用的JVM分析工具能分析哪些问题以及使用方法。执行模式解释/编译/混合模式的优缺点,Java7提供的分层编译技术,JIT即时编译技术,OSR栈上替换,C1/C2编译器针对的场景,C2针对的是server模式,优化更激进。新技术方面Java10的graal编译器编译器优化javac的编译过程,ast抽象语法树,编译器优化和运行器优化。

Java最新面试题汇总_第3张图片

2.2 JVM内存模型

内存模型图:
Java最新面试题汇总_第4张图片

虚拟机栈:

虚拟机栈是线程私有的内存空间,创建一个线程就会在虚拟机栈中获取一个线程栈,线程栈有多个栈帧,一个栈帧保存方法的局部变量表、操作数栈、动态链接、方法出口等信息。

若单个线程请求的栈深度大于虚拟机允许的深度,则会抛出StackOverflowError(栈溢出错误)。

不同于StackOverflowError,OutOfMemoryError指的是当整个虚拟机栈内存耗尽,并且无法再申请到新的内存时抛出的异常。

本地方法栈:

本地方法栈与虚拟机栈类似,虚拟机栈执行的是Java方法服务,本地方法栈执行的是本地方法服务(用native关键字修饰的方法)。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

堆:

堆(Heap),一个JVM只有一个堆内存,堆内存的大小是可以调节的。该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。

程序计数器:

程序计数器(Program Counter Register)用来储存指向下一条字节码指令的地址,也就是即将要执行的字节码指令,由存储引擎读取下一条指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。在JVM规范中,每个线程都有自己私有的程序计数器,生命周期与线程的生命周期保持一致。

方法区:

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。方法区是一种规范,不同的虚拟机厂商可以基于规范做出不同的实现,永久代和元空间就是出于不同jdk版本的实现。说白了,方法区就像是一个接口,永久代与元空间分别是两个不同的实现类而已。只不过永久代是这个接口最初的实现类,后来这个接口一直进行变更,直到最后彻底废弃这个实现类,由新实现类——元空间进行替代。

类加载子系统:

类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

2.3 类的加载与卸载

加载过程:
Java最新面试题汇总_第5张图片
类加载其中验证、准备、解析和称链接。

加载: 通过类的完全限定名,查找类字节码文件,利用字节码文件创建Class对象。

验证: 确保Class文件符合当前虚拟机要求,不会危害到虚拟机自身安全。

准备: 进行内存分配,为static修饰的类变量分配内存,并设置初始值(0或null)。不包含final修饰的静态变量,因为final变量在编译时分配。

解析: 将常量池中的符号引用替换为直接引用的过程。直接引用直接指向目标的的指针或者相对偏移量。

初始化: 主要完成静态块执行以及静态变量的赋值。先初始化父类,再初始化当前类。只有对类主动使用时才会初始化。触发条件包括,创建类的实例时,访问类的静态方法或静态变量的时候,使用Class.forName反射类的时候,或者某个子类初始化的时候。

Java自带的加载器加载的类,在虚拟机的生命周期中是不会被卸载的,只有用户自定义的加载器加载的类才可以被卸载。

2.4 加载机制,双亲委派模型

JVM中存三个默认的类加载器,BootstrapClassLoaderExtClassLoaderAppClassLoader

AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器是BootstrapClassLoader。

JVM在加载一个类时,会调用AppClassLoader的loaderClass方法来加载类,不过在这个方法中,会先使用ExtClassLoader的loaderClass方法加载类,同样ExtClassLoader的loaderClass方法中会先使用BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就就直接成功,如果BootstrapClassLoader没有加载到,那么就会自己尝试加载该类,若果没有加载到,那么则会有AppClassLoader来加载这个类。

所以双亲委派指的是,JVM在加载类时,会委派ExtClassLoader和BootstrapClassLoader进行加载,若果没有加载到才有自己进行加载。

2.5 JVM垃圾判断算法

引用计数法:

假设有个一对象ClassA,任何一个对象引用了ClassA,ClassA的引用计数器就会加1,引用失效引用计数器就会减1。如果ClassA引用计数器为0,那么ClassA就会被回收。

可达性分析法:

可达性分析法也被称之为根搜索法,可达性是指,如果一个对象被一个或多个在程序中的变量,通过直接或间接方式被其它可达的对象引用,那么该对象就是可达的。更准确的说,一个对象只有满足下述两个条件之一,就会被判断为可达的:

  1. 对象是属于根集中的对象
  2. 对象被一个可达的对象引用

在这里,我们引出了一个专有名词,“根集”,指正在执行的Java程序可以访问的引用变量的集合,程序可以使用引用变量访问对象的属性和调用对象的方法。在 JVM 中,会将以下对象标记为根集中的对象,具体包括:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中的常量引用的对象
  3. 方法区中的类静态属性引用的对象
  4. 本地方法栈中 JNI(Native 方法)的引用对象
  5. 活跃线程(已启动且未停止的 Java 线程)

根集中的对象称之为GC Roots,即根对象。可达性分析法的基本思路是:将一系列的根对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,如果一个对象到根对象没有任何引用链相连,那么这个对象就不是可达的,也称之为不可达对象。
Java最新面试题汇总_第6张图片
如上图所示,形象的展示了可达对象与不可达对象,其中灰色的是不可达对象,可以被垃圾收集的对象。在可达性分析法中,对象有两种状态,要么是可达的、要么是不可达的,在判断一个对象的可达性的时候,就需要对对象进行标记。关于标记阶段,有几个关键点是值得我们注意的,分别是:

  1. 开始进行标记前,需要先暂停应用线程,否则如果对象图一直在变化的话是无法真正去遍历它的。暂停应用线程以便 JVM 可以尽情地收拾家务的这种情况又被称之为安全点(Safe Point),这会触发一次 Stop The World(STW)暂停。触发安全点的原因有许多,但最常见的应该就是垃圾回收了。

  2. 暂停时间的长短并不取决于堆内对象的多少也不是堆的大小,而是存活对象的多少。因此,调高堆的大小并不会影响到标记阶段的时间长短。

  3. 在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:
    如果对象在进行根搜索后发现没有与根对象相连接的引用链,那它会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行 finalize()方法(可看作析构函数,类似于 OC 中的dealloc,Swift 中的deinit)。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。
    如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue的队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的finalize()方法最多只会被系统自动调用一次),稍后 GC 将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重新引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。

  4. GC 判断对象是否可达看的是强引用。

2.6 JVM垃圾回收算法

标记-清除算法:

标记-清除(Tracing Collector)算法是最基础的收集算法,为了解决引用计数法的问题而提出。它使用了根集的概念,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的可达性分析法中判定垃圾对象的标记过程。

标记-整理算法:

标记-整理(Compacting Collector)算法标记的过程与“标记-清除”算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于“标记-整理”算法的收集器的实现中,一般增加句柄和句柄表。

复制算法:

复制(Copying Collector)算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。

复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如“标记-整理”算法。一种典型的基于复制算法的垃圾回收是stop-and-copy算法,它将堆分成对象区和空闲区,在对象区与空闲区的切换过程中,程序暂停执行。

分代收集算法:

分代收集(Generational Collector)算法的将堆内存划分为新生代、老年代和永久代。新生代又被进一步划分为 Eden 和Survivor 区,其中 Survivor 由 FromSpace(Survivor0)和 ToSpace(Survivor1)组成。所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行垃圾回收,以便提高回收效率。

2.7 JVM垃圾回收器

垃圾回收(GC)线程与应用线程保持相对独立,当系统需要执行垃圾回收任务时,先停止工作线程,然后命令 GC 线程工作。以串行模式工作的收集器,称为Serial Collector,即串行收集器;与之相对的是以并行模式工作的收集器,称为Paraller Collector,即并行收集器。

Serial 收集器:

串行收集器采用单线程方式进行收集,且在 GC 线程工作时,系统不允许应用线程打扰。此时,应用程序进入暂停状态,即 Stop-the-world。Stop-the-world 暂停时间的长短,是衡量一款收集器性能高低的重要指标。Serial 是针对新生代的垃圾回收器,采用“复制”算法。

ParNew 收集器:

并行收集器充分利用了多处理器的优势,采用多个 GC 线程并行收集。可想而知,多条 GC 线程执行显然比只使用一条 GC 线程执行的效率更高。一般来说,与串行收集器相比,在多处理器环境下工作的并行收集器能够极大地缩短 Stop-the-world 时间。ParNew 是针对新生代的垃圾回收器,采用“复制”算法,可以看成是 Serial 的多线程版本

Parallel Scavenge 收集器:

Parallel Scavenge 是针对新生代的垃圾回收器,采用“复制”算法,和 ParNew 类似,但更注重吞吐率。在 ParNew 的基础上演化而来的 Parallel Scanvenge 收集器被誉为“吞吐量优先”收集器。吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。如虚拟机总运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是99%。

Serial Old 收集器:

Serial Old 是 Serial 收集器的老年代版本,单线程收集器,采用“标记-整理”算法。这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。

Parallel Old 收集器:

Parallel Old 是 Parallel Scanvenge 收集器的老年代版本,多线程收集器,采用“标记-整理”算法。

CMS收集器:

CMS(Concurrent Mark Swee)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS 收集器仅作用于老年代的收集,采用“标记-清除”算法,它的运作过程分为 4 个步骤:初始标记(CMS initial mark)、并发标记(CMS concurrent mark)、重新标记(CMS remark)、并发清除(CMS concurrent sweep)。

其中,初始标记、重新标记这两个步骤仍然需要 Stop-the-world。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始阶段稍长一些,但远比并发标记的时间短。

CMS 以流水线方式拆分了收集周期,将耗时长的操作单元保持与应用线程并发执行。只将那些必需 STW 才能执行的操作单元单独拎出来,控制这些单元在恰当的时机运行,并能保证仅需短暂的时间就可以完成。这样,在整个收集周期内,只有两次短暂的暂停(初始标记和重新标记),达到了近似并发的目的。

CMS 收集器之所以能够做到并发,根本原因在于采用基于“标记-清除”的算法并对算法过程进行了细粒度的分解。前面已经介绍过“标记-清除”算法将产生大量的内存碎片这对新生代来说是难以接受的,因此新生代的收集器并未提供 CMS 版本。

G1 收集器:

G1(Garbage First)重新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。这么做的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。区域划分的好处就是带来了停顿时间可预测的收集模型:用户可以指定收集操作在多长时间内完成,即 G1 提供了接近实时的收集特性。G1 具备如下特点:

  1. 并行与并发: G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU 来缩短 Stop-the-world 停顿的时间,部分其他收集器原来需要停顿 Java 线程执行的 GC 操作,G1 收集器仍然可以通过并发的方式让 Java 程序继续运行。
  2. 分代收集: 打破了原有的分代模型,将堆划分为一个个区域。
  3. 空间整合: 与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的。但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。
  4. 可预测的停顿: 这是 G1 相对于 CMS 的一个优势,降低停顿时间是 G1 和 CMS 共同的关注点。

G1 收集的运作过程大致如下:

  1. 初始标记(Initial Marking): 仅仅只是标记一下GC Roots能直接关联到的对象,并且修改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的 Region 中创建新对象,这阶段需要停顿线程,但耗时很短。
  2. 并发标记(Concurrent Marking): 是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
  3. 最终标记(Final Marking): 是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行。
  4. 筛选回收(Live Data Counting and Evacuation): 首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。这个阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

G1 的 GC 模式可以分为两种,分别为:

  1. Young GC: 在分配一般对象(非巨型对象)时,当所有 Eden 区域使用达到最大阀值并且无法申请足够内存时,会触发一次 YoungGC。每次 Young GC 会回收所有 Eden 以及 Survivor 区,并且将存活对象复制到 Old 区以及另一部分的 Survivor 区。
  2. Mixed GC: 当越来越多的对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,该算法并不是一个 Old GC,除了回收整个新生代,还会回收一部分的老年代,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些 Old 区域进行收集,从而可以对垃圾回收的耗时时间进行控制。G1 没有 Full GC概念,需要 Full GC 时,调用 Serial Old GC 进行全堆扫描。

JAVM垃圾回收机制推荐以下博客:
深入理解 JVM 垃圾回收机制及其实现原理

2.8 什么时候会触发FullGC?

除直接调用System.gc外,触发Full GC执行的情况有如下四种。

  1. 旧生代空间不足: 旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误: java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

  2. Permanet Generation空间满: PermanetGeneration中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息: java.lang.OutOfMemoryError: PermGen space 为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。

  3. CMS GC时出现promotion failed和concurrent mode failure: 对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。 promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。 应对措施为:增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。

  4. 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间: 这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行MinorGC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。 例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。 当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。 除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java-Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+DisableExplicitGC来禁止RMI调用System.gc。

2.9 什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?

Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。 Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。

2.10 说说对象分配规则

对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。

大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。

长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。

空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。

2.11 对象一定分配在堆中吗?有没有了解逃逸分析技术?

不一定的,JVM通过「逃逸分析」,那些逃不出方法的对象会在栈上分配。

逃逸分析(Escape Analysis),是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上。

逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。通俗点讲,如果一个对象的指针被多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。

2.12 什么是Stop The World ? 什么是OopMap?什么是安全点?

进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为「Stop The World」。也简称为STW

在HotSpot中,有个数据结构(映射表)称为「OopMap」。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。在即时编译过程中,也会在「特定的位置」生成 OopMap,记录下栈上和寄存器里哪些位置是引用。

这些特定的位置主要在:

  1. 循环的末尾(非 counted 循环)
  2. 方法临返回前 / 调用方法的call指令后
  3. 可能抛异常的位置

这些位置就叫作「安全点(safepoint)」 用户程序执行时并非在代码指令流的任意位置都能够在停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。

2.13 简述Java的对象结构

Java对象由三个部分组成:对象头、实例数据、对齐填充。

对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。

实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)

对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)

2.14 什么是指针碰撞?

一般情况下,JVM的对象都放在堆内存中(发生逃逸分析除外)。当类加载检查通过后,Java虚拟机开始为新生对象分配内存。如果Java堆中内存是绝对规整的,所有被使用过的的内存都被放到一边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是 指针碰撞。

Java最新面试题汇总_第7张图片

2.15 什么是空闲列表?

如果Java堆内存中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,不可以进行指针碰撞啦,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是空闲列表。

2.16 什么是TLAB?

可以把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块内存,这就是TLAB(Thread Local Allocation Buffer,本地线程分配缓存) 。虚拟机通过-XX:UseTLAB 设定它的。

2.17 JVM里的有几种classloader,为什么会有多种?

启动类加载器: 负责加载JRE的核心类库,如jre目标下的rt.jarcharsets.jar等。
扩展类加载器: 负责加载JRE扩展目录extJAR类包。
系统类加载器: 负责加载ClassPath路径下的类包。
用户自定义加载器: 负责加载用户自定义路径下的类包。

为什么会有多种: 分工,各自负责各自的区块。为了实现委托模型。

三、多线程与并发编程篇

3.1 有三个线程T1,T2,T3,如何保证顺序执行?

在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。

实际上先启动三个线程中哪一个都行, 因为在每个线程的run方法中用join方法限定了三个线程的执行顺序。

3.2 什么是线程安全

线程安全就是说多线程访问同一段代码,不会产生不确定的结果。又是一个理论的问题,各式各样的答案有很多,我给出一个个人认为解释地最好的:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。这个问题有值得一提的地方,就是线程安全也是有几个级别的:

  1. 不可变: 像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
  2. 绝对线程安全: 不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
  3. 相对线程安全: 相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
  4. 线程非安全: 这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类

3.3 说一下线程之间是如何通信的?

线程之间的通信有两种方式:共享内存和消息传递。

共享内存: 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。例如上图线程 A 与 线程 B 之间如果要通信的话,那么就必须经历下面两个步骤:

  1. 线程 A 把本地内存 A 更新过得共享变量刷新到主内存中去。
  2. 线程 B 到主内存中去读取线程 A 之前更新过的共享变量。

消息传递: 在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。在 Java 中典型的消息传递方式,就是 wait() 和 notify() ,或者BlockingQueue 。

3.4 CAS的原理呢?

CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:

  1. 变量内存地址,V表示
  2. 旧的预期值,A表示
  3. 准备设置的新值,B表示

当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。

CAS 缺点:

  1. ABA 问题: 比如说一个线程one 从内存位置V 中取出A,这时候另一个线程two 也从内存中取出A,并且two 进行了一些操作变成了B,然后two 又将V 位置的数据变成A,这时候线程one 进行CAS 操作发现内存中仍然是A,然后one 操作成功。尽管线程one 的CAS 操作成功,但可能存在潜藏的问题。从Java1.5 开始JDK 的atomic包里提供了一个类AtomicStampedReference 来解决ABA 问题。
  2. 循环时间长开销大: 对于资源竞争严重(线程冲突严重) 的情况, CAS 自旋的概率会比较大, 从而浪费更多的CPU 资源,效率低于synchronized
  3. 只能保证一个共享变量的原子操作: 当对一个共享变量执行操作时,我们可以使用循环CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环CAS 就无法保证操作的原子性, 这个时候就可以用锁。

3.5 什么是AQS?

简单说一下AQSAQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。

如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLockCountDownLatchSemaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。

AQS定义了对双向队列所有的操作,而只开放了tryLocktryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLocktryRelease方法,以实现自己的并发功能。

3.6 了解Semaphore吗?

emaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

3.7 什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。

这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。JDK7提供了7个阻塞队列。分别是:

  1. ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
  2. LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
  3. PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
  4. DelayQueue:一个使用优先级队列实现的无界阻塞队列。
  5. SynchronousQueue:一个不存储元素的阻塞队列。
  6. LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
  7. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

Java 5之前实现同步存取时,可以使用普通的一个集合,然后在使用线程的协作和线程同步可以实现生产者,消费者模式,主要的技术就是用好,waitnotifynotifyAllsychronized这些关键字。而在java 5之后,可以使用阻塞队列来实现,此方式大大简少了代码量,使得多线程编程更加容易,安全方面也有保障。

BlockingQueue接口是Queue的子接口,它的主要用途并不是作为容器,而是作为线程同步的的工具,因此他具有一个很明显的特性,当生产者线程试图向BlockingQueue放入元素时,如果队列已满,则线程被阻塞,当消费者线程试图从中取出一个元素时,如果队列为空,则该线程会被阻塞,正是因为它所具有这个特性,所以在程序中多个线程交替向BlockingQueue中放入元素,取出元素,它可以很好的控制线程之间的通信。

阻塞队列使用最经典的场景就是socket客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。

3.8 什么是多线程中的上下文切换?

在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。

在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称作“切换桢”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。

上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。

3.9 什么是Daemon线程?它有什么意义?

所谓后台(daemon)线程,也叫守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这个线程并不属于程序中不可或缺的部分。

因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说, 只要有任何非后台线程还在运行,程序就不会终止。

必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。注意:后台进程在不执行finally子句的情况下就会终止其run()方法。

比如:JVM的垃圾回收线程就是Daemon线程,Finalizer也是守护线程。

3.10 乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

乐观锁: 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。

乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。

在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

乐观锁的实现方式:

  1. 使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识, 不一致时可以采取丢弃和再次尝试的策略。
  2. java 中的Compare and SwapCAS ,当多个线程尝试使用CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败, 失败的线程并不会被挂起,而是被告知这次竞争中失败, 并可以再次尝试。CAS 操作中包含三个操作数—— 需要读写的内存位置( V)、进行比较的预期原值( A)和拟写入的新值(B)。如果内存位置V 的值与预期原值A 相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。

3.11 Java 中用到的线程调度算法是什么?

采用时间片轮转的方式。可以设置线程的优先级, 会映射到下层的系统上面的优先级上, 如非特别需要,尽量不要用,防止线程饥饿。

计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU 的使用权,分别执行各自的任务.在运行池中,会有多个处于就绪状态的线程在等待CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU 的使用权。

有两种调度模型:分时调度模型和抢占式调度模型。分时调度模型是指让所有的线程轮流获得cpu 的使用权,并且平均分配每个线程占用的CPU 的时间片这个也比较好理解。

java 虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行, 直至它不得不放弃CPU。

3.12 SynchronizedMap 和ConcurrentHashMap 有什么区别?

SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为map

ConcurrentHashMap 使用分段锁来保证在多线程下的性能。

ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为16 个桶,诸如getputremove 等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16 个写线程执行,并发性能的提升是显而易见的。

另外ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中, 当iterator 被创建后集合再发生改变就不再是抛出。

ConcurrentModificationException,取而代之的是在改变时new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。

3.13 CopyOnWriteArrayList 可以用于什么应用场景?

CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时, 读取操作可以安全地执行。

1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下, 可能导致young gc 或者full gc;

2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;

CopyOnWriteArrayList 透露的思想,1、读写分离,读和写分开,2、最终一致性,3、使用另外开辟空间的思路,来解决并发冲突

3.14 你对线程优先级的理解是什么?

每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个int 变量(从1-10),1 代表最低优先级, 10 代表最高优先级。

java 的线程优先级调度会委托给操作系统去处理, 所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。

3.15 什么是线程调度器(Thread Scheduler)和时间分片(TimeSlicing )?

线程调度器是一个操作系统服务,它负责为Runnable 状态的线程分配CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。同上一个问题,线程调度并不受到Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。

时间分片是指将可用的CPU 时间分配给可用的Runnable 线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。

3.16 并发编程三要素

原子性: 原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断, 要么就全部都不执行。

可见性: 可见性指多个线程操作一个共享变量时, 其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。

有序性: 有序性, 即程序的执行顺序按照代码的先后顺序来执行。

3.17 线程池原理

1)线程池的优点

1、线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。
2、可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。

2)线程池的创建

 public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
                              BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) 

corePoolSize: 线程池核心线程数量

maximumPoolSize: 线程池最大线程数量

keepAliverTime: 当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间

unit: 存活时间的单位

workQueue: 存放任务的队列

handler: 超出线程范围和队列容量的任务的处理程序

3)线程池的实现原理

提交一个任务到线程池中,线程池的处理流程如下:

  1. 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
  2. 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
  3. 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

Java最新面试题汇总_第8张图片
4)RejectedExecutionHandler:饱和策略

当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提交的任务采用一种特殊的策略来进行处理。这个策略默认配置是AbortPolicy,表示无法处理新的任务而抛出异常。JAVA提供了4中策略:

1、AbortPolicy:直接抛出异常

2、CallerRunsPolicy:只用调用所在的线程运行任务

3、DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

4、DiscardPolicy:不处理,丢弃掉。

5)Executor框架的两级调度模型

在HotSpot VM的模型中,JAVA线程被一对一映射为本地操作系统线程。JAVA线程启动时会创建一个本地操作系统线程,当JAVA线程终止时,对应的操作系统线程也被销毁回收,而操作系统会调度所有线程并将它们分配给可用的CPU。

在上层,JAVA程序会将应用分解为多个任务,然后使用应用级的调度器(Executor)将这些任务映射成固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。

Java最新面试题汇总_第9张图片
在前面介绍的JAVA线程既是工作单元,也是执行机制。而在Executor框架中,我们将工作单元与执行机制分离开来。Runnable和Callable是工作单元(也就是俗称的任务),而执行机制由Executor来提供。这样一来Executor是基于生产者消费者模式的,提交任务的操作相当于生成者,执行任务的线程相当于消费者。

四、JMM(Java内存模型)篇

4.1 JMM概念

Java内存模型(Java Memory Model,JMM)。Java语言规范中,JVM中存在一个主存区(Main Memory或Java Heap Memory),Java中所有变量都存在主存中,对所以线程是共享的,而每个线程又存在自己的工作内存(Working Memory),工作内存中保存的是主内存中某些变量的拷贝,线程对对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间不能直接相互访问,变量在程序中的传递,是依赖主存完成的。

4.2 JSR-133(Java内存模型与线程规范)

JSR-133中文文档

Java虚拟机支持多线程执行。线程是用Thread类来表示的。用户创建 一个线程的唯一方式是创建一个该类的对象,每个线程都与这样一个对象相关联。在对应的Thread 对象上调用 start()方法将启动线程。

线程的行为,尤其是未正确同步时的行为,可能会让人困惑和违背直觉。本规范描述了用 Java语言编写的多线程程序的语义;包括多线程更新共享内存时,读操作能看到什么值的规则。因为本规范与不同的硬件架构的内存模型相似,所以,这里的语义都指的是 Java内存模型。

这些语义不会去描述多线程程序该如何执行。而是描述多线程程序允许表现出的行为。任何执行策略,只要产生的是允许的行为,那它就是一个可接受的执行策略。

4.3 JMM内存模型

Java多线程内存模型与cpu缓存模型类似,是基于cpu缓存模型来建立的,Java线程内存模型是标准化的,屏蔽底层不同计算机的区别。所有的变量都 存储在主内存中,每个线程还有自己的工作内存 ,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。
Java最新面试题汇总_第10张图片

4.4 JMM三大特性

原子性: 指一个操作是不可中断的,即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

可见性: 指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。显然,对于串行程序来说,可见性问题 是不存在。因为你在任何一个操作步骤中修改某个变量,那么在后续的步骤中,读取这个变量的值,一定是修改后的新值。但是这个问题在并行程序中就不见得了。如果一个线程修改了某一个全局变量,那么其他线程未必可以马上知道这个改动。

有序性: 对于一个线程的执行代码而言,我们总是习惯地认为代码的执行时从先往后,依次执行的。这样的理解也不能说完全错误,因为就一个线程而言,确实会这样。但是在并发时,程序的执行可能就会出现乱序。给人直观的感觉就是:写在前面的代码,会在后面执行。有序性问题的原因是因为程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。

4.5 JMM数据原子操作

  1. read(读取):从主内存中读取数据
  2. load(载入):将主内存读取到的数据写入工作内存中
  3. use(使用):从工作内存读取数据来计算
  4. assign(赋值):将计算好的值重新赋值到工作内存中
  5. store(存储):将工作内存数据写入主内存中
  6. write(写入):将store过去的变量值赋值给主内存中的变量
  7. lock(锁定):将主内存变量加锁,标识为线程独占状态
  8. unlock(解锁):将主内存变量解锁

4.6 缓存一致性协议(MESI)

多个CPU从主内存读取同一个数据到各自的高速缓存,当某个CPU修改了缓存了的数据,该数据会马上同步回写到主内存,其它CPU通过总线嗅探机制可以感知到数据的变化,从而将自己缓存里的数据标记为失效。

4.7 volatile缓存可见性

底层实现主要通过汇编lock前前缀指令,它会锁定这块内存区域的缓存并回写到主内存。volatile保证可见性与有序性,保证原子性需要synchronized这样的锁机制,lock指令解释:

  1. 会将当前处理器缓存的数据立即写回到主内存。
  2. 这个写回内存的操作,会引起其它CPU缓存数据无效。
  3. 提供内存屏障功能,使lock前后指令不能重排序 。

4.8 指令重排序

在不影响单线程程序执行结果的前提下,计算机为了最大限度的发挥机器性能,会对指令重排序优化,重排序会遵守as-if-serial语义与happens-before原则。

as-if-serial语义:

不管怎么重排序,单线程程序的执行结果不能改变。编译器、runtime和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。

happens-before原则:

  1. 程序顺序规则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
  2. 锁原则:解锁操作必须发生在后续同一个锁加锁之前。
  3. volatile原则:volatile变量的写先发生于读,保证了volatile变量的可见性。
  4. 线程启动原则:线程的start()方法先于其它的每一个动作,如果 线程A在执行线程B的start()方法之前修改了共享变量的值,那么当线程B执行start()时,线程A对共享变量的修改对线程B可见。
  5. 传递性:A先于B,B先于C,那么A必然先于C。
  6. 线程终止原则:线程的所有操作先于线程的终止,假设在线程B终止之前,修改了共享变量,线程A从线程B的join()方法返回后,线程B对共享变量的修改对线程A可见。
  7. 线程中断原则:对线程interrupt()方法的调用,先发生于被终中断线程代码检查中断事件的发生。
  8. 对象终结原则:对象的构造函数执行,结束先于finalize()方法。

4.9 内存屏障

屏障类型 指令示例 说明
LoadLoad Load1; LoadLoad; Load2 保证load1的读取操作在load2及后续读取操作之前
StoreStore Store1; StoreStore; Store2; 在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存
LoadStore Load1; LoadStore; Store2 在store2及其后的写操作执行前,保证load1的读取操作已结束
StoreLoad Store1; StoreLoad; Load2 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行

JVM底层简化了内存屏障硬件指令实现,在汇编代码前加lock指令,lock指令不是一种内存屏障,但它能完成内存屏障功能。

五、Spring篇

5.1 Spring是什么?

Spring是轻量级开源的J2EE框架。它是一个容器框架、用来封装Javabean(Java对象),它是一个中间层框架(万能胶)可以起一个连接作用,⽐如说把Struts和hibernate粘合在⼀起运⽤,可以让我们的企业开发更快、更简洁。Spring是一个轻量级的控制反转(IOC)和面向切面(AOP)的容器框架。

5.2 Spring有哪些特点?

  1. 轻量: Spring 是轻量的,基本的版本大约2MB。
  2. 控制反转(IOC): Spring通过控制反转实现松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们。
  3. 面向切面编程(AOP): Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。
  4. 容器: Spring包含并管理应用中对象的生命周期和配置。
  5. MVC框架: Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。
  6. 事务管理: Spring提供一个持续事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)。
  7. 异常处理: Spring提供方便的API吧具体技术相关的异常转换为一致的unchecked 异常。

5.3 谈谈你多AOP的理解

系统是由许多不同组件组成的,每一个组件各负责一块特定功能。除了实现自身核心功能之外,这些组件还经常承担着额外的职责。例如⽇志、事务管理和安全这样的核⼼服务经常融⼊到⾃身具有核⼼业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为它们会跨越系统的多个组件。

当我们需要为分散的对象引⼊公共⾏为的时候,OOP则显得⽆能为⼒。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如⽇志功能。

⽇志代码往往⽔平地散布在所有对象层次中,⽽与它所散布到的对象的核⼼功能毫⽆关系。

在OOP设计中,它导致了⼤量代码的重复,⽽不利于各个模块的重⽤。

AOP:将程序中的交叉业务逻辑(⽐如安全,⽇志,事务等),封装成⼀个切⾯,然后注⼊到⽬标对象(具体业务逻辑)中去。AOP可以对某个对象或某些对象的功能进⾏增强,⽐如对象中的⽅法进⾏增强,可以在执⾏某个⽅法之前额外的做⼀些事情,在某个⽅法执⾏之后额外的做⼀些事情。

5.4 谈谈你对IOC的理解

IOC容器、控制反转、依赖注入

IOC容器: 实际上就是个map(key,value),⾥⾯存的是各种对象(在xml⾥配置的bean节点、@repository、@service、@controller、@component),在项⽬启动的时候会读取配置⽂件⾥⾯的bean节点,根据全限定类名使⽤反射创建对象放到map⾥、扫描到打上上述注解的类还是通过反射将对象放到map⾥。

这个时候map⾥就有各种对象了,接下来我们在代码⾥需要⽤到⾥⾯的对象时,再通过DI注⼊(autowired、resource等注解,xml⾥bean节点内的ref属性,项⽬启动的时候会读取xml节点ref属性根据id注⼊,也会扫描这些注解,根据类型或id注⼊;id就是对象名)。

控制反转: 没有引⼊IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运⾏到某⼀点的时候,⾃⼰必须主动去创建对象B或者使⽤已经创建的对象B。⽆论是创建还是使⽤对象B,控制权都在⾃⼰⼿上。

引⼊IOC容器之后,对象A与对象B之间失去了直接联系,当对象A运⾏到需要对象B的时候,IOC容会主动创建⼀个对象B注⼊到对象A需要的地⽅。

通过前后的对⽐,不难看出来:对象A获得依赖对象B的过程,由主动⾏为变为了被动⾏为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。

全部对象的控制权全部上缴给“第三⽅”IOC容器,所以,IOC容器成了整个系统的关键核⼼,它起到了⼀种类似“粘合剂”的作⽤,把系统中的所有对象粘合在⼀起发挥作⽤,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有⼈把IOC容器⽐喻成“粘合剂”的由来。

依赖注入: “获得依赖对象的过程被反转了”。控制被反转之后,获得依赖对象的过程由⾃身管理变为了由IOC容器主动注⼊。依赖注⼊是实现IOC的⽅法,就是由IOC容器在运⾏期间,动态地将某种依赖关系注⼊到对象之中。

5.5 依赖注入的三种方式

  1. 构造器注入: 将被依赖对象通过构造函数的参数注入给依赖对象,并且在初始化对象的时候注入。
    优点: 对象初始化完成后便可获得可使用的对象。
    缺点: 当需要注入的对象很多时,构造器参数列表将会很长; 不够灵活。若有多种注入方式,每种方式只需注入指定几个依赖,那么就需要提供多个重载的构造函数,麻烦。

  2. setter方法注入: IoC Service Provider通过调用成员变量提供的setter函数将被依赖对象注入给
    依赖类。
    优点: 灵活。可以选择性地注入需要的对象。
    缺点: 依赖对象初始化完成后由于尚未注入被依赖对象,因此还不能使用。

  3. 接口注入: 依赖类必须要实现指定的接口,然后实现该接口中的一个函数,该函数就是用于依赖注入。该函数的参数就是要注入的对象。
    优点: 接口注入中,接口的名字、函数的名字都不重要,只要保证函数的参数是要注入的对象类型即可。
    缺点: 侵入行太强,不建议使用。
    PS:什么是侵入行? 如果类A要使用别人提供的一个功能,若为了使用这功能,需要在自己的类中增加额外的代码,这就是侵入性。

5.6 AOP核心概念

  1. 切面(aspect): 类似对物体特征的抽象,切面就是对横切关注点的抽象。
  2. 横切关注点: 对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点。
  3. 连接点(joinpoint): 被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。
  4. 切入点(pointcut): 对连接点进行拦截的定义。
  5. 通知(advice): 所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕。
  6. 目标对象: 代理的目标对象。
  7. 织入(weave): 将切面应用到目标对象并导致代理对象创建的过程。
  8. 引入(introduction): 在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段。

5.7 AOP两种代理方式

Spring 提供了两种方式来生成代理对象: JDKProxy 和 Cglib,具体使用哪种方式生成由AopProxyFactory 根据 AdvisedSupport 对象的配置来决定。默认的策略是如果目标类是接口,则使用 JDK 动态代理技术,否则使用 Cglib 来生成代理。

JDK动态代理: JDK 动态代理主要涉及到 java.lang.reflect 包中的两个类:ProxyInvocationHandlerInvocationHandler是一个接口,通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编制在一起。Proxy 利用 InvocationHandler 动态创建一个符合某一接口的实例,生成目标类的代理对象。

CGLib 动态代理: CGLib 全称为 Code Generation Library,是一个强大的高性能,高质量的代码生成类库,可以在运行期扩展 Java 类与实现 Java 接口,CGLib 封装了 asm,可以再运行期动态生成新的 class。和 JDK 动态代理相比较:JDK 创建代理有一个限制,就是只能为接口创建代理实例,而对于没有通过接口定义业务方法的类,则可以通过 CGLib 创建动态代理。

5.8 @Autowired和@Resource的区别?

@Resource和@Autowired都是做bean的注入时使用,其实@Resource并不是Spring的注解,它的包是javax.annotation.Resource,需要导入,但是Spring支持该注解的注入。

不同点:

  1. @Autowired为Spring提供的注解,需要导入包org.springframework.beans.factory.annotation.Autowired,只按照byType注入。@Autowired注解是按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以设置它的required属性为false。
  2. @Resource默认按照byName自动注入,由J2EE提供,需要导入包javax.annotation.Resource。@Resource有两个重要的属性:name和type,而Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以,如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不制定name也不制定type属性,这时将通过反射机制使用byName自动注入策略。

5.9 Spring中bean的生命周期

spring中bean的生命周期可分为四个阶段,实例化 Instantiation、属性赋值 Populate、初始化 Initialization、销毁 Destruction。

bean详细生命周期:

  1. 启动Spring容器,也就是创建BeanFactory(bean工厂),通过applicationcontext加载配置文件,或者利用注解的方式扫描,将bean的配置信息加载到Spring容器中。
  2. 加载完后,Spring容器会将这些配置信息(bean的信息)封装成BeanDeFinition对象,赋予一些属性,比如是否单例、是否懒加载等。
  3. 然后将这些BeanDefinition对象以key为beanName,值为BeanDefinition对象的形式存入到一个map里面,将这个map传入到spring beanfactory去进行springBean的实例化。
  4. 传入到spring beanfactory之后,利用BeanFactoryPostProcessor接口这个扩展点去对BeanDefinition对象进行一些属性修改。
  5. 开始循环BeanDefinition对象进行springBean的实例化,springBean的实例化也就是执行bean的构造方法(单例的Bean放入单例池中,但是此刻还未初始化),在执行实例化的前后,可以通过InstantiationAwareBeanPostProcessor扩展点(作用于所有bean)进行一些修改。
  6. spring bean实例化之后,就开始注入属性,首先注入自定义的属性,比如标注@autowrite的这些属性,再调用各种Aware接口扩展方法,注入属性(spring特有的属性),比如BeanNameAware.setBeanName,设置Bean的ID或者Name。
  7. 初始化bean,对各项属性赋初始化值,初始化前后执行BeanPostProcessor(作用于所有bean)扩展点方法,对bean进行修改。初始化前后除了BeanPostProcessor扩展点还有其他的扩展点,执行顺序如下:(1). 初始化前postProcessBeforeInitialization()。(2). 执行构造方法之后,执行 @PostConstruct 的方法。(3). 所有属性赋初始化值之后afterPropertiesSet()。(4). 初始化时,配置文件中指定的 init-method 方法。(5). 初始化后postProcessAfterInitialization()。先执行BeanPostProcessor扩展点的前置方法postProcessBeforeInitialization(),再执行bean本身的构造方法,再执行@PostConstruct标注的方法,所有属性赋值完成之后执行afterPropertiesSet(),然后执行 配置文件或注解中指定的 init-method 方法,最后执行BeanPostProcessor扩展点的后置方法postProcessAfterInitialization()。
  8. 此时已完成bean的初始化,在程序中就可以通过spring容器拿到这些初始化好的bean。
  9. 随着容器销毁,springbean也会销毁,销毁前后也有一系列的扩展点。销毁bean之前,执行@PreDestroy 的方法,销毁时,执行配置文件或注解中指定的 destroy-method 方法。

参考资料:
springBean生命周期

5.10 什么是MVC?

MVC原理图:
Java最新面试题汇总_第11张图片
M-Model 模型(完成业务逻辑:有javaBean构成,service+dao+entity)
V-View 视图(做界面的展示 jsp,html……)
C-Controller 控制器(接收请求—>调用模型—>根据结果派发页面)

5.11 SpringMVC执行流程

Java最新面试题汇总_第12张图片
1、 用户发送请求至前端控制器DispatcherServlet。
2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。
3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
4、 DispatcherServlet调用HandlerAdapter处理器适配器。
5、 HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
6、 Controller执行完成返回ModelAndView。
7、 HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
8、 DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
9、 ViewReslover解析后返回具体View。
10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
11、 DispatcherServlet响应用户。

5.12 Spring支持的几种bean的作用域

(1)singleton: 默认,每个容器中只有一个bean的实例,单例的bean由BeanFactory自身来维
护。
(2)prototype: 为每一个bean请求提供一个实例。
(3)request: 为每一个网络请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。
(4)session: 与request范围类似,确保每个session中有一个bean的实例,在session过期后,bean会随之失效。
(5)global-session: 全局作用域,global-session和Portlet应用相关。当你的应用部署在Portlet容器中工作时,它包含很多portlet。如果你想要声明让所有的portlet共用全局的存储变量的话,那么这全局变量需要存储在global-session中。全局作用域与Servlet中的session作用域效果相同。

5.13 Spring基于xml注入bean的几种方式?

(1)Set方法注入;
(2)构造器注入:①通过index设置参数的位置;②通过type设置参数类型;
(3)静态工厂注入;
(4)实例工厂;

5.14 Spring用到了那些设计模式

简单工厂模式: Spring中的BeanFactory就是简单工厂模式的体现。根据传入一个唯一标识获取Bean对象。

工厂模式: Spring中的FactoryBean就是典型的工厂方法模式,实现了 FactoryBean 接口的 bean是一类叫做 factory 的 bean。其特点是,spring 在使用 getBean() 调用获得该 bean 时,会自动调用该 bean 的 getObject() 方法,所以返回的不是 factory 这个 bean,而是这个 bean.getOjbect()方法的返回值。

原型模式: 在 spring 中用到的原型模式有: scope=“prototype” ,每次获取的是通过克隆生成的新实例,对其进行修改时对原有实例对象不造成任何影响。

迭代器模式: 在 Spring 中有个 CompositeIterator 实现了 Iterator,Iterable 接口和 Iterator 接口,这两个都是迭代相关的接口。可以这么认为,实现了 Iterable 接口,则表示某个对象是可被迭代的。Iterator 接口相当于是一个迭代器,实现了 Iterator 接口,等于具体定义了这个可被迭代的对象时如何进行迭代的。

代理模式: Spring 中经典的 AOP,就是使用动态代理实现的,分 JDK 和 CGlib 动态代理。

适配器模式: Spring 中的 AOP 中 AdvisorAdapter 类,它有三个实现:MethodBeforAdviceAdapter、AfterReturnningAdviceAdapter、ThrowsAdviceAdapter。Spring会根据不同的 AOP 配置来使用对应的 Advice,与策略模式不同的是,一个方法可以同时拥有多个Advice。Spring 存在很多以 Adapter 结尾的,大多数都是适配器模式。

观察者模式: Spring 中的 Event 和 Listener。spring 事件:ApplicationEvent,该抽象类继承了EventObject 类,JDK 建议所有的事件都应该继承自 EventObject。spring 事件监听器:ApplicationListener,该接口继承了 EventListener 接口,JDK 建议所有的事件监听器都应该继承EventListener。

模板模式: Spring 中的 org.springframework.jdbc.core.JdbcTemplate 就是非常经典的模板模式的应用,里面的 execute 方法,把整个算法步骤都定义好了。

责任链模式: DispatcherServlet 中的 doDispatch() 方法中获取与请求匹配的处理器HandlerExecutionChain,this.getHandler() 方法的处理使用到了责任链模式。

注意:这里只是列举了部分设计模式,其实里面用到了还有享元模式、建造者模式等。可选择性的 回答,主要是怕你回答了迭代器模式,然后继续问你,结果你一问三不知,那就尴了尬了。

5.15 Spring 中的单例 Bean 是线程安全的么?

Spring 框架并没有对单例 Bean 进行任何多线程的封装处理。

关于单例 Bean 的线程安全和并发问题,需要开发者自行去搞定。单例的线程安全问题,并不是 Spring 应该去关心的。Spring 应该做的是,提供根据配置,创建单例 Bean 或多例 Bean 的功能。

当然,但实际上,大部分的 Spring Bean 并没有可变的状态,所以在某种程度上说 Spring 的单例Bean 是线程安全的。如果你的 Bean 有多种状态的话,就需要自行保证线程安全。最浅显的解决办法,就是将多态 Bean 的作用域(Scope)由 Singleton 变更为 Prototype。

5.16 Spring中什么时候@Transactional会失效

因为Spring事务是基于代理来实现的,所以某个加了@Transactional的⽅法只有是被代理对象调⽤时,那么这个注解才会⽣效,所以如果是被代理对象来调⽤这个⽅法,那么@Transactional是不会失效的。

同时如果某个⽅法是private的,那么@Transactional也会失效,因为底层cglib是基于⽗⼦类来实现的,⼦类是不能重载⽗类的private⽅法的,所以⽆法很好的利⽤代理,也会导致@Transactianal失效

5.17 Spring容器启动流程

  1. 在创建Spring容器,也就是启动Spring时:
  2. ⾸先会进⾏扫描,扫描得到所有的BeanDefinition对象,并存在⼀个Map中
  3. 然后筛选出⾮懒加载的单例BeanDefinition进⾏创建Bean,对于多例Bean不需要在启动过程中去进⾏创建,对于多例Bean会在每次获取Bean时利⽤BeanDefinition去创建
  4. 利⽤BeanDefinition创建Bean就是Bean的创建⽣命周期,这期间包括了合并BeanDefinition、推断构造⽅法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发⽣在初始化后这⼀步骤中
  5. 单例Bean创建完了之后,Spring会发布⼀个容器启动事件
  6. Spring启动结束
  7. 在源码中会更复杂,⽐如源码中会提供⼀些模板⽅法,让⼦类来实现,⽐如源码中还涉及到⼀些BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过BenaFactoryPostProcessor来实现的,依赖注⼊就是通过BeanPostProcessor来实现的
  8. 在Spring启动过程中还会去处理@Import等注解

5.18 单例Bean和单例模式

单例模式表示JVM中某个类的对象只会存在唯⼀⼀个。⽽单例Bean并不表示JVM中只能存在唯⼀的某个类的Bean对象。

5.19 Spring中的事务是如何实现的

  1. Spring事务底层是基于数据库事务和AOP机制的。
  2. ⾸先对于使⽤了@Transactional注解的Bean,Spring会创建⼀个代理对象作为Bean。
  3. 当调⽤代理对象的⽅法时,会先判断该⽅法上是否加了@Transactional注解。
  4. 如果加了,那么则利⽤事务管理器创建⼀个数据库连接。
  5. 并且修改数据库连接的autocommit属性为false,禁⽌此连接的⾃动提交,这是实现Spring事务⾮常重要的⼀步。
  6. 然后执⾏当前⽅法,⽅法中会执⾏sql。
  7. 执⾏完当前⽅法后,如果没有出现异常就直接提交事务。
  8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务。
  9. Spring事务的隔离级别对应的就是数据库的隔离级别。
  10. Spring事务的传播机制是Spring事务⾃⼰实现的,也是Spring事务中最复杂的。
  11. Spring事务的传播机制是基于数据库连接来做的,⼀个数据库连接⼀个事务,如果传播机制配置为需要新开⼀个事务,那么实际上就是先建⽴⼀个数据库连接,在此新数据库连接上执⾏sql。

5.20 Spring这么解决循环依赖的?

六、Mybatis篇

6.1 什么是Mybatis?

(1)Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直接编写原生态sql,可以严格控制sql执行性能,灵活度高。

(2)MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。

(3)通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。

6.2 MyBatis的优点和缺点

优点:

  1. 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。
  2. 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
  3. 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
  4. 能够与Spring很好的集成;
  5. 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。

缺点:

  1. SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
  2. SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

6.3 #{}和${}的区别是什么?

#{}是预编译处理、是占位符, ${}是字符串替换、是拼接符。

Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调⽤ PreparedStatement 来赋值。Mybatis 在处理{}替换成变量的值,调⽤ Statement 来赋值。

#{} 的变量替换是在DBMS 中、变量替换后,#{} 对应的变量⾃动加上单引号,{} 的变量替换是在
DBMS 外、变量替换后,{} 对应的变量不会加上单引号

使⽤#{}可以有效的防⽌ SQL 注⼊, 提⾼系统安全性。

6.4 Mybatis是如何进行分页的?分页插件的原理是什么?

Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。可以在sql内直接拼写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页,比如:MySQL数据的时候,在原有SQL后面拼写limit。

分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。

6.5 Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?

第一种是使用标签,逐一定义数据库列名和对象属性名之间的映射关系。

第二种是使用sql列的别名功能,将列的别名书写为对象属性名。

有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。

6.6 Xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签?

,加上动态sql的9个标签 trim | where | set | foreach | if | choose | when | otherwise | bind 等,其中 为sql片段标签,通过标签引入sql片段,为不支持自增的主键生成策略标签。

6.7 Mybatis的一级、二级缓存

一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该Session 中的所有 Cache 就将清空,默认打开一级缓存。

二级缓存: 与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置 。

对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有select 中的缓存将被 clear 掉并重新更新,如果开启了二级缓存,则只根据配置判断是否刷新。

6.8 通常一个Xml 映射文件,都会写一个Dao 接口与之对应,这个Dao 接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?

Dao 接口即Mapper 接口。接口的全限名,就是映射文件中的namespace 的值;接口的方法名,就是映射文件中Mapper 的Statement 的id 值; 接口方法内的参数,就是传递给sql 的参数。

Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key 值, 可唯一定位一个MapperStatement。在Mybatis 中, 每一个