跳到主要内容Java 核心面试知识点汇总:集合、多线程、JVM 及锁机制 | 极客日志Javajava算法
Java 核心面试知识点汇总:集合、多线程、JVM 及锁机制
Java 面试常考知识点涵盖数据结构、跨域处理、Tomcat 端口、异常机制、设计模式、对象实例化、序列化、单点登录、表单防重复提交、泛型、集合框架(List/Set/Map)、HashMap 原理、多线程状态与线程池、JVM 内存模型、垃圾回收算法以及锁机制优化等内容。文章详细解析了 HashMap 扩容机制、ConcurrentHashMap 实现、线程安全保证、JMM 规范及四种引用类型,适合准备 Java 后端开发岗位的求职者参考。
2. 什么是跨域?跨域的三要素
跨域指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器施加的安全限制。
- 协议
- 域名
- 端口
注意:localhost 和 127.0.0.1 虽然都指向本机,但也属于跨域。
3. Tomcat 三个默认端口及其作用
- 8005:负责监听关闭 Tomcat 的请求。
- 8009:接受其他服务器的请求(AJP 协议)。
- 8080:用于监听浏览器发送的 HTTP 请求。
4. throw 和 throws 的区别
- throw:抛出一个异常对象。
- throws:声明一个方法可能抛出的异常类型。
5. 熟悉的设计模式
- 单例模式:保证被创建一次,节省系统开销。
- 工厂模式:解耦代码,将对象的创建与使用分离。
- 观察者模式:定义对象之间的一对多依赖,当一个对象改变时,所有依赖者都会收到通知并自动更新。
- 代理模式:代理对象具备被代理对象的功能,能够在操作执行的前后对操作进行增强处理。
- 模板模式:减少代码冗余,例如 Redis 模板。
6. 实例化对象的方式
- new
- clone()
- 反射 (Reflection)
- 先序列化再反序列化
7. 什么样的类不能被实例化
8. 序列化和反序列化
- 序列化:把对象转为字节序列的过程,在传递和保存对象时,保证了对象的完整性和可传递性,便于在网络传输和保存在本地文件中。
- 反序列化:把字节序列转为对象的过程,通过字节流的状态和信息描述,来重建对象。
9. 序列化的优点
将对象转为字节流存储到硬盘上,当 JVM 进程终止的话,字节流还会在硬盘上等待,等待下一次 JVM 的启动,把序列化的对象,通过反序列化为原来的对象,减少储存空间和方便网络传输(因为是二进制)。
10. 什么是单点登录
单点登录 (SSO: Single Sign On):同一账号在多系统中,只登录一次,就可以访问其他系统。多个系统,统一登录。
例如:在一个公司下,有多个系统,比如淘宝和天猫,你登录上淘宝,就不用再去登录天猫了。
11. 实现单点登录的方式
- Cookie:用 cookie 为媒介,存放用户凭证。登录上父应用,返回一个加密的 cookie,访问子应用的时候,会对 cookie 解密校验,通过就可以登录。不安全和不能跨域免登。
- 分布式 Session 实现:用户第一次登录,会把用户信息记录下来,写入 session,再次登录查看 session 是否含有对应信息。Session 系统不共享,使用缓存等方式来解决。
- 重定向:父应用提供一个 GET 方式的登录接口 A,用户通过子应用重定向连接的方式访问这个接口,如果用户还没有登录,则返回一个登录页面,用户输入账号密码进行登录,如果已经登录,则生成加密的 token,并且重定向到子应用提供的验证 token 的接口 B,通过解密和校验之后,子应用登录当前用户。虽然解决了安全和跨域,但是没前两种简单。
12. SSO 与 OAuth2.0 的区别
- 单点登录:就是一个公司多个子系统登录问题。
- OAuth2.0:是授权问题,比如微信授权问题。是一种具体的协议。
13. 如何防止表单重复提交
- JS 屏蔽提交按钮。
- 给数据库添加唯一约束。
- 利用 Session 防止表单重复提交。会有一个 token 标记,表单提交的时候拦截器会检查是否一致,不一致就不通过。
- 使用 AOP 切入实现。自定义注解,然后新增切入点,然后每次都记录过期时间,然后做比较。
14. 泛型是什么?有什么好处?
本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
好处:
- 类型安全
- 消除强制类型转换
- 提高性能
- 提高代码的复用性
15. 值传递和引用传递
- 值传递:函数调用时会把实际参数,复制一份到函数中,函数中对参数进行操作,并不会影响参数实际的值。
- 引用传递:将实际参数的地址值传递到函数中,函数对参数进行操作,会影响到实际参数的值。
注意:Java 中不存在引用传递(即使传的是对象,那也只是传递了对象的引用地址的副本,也属于值传递)。
二、Java 集合
1. List、Set、Map 的区别
- List:有序、可重复的单列集合。
- Set:无序、不可重复的单列集合。
- Map:无序、Key 不可重复,Value 可重复的双列集合。
2. List、Set、Map 常用集合有哪些?
- Vector:底层是数组,方法加了 synchronized 来保证线程安全,所以效率较慢,建议使用 ArrayList 替代。
- ArrayList:线程不安全,底层是数组。因为数组都是连续的地址,所以查询比较快。增删比较慢,增会生成一个新数组,把新增的元素和原有元素放到新数组中,删除会导致元素移动,所以增删速度较慢。
- LinkedList:线程不安全,底层是链表。因为地址不是连续的,都是一个节点和一个节点相连,每次查询都得重头开始查询,所以查询慢,增删只是断裂某个节点对整体影响不大,所以增删速度较快。
- HashSet:底层是哈希表(数组 + 链表或数组 + 红黑树),在链表长度大于 8 时转为红黑树,在红黑树节点小于 6 时转为链表。其实就是实现了 HashMap,值存入 Key,Value 是一个 final 修饰的对象。
- TreeSet:底层是红黑树结构,就是 TreeMap 实现,可以实现有序的集合。String 和 Integer 可以根据值进行排序。如果是对象需要实现 Comparator 接口,重写 compareTo() 方法制定比较规则。
- LinkedHashSet:实现了 HashSet,多一条链表来记录位置,所以是有序的。
- TreeMap:底层是红黑树,Key 可以按顺序排列。
- HashMap:底层是哈希表,可以很快的储存和检索,无序,大量迭代情况不佳。
- LinkedHashMap:底层是哈希表 + 链表,有序,大量迭代情况佳。
3. ArrayList 的初始容量是多少?扩容机制是什么?扩容过程是怎样?
- 初始容量:默认 10,也可以通过构造方法传入大小。
- 扩容机制:原数组长度 + 原数组长度/2(源码中是原数组右移一位,也就相当于除以 2)。
注意:扩容后的 ArrayList 底层数组不是原来的数组。
- 扩容过程:因为 ArrayList 底层是数组,所以它的扩容机制和数组一样,首先新建一个新数组,长度是原数组的 1.5 倍,然后调用 Arrays.copyOf() 复制原数组的值,然后赋值给新数组。
4. 什么是哈希表
根据关键码值 (Key value) 而直接进行访问的数据结构,在一个表中,通过 H(key) 计算出 key 在表中的位置,H(key) 就是哈希函数,表就是哈希表。
5. 什么是哈希冲突
不同的 key 通过哈希函数计算出相同的储存地址,这就是哈希冲突。
6. 解决哈希冲突
- 开放地址法:如果发生哈希冲突,就会以当前地址为基准,再去寻找计算另一个位置,直到不发生哈希冲突。寻找的方法有:线性探测、二次探测、随机探测。
- 链地址法:冲突的哈希值,连到同一个链表上。
- 再哈希法:多个哈希函数,发生冲突,就在用另一个计算,直到没有冲突。
- 建立公共溢出区:哈希表分成基本表和溢出表,与基本表发生冲突的都填入溢出表。
7. HashMap 的 hash() 算法,为什么不是 h=key.hashCode(),而是 key.hashCode() ^ (h>>>16)
得到哈希值然后右移 16 位,然后进行异或运算,这样使哈希值的低 16 位也具有了一部分高 16 位的特性,增加更多的变化性,减少了哈希冲突。
8. 为什么 HashMap 的初始容量和扩容都是 2 的次幂
因为计算元素存储的下标是 (n-1) & 哈希值,数组初始容量 -1,得到的二进制都是 1,这样可以减少哈希冲突,可以更好的均匀插入。
9. HashMap 如果指定了不是 2 的次幂的容量会发生什么
会获得一个大于指定的初始值的最接近 2 的次幂的值作为初始容量。
10. HashMap 为什么线程不安全
JDK 1.7 中因为使用头插法,再扩容的时候,可能会造成闭环和数据丢失。
JDK 1.8 中使用尾插法,不会出现闭环和数据丢失,但是在多线程下,会发生数据覆盖。(put 操作中,在 putVal 函数里) 值的覆盖还有长度的覆盖。
11. 解决 HashMap 的线程安全问题
- 使用 Hashtable 解决,在方法加同步关键字,所以效率低下,已经被弃用。
- 使用 Collections.synchronizedMap(new HashMap<>()),不常用。
- ConcurrentHashMap(常用)。
12. ConcurrentHashMap 的原理
- JDK 1.7:采用分段锁,是由 Segment(继承 ReentrantLock:可重入锁,默认是 16,并发度是 16) 和 HashEntry 内部类组成,每一个 Segment(锁) 对应 1 个 HashEntry(key, value) 数组,数组之间互不影响,实现了并发访问。
- JDK 1.8:抛弃分段锁,采用 CAS(乐观锁)+synchronized 实现更加细粒度的锁,Node 数组 + 链表 + 红黑树结构。只要锁住链表的头节点 (树的根节点),就不会影响其他数组的读写,提高了并发度。
13. 为什么用 synchronized 代替 ReentrantLock
- 节省内存开销。ReentrantLock 基于 AQS 来获得同步支持,但不是每个节点都需要同步支持,只有链表头节点或树的根节点需要同步,所以使用 ReentrantLock 会带来很大的内存开销。
- 获得 JVM 支持,可重入锁只是 API 级别,而 synchronized 是 JVM 直接支持的,能够在 JVM 运行时做出相应的优化。
- 在 JDK 1.6 之后,对 synchronized 做了大量的优化,而且有多种锁状态,会从 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁一步步转换。
AQS (Abstract Queued Synchronizer):一个抽象的队列同步器,通过维护一个共享资源状态(Volatile Int State)和一个先进先出(FIFO)的线程等待队列来实现一个多线程访问共享资源的同步框架。
14. HashMap 为什么使用链表
15. HashMap 为什么使用红黑树
16. HashMap 为什么不一上来就使用红黑树
维护成本较大,红黑树在插入新的数据后,可能会进行变色、左旋、右旋来保持平衡,所以当数据少时,就不需要红黑树。
17. 说说你对红黑树的理解
- 根节点是黑色。
- 节点是黑色或红色。
- 叶子节点是黑色。
- 红色节点的子节点都是黑色。
- 从任意节点到其子节点的所有路径都包含相同数目的黑色节点。
红黑树从根到叶子节点的最长路径不会超过最短路径的 2 倍。保证了红黑树的高效。
18. 为什么链表长度大于 8,并且表的长度大于 64 的时候,链表会转换成红黑树
因为链表长度越长,哈希冲突概率就越小,当链表等于 8 时,哈希冲突就非常低了,是千万分之一,我们的 map 也不会存那么多数据,如果真要存那么多数据,那就转为红黑树,提高查询和插入的效率。
19. 为什么转成红黑树是 8 呢?而重新转为链表阈值是 6 呢
因为如果都是 8 的话,那么会频繁转换,会浪费资源。
20. 为什么负载因子是 0.75
加载因子越大,填满的元素越多,空间利用率越高,但发生冲突的机会变大了;
加载因子越小,填满的元素越少,冲突发生的机会减小,但空间浪费了更多了,而且还会提高扩容 rehash 操作的次数。
'冲突的机会'与'空间利用率'之间,寻找一种平衡与折中。
又因为根据泊松分布,当负载因子是 0.75 时,平均值时 0.5,带入可得,当链表为 8 时,哈希冲突发生概率就很低了。
21. 什么时候会扩容
元素个数 > 数组长度 * 负载因子 例如 16 * 0.75 = 12,当元素超过 12 个时就会扩容。
链表长度大于 8 并且表长小于 64,也会扩容。
22. 为什么不是满了扩容
因为元素越多,空间利用率是高了,但是发生哈希冲突的几率也增加了。
23. 扩容过程
- JDK 1.7:会生成一个新 table,重新计算每个节点放进新 table,因为是头插法,在线程不安全的时候,可能会出现闭环和数据丢失。
- JDK 1.8:会生成一个新 table,新位置只需要看 (e.hash & oldCap) 结果是 0 还是 1,0 就放在旧下标,1 就是旧下标 + 旧数组长度。避免了对每个节点进行 hash 计算,大大提高了效率。e.hash 是数组的 hash 值,oldCap 是旧数组的长度。
24. HashMap 和 Hashtable 的区别
- HashMap,运行 key 和 value 为 null,Hashtable 不允许为 null。
- HashMap 线程不安全,Hashtable 线程安全。
25. 集合为什么要用迭代器 (Iterator)
更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。
如果不用迭代器,只能 for 循环,还必须知道集合的数据结构,复用性不强。
三、多线程
1. 线程是什么?多线程是什么?
- 线程:是最小的调度单位,包含在进程中。
- 多线程:多个线程并发执行的技术。
2. 守护线程和用户线程
- 守护线程:JVM 给的线程。比如:GC 守护线程。
- 用户线程:用户自己定义的线程。比如:main() 线程。
拓展:Thread.setDaemon(false) 设置为用户线程,Thread.setDaemon(true) 设置为守护线程。
3. 线程的各个状态
- 新建 (New):新建一个线程。
- 就绪 (Runnable):抢夺 CPU 的使用权。
- 运行 (Running):开始执行任务。
- 阻塞 (Blocked):让线程等待,等待结束进入就绪队列。
- 死亡 (Dead):线程正常结束或异常结束。
4. 线程相关的基本方法
wait,notify,notifyAll,sleep,join,yield 等。
- wait():线程等待,会释放锁,用于同步代码块或同步方法中,进入等待状态。
- sleep():线程睡眠,不会释放锁,进入超时等待状态。
- yield():线程让步,会使线程让出 CPU 使用权,进入就绪状态。
- join():指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。
- notify():随机唤醒一个在等待中的线程,进入就绪状态。
- notifyAll():唤醒全部在等待中的线程,进入就绪状态。
5. wait() 和 sleep() 的区别
- wait() 来自 Object,sleep() 来自 Thread。
- wait() 会释放锁,sleep() 不会释放锁。
- wait() 只能用在同步方法或代码块中,sleep() 可以用在任何地方。
- wait() 不需要捕获异常,sleep() 需要捕获异常。
6. 为什么 wait()、notify()、notifyAll() 方法定义在 Object 类里面,而不是 Thread 类
- 锁可以是任何对象,如果在 Thread 类中,那只能是 Thread 类的对象才能调用上面的方法了。
- Java 中进入临界区 (同步代码块或同步方法),线程只需要拿到锁就行,而并不关心锁被那个线程持有。
- 上面方法是 Java 两个线程之间的通信机制,如果不能通过类似 synchronized 这样的 Java 关键字来实现这种机制,那么 Object 类中就是定义它们最好的地方,以此来使任何 Java 对象都可以拥有实现线程通信机制的能力。
7. start() 和 run() 的区别
- start() 方法:是启动线程,调用了之后线程会进入就绪状态,一旦拿到 CPU 使用权就开始执行 run() 方法,不能重复调用 start(),否则会报异常。
- run() 方法:就相当于一个普通的方法而已。直接调用 run() 方法就还只有一个主线程,还是会顺序执行,也可以重复调用 run() 方法。
8. 实现多线程的方式
- 继承 Thread 类。
- 实现 Runnable 接口。
- 实现 Callable 接口。
- 线程池。
9. Runnable 和 Callable 的区别
- Runnable 没有返回值,Callable 有返回值。
- Runnable 只能抛出异常,不能捕获,Callable 能抛出异常,也能捕获。
10. 线程池的好处
- 线程是稀缺资源,使用线程池可以减少线程的创建和销毁,每个线程都可重复使用。
- 可以根据系统的需求,调整线程池里面线程的个数,防止了因为消耗内存过多导致服务器崩溃。
11. 线程池的七大参数
- corePoolSize:核心线程数,创建不能被回收,可以设置被回收。
- maximumPoolSize:最大线程数。
- keepAliveTime:空闲线程存活时间。
- unit:单位。
- workQueue:等待队列。
- threadFactory:线程工厂,用于创建线程。
- handler:拒绝策略。
12. 线程池的执行过程
- 接到任务,判断核心线程池是否满了,没满执行任务,满了放入等待队列。
- 等待队列没满,存入队列,等待执行,满了去查看最大线程数。
- 最大线程数没满,执行任务,满了执行拒绝策略。
13. 四大方法
- Executors.newCachedThreadPool():创建一个缓存线程池,灵活回收线程,任务过多,会 OOM。
- Executors.newFixedThreadPool():创建一个指定线程数量的线程池。提高了线程池的效率和线程的创建的开销,等待队列可能堆积大量请求,导致 OOM。
- Executors.newSingleThreadPool():创建一个单线程,保证线程的有序,出现异常再次创建,速度没那么快。
- Executors.newScheduleThreadPool():创建一个定长的线程池,支持定时及周期性任务执行。
14. 四大拒绝策略
- AbortPolicy():添加线程池被拒绝,会抛出异常 (默认策略)。
- CallerRunsPolicy():添加线程池被拒绝,不会放弃任务,也不会抛出异常,会让调用者线程去执行这个任务。
- DiscardPolicy():添加线程池被拒绝,丢掉任务,不抛异常。
- DiscardOldestPolicy():添加线程池被拒绝,会把线程池队列中等待最久的任务放弃,把拒绝任务放进去。
15. shutdown 和 shutdownNow 的区别
- shutdown 没有返回值,shutdownNow 会返回没有执行完任务的集合。
- shutdown 不会抛出异常,shutdownNow 会抛出异常。
- shutdown 会等待执行完线程池的任务在关闭,shutdownNow 会给所有线程发送中断信号,然后中断任务,关闭线程池。
16. 什么是死锁
各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进的现象。
17. 造成死锁的四个必要条件
- 互斥:当资源被一个线程占用时,别的线程不能使用。
- 不可抢占:进程阻塞时,对占用的资源不释放。
- 不剥夺:进程获得资源未使用完,不能被强行剥夺。
- 循环等待:若干进程之间形成头尾相连的循环等待资源关系。
18. 线程安全主要是三方面
- 原子性:一个或多个操作,要么全部执行,要么全部不执行 (执行的过程中是不会被任何因素打断的)。
- 可见性:一个线程对主内存的修改可以及时的被其他线程观察到。
- 有序性:程序执行的顺序按照代码的先后顺序执行。
保证原子性:使用锁 synchronized 和 lock。使用 CAS (compareAndSet:比较并交换),CAS 是 CPU 的并发原语)。
保证可见性:使用锁 synchronized 和 lock。使用 volatile 关键字。
保证有序性:使用 volatile 关键字。使用 synchronized 关键字。
19. volatile 和 synchronized 的区别
- volatile 仅能使用在变量级别的,synchronized 可以使用在变量、方法、类级别的。
- volatile 不具备原子性,具备可见性,synchronized 有原子性和可见性。
- volatile 不会造成线程阻塞,synchronized 会造成线程阻塞。
- volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比 synchronized 要好。
20. synchronized 和 lock 的区别
- synchronized 是关键字,lock 是 java 类,默认是不公平锁(源码)。
- synchronized 适合少量同步代码,lock 适合大量同步代码。
- synchronized 会自动释放锁,lock 必须放在 finally 中手工 unlock 释放锁,不然容易死锁。
21. JMM (Java 内存模型)
Java 内存模型,一个抽象的概念,不是真实存在,描述的是一种规则或规范,和多线程相关的规则。需要每个 JVM 都遵循。
22. JMM 的约定
- 线程解锁前,必须把共享变量立即刷回主存。
- 线程加锁前,必须读取主存中的最新值到工作内存中。
- 加锁和解锁必须是同一把锁。
23. JMM 的八个命令
为了支持 JMM,定义了 8 条原子操作,用于主存和工作内存的交互。
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
- load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以遍随后的 write 的操作。
- write(写入):作用于主内存的变量,它把 store 操作从工作内存中的一个变量的值传送到主内存的变量中。
24. 为什么要有 JMM,用来解决什么问题
解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。
四、JVM
1. JVM 是什么
Java 虚拟机,是实现 Java 跨平台的核心组件。
2. JVM 的作用
Java 中所有的类,必须被装载到 JVM 中才能使用,装载由类加载器完成,.class 这个类型可以在虚拟机运行,但不是直接和操作系统交互,需要 JVM 解释给操作系统,解释的时候需要 Java 类库,这样就能和操作系统交互。
3. Java 文件的加载过程
.java -> .class -> 类加载器 -> JVM
4. JDK、JRE、JVM 的区别
- JDK:包含 Java 运行环境和开发环境、JVM、Java 类库。
- JRE:包含 Java 运行环境和 JVM、Java 类库。
- JVM:Java 虚拟机,是跨平台的核心组件。
5. 类加载器的作用
将.class 文件装载到 JVM 中,实质就是把文件从硬盘写到内存。
6. 类加载器的类型
- 引导类加载器 (Bootstrap ClassLoader):C++ 编写,JVM 自带的加载器,负责加载 Java 核心类库,该加载器无法直接获取。
- 拓展类加载器 (Extension ClassLoader):加载 jre/lib/ext 目录下的 jar 包。
- 系统类加载器 (Application ClassLoader):加载当前项目目录下的类或 jar 包,最常用的加载器。
- 自定义加载器 (Custom ClassLoader):开发人员自定义的。需要继承 ClassLoader。
7. 双亲委派机制的加载过程
- 接到类加载的请求。
- 向上委托给父类加载器,直到引导类加载器。
- 引导类加载器检查能否加载当前这个类,如果能,使用当前加载器,请求结束,如果不能,抛出异常,通知子加载器进行加载。
- 重复步骤 3。
8. 双亲委派机制的优缺点
- 优点:保证类加载的安全性,不管哪个类被加载,都会被委托给引导类加载器,只有类加载器不能加载,才会让子加载器加载,这样保证最后得到的对象都是同样的一个。
- 缺点:子加载器可以使用父加载器加载的类,而父加载器不能使用子加载器加载的类。
9. 为什么要打破双亲委派机制
子加载器可以使用父加载器加载的类,而父加载器不能使用子加载器加载的类。
例如:使用 JDBC 连接数据库,需要用到 com.mysql.jdbc.Driver 和 DriverManager 类。然而 DriverManager 被引导类加载器所加载,而 com.mysql.jdbc.Driver 被当前调用者的加载器加载,使用引导类加载器加载不到,所以要打破双亲委派机制。
10. 打破双亲委派机制的方式
- 自定义类加载器,重写 loadClass 方法。
- 使用线程上下文类 (ServiceLoader:使父加载器可以加载子加载器的类)。
11. JVM 的每个部分储存的都是什么
- 方法区 (线程共享):常量池、静态 (static) 变量以及方法信息 (方法名、返回值、参数、修饰符等) 等。
- 堆 (线程共享):是虚拟机内存中最大的一块,储存的是实例对象和数组。
- 本地方法栈 (线程不共享):调用的本地方法,被 native 修饰的方法,Java 不能直接操作操作系统,所以需要 native 修饰的方法帮助。
- 虚拟机栈 (线程不共享):8 大基本类型、对象引用、实例方法。
- 程序计数器 (线程不共享):每个线程启动是都会创建一个程序计数器,保存的是正在执行的 JVM 指令,程序计数器总是指向下一条将被执行指令的地址。
12. 内存溢出 (OOM) 和栈溢出
-
内存溢出的原因:
- 内存使用过多或者无法垃圾回收的内存过多,使运行需要的内存大于提供的内存。
- 长期持有某些资源并且不释放,从而使资源不能及时释放,也称为内存泄漏。
-
解决:
- 进行 JVM 调优。-Xmx:JVM 最大内存。-Xms:启动初始内存。-Xmn:新生代大小。-Xss:每个虚拟机栈的大小。
- 使用专业工具测试。
-
手动制造:一直 new 对象就 ok。
-
栈溢出原因:线程请求的栈容量大于分配的栈容量。
-
解决:
- 修改代码。
- 调优 -Xss。
-
手动制造:一直调用实例方法。
13. 垃圾回收的作用区域
作用在方法区和堆,主要实在堆中的 Eden 区。年轻代分为 (Eden 区和幸存区)。
14. 怎么判断对象是否可回收
- 可达性分析算法:简单来说就是一个根对象通过引用链向下走,能走到的对象都是不可回收的。可作为根对象有:虚拟机栈的引用的对象,本地栈的引用的对象,方法区引用的静态和常量对象。
- 引用计数算法:每个对象都添加一个计数器,每多一个引用指向对象,计数器就加一,如果计数器为零,那么就是可回收的。
15. 四种引用类型
- 强引用:基于可达性分析算法,只有当对象不可达才能被回收,否则就算 JVM 满了,也不会被回收,会抛出 OOM。
- 软引用:一些有用但是非必须的对象,当 JVM 即将满了,会将软引用关联对象回收,回收之后如果内存还是不够,会抛出 OOM。
- 弱引用:不论内存是否够,只要开始垃圾回收,弱引用的关联对象就会被回收。
- 虚引用:最弱的引用和没有一样,随时可能被回收。
16. 垃圾回收算法
- 标记 - 清除算法 (适用老年代):先把可回收的对象进行标记,然后再进行清除。
- 优点:算法简单。
- 缺点:产生大量的内存碎片,效率低。
- 复制算法 (适用年轻代):把内存分成两个相同的块,一个是 from,一个是 to,每次只使用一个块,当一个块满了,就把存活的对象放到另一个块中,然后清空当前块。主要用在年轻区中的幸存区。
- 优点:效率较高,没有内存碎片。
- 缺点:内存利用率低。
- 标记 - 整理算法 (适用老年代):标记 - 清除算法的升级版,也叫标记 - 压缩算法,先进行标记,然后让存活对象向一端移动,然后清除掉边界以外的内存。
- 优点:解决了内存利用率低和避免了内存碎片。
- 缺点:增加了一个移动成本。
17. 轻 GC(Minor GC)和 重 GC(Full GC)
- 轻 GC:普通 GC,当新对象在 Eden 园区申请内存失败时,进行轻 GC,会回收可回收对象,没有被回收的对象进入幸存区,新对象分配内存极大部分都是在 Eden 园区,所以这个区 GC 比较频繁。一个对象经历 15 次 GC,会进入老年区,可以设置。
- 重 GC:全局 GC,对整个堆进行回收,所以要比轻 GC 慢,因此要减少重 GC,我们所说的 JVM 调优,大部分都是针对重 GC。
18. 什么时候会发生重 GC
- 当老年区满了会重 GC:年轻区对象进入或创建大对象会满。
- 永久代满了会重 GC。
- 方法区满了会重 GC。
- system.gc() 会重 GC。
- 轻 GC 后,进入老年代的大小大于老年代的可用内存会,第一次轻 GC 进入老年代要 2MB,第二次的时候会判断是否大于 2MB,不满足就会重 GC。
五、锁
1. 悲观锁和乐观锁
- 悲观锁:在修改数据时,一定有别的线程来使用,所以在获取数据的时候会加锁。Java 中的 synchronized 和 Lock 都是悲观锁。
- 乐观锁:在修改数据时,一定没有别的线程来使用,所以不会添加锁。但是在更新数据的时候,会查看有没有线程修改数据。比如:版本号和 CAS 原理 (无锁算法)。
2. 悲观锁和乐观锁的场景
- 悲观锁:更适合写操作多的场景,因为先加锁可以保证数据的正确。
- 乐观锁:更适合读操作多的场景,因为不加锁会让读操作的性能提升。
3. 自旋锁和自适应自旋锁
前言:因为线程竞争,会导致线程阻塞或者挂起,但是如果同步资源的锁定时间很短,那么阻塞和挂起的花费的资源就得不偿失。
- 自旋锁:当竞争的同步资源锁定时间短,就让线程自旋,如果自旋完成后,资源释放了锁,那线程就不用阻塞,直接获取资源,减少了切换线程的开销。实现原理是 CAS。
缺点:占用了处理器的时间,如果锁被占用的时间短还好,如果长那就白白浪费了处理器的时间。所以要限定自旋次数(默认是 10 次,可以使用 -XX:PreBlockSpin 来更改)没有成功获得锁,就应当挂起线程。
- 自适应自旋锁:自旋次数不固定,是由上一个在同一个锁上的自旋时间和锁拥有者的状态决定。如果在同一个锁对象上,自旋刚刚获得锁,并且持有锁的线程在运行,那么虚拟机会认为这次自旋也可能成功,那么自旋的时间就会比较长,如果某个锁,自旋没成功获得过,那么可能就会直接省掉自旋,进入阻塞,避免浪费处理器时间。
4. 无锁、偏向锁、轻量级锁、重量级锁
这四个锁是专门针对 synchronized 的,在 JDK 1.6 中,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态。级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。
- 无锁:就是乐观锁。
- 偏向锁:当只有一个线程访问加锁的资源,不存在多线程竞争的情况下,那么线程不需要重复获取锁,这时候就会给线程加一个偏向锁。(对比 Mark Word 解决加锁问题,避免 CAS 操作)
- 轻量级锁:是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。(CAS+ 自旋)
- 重量级锁:若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。(将除了拥有锁的线程以外的线程都阻塞)
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online