Skip to content

面试

数据库

语法

GROUP BY

  1. 满足, select子句中的列名必须为分组或者列函数
  2. 列函数对于group by子句定义的每个组各返回一个结果

HAVING

  1. 通常与group by子句一起使用
  2. where过滤行而having过滤组
  3. 出现在同一sql的顺序where>group by>having

索引

B树

对于一个m阶的B树则有:

  1. 根节点至少包括两个孩子

  2. 树中每个节点最多含有m个孩子(m>=2)

  3. 除根节点和叶节点外, 其他每个节点至少都有ceil(m/2)个孩子

  4. 所有叶子节点都位于同一层

  5. 假设每个非终端节点中包含有n个关键字信息, 其中

    a. K[i]$(i=1…n)$为关键字, 且关键字按照顺序升序排序,K[i-1]<K[i]

    b. 关键字的个数n必须满足: [ceil(m/2)-1]<=n<=m-1

    c. 非叶子节点的指针: P[1], P[2], ……, P[M]; 其中P[1]指向关键字小于K[1]的子树, P[M]指向关键字大于K[M-1]的子树, 其他P[i]指向关键字属于(K[i-1], K[i])的子树

  6. 如果经过新增导致节点个数大于m-1, 则通过向上拆分中间值达成新树

  7. 如果经过删除导致节点个数小于m/2, 则通过合并上层关键字与包含过小节点在内的两侧的叶子, 形成新的叶子, 达成新树, 如果此时叶子大于m-1则进行拆分操作

B+树

  1. B+树是B树的变体, 优势有
    1. B+树的磁盘读写代价更低
    2. B+树的查询效率更加稳定
    3. B+树更有利于对数据库的扫描(范围查询效率更好)
  2. 非叶子节点的子树指针与关键字个数相同
  3. 非叶子节点的子树指针P[i], 指向关键字值(K[i], K[i+1])的子树
  4. 非叶子节点仅用来索引, 数据都保存在叶子节点中
  5. 所有叶子节点均有一个链指针指向下一个叶子节点

Hash

  1. 仅能满足=IN, 不能使用范围查询
  2. 无法被用来避免数据的排序操作
  3. 不能利用部分索引键查询
  4. 不能避免表扫描
  5. 遇到大量Hash值相等的情况后性能并不一定比B树索引高

BitMap

  1. <关键字, 开始, 结束, 位图>
  2. 适用于值被限定在某几种的情况.(比如性别)
  3. 锁的粒度非常大, 会占用一整个位图, 不适用于高并发

密集索引和稀疏索引

  1. 密集索引文件中的每个搜索码值都对应一个索引值
  2. 稀疏索引文件只为索引码的某些值建立索引项
  3. 对于InnoDB
    1. 若一个主键被定义, 该主键则作为密集索引
    2. 若没有主键被定义, 该表的第一个唯一非空索引则作为密集索引
    3. 说不满足以上条件, innodb内部会生成一个隐藏主键(密集索引)
    4. 非主键索引(稀疏索引)存储关键键位和其对应的主键值, 包含两次查找
  4. 延伸问题
    1. 如何定位并优化慢查询sql
      1. 根据慢日志定位慢查询sql
        1. slow_query_log: 是否开启慢日志
        2. slow_query_log_file: 慢日志存储地址
        3. long_query_time: 大于多少秒时记录慢日志
        4. slow_queries: 慢查询的数量
      2. 使用explain等工具分析sql
        1. type: system>const>eq_ref>ref>fulltext>ref_or_null>index_merge>unique_subquery>index_subquery>range>index>all
        2. extra:
          1. Using filesort: 表示MySql会对结果是用一个外部索引排序, 而不是从表里按索引次序读到相关内容. 可能在内存或者磁盘上进行排序. MySql中无法利用索引完成的排序操作称为"文件排序".
          2. Using temporary: 表示MySql在对查询结果排序时使用临时表. 常见于排序order by和分组排序group by.
      3. 修改sql或者尽量让sql走索引
        1. force index(primary)
    2. 联合索引的最左匹配原则
      1. MySql会一直向右匹直到遇到范围查询就停止匹配
      2. =in可以乱序, MySql的查询优化器会帮你优化成索引可以识别的形式
    3. 索引不是建立的越多越好
      1. 数据量小的表不需要建立索引, 建立会增加额外的索引开销
      2. 数据变更需要维护索引, 因此更多的索引意味着更多的维护成本
      3. 更多的索引意味着也需要更多的空间

索引失效

  1. 违反最左匹配法则
  2. 在索引上做任何操作
  3. 类型不一致导致的索引失效
  4. 索引未覆盖
  5. 使用不等于
  6. like以通配符(%)开头
  7. 字符串不加单引号
  8. 如果OR连接的是同一个字段,那么索引不会失效,反之索引失效。
  9. NOT INNOT EXISTS导致索引失效

  1. 锁的分类
    1. 粒度: 表级锁、行级锁、页级锁
    2. 级别: 共享锁、排他锁
    3. 加锁方式: 自动锁、显式锁
    4. 操作: DML锁(对数据进行操作)、DDL锁(对表结构进行操作)
    5. 使用方式: 乐观锁、悲观锁
  2. MyISAM与InnoDB关于锁方面的区别是什么
    1. MyISAM默认用的是表级锁, 不支持行级锁
    2. InnoDB默认用的是行级锁, 也支持表级锁
  3. 数据库事务的四大特性(ACID)
    1. 原子性(Atomic)
    2. 一致性(Consistency)
    3. 隔离性(Isolation)
    4. 持久性(Durability)
  4. 事务隔离级别以及各级别下的并发访问问题(select @@tx_isolation; set session transaction isolation level read committed;)
    1. 更新丢失: MySql所有事务隔离级别都在数据库层面上均可避免
    2. 脏读: READ-COMMITTED事务隔离级别以上可以避免
    3. 不可重复读: REPEATABLE-READ事务隔离级别以上可以避免
    4. 幻读: SERIALIZABLE事务隔离级别可以避免
    5. 隔离级别: 未提交读、已提交读、可重复读、串行化
  5. InnoDB可重复读隔离级别下如何避免幻读
    1. 当前读: select …… lock in share mode;,select …… for update;
    2. 当前读: update, delete, insert
    3. 快照读: 不加锁的非阻塞读, select
  6. RC、RR级别下的InnoDB的非阻塞读(伪MVCC:多版本并发控制)如何实现
    1. 数据行内的DB_TRX_IDDB_ROLL_PTRDB_ROW_ID字段
    2. undo日志: 通过DB_TRX_ID控制前后顺序, 再通过DB_ROLL_PTR指向前一项修改
  7. 对主键索引或者唯一索引会用Gap锁吗
    1. 如果where条件全部命中, 则不会用Gap锁(间隙锁), 只会加记录锁
    2. 如果没有全部命中, 则会部分加上Gap锁

彻底搞懂 MySQL 事务的隔离级别

