Summer Blog

面试小抄

JVM

内存模型

线程共享隔离

解释

对象创建时内存分配方式

GC

回收哪里的对象:

堆,方法区,本地方法栈,方法栈

哪些对象需要被回收:

当对象访问不到时会被回收,不再被任何存活的对象继续引用。

何时回收

方法

类的生命周期

加载–》验证–》准备–》解析–》初始化–》使用–》卸载

synchronized 原理

synchronized 锁优化

  1. 锁消除:代码要求同步,但被检测无竞争的锁进行消除。
  2. 锁粗化:粗化对于一个锁的请求和释放
  3. 轻量级锁:轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要 CPU 从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放
    1. 自旋锁:锁被其他线程持有,当前等待一定时间次数的忙循环,不放弃运行时间
    2. 自适应自旋锁:旋转时间不固定,由上一次同一个锁的自旋时间和当前锁状态决定
  4. 偏向锁:大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。偏向锁不主动释放,在产生竞争时判断是否退出同步块,若退出则释放,未退出则升级为轻量级锁

内存溢出问题排查

  1. 配置 jvm 参数,生成内存溢出是的堆存储快照,-XX:+HeapDumOnOutOfMemoryError
  2. 使用 Memory Analyzer 查看 dump 出的文件

Jvm 监控工具

工具 作用
jconsole 监控当前执行中的 java 进程
jps: 查看所有的 java 进程

常用参数

参数 描述
-Xms 设置 Java 堆大小的初始值/最小值。例如:-Xms512m (请注意这里没有”=”).
-Xmx 设置 Java 堆大小的最大值
-Xmn 设置年轻代对空间的初始值,最小值和最大值。请注意,年老代堆空间大小是依赖于年轻代堆空间大小的
-XX:PermSize=n [g/m/k] 设置持久代堆空间的初始值和最小值
-XX:MaxPermSize=n[g/m/k] 设置持久代堆空间的最大值

JDK 源码

JDK 线程池实现

CurrentHashMap 实现

concurrent 包中的类

CyclicBarrier

字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier 可以被重用。我们暂且把这个状态就叫做 barrier,当调用 await()方法之后,线程就处于 barrier 了。

CountDownLatch

利用它可以实现类似计数器的功能。比如有一个任务 A,它要等待其他 4 个任务执行完毕之后才能执行,此时就可以利用 CountDownLatch 来实现这种功能了。

Semaphore

Semaphore 翻译成字面意思为 信号量,Semaphore 可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

总结

Java 基础

Object 类中的方法

jdk8 ~ 11 升级

java 8:

  1. lambda 表达式:匿名内部类的特殊化,函数式接口对象的实现。但与匿名内部类不同,每个内部类都有独立的类文件,类加载增加了相当打的运行时开销,lambda 没有使用独立的类文件,而是使用了 invokedynamic 字节码指令,避免了独立类文件的空间开销以及加载类的大量运行时开销。

  2. hotSpots 取消了永久代,增加了元空间。元空间存在于本地内存,是 jvm 规范中的方法区的实现。原因是这样元空间的大小仅受本地内存的限制。
  3. Metaspace
    • 什么是 Metaspace :用来存放 class metadata(记录一个 java 类在 jvm 中的信息),如 Klass 结构(java 类在虚拟机内部的表示)、method metadata(方法的字节码、局部变量表、异常表、参数等信息)、常量池、注解、方法计数器(记录方法调用次数,辅助 JIT 决策)
    • 什么时候分配 Metaspace :当一个类加载时,他的类加载器会负责在 Metaspace 中分配空间
    • 什么时候回收 Metaspace :分配给一个类的空间,归属与这个类的类加载器,只有当这个类加载器卸载的时候,这个空间才会被释放。所以,只有当这个类加载器加载的所有类都没有存活的对象,并且没有到达这些类和类加载器的引用时,相应的 Metaspace 才会被释放。一个例外是匿名内部类,他们拥有自己独立的 ClassLoaderData,它的生命周期是跟随这个匿名类的,而不是类加载器。
    • Metaspace GC:
      • 分配空间时:当已分配的空间超过阈值时,虚拟机会在新的空间分配申请时收集可以卸载的类加载器,从而达到空间复用的目的,而不是扩大空间,这时会出发 GC。这个阈值会上下调整,和 Metaspace 已经占用的操作系统内存保持一个距离
      • 碰到 Metaspace OOM 时:Metaspace 的总使用空间达到了 MaxMetaspaceSize 设置的阈值,或者 Compressed Class Space 被使用光了,如果这次 GC 真的通过卸载类加载器腾出了很多的空间,这很好,否则的话,我们会进入一个糟糕的 GC 周期,即使我们有足够的堆内存。所以千万不要将 MaxMetaspaceSize 设置得太小

