前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >今年后端爆了???

今年后端爆了???

作者头像
沉默王二
发布2024-05-06 14:43:22
1440
发布2024-05-06 14:43:22
举报
文章被收录于专栏:沉默王二

大家好,我是二哥呀。

每次登录牛客,看到最多的就是各种 Java 后端岗位的喜讯,美团 OC了、快手 OC 了、就连腾讯 OC 的都是 Java 岗,我怀疑牛客是不是给我打了“只报喜不报忧”的标签?

星球里也有不少球友给我发来喜讯,难道说每年都在凉凉的 Java 后端又承担起了就业的重任?!

不可能,绝对不可能,这一切都是假象!反正我敢肯定,还有不少同学在嗷嗷叫,尤其是 24 届春招还没上岸的,25 届没找到暑期实习的(😭)。

我只能说拿到 offer 的,这个假期就放肆几天吧;没拿到的要不就苟一苟,继续背背八股,优化优化简历?也许好运就要降临到你头上了。

这次我们就以《Java 面试指南——携程面经》为例,继续来看看携程这家不错的互联网中厂面试官都喜欢问哪些问题,好做到知彼知己百战不殆,我会用通俗易懂+手绘图的方式,让天下所有的面渣都能逆袭 😁

二哥的 Java 面试指南

内容较长,建议正在冲刺 24 届春招和 25 届暑期实习、秋招的同学先收藏起来,面试的时候大概率会碰到,

携程面经(详细)

对象创建到销毁,内存如何分配的,(类加载和对象创建过程,CMS,G1内存清理和分配)

当我们使用 new 关键字创建一个对象的时候,JVM 首先会检查 new 指令的参数是否能在常量池中定位到一个类的符号引用,然后检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就先执行相应的类加载过程。

如果已经加载,JVM 会为新生对象分配内存,内存分配完成之后,JVM 将分配到的内存空间初始化为零值(成员变量,数值类型是 0,布尔类型是 false,对象类型是 null),接下来设置对象头,对象头里包含了对象是哪个类的实例、对象的哈希码、对象的 GC 分代年龄等信息。

最后,JVM 会执行构造方法(<init>),将成员变量赋值为预期的值,这样一个对象就创建完成了。

二哥的 Java 进阶之路:对象的创建过程

对象的销毁过程了解吗?

对象创建完成后,就可以通过引用来访问对象的方法和属性,当对象不再被任何引用指向时,对象就会变成垃圾。

垃圾收集器会通过可达性分析算法判断对象是否存活,如果对象不可达,就会被回收。

垃圾收集器会通过标记清除、标记复制、标记整理等算法来回收内存,将对象占用的内存空间释放出来。

常用的垃圾收集器有 CMS、G1、ZGC 等,它们的回收策略和效率不同,可以根据具体的场景选择合适的垃圾收集器。

内存如何分配的?

在堆内存分配对象时,主要使用两种策略:指针碰撞和空闲列表。

三分恶面渣逆袭:指针碰撞和空闲列表

①、指针碰撞(Bump the Pointer)

假设堆内存是一个连续的空间,分为两个部分,一部分是已经被使用的内存,另一部分是未被使用的内存。

在分配内存时,Java 虚拟机维护一个指针,指向下一个可用的内存地址,每次分配内存时,只需要将指针向后移动(碰撞)一段距离,然后将这段内存分配给对象实例即可。

②、空闲列表(Free List)

JVM 维护一个列表,记录堆中所有未占用的内存块,每个空间块都记录了大小和地址信息。

当有新的对象请求内存时,JVM 会遍历空闲列表,寻找足够大的空间来存放新对象。

分配后,如果选中的空闲块未被完全利用,剩余的部分会作为一个新的空闲块加入到空闲列表中。

指针碰撞适用于管理简单、碎片化较少的内存区域(如年轻代),而空闲列表适用于内存碎片化较严重或对象大小差异较大的场景(如老年代)。

能详细说一下 CMS 收集器的垃圾收集过程吗?

三分恶面渣逆袭:Concurrent Mark Sweep收集器运行示意图