Redis

多路I/O复用模型

  1. 因地制宜
  2. 优先选择时间复杂度为O(1)的I/O多路复用函数作为底层实现(epoll/kqueue/evport)
  3. 以时间复杂度为O(n)的select作为保底
  4. 基于reactor设计模式监听I/O事件
  5. https://zhuanlan.zhihu.com/p/347779760

从海量Key里查询出某一固定前缀的Key

  1. 摸清数据规模, 即问清楚边界
  2. Keys pattern: 查找所有符合给定模式pattern的key
    1. Keys指令一次性返回所有匹配的key
    2. 键的数量过大会使服务卡顿
  3. Scan cursor [Match pattern] [Count count]
    1. 基于游标的迭代器, 需要基于上一次的游标延续之前的迭代过程
    2. 以0作为游标开始一次新的迭代, 直到命令返回游标0完成一次遍历
    3. 不保证每次执行都返回某个给定数量的元素, 支持模糊查询
    4. 一次返回的数量不可控, 只能是大概率符合count参数

如何通过Redis实现分布式锁

  1. Setnx key value: 如果key不存在, 则创建并赋值
    1. 时间复杂度: O(1)
    2. 返回值: 设置成功返回1; 设置失败返回0
    3. 会一直存在
  2. Expire key seconds
    1. 设置key的生存时间, 当key过期时(生存时间为0), 会被自动删除
    2. 缺点: 原子性不足
  3. Set key value [EX seconds] [PX milliseconds] [NX|XX]
    1. EX seconds: 设置键的过期时间为seconds秒
    2. PX millisecond: 设置键的过期时间为millisecond毫秒
    3. NX: 只有键不存在时, 才对键进行设置操作
    4. XX: 只有键已经存在时, 才对键进行设置操作
    5. Set操作成功完成后, 返回OK, 否则返回nil
  4. 如果有大量的key同时过期的注意事项
    1. 集中过期, 由于清除大量的key很耗时, 会出现短暂的卡顿现象
    2. 添加一定的随机值, 避免同时过期(缓存雪崩?)

如何使用Redis做异步队列

  1. 可以使用List作为队列, RPush生产消息, LPop消费消息
    1. 缺点: 没有等待队列里有值就直接消费
    2. 弥补: 可以通过在应用层引入Sleep机制去调用LPop重试
  2. BLPop key [key…] timeout
    1. 阻塞直到队列有消息或者超时
    2. 只能供一个消费者消费
  3. pub/sub: 主题订阅者模式
    1. 发送者(pub)发送消息, 订阅者(sub)接受消息
    2. 订阅者可以订阅任意数量的频道
    3. 消息的发布时无状态的, 无法保证可达
    4. 语法
      1. subscribe myTopic
      2. public myTopic "something"

Redis持久化

  1. RDB(快照)持久化: 保存某个时间点的全量数据快照
    1. stop-writes-on-bgsave-error 在备份出错时停止写入
    2. rdbcompression 备份时压缩(建议设置为no)
    3. SAVE: 阻塞Redis的服务器进程, 直到RDB文件被创建完成(不常用)
    4. BGSAVE: Fork出一个子进程来创建RDB, 不阻塞服务器进程
    5. 自动保存
      1. 根据redis.conf配置里的SAVE m n 定时触发(用的是BGSAVE)
      2. 主从复制时, 主节点自动触发
      3. 执行Debug Reload
      4. 执行Shutdown且没有开启AOF持久化
    6. 缺点
      1. 内存数据的全量同步, 数据量大会由于I/O而严重影响性能
      2. 可能会因为Redis挂掉而丢失从当前到最近一次快照期间的数据
  2. AOF(Append-Only-File)持久化: 保持写状态
    1. 记录下除了查询意外的所有变更数据库状态的指令
    2. 以append的形式最佳保存到AOF文件中(增量)
    3. appendonly yes 开启AOF
    4. appendfsync
      1. always: 只要发生了变化就同步
      2. everysec: 每隔一秒同步
      3. no: 将写入时机交给系统, 一般是写满缓存区
    5. 日志重写解决AOF文件大小不断增大的问题
      1. 调用fork(), 创建一个子进程
      2. 子进程把新的AOF写到一个临时文件里, 不依赖原来的AOF文件
      3. 主进程持续将新的变动同时写到内存和原来的AOF里
      4. 主进程获取子进程重写AOF的完成信号, 往新AOF同步增量变动
      5. 使用新的AOF文件替换掉旧的AOF文件
      6. 文件体积大, 恢复时间长
  3. RDB-AOF混合持久化方式
    1. BGSAVE做镜像全量持久化, AOF做增量持久化

使用Pipeline

  1. Pipeline和Linux的管道类似
  2. Redis基于请求/响应模型, 单个请求处理需要一一应答
  3. Pipeline批量执行指令, 节省多次IO往返的时间
  4. 有顺序依赖的指令建议分批发送
  5. 主从同步(缺乏高可用性)
    1. 全同步
      1. Slave发送sync命令到Master
      2. Master启动一个后台进程, 将Redis中的数据快照保存到文件中
      3. Master将保存数据快照期间接受到的写命令缓存起来
      4. Master完成写文件操作后, 将该文件发送给Slave
      5. 使用新的RDB文件替换掉旧的RDB文件
      6. Master将这期间收集到的增量写命令发给Slave端
    2. 增量同步过程
      1. Master接受到用户的操作指令, 判断是否需要传播到Slave
      2. 将操作记录追加到AOF文件
      3. 将操作传播到其他Slave
        1. 对齐主从库
        2. 往响应缓存写入指令
      4. 将缓存中的数据发送给Slave
  6. Redis Sentinel(哨兵模式)
    1. 解决主从同步Master宕机后的主从切换问题
      1. 监控: 检查主从服务器是否运行正常
      2. 提醒: 通过API向管理员或者其他应用程序发送故障通知
      3. 自动故障迁移: 主从切换
    2. 流言协议Gossip
      1. 每个节点都随机地与对方通信, 最终所有节点的状态达成一致
      2. 种子节点定期随机向其他节点发送节点列表以及需要传播的消息
      3. 不保证信息一定会传递给所有节点, 但是最终会趋于一致

Redis的集群原理

  1. 如何从海量数据里快速找到所需
    1. 分片: 按照某种规则去划分数据, 分散存储在多个节点上
    2. 常规的按照哈希划分无法实现节点的动态增减, 导致大量key无法被命中
  2. 一致性哈希算法
    1. 对2^32取膜, 将哈希值空间组织成虚拟的圆环
    2. 对服务器取哈希, 定位其在哈希环上的位置
    3. 将数据key使用相同的函数计算出哈希值, 放入到相应服务器(顺时针放入)
    4. 哈希环倾斜问题: 引入虚拟节点, 对每个服务器计算多个哈希值(增加后缀编号)

Linux

