八股选手进了公司就原形毕露。

沉默王二

共 7200字,需浏览 15分钟

 ·

2024-07-25 14:04

大家好,我是二哥呀。

牛客上刷到一条比较有意思的帖子,名叫“八股选手进了公司就原形毕露了”,原因是不太会用 Git,checkout 和 merge 都搞错了。

截图来自牛客的仙林第一深情

不过讲良心话,这真的情有可原,好吧,因为这玩意学校又不教,面试的时候也几乎不怎么问,属于大多数初入职场的小伙伴非常欠缺的一项技能点。

反正我刚参加工作那会也不会用 Git,更没用过 Xshell 这种可以远程操作 Linux 服务器的终端,而这两项技能包又属于工作党必须具备的。

对 Git 还比较陌生的小伙伴可以去二哥的 Java进阶之路上看一下这篇教程,1.3 万字,大约 43 分钟就能读完,读完后你对 Git 会有一个全新的认知和理解。

网址:https://javabetter.cn/git/git-qiyuan.html

对 Linux 比较陌生的小伙伴,推荐大家去看看这个开源仓库 ./missing-semester,会把 Shell、Vim、命令行、Git、持续集成等几个方面都讲清楚。

https://missing-semester-cn.github.io/2020/course-shell

你别说,国内的互联网公司,面试真的很接近古代的科举考试。所以说,面试变成八股我一点也不意外。再说,能背会八股难道不也是一种能力吗?(手动狗头)

好,接下来就分享一份二哥编程星球里一位球友刚刚分享的得物八股,大家来感受一下,这八股不背能行吗?(🤣)

得物一面八股

说一说jvm内存区域

JVM 的内存区域,有时叫 JVM 的内存结构,有时也叫 JVM 运行时数据区,按照 Java 的虚拟机规范,可以细分为程序计数器虚拟机栈本地方法栈方法区等。

三分恶面渣逆袭:Java虚拟机运行时数据区

其中方法区是线程共享的,虚拟机栈本地方法栈程序计数器是线程私有的。

Java中堆内存怎么组织的

Java 堆被划分为新生代(Young Generation)和老年代(Old Generation)两个区域。

三分恶面渣逆袭:Java堆内存划分

新生代又被划分为 Eden 空间和两个 Survivor 空间(From 和 To)。

  • Eden 空间:大多数新创建的对象会被分配到 Eden 空间中。当 Eden 区填满时,会触发一次轻量级的垃圾回收(Minor GC),清除不再使用的对象。
  • Survivor 空间:每次 Minor GC 后,仍然存活的对象会从 Eden 区或From 区复制到 To 区。From 和 To 区交替使用。

对象在新生代中经历多次 GC 后,如果仍然存活,会被移动到老年代。

Java中垃圾回收的原理

垃圾回收(Garbage Collection,GC)就是对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。

JVM 在做 GC 之前,会先搞清楚什么是垃圾,什么不是垃圾,通常会通过可达性分析算法来判断对象是否存活。

二哥的 Java 进阶之路:可达性分析

在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是进行垃圾回收,可以采用标记清除算法、复制算法、标记整理算法、分代收集算法等。

JVM 提供了多种垃圾回收器,包括 CMS GC、G1 GC、ZGC 等,不同的垃圾回收器采用的垃圾收集算法也不同,因此适用于不同的场景和需求。

比如说 CMS 是第一个关注 GC 停顿时间(STW 的时间)的垃圾收集器,JDK 1.5 时引入,JDK9 被标记弃用,JDK14 被移除。

G1(Garbage-First Garbage Collector)在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为了默认的垃圾收集器。

有梦想的肥宅:G1 收集器

ZGC 是 JDK11 推出的一款低延迟垃圾收集器,适用于大内存低延迟服务的内存管理和回收,在 128G 的大堆下,最大停顿时间才 1.68 ms,性能远胜于 G1 和 CMS。

Java中full gc频繁,有哪些原因

Full GC 是指对整个堆内存(包括新生代和老年代)进行垃圾回收操作。Full GC 频繁会导致应用程序的暂停时间增加,从而影响性能。

