前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【面经】淘天Java一面面经(下)

【面经】淘天Java一面面经(下)

原创
作者头像
后端码匠
发布2023-11-10 20:55:33
2500
发布2023-11-10 20:55:33
举报
文章被收录于专栏:后端码匠后端码匠

五、JVM怎么创建一个对象

说之前先捋清一个大致的思路:创建对象的过程大致分为 5 步:

1、类加载检查

当我们在 Java 程序中 new 一个对象的时候,在底层其实会有大概以下几步:

  • 首先它会检查这个指令是否能在常量池中能否定位到一个类的符号引用;
  • 接着会检查这个符号引用代表的类是否已经被加载、解析、初始化。如果没有会进行一个类加载

检查完类加载后就是分配内存了。(这里有人可能会问那该对象的具体内存是否确认呢?其实类加载完成后可以确认它所需要的内存了)。

2、分配内存

现在我们已经知道了对象所占的内存,那么虚拟机是如何给对象在 Java 堆中分配内存的呢?主要有两种分配方式:

  • 指针碰撞;
  • 空闲列表。

接下来我们详细说说这两种分配内存的方式:

指针碰撞

其实这种方式理解起来比较简单的,假设 Java 堆中的内存是绝对完整的,它会把使用过的内存和未使用过的内存划分开来。此时一边就是使用过的内存,一边就是未使用过的内存;那么他如何去给一个新的对象去划分空闲内存中的某块区域呢?其实很简单,就是借助一个指针(这里是不是呼应上了所谓的指针碰撞);

当我们分配内存的时候就是把指针在空闲的内存区域中移动一个与要被创建对象大小相等的距离。这就是指针碰撞的方式

适用场景:内存规整,不碎片化

空闲列表

这个其实理解起来更为简单。它无非就是指在 Java 堆中的内存并非是规整的(使用的内存和未使用过的内存没有划分开来),比较杂乱无章,此时虚拟机就得需要列表记录内存中哪些是已经使用的哪些是没有使用的,然后在给对象分配内存空间的时候在该列表中找一个足够的内存分给对象实例;并更新维护的列表。这种就叫做空闲列表(Free List)

适用场景:堆内存碎片化

Tip:说到分配内存的两种方式,就顺便提一句,

  • 当使用的是Serial、ParNew等压缩整理过程的收集器的时候,系统采用的是指针碰撞的方式。
  • 而当使用的是CMS这种基于清除的算法收集器,理论上就只能采用空闲列表。
分配内存如何保证线程安全的

上面我们将给新的对象分配内存的方式以及分配内存前的逻辑大致理完了。你是不是觉得很简单。其实就是这么简单。但是其实我们忽略了一个很重要的问题。我们回想起本篇文中第一段话:Java 程序在运行过程中无时无刻不在创建对象,那么它是如何在并发环境下保证线程安全的呢?接下来我们简单的捋一下其实保证线程安全还是两种方式:

  • 将分配内存空间的动作进行同步处理(虚拟机底层的实现逻辑就是CAS + 失败重试)来保证分配内存空间的原子性;
  • 还有一种就是将分配内存的动作按照线程划分在不同的空间中进行,也就是每个线程在 Java 堆中有有属于自己的一小块内存,这种方式叫做本地线程分配缓冲 Thread Local Allocation Buffer TLAB,当本地线程缓冲使用完了,再分配缓存区时才需要同步锁定。至于虚拟机是否使用 TLAB 可通过参数-XX: +/-UseTLAB来控制。

3、初始零值

当分配完内存后,虚拟机必须将分配到的内存空间(不包含对象头)都初始化为零值。如果使用了 TLAB,那么这一步会在 TLAB 分配时进行。为什么虚拟机要有这番操作呢?

主要是为了保证对象的实例字段能够在 Java 代码中可以在不赋值的是否就可以访问直接使用,这样就能使 Java 程序访问这些字段所对应的数据类型的初始零值