体系结构

  1. 主要分为用户态和内核态
  2. 内核: 本质是一段管理计算机硬件设备的程序
  3. 系统调用: 内核的访问接口, 是一种不能再简化的操作
  4. 公用函数库: 系统调用的组合拳
  5. Shell: 命令解释器, 可编程

使用技巧

  1. 在指定目录下查找文件: find path [option] params
    1. -name 根据名字 target* 匹配模式 -iname 模糊搜索
  2. 检索文件内容: grep [option] pattern file
    1. 全称: Global Regular Expression Print
    2. 查找文件里符合条件的字符串
  3. 对文件内容做统计
    1. awk [option] ‘cmd’ file
    2. 一次读取一行文本, 按输入分隔符进行切片, 切成多个组成部分
    3. 将切片直接保存在内建的变量中, $1, $2……($0表示行的全部)
    4. 支持对单个切片的判断, 支持循环判断, 默认分隔符为空格
  4. 批量替换文本内容
    1. sed [option] 'sed command' filename
    2. 全名stream editor, 流编辑器
    3. 适合用于对文本的行内容进行处理

JVM

JVM如何加载class文件

  1. Class Loader: 依据特定格式, 加载class文件到内存
  2. Runtime Data Area: JVM内存结构模型空间
  3. Execution Engine: 对命令进行解析
  4. Native Interface: 融合不同开发语言的原生库为Java所用

什么是反射

  1. Java反射机制是在运行状态中, 对于任意一个类, 都能知道这个类的所有属性和方法; 对于任意一个对象, 都能调用它的任意方法和属性; 这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制

  2. 反射实例

    Java
    public class Robot {
      private String name;
      public void sayHi(String helloSentence) {
        System.out.println(helloSentence + " " + name);
      }
      private String throwHello(String tag) {
        return "Hello" + tag;
      }
    }
    
    public class ReflectSample {
      public static void main(String[] args) throws Exception{
        Class rc = Class.forname("package.name");
        Robot r = (Robot) rc.newInstance();
        System.out.println("Class name is " + rc.getName());
        
        Method getHello = rc.getDeclaredMethod("throwHello", String.class);
        getHello.setAccessible(true);
        Object str = getHello.invoke(r, "Bob");
        System.out.println("result: " + str);
        
        Method sayHi = rc.getMethod("syaHi", String.class);
        Field name = rc.getDeclaredField("name");
        name.setAccessible(true);
        name.set(r, "Alice");
        sayHi.invoke(r, "welcome");
      }
    }

ClassLoader

  1. ClassLoader在Java中有着非常重要的作用, 它主要工作在Class装在的加载阶段, 其主要作用是从系统外部获得Class二进制数据流. 他是Java的核心组件, 所有的Class都是由ClassLoader进行加载的, ClassLoader负责通过将Class文件里的二进制数据流装载进系统, 然后交给Java虚拟机进行连接、初始化等操作.

  2. BootStrap ClassLoader: C++编写, 加载核心库java.*

  3. Extension ClassLoader: Java编写, 加载扩展库javax.*

  4. App ClassLoader: Java编写, 加载程序所在目录

  5. Custom ClassLoader: Java编写, 定制化加载

    1. 关键函数

      Java
      protected Class<?> findClass(String name) throws ClassNotFoundException{
        throw new ClassNotFoundException(name);
      }
      
      protected final Class<?> defineClass(byte[] b, int off, int len) throw ClassFormatError {
        return defineClass(null, b, off, len, null);
      }
    2. 自定义

      Java
      public class Wali{
        static{
      		System.out.println("Hello World");
        }
      }
      Java
      public class MyClassLoader extends ClassLoader {
        private String path;
        private String classLoaderName;
        
        public MyClassLoader(String path, String classLoaderName) {
          this.path = path;
          this.classLoaderName = classLoaderName;
        }
        
        //用于寻找类文件
        @Override
        public Class findClass(String name) {
          byte[] b = loadClassData(name);
          return defineClass(name, b, 0 , b.length);
        } 
        
        //用于加载类文件
        private byte[] loadClassData(String name) {
          name = path + name + ".class";
          InputStream in = null;
          ByteArrayOutputStream out = null;
          try {
      			in = new FileInputStream(new File(name));
            out = new ByteArrayOutputStream();
            int i = 0;
            while((i = in.read()) != -1) {
      				out.write(i);
            }
          } catch (Exception e) {
            e.printStackTrace();
          } finally {
            try {
              out.close();
              in.close();
            } catch (Exception e) {
            	e.printStackTrace();
          	}
          }
          return out.toByteArray();
        }
      }
      
      public class ClassLoaderChecker {
        public static void main(String[] args) throws Exception {
          MyClassLoader m = new MyClassLoader("/PATH", "myClassLoader");
          Class c = m.loadClass("Wali");
          System.out.println(c.getClassLoader());
          c.newInstance();
        }
      }
  6. 双亲委派机制

    1. 自底向上检查类是否已经加载, 自顶向下尝试加载类
    2. 所谓的双亲并不是有"双", 而是指代源码中的parent字段, 而parent值得就是上一层的加载类
    3. 为什么要使用双亲委派机制去加载: 避免多份同样字节码的加载
  7. 类的加载方式

    1. 隐式加载: new
    2. 显式加载: classLoader(不执行static, 应用于Spring IOC的懒加载等), forName(会执行static)等(需要调用newInstance, 且不支持调用有参构造)
  8. 类的加载过程:

    1. 加载: 通过ClassLoader加载class文件字节码, 生成Class对象
    2. 链接:
      1. 校验: 检查加载的class的正确性和安全性(格式等)
      2. 准备: 为类变量分配存储空间并设置类变量(static)初始值
      3. 解析: JVM将常量池内的符号引用转换为直接引用
    3. 初始化: 执行类变量赋值和静态代码块