String s = new String(“abc”) 产生了几个对象?分别放在哪里?JDK1.8 前后存放的区域有什么不同?

2 个对象:堆中new String();字符串常量池中abc

JDK1.8 后,字符串常量池从永久代移动到了元数据区。为什么呢?

  1. 字符串在永久代中,容易出现性能问题和内存溢出
  2. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低

ArrayListLinkedList

  1. ArrayList基于数组。使用索引在数组中搜索和读取是很快的,并且内存也是相邻的所以大部分是顺序读。但在增删数据时性能不好,因为涉及移动和扩容
  2. LinkedList基于双向链表。随机访问性能差,但增删操作更快,同时需要更多的内存。

线程的生命周期

通用的五个状态:

  1. 初始:指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建
  2. 可运行:指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行
  3. 运行:当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了运行状态
  4. 休眠:运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态
  5. 终止:线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了

Java 中的生命周期

  1. NEW(初始化)
  2. RUNNABLE(可运行/运行)
  3. BLOCKED(阻塞)
  4. WAITING(无时限等待)
  5. TIMED_WAITING(有时限等待)
  6. TERMINATED(终止)

 thread status

状态转换:

  1. RUNNABLE 与 BLOCKED 的状态转换:只有一种场景会触发这种转换,就是线程等待 synchronized 的隐式锁。synchronized 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,这种情况下,等待的线程就会从 RUNNABLE 转换到 BLOCKED 状态。而当等待的线程获得 synchronized 隐式锁时,就又会从 BLOCKED 转换到 RUNNABLE 状态
  2. RUNNABLE 与 WAITING 的状态转换:

    • 获得 synchronized 隐式锁的线程,调用无参数的 Object.wait() 方法
    • 调用无参数的 Thread.join() 方法
    • 调用 LockSupport.park() 方法。其中的 LockSupport 对象,也许你有点陌生,其实 Java 并发包中的锁,都是基于它实现的。调用 LockSupport.park() 方法,当前线程会阻塞,线程的状态会从 RUNNABLE 转换到 WAITING。调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,目标线程的状态又会从 WAITING 状态转换到 RUNNABLE
  3. RUNNABLE 与 TIMED_WAITING 的状态转换

    • 调用带超时参数的 Thread.sleep(long millis) 方法
    • 获得 synchronized 隐式锁的线程,调用带超时参数的 Object.wait(long timeout) 方法
    • 调用带超时参数的 Thread.join(long millis) 方法
    • 调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline) 方法
    • 调用带超时参数的 LockSupport.parkUntil(long deadline) 方法
  4. 从 NEW 到 RUNNABLE 状态
  5. 从 RUNNABLE 到 TERMINATED 状态
    • 运行完成后 或 抛出异常
    • 强制终止:stop() interrupt()。被 interrupt 的线程可以通过异常或主动检测的方式收到通知。

悲观锁和乐观锁的区别

优点: 无锁方案相对互斥锁方案,最大的好处就是性能

问题:

synchronizedReentrantLock的区别

synchronized 是 Java 内建的同步机制,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。在代码中, synchronized 可以用来修饰方法,也可以使用在特定的代码块儿上,本质上 synchronized 方法等同于把方法全部语句用 synchronized 块包起来。

ReentrantLock 它的语义和 synchronized 基本相同。再入锁通过代码直接调用 lock() 方法获取,代码书写也更加灵活。ReentrantLock 提供了很多实用的方法,能够实现很多 synchronized 无法做到的细节控制,比如可以控制 fairness,也就是公平性,或者利用定义条件等。但是,编码中也需要注意,必须要明确调用 unlock() 方法释放,不然就会一直持有该锁。