CMS(Concurrent Mark Sweep)分 4 大步进行垃圾收集:

  • 初始标记(Initial Mark):标记所有从 GC Roots 直接可达的对象,这个阶段需要 STW,但速度很快。
  • 并发标记(Concurrent Mark):从初始标记的对象出发,遍历所有对象,标记所有可达的对象。这个阶段是并发进行的,STW。
  • 重新标记(Remark):完成剩余的标记工作,包括处理并发阶段遗留下来的少量变动,这个阶段通常需要短暂的 STW 停顿。
  • 并发清除(Concurrent Sweep):清除未被标记的对象,回收它们占用的内存空间。
G1 垃圾收集器了解吗?

G1 收集器的运行过程大致可划分为这几个步骤:

①、并发标记,G1 通过并发标记的方式找出堆中的垃圾对象。并发标记阶段与应用线程同时执行,不会导致应用线程暂停。

②、混合收集,在并发标记完成后,G1 会计算出哪些区域的回收价值最高(也就是包含最多垃圾的区域),然后优先回收这些区域。这种回收方式包括了部分新生代区域和老年代区域。

选择回收成本低而收益高的区域进行回收,可以提高回收效率和减少停顿时间。

③、可预测的停顿,G1 在垃圾回收期间仍然需要「Stop the World」。不过,G1 在停顿时间上添加了预测机制,用户可以 JVM 启动时指定期望停顿时间,G1 会尽可能地在这个时间内完成垃圾回收。

三分恶面渣逆袭:G1收集器运行示意图

ThreadLocal,(作用,演进,软指针,删除过程)

ThreadLocal 是 Java 中提供的一种用于实现线程局部变量的工具类。它允许每个线程都拥有自己的独立副本,从而实现线程隔离,用于解决多线程中共享对象的线程安全问题。

三分恶面渣逆袭:ThreadLocal线程副本

ThreadLocal 本身并不存储任何值,它只是作为一个映射,来映射线程的局部变量。当一个线程调用 ThreadLocal 的 set 或 get 方法时,实际上是访问线程自己的 ThreadLocal.ThreadLocalMap。

二哥的 Java 进阶之路

ThreadLocalMap 是 ThreadLocal 的静态内部类,它内部维护了一个 Entry 数组,key 是 ThreadLocal 对象,value 是线程的局部变量本身。

三分恶面渣逆袭:ThreadLoca结构图

早期的 ThreadLocal 不是这样的,它的 ThreadLocalMap 中使用 Thread 作为 key,这也是最简单的实现方式。

黑马:JDK 早期设计

优化后的方案有两个好处,一个是 Map 中存储的键值对变少了;另一个是 ThreadLocalMap 的生命周期和线程一样长,线程销毁的时候,ThreadLocalMap 也会被销毁。

Entry 继承了 WeakReference,它限定了 key 是一个弱引用,弱引用的好处是当内存不足时,JVM 会回收 ThreadLocal 对象,并且将其对应的 Entry 的 value 设置为 null,这样在很大程度上可以避免内存泄漏。

使用完 ThreadLocal 后,及时调用 remove() 方法释放内存空间。

代码语言:javascript
复制
try {
    threadLocal.set(value);
    // 执行业务操作
} finally {
    threadLocal.remove(); // 确保能够执行清理
}

remove() 方法会将当前线程的 ThreadLocalMap 中的所有 key 为 null 的 Entry 全部清除。

代码语言:javascript
复制
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

public void clear() {
    this.referent = null;
}

线程上下文切换(我答的内核态和用户态切换时机,和切换需要加载哪些内容)

使用多线程的目的是为了充分利用 CPU,但是我们知道,并发其实是一个 CPU 来应付多个线程。

三分恶面渣逆袭:线程切换

为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是上下文切换。

三分恶面渣逆袭:上下文切换时机

cas和aba(原子操作+时间戳)

CAS(Compare-and-Swap)是一种乐观锁的实现方式,全称为“比较并交换”,是一种无锁的原子操作。

在 Java 中,我们可以使用 synchronized关键字和 CAS 来实现加锁效果。

synchronized 是悲观锁,尽管随着 JDK 版本的升级,synchronized 关键字已经“轻量级”了很多,但依然是悲观锁,线程开始执行第一步就要获取锁,一旦获得锁,其他的线程进入后就会阻塞并等待锁。