JAVA内存模型

  1. 地址空间的划分

    1. 内核空间
    2. 用户空间
  2. JVM内存模型

    1. 线程私有: 程序计数器、虚拟机栈、本地方法栈
    2. 线程共享: MetaSpace、Java堆
    3. 程序计数器(Program Counter Register)
      1. 当前线程所执行的字节码行号指示器(逻辑计数器)
      2. 改变计数器的值来选取下一条需要执行的字节码指令
      3. 和线程是一对一的关系, 即"线程私有"
      4. 对Java方法是正在执行的虚拟机方法计数, 对Native方法则计数器值为Undefined
      5. 不会发生内存泄露
    4. Java虚拟机栈(Stack)
      1. Java方法执行的内存模型
      2. 包含多个栈帧
      3. 每个栈帧都包含:
        1. JVM指令
        2. 局部变量表: 包含方法执行过程中的所有变量
        3. 操作数栈: 入栈、出栈、复制、交换、产生消费变量, 类似于原生CPU寄存器
          1. 递归为什么会引发java.lang.StackOverflowError: 每一次调用都会生成新的栈帧, 同时旧的栈帧不会被移除, 所以就会溢出
          2. 虚拟机栈过多还会引发java.lang.OutOfMemoryError异常
        4. 动态链接
        5. 方法返回地址等
    5. 本地方法栈: 与虚拟机栈相似, 主要作用于标注了native的方法
    6. 元空间(MetaSpace)与永久代(PermGen)的区别
      1. 都是用来class相关信息, 都是方法区的实现
      2. 元空间使用本地内存, 而永久代使用的是jvm的内存
      3. MetaSpace相比PermGen的优势
        1. 字符串常量存在永久代中, 容易出现性能问题和内存溢出
        2. 类和方法的信息大小难以确定, 给永久代的大小指定带来困难
        3. 永久代会为GC带来不必要的复杂性
        4. 方便HotSpot与其他JVM如Jrockit的集成
    7. Java堆(Heep)
      1. 对象实例的分配区域
      2. GC管理的主要区域
  3. 相关问题

    1. JVM三大性能调优参数的含义

      1. -Xss: 规定了每个线程虚拟机栈(堆栈)的大小
      2. -Xms: 堆的初始值
      3. -Xmx: 堆能达到的最大值
    2. Java内存模型中堆和栈的区别——内存分配策略

      1. 静态存储: 编译时确定每个数据目标在运行时的存储空间需求
      2. 栈式存储: 数据区需求在编译时未知, 运行时模块入口前确定
      3. 堆式存储: 编译时或运行时模块入口都无法确定, 动态分配
      4. 联系: 引用对象、数组时, 栈里定义变量保存堆中目标的首地址
      5. 管理方式: 栈自动释放, 堆需要GC
      6. 空间大小: 栈比堆小
      7. 碎片相关: 栈产生的碎片远小于堆
      8. 分配方式: 栈支持静态和动态分配, 而堆仅支持动态分配
      9. 效率: 栈的效率比堆高
    3. 元空间、堆、线程独占部分间的联系——内存角度

      Java
      public class HelloWorld {
        private String name;
        public void sayHello() {
      		System.out.println("Hello " + name);
        }
        
        public void setName(String name) {
          this.name = name;
        }
        
        public static void main(String[] args) {
          int a = 1;
          HelloWorld hw = new HelloWorld();
          hw.setName("test");
          hw.sayHello();
        }
      }
      1. 元空间:

        1. Class: HelloWorld - Method:……\main - Field: name
        2. Class: System
      2. Java堆

        1. Object: String("test")
        2. Object: HelloWorld
      3. 线程独占

        1. Parameter reference: "test" to String object
        2. Variable reference: "hw" to HelloWorld object
        3. Local Variables: a with 1, lineNo

Java垃圾回收机制

判定对象是否为垃圾的算法

  1. 引用计数算法
    1. 通过判断对象的引用数量来决定对象是否可以被回收
    2. 每个对象实例都有一个引用计数器, 被引用则+1, 完成引用则-1
    3. 任何引用计数为0的对象实例可以被当作垃圾回收
    4. 缺点: 无法检测出循环引用的情况导致内存泄漏
  2. 可达性分析算法
    1. 通过判断对象的引用链是否可达来决定对象是否可以被回收
    2. 可作为GC Root的对象
      1. 虚拟机栈中引用的对象(栈帧中的本地变量表)
      2. 方法区中的常量引用的对象
      3. 方法区中的类静态属性引用的对象
      4. 本地方法栈中JNI(Native方法)的引用对象
      5. 活跃线程的引用对象

垃圾回收算法

  1. 标记-清除算法(Mark and Sweep)
    1. 标记: 从根集合进行扫描, 对存活的对象进行标记
    2. 清除: 对堆内存从头到尾进行线性遍历, 回收不可达对象内存
  2. 复制算法(Copying)
    1. 分为对象面和空闲面, 对象在对象面上创建
    2. 存活的对象从对象面复制到空闲面
    3. 将对象面所有对象内存清理
    4. 解决了碎片化问题, 顺序分配内存, 简单高效, 适用于对象存活率低的场景
  3. 标记-整理算法(Compacting)
    1. 标记: 从根集合进行扫描, 对存活的对象进行标记
    2. 清除: 移动所有存活的对象, 且按照内存地址依次序依次排列, 然后将末端内存地址以后的内存全部回收
    3. 相对耗时
    4. 避免内存的不连续行
    5. 不用设置两块内存互换
    6. 适用于存活率高的场景
  4. JVM的运行模式
    1. Server: 启动较慢, 但是稳定后运行速度比Client快
    2. Client: 启动较快
  5. 分代收集算法(Generational Collector)
    1. 垃圾回收算法的组合拳
    2. 按照对象生命周期的不同划分区域以采用不同的垃圾回收算法
    3. 目的: 提高JVM的回收效率
    4. JDK8及其以后的版本没有没永久代了, 只有年轻代和老年代
    5. GC的分类
      1. Minor GC
      2. Full GC
    6. 年轻代: 尽可能快速地收集掉那些生命周期短的对象
      1. 复制算法, Minor GC
      2. Eden区: 被创建的对象最优先放在Eden区, 但是也不绝对
      3. 两个Surivivor区: 循环接受Eden和另一个Surrivivor区的存活对象, 并且寿命+1, 寿命上限由-XX:MaxTenuringThreshold决定, 通常为15
      4. 经历一定Minor次数依然存活或者Survivor区中放不下的对象, 会被放到老年代
      5. 常用的调优参数
        1. -XX:SurvivorRatio: Eden和Survivor的比值, 默认8:1
        2. -XX:NewRatio: 老年代和年轻代内存大小的比例
        3. -XX:MaxTenuringThreshold: 对象从年轻代晋升到老年代经过GC次数的最大阈值
      6. 垃圾收集器: (-XX:UseAdaptiveSivePolicy: 虚拟机自动调优)
        1. Serial收集器(-XX:+UseSerialGC, 复制算法)
          1. 单线程收集, 进行垃圾收集时, 必须暂停所有工作线程
          2. 简单高效, Client模式下默认的年轻代收集器
        2. ParNew收集器(-XX:+UseParNewGC, 复制算法)
          1. 多线程收集, 其余的行为、特点和Serial收集器一样
          2. 单核执行效率不如Serial, 在多核下执行才有优势
        3. Parallel Scavenge收集器(-XX:+UseParallelGC, 复制算法)
          1. 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
          2. 比起关注用户线程停顿时间, 更关注系统的吞吐量
          3. 在多核下执行才有优势, Server模式下默认的年轻代收集器
    7. 老年代: 存放生命周期较长的对象
      1. 标记-清理算法、标记整理算法, Full GC、Major GC
      2. Full GC比Minor GC慢, 但执行频率低
      3. 触发Full GC的条件
        1. 老年代空间不足
        2. 永久代空间不足(JDK1.7之前)
        3. CMS GC时出现promotion failed, concurrent mode failure
          1. promotion failed: survivor区放不下想要放到老年代区域时, 发现老年代区域也放不下了, 则会触发该警告
          2. concurrent mode failure: CMS GC的同时有对象要放入老年代中, 而此时老年代空间不足, 则会触发该警告
        4. Minor GC晋升到老年代的平均大小大于老年代的剩余空间
        5. 调用System.gc()
        6. 使用RMI来进行RPC或管理JDK应用, 每小时执行一次Full GC
      4. 垃圾收集器:
        1. Serial Old收集器(-XX:UseSerialOldGC, 标记-整理算法)
          1. 单线程收集, 进行垃圾收集时, 必须暂停所有工作线程
          2. 简单高效, Client模式下默认的老年代收集器
        2. Parallel Old收集器(-XX:+UseParallelOldGC, 标记-整理算法)
          1. 多线程, 吞吐量优先
        3. CMS收集器(-XX:+UseConcMarkSweepGC, 标记-清除算法)
          1. 初始化标记: stop-the-world
          2. 并发标记: 并发追溯标记, 程序不会停顿
          3. 并发预清理: 查找执行并发标记阶段从年轻代晋升到老年代的对象
          4. 重新标记: 暂停虚拟机, 扫描CMS堆中的剩余对象
          5. 并发清理: 清理垃圾对象, 程序不会停顿
          6. 并发重制: 重制CMS收集器的数据结构
        4. G1收集器(-XX:+UseG1GC, 复制+标记-整理算法)
          1. 并行和并发
          2. 分代收集
          3. 空间整合
          4. 可预测的停顿
          5. 将整个Java堆内存划分成多个大小相等的Region, 年轻代和老年代不再物理隔离
        5. JDK11: Epsilon GC和ZGC