ThreadPoolExecutor 的工作流程

  1. 每次提交任务时,如果线程数还没达到 coreSize 就创建新线程并绑定该任务。所以第 coreSize 次提交任务后线程总数必达到 coreSize,不会重用之前的空闲线程。在生产环境,为了避免首次调用超时,可以调用 executor.prestartCoreThread()预创建所有 core 线程,避免来一个创一个带来首次调用慢的问题。

  2. 线程数达到 coreSize 后,新增的任务就放到工作队列里,而线程池里的线程则努力的使用 take()阻塞地从工作队列里拉活来干。

  3. 如果队列是个有界队列,又如果线程池里的线程不能及时将任务取走,工作队列可能会满掉,插入任务就会失败,此时线程池就会紧急的再创建新的临时线程来补救。

  4. 临时线程使用 poll(keepAliveTime,timeUnit)来从工作队列拉活,如果时候到了仍然两手空空没拉到活,表明它太闲了,就会被解雇掉。

  5. 如果 core 线程数+临时线程数 > maxSize,则不能再创建新的临时线程了,转头执行 RejectExecutionHanlder。默认的 AbortPolicy 抛 RejectedExecutionException 异常,其他选择包括静默放弃当前任务(Discard),放弃工作队列里最老的任务(DisacardOldest),或由主线程来直接执行(CallerRuns),或你自己发挥想象力写的一个。

创建多少线程才是合适的

在并发编程领域,提升性能本质上就是提升硬件的利用率,再具体点来说,就是提升 I/O 的利用率和 CPU 的利用率。单核时代多线程用来平衡 I/O 和 CPU。多核时代用来充分利用 CPU。

Spring

Spring IOC

控制反转,是依赖倒置的一种实现形式。

流程

初始化

容器初始化的过程就是将我们定义在 xml 或者使用注解的 bean 信息注册到 ioc 容器的过程。首先会做的是解析元信息,然后将解析到的信息封装成 BeanDefinition 对象,最后将对象保存在 BeanDefinition 容器(一个 hashmap)中。

依赖注入

容器注册完成 BeanDefinition 后,如果 bean 没有设置 lazyInit 会对 bean 进行实例化,如果设置了会在第一次调用时对 bean 实例化。首先取得 BeanDefinition,然后根据里面的信息循环调用得到依赖的 Bean,这里会触发一个第归调用 getBean 方法,直到当前初始化 bean 的所有依赖都得到,将他们注册到当前 bean 的依赖关系中。然后进行当前 bean 的创建,并根据 BeanDefinition 设置它的依赖和属性。

bean 生命周期

  1. Bean 实例的创建
  2. 设置实例的属性
  3. 调用 Bean 初始化方法
  4. 应用可以通过 IOC 容器使用 Bean
  5. 当容器关闭时,调用 Bean 销毁的方法

FactoryBean

FactoryBean 是一个类似于 AbstractFactory,在获取 Bean 的时候如果发现是 FactoryBean 将调用 getObject 返回其生成的对象。

Spring AOP

AOP 面向切面编程,生成代理类

spring 事务传播机制如何实现的

spring aop 使用注意点是什么

  1. 只有 public 方法才能使用 aop 的方式增强实现。因为 spring aop 是通过动态代理实现的
  2. 在类内部的方法互相调用不会被增强实现。因为类内部通过 this 互相调用,而不是 spring 生成的代理对象

Spring Bean 创建过程

分布式缓存设计

问题与解决

MySQL

粒度:服务器,表,行

表级锁与后续操作关系

关系 A 读锁表 A 读其他 A 写 B 读 B 写 B 加读 B 加写
A 加读表 可以 报错 报错 可以 阻塞 可以 阻塞
A 加写表 可以 报错 可以 阻塞 阻塞 阻塞 阻塞

行级锁与后续操作关系

关系 B 加行共享锁(隐式添加共享意向锁) B 加行排他锁(隐式添加排他意向锁) B 加表共享锁 B 加表排他锁
A 加行共享锁(隐式添加共享意向锁) 可以 可以 可以 阻塞
A 加行排他锁(隐式添加排他意向锁) 可以 可以 阻塞 阻塞