常见的原因有:

  • 大对象(如大数组、大集合)直接分配到老年代,导致老年代空间快速被占用。
  • 程序中存在内存泄漏,导致老年代的内存不断增加,无法被回收。比如 IO 资源未关闭。
  • 一些长生命周期的对象进入到了老年代,导致老年代空间不足。
  • 不合理的 GC 参数配置也导致 GC 频率过高。比如说新生代的空间设置过小。

该怎么排查 Full GC 频繁问题?

大厂一般都会有专门的性能监控系统,可以通过监控系统查看 GC 的频率和堆内存的使用情况。

否则可以使用 JDK 的一些自带工具,包括 jmap、jstat 等。

# 查看堆内存各区域的使用率以及GC情况
jstat -gcutil -h20 pid 1000
# 查看堆内存中的存活对象,并按空间排序
jmap -histo pid | head -n20
# dump堆内存文件
jmap -dump:format=b,file=heap pid

或者使用一些可视化的工具,比如 VisualVM、JConsole 等。

如何解决 Full GC 频繁问题?

假如是因为大对象直接分配到老年代导致的 Full GC 频繁,可以通过 -XX:PretenureSizeThreshold 参数设置大对象直接进入老年代的阈值。

或者能不能将大对象拆分成小对象,减少大对象的创建。比如说分页。

假如是因为内存泄漏导致的 Full GC 频繁,可以通过分析堆内存 dump 文件找到内存泄漏的对象,再找到内存泄漏的代码位置。

假如是因为长生命周期的对象进入到了老年代,要及时释放资源,比如说 ThreadLocal、数据库连接、IO 资源等。

假如是因为 GC 参数配置不合理导致的 Full GC 频繁,可以通过调整 GC 参数来优化 GC 行为。或者直接更换更适合的 GC 收集器,如 G1、ZGC 等。

在并发量特别高的情况下是使用 synchronized 还是 ReentrantLock

在并发量特别高的情况下,ReentrantLock 的性能可能会优于 synchronized,原因包括:

  • ReentrantLock 提供了超时和公平锁等特性,可以更好地应对复杂的并发场景 。
  • ReentrantLock 允许更细粒度的锁控制,可以有效减少锁竞争。
  • ReentrantLock 支持条件变量 Condition,可以实现比 synchronized 更复杂的线程间通信机制。

说一下 ConcurrentHashMap 中并发安全的实现

ConcurrentHashMap 在 JDK 7 时采用的是分段锁机制(Segment Locking),整个 Map 被分为若干段,每个段都可以独立地加锁。因此,不同的线程可以同时操作不同的段,从而实现并发访问。

初念初恋:JDK 7 ConcurrentHashMap

在 JDK 8 及以上版本中,ConcurrentHashMap 的实现进行了优化,不再使用分段锁,而是使用了一种更加精细化的锁——桶锁,以及 CAS 无锁算法。每个桶(Node 数组的每个元素)都可以独立地加锁,从而实现更高级别的并发访问。

初念初恋:JDK 8 ConcurrentHashMap

同时,对于读操作,通常不需要加锁,可以直接读取,因为 ConcurrentHashMap 内部使用了 volatile 变量来保证内存可见性。

对于写操作,ConcurrentHashMap 使用 CAS 操作来实现无锁的更新,这是一种乐观锁的实现,因为它假设没有冲突发生,在实际更新数据时才检查是否有其他线程在尝试修改数据,如果有,采用悲观的锁策略,如 synchronized 代码块来保证数据的一致性。

你说高并发下 ReentrantLock 性能比 synchronized 高,那为什么 ConcurrentHashMap 在 JDK 1.7 中要用 ReentrantLock,而在 JDK 1.8 要用 synchronized

ConcurrentHashMap 在 JDK 1.7 和 JDK 1.8 中的实现机制不同,主要体现在锁的机制上。

JDK 1.7 中的 ConcurrentHashMap 使用了分段锁机制,即 Segment 锁,每个 Segment 都是一个 ReentrantLock,这样可以保证每个 Segment 都可以独立地加锁,从而实现更高级别的并发访问。

而在 JDK 1.8 中,ConcurrentHashMap 取消了 Segment 分段锁,采用了更加精细化的锁——桶锁,以及 CAS 无锁算法,每个桶(Node 数组的每个元素)都可以独立地加锁,从而实现更高级别的并发访问。