4、设置对象头

接下来,Java 虚拟机还需要对这些对象进行必要的设置,例如这些对象是哪些类的实例、以及如何才能找到类的元信息、对象的哈希码(实际对象的哈希码会延期到真正调用 Object::hashCode()方法时才计算)、对象 GC 的分代年龄等信息,这些信息都会保存在对象头中(Object Header)之中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式

5、执行 init

执行完上述操作后,对于 Java 虚拟机来说对象已经创建完了,但是对于 Java 视角来说,对象的创建才刚刚开始,还没有执行init方法。所有的字段还都为零。对象中需要的其它资源和状态信息还没有按照原有的意图去构造好。所以一般来说,new指令之后就会执行init方法,按照 Java 程序员的意图去对对象做一个初始化,这样之后一个真正完整可用的对象才构造出来

六、有哪些场景会触发类的加载

虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前):

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令是,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或配置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果没有发现其父类还有没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包括main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

七、双亲委派机制,如果不按这种会有什么问题

双亲委派机制是 Java 类加载器的一种设计模式,其核心思想是每个类加载器在加载类时首先将请求委派给父类加载器,只有在父类加载器无法完成加载时才由当前类加载器自己加载。这一机制的目的是确保 Java 类库的一致性,防止不同的类加载器重复加载相同的类,从而保证 Java 应用程序的稳定性和安全性。

如果不按照双亲委派机制进行类加载,可能会导致以下问题:

  1. 类的重复加载:如果没有双亲委派机制,每个类加载器都可以独立地加载类。这样就可能导致同一个类被不同的类加载器加载,从而导致类的冲突和不一致性。
  2. 安全性问题:通过双亲委派机制,Java 类库中的类是由引导类加载器加载的,这样可以确保这些类的来源是可信的。如果不使用双亲委派机制,可能会打破这种安全性,导致恶意代码被加载执行。
  3. 性能问题:双亲委派机制通过委派给父类加载器来避免重复加载,从而提高了加载效率。如果每个类加载器都独立加载,可能会增加加载时间和资源占用。
  4. 类库冲突:在复杂的应用中,可能会使用多个类库,这些类库可能有相同的类名。通过双亲委派机制,这些类库的类加载是有序的,可以避免类的冲突。

八、线程状态,一个线程包含哪些信息

线程是程序执行的单元,它包含了一些状态信息,线程的状态是线程在执行过程中不同阶段的表现。在Java中,线程的状态有几种,主要包括以下几种:

  1. 新建(New): 线程刚刚被创建,但还没有开始执行。
  2. 就绪(Runnable): 线程已经被创建,处于就绪状态,等待系统为其分配执行的时间片。一旦得到时间片,线程就会进入运行状态。
  3. 运行(Running): 线程正在执行其任务。
  4. 阻塞(Blocked): 线程被阻塞,通常是由于等待某个事件的发生。在阻塞状态下的线程不会占用 CPU 资源。
  5. 等待(Waiting): 线程正在等待另一个线程的通知,进入等待状态的线程会释放它所持有的锁。
  6. 超时等待(Timed Waiting): 线程在等待另一个线程的通知,但有一个超时时间,如果超过这个时间线程仍未收到通知,则会自动唤醒。
  7. 终止(Terminated): 线程执行完毕或者因异常退出,进入终止状态。

线程对象包含了一些信息,这些信息主要有:

  • 线程ID(Thread ID): 线程的唯一标识符。
  • 程序计数器(Program Counter): 指向线程当前要执行的指令的地址。
  • 栈(Stack): 包含了线程的局部变量、方法参数、返回值以及用于方法调用和返回的信息。每个线程都有自己的栈。
  • 寄存器集合: 包含了线程执行过程中使用到的寄存器的集合。
  • 线程状态: 描述了线程当前的状态,如上面所述的新建、就绪、运行、阻塞、等待、超时等待、终止。
  • 优先级: 线程的优先级,用于决定在就绪状态时获取 CPU 时间片的顺序。