意象锁:简化加行级别锁后,再加表锁时的 check

隔离级别

ACID:原子(atomicity),一致性(consistency),隔离性(isolation),持久性(durability)

  脏读 不可重复读 幻读
read uncommitted Y Y Y
read committed N Y Y
repeatable read(default) N N Y
serialized N N N

并发处理事务可能的问题:

  1. 更新丢失
  2. 脏读:读到其他事务未提交的数据
  3. 不可重复读:同一事务中两次读,数据值不同
  4. 幻读:同一事务中两次读,数据数量不同

索引

类型

聚集索引

Inno DB 的聚集索引规则:

执行过程

  1. 链接器(登录用户认证)
  2. 查询缓存(若缓存中存在,则直接返回)
  3. 查询分析器(是否有语法错误)
  4. 优化器(优化查询语句,制定执行计划)
  5. 执行器(操作引擎,返回结果)
  6. 存储器(存储数据,提供读写接口)

ElasticSearch

设计优点:

  1. API 设计的好,简洁易用
  2. 分布式存储,每个索引可以设置分区和备份,防止数据的丢失
  3. 写操作会被转发到主分区,但备份可以进行读操作的计算,增加效率
  4. 一个搜索会在多台机器上分布式的进行,提升搜索效率
  5. 删除和更新,都是先标记为逻辑删除,再新增数据接在后面,可以保证一定的顺序存储,提升读取效率
  6. ES 的倒排索引是不可变得
    • 好处是:不需加锁,可以一直放在缓存中,也可以整块压缩节约 io 和 cpu
    • 坏处是:修改需要重新构建索引

建议

  1. 不要返回很大的结果集。es 被设计为搜索引擎,非常擅长返回满足查询的前几个 document。es 并不擅长返回所有数据。如果一定要这么做需要使用scroll api
  2. 避免很大的 document。默认最大限制是 100MB,可以修改但底层的 lucene 限制为 2GB。大 document 会给网络、磁盘和缓存带来巨大压力。当文章真的很长时,可以考虑改变 doc 的单位,如分段。

Recipes

  1. 混合精确查询和提取词干:对于搜索应用,提取词干(stemming)都是必须的。例如查询 skiing 时,ski 和 skis 都是期望的结果。解决方法是:使用 multi-field。同一份内容,以两种不同的方式来索引存储
  2. 获取一致性的打分。原因就是标注为“已删除”的文档。如你所知,doc 更新或删除时,旧 doc 并不删除,而是标注为“已删除”,只有等到 旧 doc 所在的 segment 被 merge 时,“已删除”的 doc 才会从磁盘删除掉。索引统计(index statistic)是打分时非常重要的一部分,但由于 deleted doc 的存在,在同一个 shard 的不同 copy(即:各个 replica)上 计算出的 索引统计 并不一致。
    • 使用preference参数查询,例如 user session 为xyzabc123,查询为GET /_search?preference=xyzabc123
    • 如果数据很少,可以将所有数据放到一个 shards 中
    • 使用dfs_query_then_fetch搜索类型

但,如果用户就是要查询 skiing 呢?

Redis

Redis 为什么是单线程,如何利用多核 cpu 机器

因为 CPU 不是 Redis 的瓶颈。Redis 的瓶颈最有可能是机器内存或者网络带宽。而且 Redis 操作内存中的数据,采用多线程可能有加锁解锁的性能消耗。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。

如果万一 CPU 成为你的 Redis 瓶颈了,或者,你就是不想让服务器其他核闲置,那怎么办?

那也很简单,你多起几个 Redis 进程就好了。Redis 是 keyvalue 数据库,又不是关系数据库,数据之间没有约束。只要客户端分清哪些 key 放在哪个 Redis 进程上就可以了。redis-cluster 可以帮你做的更好。

如何用 zk 实现分布式锁,与 redis 分布式锁有和优缺点

Redis 设置值和超时时间,设置成功表示加锁成功

Zookeeper 创建临时节点,创建成功就代表加锁成功;失败的监听这个节点,当节点删除后,再次尝试