CAS 是乐观锁,线程执行的时候不会加锁,它会假设此时没有冲突,然后完成某项操作;如果因为冲突失败了就重试,直到成功为止。

在 CAS 中,有这样三个值:

  • V:要更新的变量(var)
  • E:预期值(expected)
  • N:新值(new)

比较并交换的过程如下:

判断 V 是否等于 E,如果等于,将 V 的值设置为 N;如果不等,说明已经有其它线程更新了 V,于是当前线程放弃更新,什么都不做。

这里的预期值 E 本质上指的是“旧值”。

这个比较和替换的操作是原子的,即不可中断,确保了数据的一致性。

什么是 ABA 问题?如何解决?

如果一个位置的值原来是 A,后来被改为 B,再后来又被改回 A,那么进行 CAS 操作的线程将无法知晓该位置的值在此期间已经被修改过。

可以使用版本号/时间戳的方式来解决 ABA 问题。

比如说,每次变量更新时,不仅更新变量的值,还更新一个版本号。CAS 操作时不仅要求值匹配,还要求版本号匹配。

Java 的 AtomicStampedReference 类就实现了这种机制,它会同时检查引用值和 stamp 是否都相等。

二哥的 Java 进阶之路:AtomicStampedReference

volatile如何保证可见性(cup缓存和主缓存)

当一个变量被声明为 volatile 时,Java 内存模型会确保所有线程看到该变量时的值是一致的。

深入浅出 Java 多线程:Java内存模型

也就是说,当线程对 volatile 变量进行写操作时,JMM 会在写入这个变量之后插入一个 Store-Barrier(写屏障)指令,这个指令会强制将本地内存中的变量值刷新到主内存中。

三分恶面渣逆袭:volatile写插入内存屏障后生成的指令序列示意图

当线程对 volatile 变量进行读操作时,JMM 会插入一个 Load-Barrier(读屏障)指令,这个指令会强制让本地内存中的变量值失效,从而重新从主内存中读取最新的值。

三分恶面渣逆袭:volatile写插入内存屏障后生成的指令序列示意图

例如,我们声明一个 volatile 变量 x:

代码语言:javascript
复制
volatile int x = 0

线程 A 对 x 写入后会将其最新的值刷新到主内存中,线程 B 读取 x 时由于本地内存中的 x 失效了,就会从主内存中读取最新的值,内存可见性达成!

三分恶面渣逆袭:volatile内存可见性

HashMap为什么用红黑树,链表转数条件,红黑树插入删除规则

三分恶面渣逆袭:JDK 8 HashMap 数据结构示意图

HashMap 的核心是一个动态数组(Node[] table),用于存储键值对。这个数组的每个元素称为一个“桶”(Bucket),每个桶的索引是通过对键的哈希值进行哈希函数处理得到的。

当多个键经哈希处理后得到相同的索引时,会发生哈希冲突。HashMap 通过链表来解决哈希冲突——即将具有相同索引的键值对通过链表连接起来。

不过,链表过长时,查询效率会比较低,于是当链表的长度超过 8 时(且数组的长度大于 64),链表就会转换为红黑树。红黑树的查询效率是 O(logn),比链表的 O(n) 要快。数组的查询效率是 O(1)。

红黑树怎么保持平衡的?

红黑树有两种方式保持平衡:旋转染色

①、旋转:旋转分为两种,左旋和右旋

三分恶面渣逆袭:左旋

三分恶面渣逆袭:右旋

②、染⾊:

三分恶面渣逆袭:染色

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-04-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 沉默王二 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 携程面经(详细)
    • 对象创建到销毁,内存如何分配的,(类加载和对象创建过程,CMS,G1内存清理和分配)
      • 对象的销毁过程了解吗?
      • 内存如何分配的?
      • 能详细说一下 CMS 收集器的垃圾收集过程吗?
      • G1 垃圾收集器了解吗?
    • ThreadLocal,(作用,演进,软指针,删除过程)
      • 线程上下文切换(我答的内核态和用户态切换时机,和切换需要加载哪些内容)
        • cas和aba(原子操作+时间戳)
          • 什么是 ABA 问题?如何解决?
        • volatile如何保证可见性(cup缓存和主缓存)
          • HashMap为什么用红黑树,链表转数条件,红黑树插入删除规则
            • 红黑树怎么保持平衡的?
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档