9 -10 月份会有不少企业在线下面试,正在参加 25 届秋招的同学们多关注一下线下招聘渠道,有在自己附近的最好去面一下,线下面试的优势是当天能把面试流程全部走完,比如最近携程和深信服线下面试机会比较多。
最近,有同学刚参加完携程的线下面试,流程是一二面+HR面,当天下午就直接速通了,速通之后就等后面的 offer 录取通知了,如果是线上面试的话,有时候流程会需要走 2-3 周,整体还是比较慢,最快也需要一周,所以线下面试效率还是非常快的,快人一步拿 offer。
携程的校招薪资,我查了一下去年的情况,大概范围是 21~24k * 15,也就是年薪有 30w+,其实薪资待遇还是不错的,跟一线大厂差不多了。
当然,携程面试也是需要考察算法的,基本互联网中大厂都逃不了算法,对于携程的线下面试,算法也是会出的,这时候就需要你在纸上写代码了,然后跟面试官讲讲思路。
那携程面试到底难度如何呢?
那么,这次来分享一位同学携程的Java 后端开发的面经,主要是考察了 Java 线程池、Spring、JVM、MySQL方面的知识。
大家觉得难度如何呢?
MySQL
说一下mysql索引结构
MySQL InnoDB 引擎是用了B+树作为了索引的数据结构。
B+Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表。
主键索引的 B+Tree 如图所示:
比如,我们执行了下面这条查询语句:
select * from product where id= 5;
这条语句使用了主键索引查询 id 号为 5 的商品。查询过程是这样的,B+Tree 会自顶向下逐层进行查找:
将 5 与根节点的索引数据 (1,10,20) 比较,5 在 1 和 10 之间,所以根据 B+Tree的搜索逻辑,找到第二层的索引数据 (1,4,7); 在第二层的索引数据 (1,4,7)中进行查找,因为 5 在 4 和 7 之间,所以找到第三层的索引数据(4,5,6); 在叶子节点的索引数据(4,5,6)中进行查找,然后我们找到了索引值为 5 的行数据。
数据库的索引和数据都是存储在硬盘的,我们可以把读取一个节点当作一次磁盘 I/O 操作。那么上面的整个查询过程一共经历了 3 个节点,也就是进行了 3 次 I/O 操作。
B+Tree 存储千万级的数据只需要 3-4 层高度就可以满足,这意味着从千万级的表查询目标数据最多需要 3-4 次磁盘 I/O,所以B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次。
B+树和B树的区别是什么?
在B+树中,数据都存储在叶子节点上,而非叶子节点只存储索引信息;而B树的非叶子节点既存储索引信息也存储部分数据。 B+树的叶子节点使用链表相连,便于范围查询和顺序访问;B树的叶子节点没有链表连接。 B+树的查找性能更稳定,每次查找都需要查找到叶子节点;而B树的查找可能会在非叶子节点找到数据,性能相对不稳定。
mysql隔离级别 你知道哪些?
读未提交(read uncommitted),指一个事务还没提交时,它做的变更就能被其他事务看到; 读提交(read committed),指一个事务提交之后,它做的变更才能被其他事务看到; 可重复读(repeatable read),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别; 串行化(serializable);会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
按隔离水平高低排序如下:
针对不同的隔离级别,并发事务时可能发生的现象也会不同。
也就是说:
在「读未提交」隔离级别下,可能发生脏读、不可重复读和幻读现象; 在「读提交」隔离级别下,可能发生不可重复读和幻读现象,但是不可能发生脏读现象; 在「可重复读」隔离级别下,可能发生幻读现象,但是不可能脏读和不可重复读现象; 在「串行化」隔离级别下,脏读、不可重复读和幻读现象都不可能会发生。
接下来,举个具体的例子来说明这四种隔离级别,有一张账户余额表,里面有一条账户余额为 100 万的记录。然后有两个并发的事务,事务 A 只负责查询余额,事务 B 则会将我的余额改成 200 万,下面是按照时间顺序执行两个事务的行为:
在不同隔离级别下,事务 A 执行过程中查询到的余额可能会不同:
在「读未提交」隔离级别下,事务 B 修改余额后,虽然没有提交事务,但是此时的余额已经可以被事务 A 看见了,于是事务 A 中余额 V1 查询的值是 200 万,余额 V2、V3 自然也是 200 万了; 在「读提交」隔离级别下,事务 B 修改余额后,因为没有提交事务,所以事务 A 中余额 V1 的值还是 100 万,等事务 B 提交完后,最新的余额数据才能被事务 A 看见,因此额 V2、V3 都是 200 万; 在「可重复读」隔离级别下,事务 A 只能看见启动事务时的数据,所以余额 V1、余额 V2 的值都是 100 万,当事务 A 提交事务后,就能看见最新的余额数据了,所以余额 V3 的值是 200 万; 在「串行化」隔离级别下,事务 B 在执行将余额 100 万修改为 200 万时,由于此前事务 A 执行了读操作,这样就发生了读写冲突,于是就会被锁住,直到事务 A 提交后,事务 B 才可以继续执行,所以从 A 的角度看,余额 V1、V2 的值是 100 万,余额 V3 的值是 200万。
这四种隔离级别具体是如何实现的呢?
对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了; 对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问; 对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View来实现的,它们的区别在于创建 Read View 的时机不同,「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View。
mysql串行化隔离级别,用的什么锁 ?
加是 next-key 锁,即记录锁+间隙锁的组合。
在串行化隔离级别下,普通的 select 查询是会对记录加 S 型的 next-key 锁,其他事务就没没办法对这些已经加锁的记录进行增删改操作了,从而避免了脏读、不可重复读和幻读现象。
Java
线程池运行流程是怎么样的?
线程池是为了减少频繁的创建线程和销毁线程带来的性能损耗,线程池的工作原理如下图:
线程池分为核心线程池,线程池的最大容量,还有等待任务的队列,提交一个任务,如果核心线程没有满,就创建一个线程,如果满了,就是会加入等待队列,如果等待队列满了,就会增加线程,如果达到最大线程数量,如果都达到最大线程数量,就会按照一些丢弃的策略进行处理。
spring和springboot有什么区别 ?
Spring 和 Spring Boot 的区别在于它们的目标和用途不同。
Spring 是一个轻量级的开源框架,它提供了一种简单的方式来构建企业级应用程序。 Spring Boot 则是 Spring 框架的延伸和扩展,它提供了一种快速构建应用程序的方式。开发人员可以通过使用 Spring Boot Starter 来快速集成常用的第三方库和框架,使得开发人员可以快速构建出一个可运行的应用程序。
AOP和IOC 应用场景是什么?实现原理呢?
Spring IoC和AOP 区别:
IoC:即控制反转的意思,它是一种创建和获取对象的技术思想,依赖注入(DI)是实现这种技术的一种方式。传统开发过程中,我们需要通过new关键字来创建对象。使用IoC思想开发方式的话,我们不通过new关键字创建对象,而是通过IoC容器来帮我们实例化对象。通过IoC的方式,可以大大降低对象之间的耦合度。 AOP:是面向切面编程,能够将那些与业务无关,却为业务模块所共同调用的逻辑封装起来,以减少系统的重复代码,降低模块间的耦合度。Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理。
在 Spring 框架中,IOC 和 AOP 结合使用,可以更好地实现代码的模块化和分层管理。例如:
通过 IOC 容器管理对象的依赖关系,然后通过 AOP 将横切关注点统一切入到需要的业务逻辑中。 使用 IOC 容器管理 Service 层和 DAO 层的依赖关系,然后通过 AOP 在 Service 层实现事务管理、日志记录等横切功能,使得业务逻辑更加清晰和可维护。
Java类加载机制过程说一
类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括以下 7 个阶段:
加载:通过类的全限定名(包名 + 类名),获取到该类的.class文件的二进制字节流,将二进制字节流所代表的静态存储结构,转化为方法区运行时的数据结构,在内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
连接:验证、准备、解析 3 个阶段统称为连接。
验证:确保class文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的class类的正确性,不会危害到虚拟机的安全。验证阶段大致会完成以下四个阶段的检验动作:文件格式校验、元数据验证、字节码验证、符号引用验证 准备:为类中的静态字段分配内存,并设置默认的初始值,比如int类型初始值是0。被final修饰的static字段不会设置,因为final在编译的时候就分配了 解析:解析阶段是虚拟机将常量池的「符号引用」直接替换为「直接引用」的过程。符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用的时候可以无歧义地定位到目标即可。直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,直接引用是和虚拟机实现的内存布局相关的。如果有了直接引用, 那引用的目标必定已经存在在内存中了。
初始化:初始化是整个类加载过程的最后一个阶段,初始化阶段简单来说就是执行类的构造器方法(() ),要注意的是这里的构造器方法()并不是开发者写的,而是编译器自动生成的。
使用:使用类或者创建对象
卸载:如果有下面的情况,类就会被卸载:1. 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。2. 加载该类的ClassLoader已经被回收。3. 类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
JVM内存结构说一下
根据 JVM8 规范,JVM 运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。
JVM的内存结构主要分为以下几个部分:
元空间:元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 Java 虚拟机栈:每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。 本地方法栈:与虚拟机栈类似,区别是虚拟机栈执行java方法,本地方法站执行native方法。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法与数据结构没有强制规定,因此虚拟机可以自由实现它。 程序计数器:程序计数器可以看成是当前线程所执行的字节码的行号指示器。在任何一个确定的时刻,一个处理器(对于多内核来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,我们称这类内存区域为“线程私有”内存。 堆内存:堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:所有的对象实例及数组都在对上进行分配。jdk1.8后,字符串常量池从永久代中剥离出来,存放在队中。 直接内存:直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中农定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
方法区为什么后来要移到元空间 ?
方法区(Method Area)是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区只是 JVM 规范中定义的一个概念,针对 Hotspot 虚拟机:
Java8 之前使用永久代(Permanent Generation,简称 PermGen)实现 而 Java8 之后使用元空间(Metaspace)实现。
永久代问题:
内存溢出:永久代的空间是有限制的,可以通过 -XX:PermSize
设置永久代初始容量,通过-XX:MaxPermSize
设置永久代最大容量。但是当加载过多的类或者常量的时候,就可能导致永久代的空间不足,抛出java.lang.OutOfMemoryError: PermGen space
异常。尤其是web应用会使用很多框架,这些框架会动态加载很多基础类,更容易导致OOM。垃圾回收效率低下:永久代中的类信息一般是在应用程序运行期间不会发生变化的,因此,如果开启了永久代的垃圾回收,就会造成大量的垃圾回收操作,导致垃圾回收效率低下,甚至会引起应用程序的暂停。此外,由于永久代主要存放 JVM 加载的类信息等永久存在的数据,这使得它在垃圾回收过程中的回收效率相对较低。在某些情况下,频繁触发的 Full GC 不仅无法有效回收永久代空间,还会严重影响 JVM 的性能。 无法动态调整大小:永久代的大小一旦被设置,就无法动态调整,如果预估错误,就可能导致浪费内存或内存不足的问题。 无法回收常量池中的内存:在永久代中,常量池是一个非常重要的部分,但是其中的常量无法被回收,即使这些常量已经不再被使用,也无法被垃圾回收器回收,这会浪费内存。
与永久代相比,使用元空间使用方法区具有以下优点:
突破内存限制,减少OOM。 由于元空间使用的是本地内存,而不是 JVM 内存,因此理论上,其大小只受限于操作系统的实际可用内存。这大大减少了内存溢出的可能性。相较于永久代在 JVM 堆中预分配的有限空间,元空间的引入提供了更大的空间来存储类元数据。 提高 Full GC 的效率。 在永久代中,Full GC 的触发比较频繁,而且效率较低。因为永久代中存放了很多 JVM 需要的类信息,这些数据大多数是不会被清理的,所以 Full GC 往往无法回收多少空间。但在元空间模型中,由于字符串常量池已移至堆中,静态变量也移至 Java 堆或者本地内存,因此可以更有效地进行垃圾回收,避免了因频繁的 Full GC 导致的性能影响。 满足不同的类加载需求和动态类加载的情况。 在一些大型的、模块化的应用中,可能需要加载大量的类,这就需要大量的元数据存储空间。元空间可以动态地调整大小,能更好地满足这种需求。 避免永久代调优和大小设置的复杂性。 在 Java8 之前的版本中,通常需要手动设置永久代的大小,以避免内存溢出的错误。这增加了应用的配置和管理的复杂性。而元空间使用本地内存,根据实际需求动态调整,大大简化了内存管理的复杂性。
总的来说,Java8 选择使用元空间(Metaspace)替代永久代(PermGen)是 JVM 内存模型的一次重大改进。解决了永久代面临的空间限制、低效的垃圾回收、以及复杂的内存管理等问题。元空间利用本地内存,能够动态调整大小,提供了更大的空间来存储类元数据,也更好地适应了大型、模块化应用的需求。
项目
项目中数据用什么存储的 ? 项目中怎么用的多线程
算法
判断两个字符串是否存在包含关系(在纸上手撕)
你好,我是阿秀,普通学校毕业,校招时拿到字节跳动SP、百度、华为、农业银行等6个互联网中大厂offer,这是我在校期间的编程学习之路,详细记录了我是如何自学技术以应对第二年的校招秋招的。
毕业后我先于抖音部门担任全栈开发工程师,目前在上海某外企带领团队继续从事全栈开发,负责的项目顺利盈利 300w+。在研三那年就组建了一个阿秀的学习圈,一直持续分享校招/社招跳槽找工作的经验,都是自己一路走过来的经验,目前已经累计服务超过 4000 +人,欢迎点此了解一二。