Tips

ErrorException

Error 和 Exception 都是继承于 Throwable,Error 和 RuntimeException 及其子类称为未检查异常,其它异常成为受检查异常(Checked Exception)。

Error 类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和预防,遇到这样的错误,建议让程序终止。如StackOverFlowError,outOfMemoryError

Exception 类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常

RuntimeException 其特点是 Java 编译器不去检查它,处理 RuntimeException 的原则是:如果出现 RuntimeException,那么一定是程序员的错误。如IndexOutOfBoundsException,RuntimeException

Checked Exception,继承自Exception,必须被显式地捕获或者传递。如IOException,NoSuchFieldException

覆盖equals()

必须满足的约定:自反性,对称性,传递性,一致性,x.equals(null)必须为false

注意:

重写(overloading)与重载(overwrite)

重写是在同一个类中定义的方法名相同但参数不同的一组函数;重载是子类中重写父类中方法,必须与父类中方法同名且参数一致,子类的访问性不能小于父类的定义

final, finally, finalize

Object 类方法

微服务

什么是

微服务是一系列实现不同功能的服务。多个实现特定功能的服务组成一个系统,互相之间通过轻量级的传输协议协作完成功能任务。不同的微服务运行在自己的进程中,互相不干扰。

优点

挑战

设计原则

消息中间件

为什么使用

消息重复问题

避免重复消费,需要保证消息的生产、消费过程的幂等性:

实际经验:监听以太坊交易的处理。

Kafka 数据丢失

情况:

  1. 生产者发送消息时网络错误,以为发送给了 MQ,实际 MQ 没有接收到
    • 设置副本数大于 1
    • 设置活跃 follower 大于 1
    • 设置 leader 需要把消息同步到最小活跃同步 follower 才算写入成功
    • 重试次数和处理设置
  2. MQ 仅接收到消息,还未来得及写入磁盘保存;只写入到 leader 没有同步到 follower 时,leader 挂了
    • 设置副本数大于 1
    • 设置活跃 follower 大于 1
    • 设置 leader 需要把消息同步到最小活跃同步 follower 才算写入成功
    • 重试次数和处理设置
  3. 消费者已经提交了确认,但消费途中没有处理完就 down 了 –> 关闭自动提交确认,手动保证处理完成后提交

Kafka 设计优点

  1. Kafka 使用硬盘存储,但做了很多优化的设计使存储性能很高
    1. 顺序存储,所有的消息在文件后面追加
    2. 通过维护一个 offset 来实现顺序访问
  2. IO 采用 0 拷贝技术,减少了从内核空间读取到用户缓存,再从用户缓存输出到网络流的时间。直接从页缓存写入网络流
  3. 网络带宽上的设计考虑,会对消息做压缩,减少带宽消耗。也可以设置消息批量发送,减少网络请求次数
  4. 分布式存储设计,有备份和主分区,保证消息不丢失。用户通过 offset 也能从宕机事故中快速恢复

Docker

docker 与 虚拟机的区别

Docker 和传统虚拟化方式的不同之处。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。更多

网络协议

TCP

HTTP

HTTP 是应用层协议

HTTP 1.0

多线程

wait()sleep的区别

wait()方法与sleep()方法的不同之处在于:

  1. wait()会释放资源,sleep()不会

    • wait()方法会释放对象的“锁标志”。当调用某一对象的wait()方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了notify()方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的notifyAll()方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。

    • sleep()方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是sleep()方法不会释放“锁标志”,也就是说如果有 synchronized 同步块,其他线程仍然不能访问共享数据。

  2. wait()需要在同步代码块中调用,因为要获取监视器,否则会抛出IllegalMonitorStateException异常
  3. wait()Object的方法,sleepThread的静态方法
  4. wait()需要被唤醒,sleep()不需要

项目

如何确保项目的质量

  1. 开发阶段:单元测试,code review,静态代码扫描(sonar)
  2. 测试阶段:功能测试,非功能测试,兼容性测试,压力测试
  3. 部署上线阶段:预上线,回滚方案,线上监控-

设计模式

SOLID

工厂模式


comments powered by Disqus