这些信息共同组成了线程的上下文,用于保存和恢复线程的执行状态。线程的状态会随着线程的执行过程而不断变化。在多线程编程中,了解线程状态和线程的上下文是非常重要的,可以帮助开发人员调试和优化多线程程序。

九、线程池执行任务的过程

线程池是一种管理和复用线程的机制,它可以有效地控制并发线程的数量,提高系统性能。线程池的执行任务过程通常包括以下几个步骤:

  1. 任务提交(Submit Task): 任务首先被提交到线程池。任务可以是 Runnable 接口的实例,也可以是 Callable 接口的实例。 ExecutorService executor = Executors.newFixedThreadPool(5); executor.submit(new MyTask());
  2. 任务队列(Task Queue): 提交的任务被存储在任务队列中。线程池维护一个队列,用于存储等待执行的任务。
  3. 线程分配(Thread Allocation): 线程池根据自身的管理策略,从任务队列中选择任务,并为其分配一个空闲线程。线程可以是预先创建的线程,也可以是动态创建的线程。
  4. 任务执行(Task Execution): 选定的线程执行被分配的任务。任务在执行时可以访问线程池中的资源,如共享的数据结构。
  5. 线程复用(Thread Reuse): 执行完任务的线程并不会立即销毁,而是返回到线程池的线程池中,以便复用。这样可以减少线程的创建和销毁开销,提高性能。
  6. 任务完成(Task Completion): 执行的任务完成后,线程会返回到线程池中,而不是销毁。线程池会等待新的任务,或者等待超时,如果没有新任务则继续执行。
  7. 异常处理(Exception Handling): 线程池会处理任务执行过程中可能抛出的异常。通常,异常会被捕获并记录,以确保线程不会因为异常而终止。
  8. 线程池关闭(Shutdown): 当应用程序结束或者不再需要线程池时,应该关闭线程池。关闭线程池时,线程池不再接受新的任务,但会等待已经提交的任务执行完毕。 executor.shutdown();

以上是线程池执行任务的基本过程。不同类型的线程池(如FixedThreadPoolCachedThreadPool等)有不同的管理策略,但执行任务的基本原理是相似的。线程池的使用可以提高程序的性能,减少因线程的创建和销毁而带来的开销。

十、线程同步有哪些策略和类,有没有实测过关键字的性能