面试题合集

  1. Object的finalize()方法的作用是否与C++的析构函数作用相同

    1. 与C++的析构函数不同, 析构函数调用确定, 而它的是不确定的
    2. 将未被引用的对象放置于F-Queue队列
    3. 方法执行随时可能会被终止
    4. 给予对象最后一次重生的机会
    5. 代价高昂, 不确定性强, 不建议使用
  2. Java中的强引用、软引用、弱引用、虚引用有什么用

    1. 强引用(Strong Reference)

      1. 最普遍的引用: Object obj = new Object();
      2. 抛出OutOfMemoryError终止程序也不会回收具有强引用的对象
      3. 通过将对象设置为null来弱化引用, 使其被回收
    2. 软引用(Soft Reference)

      1. 对象处在游泳但非必须的状态

      2. 只有当内存空间不足时, GC会回收该引用的对象的内存

      3. 可以用来实现高速缓存

        Java
        String str = new String("adc"); // 强引用
        SoftReference<String> softRef = new SoftReference<String>(str); // 软引用
    3. 弱引用(Weak Reference)

      1. 非必须的对象, 比软引用更弱一些

      2. GC时会被回收

      3. 被回收的概率也不大, 因为GC线程的优先级比较低

      4. 适用于引用偶尔被使用且不影响垃圾回收的对象

        Java
        String str = new String("adc"); // 强引用
        WeakReference<String> weakRef = new WeakReference<String>(str); // 弱引用
    4. 虚引用(Phantom Reference)

      1. 不会决定对象的生命周期

      2. 任何时候都可能被垃圾收集器回收

      3. 跟踪对象被垃圾收集器回收的活动, 起哨兵作用

      4. 必须和引用队列Reference Queue联合使用

        java
        String str = new String("adc"); // 强引用
        ReferenceQueue queue = new ReferenceQueue();
        PhantomReference<String> ref = new PhantomReference(str, queue); // 虚引用
    5. 引用队列(Reference Queue)

      1. 无实际存储结构, 存储逻辑依赖于内部节点之间的关系来表达, 类似于链表的结构
      2. 储存关联的且被GC的软引用、弱引用或者虚引用

线程

进程与线程

  1. 由来

    1. 串行: 初期的计算机串行执行任务, 并且需要长时间等待用户输入
    2. 批处理: 预先将用户的指令集中成清单, 批量串行处理用户指令, 仍然无法并行执行
    3. 进程: 进程独占内存空间, 保存各自运行状态, 互相间不干扰且可以互相切换, 为并发处理任务提供了可能
    4. 线程: 共享进程的内存资源, 互相间切换更快速, 支持更细粒度的任务控制, 使进程内的子任务得以并发执行
  2. 进程时资源分配的最小单位, 线程时CPU调度的最小单位

    1. 所有与进程有关的资源, 都被记录在PCB中
    2. 进程时抢占处理机的调度单位, 线程属于某个进程, 共享其资源
    3. 线程只由堆栈寄存器、程序计数器和TCB组成
  3. 区别

    1. 线程不能看作独立应用, 而进程可以
    2. 进程有独立的地址空间, 互相不影响, 线程只是进程的不同执行路径
    3. 线程没有独立的地址空间, 多进程的程序比多线程程序健壮
    4. 进程的切换比线程的切换开销大
  4. Java进程和线程的关系

    1. Java对操作系统提供的功能进行封装, 包括进程和线程
    2. 运行一个程序会产生一个进程, 进程包含至少一个线程
    3. 每个进程对应一个JVM实例, 多个线程共享JVM里的堆
    4. Java采用单线程编程模型, 程序会自动创建主线程
    5. 主线程可以创建子线程, 原则上要后于子线程完成执行