再加上 JVM 对 synchronized 做了大量优化,如锁消除、锁粗化、自旋锁和偏向锁等,在低中等的竞争情况下,synchronized 的性能并不比 ReentrantLock 差,并且使用 synchronized 可以简化代码实现。

Redis为什么快

速度快的原因主要有⼏点:

①、基于内存的数据存储,Redis 将数据存储在内存当中,使得数据的读写操作避开了磁盘 I/O。而内存的访问速度远超硬盘,这是 Redis 读写速度快的根本原因。

②、单线程模型,Redis 使用单线程模型来处理客户端的请求,这意味着在任何时刻只有一个命令在执行。这样就避免了线程切换和锁竞争带来的消耗。

③、IO 多路复⽤,基于 Linux 的 select/epoll 机制。该机制允许内核中同时存在多个监听套接字和已连接套接字,内核会一直监听这些套接字上的连接请求或者数据请求,一旦有请求到达,就会交给 Redis 处理,就实现了所谓的 Redis 单个线程处理多个 IO 读写的请求。

三分恶面渣逆袭:Redis使用IO多路复用和自身事件模型

④、高效的数据结构,Redis 提供了多种高效的数据结构,如字符串(String)、列表(List)、集合(Set)、有序集合(Sorted Set)等,这些数据结构经过了高度优化,能够支持快速的数据操作。

跳表的结构

跳表(skiplist)是一种有序的数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。

三分恶面渣逆袭:跳表

内核空间和用户空间是什么,什么时候会进入内核空间

在计算机系统中,内存可以分为两大区域:内核空间(Kernel Space)和用户空间(User Space)。这种划分主要用于保护系统稳定性和安全性。

  • 内核空间,是操作系统内核代码及其运行时数据结构所在的内存区域,拥有对系统所有资源的完全访问权限,如进程管理、内存管理、文件系统、网络堆栈等。
  • ⽤户空间,是操作系统为应用程序(如用户运行的进程)分配的内存区域,用户空间中的进程不能直接访问硬件或内核数据结构,只能通过系统调用与内核通信。
二哥的 Java 进阶之路:用户空间和内核空间

当程序使⽤⽤户空间时,我们常说该程序在 ⽤户态 执⾏,⽽当程序使内核空间时,程序则在 内核态 执⾏。

当应用程序执行系统调用时,CPU 将从用户态切换到内核态,进入内核空间执行相应的内核代码,然后再切换回用户态。

三分恶面渣逆袭:用户态&内核态切换

系统调用是应用程序请求操作系统内核提供服务的接口,如文件操作(如 open、read、write)、进程控制(如 fork、exec)、内存管理(如 mmap)等。

Linux中进程和线程的区别

进程是一个正在执行的程序的实例。每个进程有自己独立的地址空间、全局变量、堆栈、和文件描述符等资源。

线程是进程中的一个执行单元。一个进程可以包含多个线程,它们共享进程的地址空间和资源。

多线程-图片来源于网络

每个进程在独立的地址空间中运行,不会直接影响其他进程。线程共享同一个进程的内存空间、全局变量和文件描述符。

进程切换需要保存和恢复大量的上下文信息,代价较高。线程切换相对较轻量,因为线程共享进程的地址空间,只需要保存和恢复线程私有的数据。

线程的生命周期由进程控制,进程终止时,其所有线程也会终止。

特性 进程 线程
地址空间 独立 共享
内存开销
上下文切换 慢,开销大 快,开销小
通信 需要 IPC 机制,开销较大 共享内存,直接通信
创建销毁 开销大,较慢 开销小,较快
并发性
崩溃影响 一个进程崩溃不会影响其他进程 一个线程崩溃可能导致整个进程崩溃

ending

一个人可以走得很快,但一群人才能走得更远。二哥的编程星球已经有 5800 多名球友加入了,如果你也需要一个良好的学习环境,戳链接 🔗 加入我们吧。这是一个编程学习指南 + Java 项目实战 + LeetCode 刷题的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。

两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的学习资源,相信能帮助你走的更快、更稳、更远

欢迎点击左下角阅读原文了解二哥的编程星球,这可能是你学习求职路上最有含金量的一次点击。

最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。

浏览 2796
10点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
10点赞
评论
收藏
分享

手机扫一扫分享

分享
举报