线程同步是为了确保多个线程在访问共享资源时能够安全地进行操作,防止数据不一致和并发问题。Java 中有多种线程同步的策略和类,其中最常见的包括:

  1. Synchronized 关键字: synchronized 是 Java 内置的关键字,用于保护代码块或方法,确保同一时间只有一个线程能够访问被 synchronized 修饰的代码。使用 synchronized 可以确保线程安全,但过多的锁竞争可能导致性能问题。 public synchronized void synchronizedMethod() { // 同步的代码块 }
  2. ReentrantLock 类: ReentrantLockjava.util.concurrent.locks 包下的类,提供了显式的锁机制。相对于 synchronizedReentrantLock 提供了更多的灵活性,例如可以实现公平锁和可中断锁。 ReentrantLock lock = new ReentrantLock(); lock.lock(); try { // 同步的代码块 } finally { lock.unlock(); }
  3. ReadWriteLock 接口: ReadWriteLock 是用于支持读写分离的锁,包括 ReentrantReadWriteLock 实现。在读操作较多的情况下,使用读写锁可以提高并发性。 ReadWriteLock lock = new ReentrantReadWriteLock(); lock.readLock().lock(); try { // 读操作的同步代码块 } finally { lock.readLock().unlock(); } lock.writeLock().lock(); try { // 写操作的同步代码块 } finally { lock.writeLock().unlock(); }
  4. Atomic 类: java.util.concurrent.atomic 包提供了一系列的原子类,如 AtomicIntegerAtomicLong 等。这些类提供了一些原子性操作,可以用来替代传统的锁机制。 AtomicInteger counter = new AtomicInteger(); // 原子性的自增操作 counter.incrementAndGet();

关于性能方面,一般而言,synchronized 关键字的性能已经在 JDK 的不断优化中有所提升,而且在某些场景下可能比较适用。ReentrantLock 提供了更多的控制和可定制性,但可能会稍微增加一些复杂性。在具体场景中,性能的优劣很大程度上取决于实际使用情况和并发压力。

在实际应用中,性能的评估通常需要基于具体的场景和需求进行测试和比较。不同的同步机制在不同的应用场景下可能表现出不同的优势和劣势。

十一、SpringBoot搭建的Web服务处理过程

Spring Boot 是一个用于简化 Spring 应用开发的框架,特别适用于构建基于 RESTful 架构的 Web 服务。下面是 Spring Boot 搭建的 Web 服务的处理过程:

  1. 项目初始化: 创建一个 Spring Boot 项目,可以使用 Spring Initializr(https://start.spring.io/)进行快速初始化,选择项目的依赖和配置。
  2. 项目结构: Spring Boot 项目的结构通常包括控制器(Controller)、服务(Service)、数据访问层(Repository/DAO)、实体类(Entity)等。在 src/main/java 目录下创建相应的包和类。
  3. 定义实体类: 定义与业务相关的实体类,这些实体类通常映射数据库表的结构。可以使用 JPA 注解进行实体类的定义。 @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String email; // getters and setters }
  4. 定义数据访问层(Repository): 使用 Spring Data JPA 或其他持久化框架定义数据访问层,负责与数据库进行交互。 public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); }
  5. 定义服务层(Service): 编写服务层代码,包含业务逻辑。服务层通常调用数据访问层完成数据的增删改查操作。 @Service public class UserService { @Autowired private UserRepository userRepository; public Optional<User> findUserByUsername(String username) { return userRepository.findByUsername(username); } }
  6. 定义控制器(Controller): 创建控制器来处理 HTTP 请求和响应。使用 @RestController 注解将一个类标记为控制器。 @RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @GetMapping("/{username}") public ResponseEntity<User> getUserByUsername(@PathVariable String username) { Optional<User> user = userService.findUserByUsername(username); return user.map(value -> ResponseEntity.ok().body(value)) .orElseGet(() -> ResponseEntity.notFound().build()); } }
  7. 启动应用: Spring Boot 应用的入口是一个包含 main 方法的类。通常使用 @SpringBootApplication 注解标记这个类。 @SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }
  8. 运行应用: 通过 IDE 或命令行运行应用。Spring Boot 会自动扫描并初始化相关组件,创建嵌入式的 Web 服务器(如 Tomcat),并监听指定的端口。
  9. 访问 API: 使用浏览器、Postman 或其他 HTTP 客户端工具访问定义的 API。根据上述示例,可以通过 http://localhost:8080/api/users/{username} 访问用户信息。

以上是简化的 Spring Boot Web 服务搭建过程。实际开发中,可能会涉及更多的细节,如异常处理、日志记录、安全性等。

十二、有没有看过开源框架的源码,举一个例子讲讲;

~

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 五、JVM怎么创建一个对象
    • 1、类加载检查
      • 2、分配内存
        • 3、初始零值
          • 4、设置对象头
            • 5、执行 init
            • 六、有哪些场景会触发类的加载
            • 七、双亲委派机制,如果不按这种会有什么问题
            • 八、线程状态,一个线程包含哪些信息
            • 九、线程池执行任务的过程
            • 十、线程同步有哪些策略和类,有没有实测过关键字的性能
            • 十一、SpringBoot搭建的Web服务处理过程
            • 十二、有没有看过开源框架的源码,举一个例子讲讲;
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
            http://www.vxiaotou.com