常见问题

  1. Thread中的start和run方法的区别

    1. start方法会去调用JVM_StartThread方法, 创建新的线程, 然后调用Thread的run()方法
    2. run()方法只是Thread的一个普通方法的调用
  2. Thread和Runnable时什么关系

    1. Thread时实现了Runnable接口的类, 使得run支持多线程
    2. 因类的单一继承原则, 推荐多使用Runnable接口
  3. 如何给run()方法传参

    1. 构造函数传参
    2. 成员变量传参
    3. 回调函数传参
  4. 如何实现处理线程的返回值

    1. 主线程等待法

      java
      public class CycleWait implements Runnable{
        private String value;
        
        public void run() {
          try {
            Thread.currentThread().sleep(5000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          value = "we have data now";
        }
        
        public static void main(String[] args) {
          CycleWait cw = new CycleWait();
          Thread t = new Thread(cw);
          t.start();
          while (cw.value == null) {
            Thread.currentThread().sleep(100); // 主线程等待
          }
          System.out.println("value" + cw.value);
        }
      }
    2. 使用Thread类的join()阻塞当前线程以等待子线程处理完毕

      1. 粒度过粗, 无法在线程内精细控制
    3. 通过Callable接口实现: 通过FutureTask或者线程池获取

      java
      public class MyCallable implements Callable<String> {
        @Override
        public String call() throws Exception{
          String value = "test";
          System.out.println("Ready to work");
          Thread.currentThread().slepp(5000);
          System.out.println("task done");
          return value;
        }
      }
      
      public class FutureTaskDemo {
      	public static void main(String[] args) {
          FutureTask<String> task = new FutureTask<String>(new MyCallable());
          new Thread(task).start();
          if (!task.isDone()) {
            System.out.println("task has not finished, please wait!");
          }
          System.out.println("task return: " + task.get());
        }
      }
      java
      public class ThreadPoolDemo {
        public static void main(String[] args) {
          ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
          Future<String> future = newCachedThreadPool.submit(new MyCallable());
          if (!future.isDone()) {
            System.out.println("task has not finished, please wait!");
          }
          try {
            System.out.println(future.get());
          } catch (InterruptedException e) {
            e.printStackTrace();
          } catch (ExecutionException e) {
            e.printStackTrace();
          } finally {
            newCachedThreadPool.shutdown();
          }
        }
      }
  5. 线程的状态

    1. 新建(new): 创建后尚未启动的线程的状态
    2. 运行(Runnable): 包含Running和Ready
    3. 无限期等待(Waiting): 不会被分配CPU执行时间, 需要显式被唤醒
      1. 没有设置Timeout参数的Object.wait()方法
      2. 没有设置Timeout参数的Thread.join()方法
      3. LockSupport.park()方法
    4. 限期等待(Timed Waiting): 在一定时间后会由系统自动唤醒
      1. Thread.sleep()方法
      2. 设置了Timeout参数的Object.wait()方法
      3. 设置了Timeout参数的Thread.join()方法
      4. LockSupport.parkNanos()方法
      5. LockSupport.parkUntil()方法
    5. 阻塞(Blocked): 等待获取排它锁
    6. 结束状态(Terminated): 已终止线程的状态, 线程已经结束执行
  6. sleep和wait的区别

    1. sleep时Thread类的方法, wait是Object类中定义的方法
    2. sleep()方法可以在任何地方使用
    3. wait()方法只能在synchronized方法或synchronized块中使用
    4. Thread.sleep只会让出CPU, 不会导致锁行为的改变
    5. Object.wait不仅会让出CPU, 还会释放已经占有的同步资源锁
  7. notify()和notifyAll()的区别

    1. 锁池EntryList

      假设线程A已经拥有了某个对象(不是类)的锁, 而其他线程B、C想要调用这个对象的某个synchronized方法(或者块), 由于B、C线程在进入对象的synchronized(或者块)之前必须先获得该对象锁的拥有权, 而恰巧该对象的锁目前正被线程A所占用, 此时B、C线程就会被阻塞, 进入一个地方去等待锁的释放, 这个地方便是该对象的锁池

    2. 等待池WaitSet

      假设线程A调用了某个对象的wait()方法, 线程A就会释放该对象的锁, 同时线程A就进入到了该对象的等待池中, 进入到等待池中的线程不会去竞争的锁

    3. notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会

    4. notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会

  8. yield

    1. 当调用Thread.yield()函数时, 会给线程调度器一个当前线程愿意让出CPU使用的暗示, 但是线程调度器可能会忽略这个暗示
  9. interrupt

    1. 调用interrupt(), 通知线程应该中断了
      1. 如果线程处于被阻塞状态, 那么线程将立即退出被阻塞状态, 并抛出一个InterruptedException异常
      2. 如果线程处于正常状态, 那么会将该线程的中断标志设置为true, 被设置中断标志的线程将继续正常运行, 不受影响
    2. 需要被调用的线程配合中断
      1. 在正常运行任务时, 经常检查本线程的中断标志位, 如果被设置了中断标志就自行停止线程
      2. 如果线程处于正常活动状态, 那么会将该线程的中断标志设置为true, 被设置中断标志的线程将继续正常运行, 不受影响

Synchronized

  1. 底层原理

    1. 对象头
    虚拟机位数头对象结构说明
    32/64 bitMark Word默认存储对象的hashCode, 分代年龄, 锁类型, 锁标志位等信息
    32/64 bitClass Metadata Address类型指针指向对象的类元数据, JVM通过这个指针确定该对象是哪个类的数据
    1. Monitor: 每个Java对象天生自带了一把看不见的锁
  2. 锁消除

    1. JIT编译时, 堆运行上下文进行扫描, 去除不可能存在竞争的锁
  3. 锁粗化

    1. 防止反复加锁去锁, 扩大锁的范围
  4. synchronized的四种状态

    1. 无锁

    2. 偏向锁

      1. 减少同一线程获取锁的代价
      2. 大多数情况下, 锁不存在多线程竞争, 总是由同一线程多次获取

      如果一个线程获得了锁, 那么锁就进入偏向模式, 此时Mark Word的结构也变为了偏向锁结构, 当该线程再次请求锁时, 无需再做任何同步操作, 即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可, 这样就省去了大量有关锁申请的操作

    3. 轻量级锁

      1. 轻量级锁是由偏向锁升级来的, 偏向锁运行在一个线程进入同步块的情况下, 当第二个线程加入锁争用的时候, 偏向锁就会升级为轻量级锁
      2. 适用场景: 线程交替执行同步块
    4. 重量级锁

      1. 线程竞争不断自旋, 不消耗CPU
      2. 线程阻塞, 响应时间缓慢, 在多线程下, 平凡的获取释放锁, 会带来巨大的性能消耗
  5. 解锁

    1. 锁的内存语义
    2. 当线程释放锁时, Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中
    3. 而当线程获取锁时, Java内存模型会把该线程对应的本地内存置为无效, 从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
  6. synchronized和ReentrantLock的区别

    1. ReentrantLock(重入锁)
      1. 位于java.util.concurrent.lcoks包
      2. 和CountDownLatch、FutureTask、Semaphore一样基于AQS实现
      3. 能够实现比synchronized更细的粒度控制, 如控制fairness
        1. ReentrantLock fairLock = new ReentrantLock(true);
        2. 参数为true时, 倾向于将锁赋予等待时间最久的线程
        3. 公平锁: 获取锁的顺序按先后调用lock方法的顺序(慎用)
        4. 非公平锁: 抢占的顺序不一定, 看运气
        5. synchronized是非公平锁
      4. 调用lock()之后, 必须调用unlock()释放锁
      5. 性能未必比synchronized高, 并且也是可重入的
      6. ReentrantLock将锁对象化
        1. 判断是否有线程, 或者某个特定线程, 在排队等待获取锁
        2. 带超时的获取锁的尝试
        3. 感知有没有成功
      7. 将wait\notify\notifyAll对象化
        1. java.util.concurrent.locks.Condition
    2. 总结
      1. synchronized是关键字, RentrantLock是类
      2. ReentrantLock可以对获取锁的等待时间进行设置, 避免死锁
      3. ReentrantLock可以获取各种锁的信息
      4. ReentrantLock可以灵活地实现多路通知
      5. sync操作Mark Word, lock调用Unsafe类的park()方法

线程池

  1. Executor接口

    1. Executor: 运行新任务的简单接口, 将任务提交和任务执行细节解藕

    2. ExecutorService: 具备管理执行器和任务生命周期的方法, 提交任务机制更完善

    3. ThreadPoolExecutor:

      1. corePoolSize: 核心线程数量

      2. maximumPoolSize: 线程不够用时能够创建的最大线程数

      3. workQueue: 任务等待队列

      4. keepAliveTime: 空闲线程存在时长

      5. threadFactory: 创建新线程, Executors.defaultThreadFactory()

      6. handler: 线程池的饱和策略

        1. AbortPolicy: 抛出异常
        2. CallerRunsPolicy: 用调用者所在的线程来执行任务
        3. DiscardOldestPolicy: 丢弃队列中最靠前的任务, 并执行当前任务
        4. DiscardPolicy: 直接丢弃任务
      7. 如果运行的线程少于corePoolSize, 则创建新线程来处理任务, 即使线程池中的其他线程是空闲的;

        如果线程池中的线程数量大于等于corePoolSize且小于maximumPoolSize, 则只有当workQueue满时菜创建新的线程去处理任务;

        如果设置的corePoolSize和maximumPoolSize相同, 则创建的线程池的大小是固定的, 这时如果有新任务提交, 若workQueue未满, 则将请求放入workQueue中, 等待有空闲的线程去workQueue中取任务并处理;

        如果运行的线程数量大于等于maximumPoolSize, 这时如果workQueue已经满了, 则通过handler所指定的策略来处理任务;

  2. 线程池的状态

    1. RUNNING: 能接受新提交的任务, 并且也能处理阻塞队列中的任务
    2. SHUTDOWN: 不接受新提交的任务, 但可以处理存量任务
    3. STOP: 不接受新的任务, 也不处理存量任务
    4. TIDYING: 所有的任务都已终止
    5. TERMINATED: terminated()方法执行完后进入该状态
  3. 线程池的大小如何选定

    1. CPU密集型: 线程数=核心数+1
    2. I/O密集型: 线程数=核心数*(1 + 平均等待时间/平均工作时间)

Java常用类

Java异常体系

  1. Error和Exception的区别
    1. 都继承于Throwable
    2. Error: 系统无法处理的系统错误, 编译器不做检查
    3. Exception: 程序可以处理的异常, 捕获后可能恢复
      1. RuntimeException: 不可预知的, 程序应当自行避免
      2. 非RuntimeException: 可预知的, 从编译器校验的异常
    4. 从责任角度
      1. Error属于JVM需要负担的责任
      2. RuntimeException是程序应该负担的责任
      3. Checked Exception可检查异常是Java编译器应负担的责任
    5. 总结: 前者是程序无法处理的错误, 后者是可以处理的错误
  2. 常见Error以及Exception
    1. RuntimeException
      1. NullPointerException -空指针异常
      2. ClassCastException -类型强制转换异常
      3. IllegalArgumentException -传递非法参数异常
      4. IndexOutOfBoundsException -下标越界异常
      5. NumberFormatException -数字格式异常
    2. 非RuntimeException
      1. ClassNotFoundException -找不到指定class的异常
      2. IOException -IO操作异常
    3. Error
      1. NoClassDefFoundError -找不到class定义的异常
        1. 类依赖的class或者jar不存在
        2. 类文件存在, 但是存在不同的域中
        3. 大小写问题, javac编译的时候是无视大小写的, 很有可能编译出来的class文件就与想要的不一样
      2. StackOverflowError -深递归导致栈被耗尽而抛出的异常
      3. OutOfMemoryError -内存溢出异常
    4. 异常处理机制
      1. 抛出异常: 创建异常对象, 交由运行时系统处理
      2. 捕获异常: 寻找合适的异常处理器处理异常, 否则终止运行
      3. 延迟捕获: 异常的捕获和处理应尽可能延迟, 让掌握更多信息的作用域来处理异常
    5. 高效主流的异常处理框架
      1. 设计一个通用的继承自RuntimeException的异常来统一处理
      2. 其余异常都统一转译为上述异常AppException
      3. 在catch之后, 抛出上述异常的子类, 并提供足以定位的信息
      4. 由前端接收AppException做统一处理
    6. try-catch的性能
      1. try-catch块影响JVM的优化
      2. 异常对象实例需要保存栈快照等信息, 开销较大

Java集合框架

Collection

  1. List
    1. 底层是数组, 查询快, 增删慢
      1. ArrayList: 线程不安全, 效率高
      2. Vector: 线程安全, 效率低
    2. 底层是链表, 查询慢, 增删快
      1. LinkedList: 线程不安全, 效率高
  2. Set
    1. 底层是哈希表
      1. HashSet: 保证元素唯一性
    2. 底层是二叉树
      1. TreeSet: 保证元素排序
        1. 自然排序, 让对象所属的类去实现comparable接口, 无参构造
        2. 比较器接口comparator, 带参构造
  3. Map
    1. HashMap
      1. put方法的逻辑
        1. 若HashMap未被初始化, 这进行初始化操作
        2. 对Key求Hash值, 依据Hash值计算下标
        3. 若未发生碰撞, 则直接放入桶中
        4. 若发生碰撞, 则以链表的方式链接到后面
        5. 若链表长度超过阈值, 且HashMap元素超过最低树化容量, 则将链表转成红黑树
        6. 若节点已经存在, 则用新值替换旧值
        7. 若桶满了(默认容量16*扩容因子0.75), 就需要resize(扩容2倍后重排)
      2. 如何减少碰撞
        1. 扰动函数: 促使元素位置分布均匀, 减少碰撞几率
        2. 使用final对象, 并采用合适的equals()和hashCode()方法
    2. Hashtable
      1. 早期Java类库提供的哈希表的实现
      2. 线程安全: 涉及到修改Hashtable的方法, 使用synchronized修饰
      3. 串行化的方式运行, 性能较差
    3. ConcurrentHashMap
      1. CAS+synchronized使锁更细化
      2. 比起Segment, 锁拆得更细
        1. 首先使用无锁操作CAS插入头节点, 失败则循环重试
        2. 若头节点已存在, 则尝试获取头节点的同步锁, 再进行操作
      3. size()方法和mappingCount()方法的异同, 两者计算是否准确
      4. 多线程环境下如何扩容
    4. 三者的区别
      1. HashMap线程不安全, 数组+链表+红黑树
      2. Hashtable线程安全, 锁住整个对象, 数组+链表
      3. ConcurrentHashMap线程安全, CAS+同步锁, 数组+链表+红黑树
      4. HashMap的key、value均可以为null, 其他两个则不可以

JUC知识点梳理

  1. CountDownLatch: 让主线程等待一组事件发生后继续执行, 事件指的是CountDownLatch里的countDown()方法
  2. CyclicBarrier: 阻塞当前线程, 等待其他线程
    1. 等待其他线程, 且会阻塞自己当前线程, 所有线程必须同时到达栅栏位置后, 才能继续执行
    2. 所有线程到达栅栏处, 可以触发执行另一个预先设置的线程
  3. Semaphore: 控制某个资源可被同时访问的线程个数
  4. Exchanger: 两个线程到达同步点后, 互相交换数据
  5. BlockingQueue: 提供可阻塞的入队和出队操作
    1. ArrayBlockingQueue: 一个由数组结构组成的有界阻塞队列
    2. LinkedBlockingQueue: 一个由链表结构组成的有界/无界阻塞队列
    3. PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列
    4. DealyQueue: 一个使用优先级队列实现的无界阻塞队列
    5. SynchronousQueue: 一个不存储元素的阻塞队列
    6. LinkedTranserQueue: 一个由链表结构组成的无界阻塞队列
    7. LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列

IO操作

  1. Block-IO: InputStream和OutputStream, Reader和Writer
  2. NonBlock-IO: 构建多路复用、同步非堵塞的IO操作(轮询)
  3. NIO:
    1. Channels
      1. FileChannel:
        1. transferTo: 把FileChannel中的数据拷贝到另一个Channel
        2. transferFrom: 把另一个Channel中的数据拷贝到FileChannel
        3. 避免了两次用户态和内核态间的上下文切换, 即“零拷贝”, 效率较高
      2. DatagramChannel
      3. SocketChannel
      4. ServerSocketChannel
    2. Buffers
      1. ByteBuffer(Char、Double、Float、Int、Long、Short)
      2. MappedByteBuffer
    3. Selectors
      1. 最大连接数
        1. select: 单个进程所能打开的最大链接数由FD_SETSIZE宏定义, 其大小是32个整数的大小(在32位的机器上, 大小是32*32, 64位机器上FD_SETSIZE为32*64), 我们可以对其进行修改, 然后重新编译内核, 但是性能无法保证, 需要做进一步测试
        2. poll: 本质上select没有区别, 但是它没有最大链接数的限制, 原因是它是基于链表来存储的
        3. epoll: 虽然连接数有上限, 但是很大, 1G内存的机器上可以开10万左右的连接
      2. FD(文件句柄)剧增带来的IO效率问题
        1. select: 因为每次调用时都会对连接进行线性遍历, 所以睡着FD的增加会造成便利速度的线性下降
        2. poll: 同上
        3. epoll: 由于epoll是根据每个FD上的callback函数来实现的, 只有活跃的socket才会主动调用callback, 所以在活跃socket较少的情况下, 使用epoll不会有线性下降的问题, 但是所有socket都很活跃的情况下, 可能会有性能问题
      3. 消息传递方式
        1. select: 内核需要将消息传递到用户空间, 需要内核的拷贝动作
        2. poll: 同上
        3. epoll: 通过内核和用户空间共享一块内存来实现, 性能较高
  4. Asynchronous IO: 基于事件和回调机制
    1. 基于回调: 实现CompletionHandler接口, 调用时触发回调函数
    2. 返回Future: 通过isDone()查看是否准备好, 通过get()等待返回数据

Spring

IOC

  1. 实现方式
    1. Setter
    2. Interface
    3. Constructor
    4. Annotation
  2. 流程
    1. 读取Bean配置信息, 生成注册表(利用反射)
      1. XML
      2. @Coniguration
      3. @Autowired
    2. 根据Bean注册表实例化Bean
    3. 将Bean实例放到Spring容器中(Bean缓存池)
    4. 使用Bean
  3. 支持的功能
    1. 依赖注入
    2. 依赖检查
    3. 自动装配
    4. 支持集合
    5. 指定初始化方法和销毁方法
    6. 支持回调方法
  4. BeanDefinition
    1. 主要用来描述Bean的定义
    2. 会被包装为<String(name), BeanDefinition>的ConcurrentHashMap
  5. BeanDefinitionRegistry
    1. 提供向IOC容器注册Beandefinition对象的方法
  6. BeanFactroy: Spring框架最核心的接口
    1. 提供IOC的配置机制
    2. 包含Bean的各种定义, 便于实例化Bean
    3. 建立Bean之间的依赖关系
    4. Bean生命周期的控制
  7. BeanFactory与ApplicationContext的比较
    1. BeanFactory是Spring框架的基础设施, 面向Spring
    2. ApplicationContext面向使用Spring框架的开发者
  8. ApplicationContext的功能(继承多个接口)
    1. BeanFactory: 能够管理、装配Bean
    2. ResourcePatternResolver: 能够加载资源文件
    3. MessageSource: 能够实现国际化等功能
    4. ApplicationEventPublisher: 能够注册监听器, 实现监听机制

AOP

  1. 织入方式:

    1. 编译时织入: 需要特殊的Java编译器, 如AspectJ
    2. 类加载时织入: 需要特殊的Java编译器, 如AspectJ和AspectWerkz
    3. 运行时织入: Spring采用的方式, 通过动态代理的方式, 实现简单
  2. java
    @Pointcut("execution(public * com.xxx.xxx.controller..*.*(..))")
    public void webLog(){}
    
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) {
      ServletRequestAttributes attributes = (ServletRequestAttributes) Request
      HttpServletRequest request = attributes.getRequest();
    }
  3. 重要名词解释

    1. Aspect: 通用功能的代码实现
    2. Target: 被织入Aspect的对象
    3. Join Point: 可以作为切入点的机会, 所有方法都可以作为切入点
    4. Pointcut: Aspect实际被应用在的Join Point, 支持正则
    5. Advice: 类里的方法以及这个方法如何织入到目标方法的方式
      1. 前置通知
      2. 后置通知
      3. 异常通知
      4. 最终通知
      5. 环绕通知
    6. Weaving: Aop的实现过程
  4. AOP实现

    1. 由AopProxyFactory根据AdvisedSupport对象的配置来决定
    2. 默认策略如果目标类是接口, 则用JDKProxy来实现, 否则用后者
    3. JdkProxy核心: InvocationHandler接口和Proxy类
      1. 通过Java的内部反射机制实现
      2. 反射机制在生成类的过程中比较高效
    4. Cglib: 以继承的方式动态生成目标类的代理
      1. 借助ASM实现
      2. ASM在生成类之后的执行过程中比较高效
    5. Spring里的代理模式的实现
      1. 真实实现类的逻辑包含在了getBean方法里
      2. getBean方法返回的实际上是Proxy的实例
      3. Proxy实例是Spring采用JDK Proxy或Cglib动态生成的

Spring事务

事物传播策略效果
REQUIRED支持当前事务,如果没有事务会创建一个新的事务
SUPPORTS支持当前事务,如果没有事务的话以非事务方式执行
MANDATORY支持当前事务,如果没有事务抛出异常
REQUIRES_NEW创建一个新的事务并挂起当前事务
NOT_SUPPORTED以非事务方式执行,如果当前存在事务则将当前事务挂起
NEVER以非事务方式进行,如果存在事务则抛出异常
NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。