JVM 内存区域有哪些部分
- 程序计数器:程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。在多线程环境下,每个线程都有自己独立的程序计数器。当线程执行 Java 方法时,程序计数器记录的是正在执行的虚拟机字节码指令的地址。
- Java 虚拟机栈:每个 Java 线程都有一个私有的 Java 虚拟机栈,与线程同时创建。每个方法在执行时都会创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。栈帧在方法调用时入栈,方法返回时出栈。
- 本地方法栈: 本地方法栈与 Java 虚拟机栈类似,但它为本地方法服务。本地方法是用其他编程语言(如 C/C++)编写的,通过 JNI 与 Java 代码进行交互。
- 堆:Java 堆是 Java 虚拟机中最大的一块内存区域,用于存储对象实例。所有的对象实例和数组都在堆上分配内存。堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。堆可以分为新生代和老年代等不同的区域,其中新生代又包括 Eden 空间、Survivor 空间(From 和 To)。
- 方法区: 方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 HotSpot 虚拟机中,方法区也被称为永久代,但在较新的 JVM 版本中,永久代被元空间所取代。
- 运行时常量池:是方法区的一部分,用于存储编译期生成的类、方法和常量等信息。
- 字符串常量池:字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
- 直接内存:不是 Java 虚拟机运行时数据区的一部分,但 Java 可以通过 NIO 操作直接内存,提高 I/O 性能。
简述 JVM 中的堆
堆主要作用是存放对象实例,Java 里几乎所有对象实例都在堆上分配内存,堆也是内存管理中最大的一块。Java的垃圾回收主要就是针对堆这一区域进行。 可通过 -Xms 和 -Xmx 设置堆的最小和最大容量。
堆会抛出 OutOfMemoryError异常。
简述方法区
方法区用于存储被虚拟机加载的类信息、常量、静态变量等数据。
JDK 6 之前使用永久代实现方法区,容易内存溢出。JDK 7 把放在永久代的字符串常量池、静态变量等移出,JDK 8 中抛弃永久代,改用在本地内存中实现的元空间来实现方法区,把 JDK 7 中永久代内容移到元空间。
方法区会抛出 OutOfMemoryError异常。
简述运行时常量池
运行时常量池存放常量池表,用于存放编译器生成的各种字面量与符号引用。一般除了保存 Class 文件中描述的符号引用外,还会把符号引用翻译的直接引用也存储在运行时常量池。除此之外,也会存放字符串。
JDK 8 之前,位于方法区,大小受限于方法区。JDK 8 将运行时常量池存放堆中。
什么是强引用、软引用、弱引用、虚引用
哪些对象可以作为 GC Roots
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
- JNI(Java Native Interface)引用的对象
有哪些垃圾回收算法
- 标记-清除算法
标记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段首先通过根节点(GC Roots),标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。适用场合:
- 存活对象较多的情况下比较高效。
- 适用于老年代。
- 标记-复制算法
从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存上去,之后将原来的那一块儿内存全部回收掉 现在的商业虚拟机都采用这种收集算法来回收新生代。 适用场合:
- 存活对象较少的情况下比较高效。
- 扫描了整个空间一次(标记存活对象并复制移动)。
- 适用于年轻代(即新生代):基本上 98% 的对象是 “朝生夕死” 的,存活下来的会很少。
缺点:
- 需要一块空的内存空间。
- 需要复制移动对象。
- 复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。
- 标记-整理算法
标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。 首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
- 分代收集算法
分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于新年代的问题,将内存分 为各个年代。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。 在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率高,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。
有哪些垃圾回收器
- Serial 收集器(新生代使用标记复制算法,老年代使用标记整理算法)是新生代单线程收集器,优点是简单高效,算是最基本、发展历史最悠久的收集器。它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。
- ParNew/Parallel Old 收集器(新生代使用标记复制算法,老年代使用标记整理算法)是新生代并行收集器,其实就是 Serial 收集器的多线程版本。
- Parallel Scavenge 收集器(新生代使用标记复制算法,老年代使用标记整理算法)追求高吞吐量,高效利用 CPU。
-
CMS 垃圾收集器主要用于老年代,采用标记清除算法,注重最短时间停顿。CMS 垃圾收集器为最早提出的并发收集器,垃圾收集线程与用户线程同时工作。该收集器分为初始标记、并发标记、重新标记、并发清除这么几个步骤。
- 初始标记:暂停其他线程(stop the world),标记与 GC roots 直接关联的对象。
- 并发标记:可达性分析过程(程序不会停顿)。
- 重新标记:暂停虚拟机(stop the world),修正并发标记期间变动的记录。
- 并发清除:清理垃圾对象(程序不会停顿)。
-
G1 垃圾收集器把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。G1 从整体来看是基于 “标记-整理” 算法实现的收集器;从局部上来看是基于 “标记-复制” 算法实现的。G1 收集器的阶段分以下几个步骤:
- 初始标记:暂停其他线程(stop the world),标记与 GC roots 直接关联的对象。
- 并发标记:可达性分析过程(程序不会停顿)。
- 最终标记:暂停其他线程(stop the world),标记那些在并发标记阶段发生变化的对象,将被回收。
- 筛选回收:首先对各个 Regin 的回收价值和成本进行排序,再根据用户所期待的 GC 停顿时间指定回收计划,回收一部分 Region。
JVM 默认分代内存分配策略
大多数情况下的对象在新生代 Eden 区分配,当 Eden 没有足够空间时将发起一次 Minor GC。大对象由于需要大量连续内存空间,直接进入老年代分配。
如果经历过第一次 Minor GC 仍然存活且能被 Survivor 容纳,该对象就会被移动到 Survivor 中并将年龄设置为 1,并且每熬过一次 Minor GC 年龄就加 1,当增加到一定程度(默认15)就会被晋升到老年代。
如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 的一半,年龄不小于该年龄的对象就可以直接进入老年代。
Minor GC 前,虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,如果满足则说明这次 Minor GC 确定安全。如果不,JVM 会查看 HandlePromotionFailure 参数是否允许担保失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足将 Minor GC,否则改成一次 FullGC。
JVM 常见调优参数
- -Xms 和 -Xmx:设置堆的起始大小和最大大小。
- -XX:+UseG1GC:使用 G1 垃圾收集器,适合于大堆内存和多核处理器的场景。
- -XX:NewSize:新生代大小
- -XX:MaxNewSize:新生代最大值
- -XX:PermSize:永久代初始值
- -XX:MaxPermSize:永久代最大值
- -XX:NewRatio:新生代与老年代区域比例
- -XX:SurvivorRatio:Eden 区与 Survivor 区比值
- -XX:MaxGCPauseMillis:设置期望的最大 GC 暂停时间(毫秒),以便于优化延迟。
- -XX:ParallelGCThreads:设置并行垃圾收集线程数。一般设置为可用 CPU 核心数。
- -XX:ConcGCThreads:设置 G1 的并发标记线程数,一般为 ParallelGCThreads 的一半。
- -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize:设置初始元空间大小和最大元空间大小,元空间用于存放类元数据。
- -XX:+PrintGCDetails 和 -XX:+PrintGCDateStamps:打印垃圾收集细节和时间戳。
- -Xloggc:将 GC 日志写入指定文件。
- -XX:+UseGCLogFileRotation 和 -XX:NumberOfGCLogFiles:开启GC日志文件的轮替和指定GC日志文件的数量。
类加载过程介绍一下
加载:在此阶段,类加载器负责查找类的字节码文件,并将其加载到内存中。字节码可以来自文件系统、网络等位置。加载阶段不会执行类中的静态初始化代码。
连接:连接阶段包括三个子阶段:
- 验证:确保加载的类文件格式正确,并且不包含不安全的构造。
- 准备:在内存中为类的静态变量分配内存空间,并设置默认初始值。这些变量在此阶段被初始化为默认值,比如数值类型为 0,引用类型为 null。
- 解析:将类、接口、字段和方法的符号引用解析为直接引用,即内存地址。这一步骤可能包括将常量池中的符号引用解析为直接引用。
初始化:在此阶段,执行类的静态初始化代码,包括类的构造函数、静态字段的赋值和静态代码块的执行。静态初始化在类的首次使用时进行,可以是创建实例、访问静态字段或调用静态方法。
一文彻底搞懂 Java 类加载机制 | 二哥的Java进阶之路
类加载器有哪些
- BootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库(%JAVA_HOME%/lib 目录下的 rt.jar、resources.jar、charsets.jar 等 jar 包和类)以及被 -Xbootclasspath 参数指定的路径下的所有类。
- ExtensionClassLoader(扩展类加载器):主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
- AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
介绍一下双亲委派机制
双亲委派机制是 Java 类加载器中的一种设计模式,用于确定类的加载方式和顺序。这个机制确保了 Java 核心库的安全性和一致性。该机制的核心思想是:如果一个类加载器收到了类加载请求,默认先将该请求委托给其父类加载器处理。只有当父级加载器无法加载该类时,才会尝试自行加载。
双亲委派机制能够提高安全性,防止核心库的类被篡改。因为所有的类最终都会通过顶层的启动类加载器进行加载。另外由于类加载器直接从父类加载器那里加载类,也避免了类的重复加载。
Java 对象的内存布局
Java 对象保存在内存中时,由以下三部分组成:
- 对象头
- 实例数据
- 对齐填充字节
其中对象头又由以下三部分组成:
- Mark Word(用于存储哈希码 HashCode、GC 分代年龄、锁状态标志位、线程持有的锁、偏向线程 ID 等信息。)
- 指向类的指针
- 数组长度(只有数组对象才有)
简述 JVM 给对象分配内存的策略
- 指针碰撞:这种方式在内存中放一个指针作为分界指示器将使用过的内存放在一边,空闲的放在另一边,通过指针挪动完成分配。
- 空闲列表:对于 Java 堆内存不规整的情况,虚拟机必须维护一个列表记录哪些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。
Java 对象内存分配是如何保证线程安全的
- 第一种方法采用 CAS 机制,配合失败重试的方式保证更新操作的原子性。该方式效率低。
- 第二种方法,每个线程在 Java 堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块"私有"内存中分配。一般采用这种策略。
Java 对象的创建过程
- 检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载。
- 通过检查通过后虚拟机将为新生对象分配内存。
- 完成内存分配后虚拟机将成员变量设为零值
- 设置对象头,包括哈希码、GC 信息、锁信息、对象所属类的类元信息等。
- 执行 init 方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
说一说对 Spring AOP 的了解
面向切面编程,可以说是面向对象编程的补充和完善。OOP 引入封装、继承、多态等概念来建立一种对象层次结构。不过 OOP 允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。 AOP 技术恰恰相反,它利用一种称为 “横切” 的技术,剖解开封装的对象内部,并将那些影响了多个类的 公共行为封装到一个可重用模块,并将其命名为切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的 耦合度,并有利于未来的可操作性和可维护性。
Spring 中 AOP 代理由 Spring 的 IOC 容器负责生成、管理,其依赖关系也由 IOC 容器负责管理。因此,AOP 代理可以直接使用容器中的其它 bean 实例作为目标,这种关系可由 IOC 容器的依赖注入提供。Spring 创建代理的规则为:
- 默认使用 JDK 动态代理来创建 AOP 代理,这样就可以为任何接口实例创建代理了。
- 当需要代理类,而不是代理接口的时候,Spring 会切换为使用 CGLIB 代理 ,也可强制使用 CGLIB。
AOP 编程其实是很简单的事情,纵观 AOP 编程,程序员只需要参与三个部分:
- 定义普通业务组件
- 定义切入点,一个切入点可能横切多个业务组件
- 定义增强处理,增强处理就是在 AOP 框架为普通业务组件织入的处理动作
所以进行 AOP 编程的关键就是定义切入点和定义增强处理,一旦定义了合适的切入点和增强处理,AOP 框架将自动生成 AOP 代理,即:代理对象的方法=增强处理+被代理对象的方法。
说一说对 Spring IOC 的理解
- 什么是 IOC
Spring 的 IOC,也就是控制反转,它的核心思想是让对象的创建和依赖关系由容器来控制,不是我们自己 new 出来的,这样各个组件之间就能保持松散的耦合。
这里的容器实际上就是个 Map,Map 中存放的是各种对象。通过 DI 依赖注入,Spring 容器可以在运行时动态地将依赖注入到需要它们的对象中,而不是对象自己去寻找或创建依赖。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。举例来说,在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了。
- 如何配置
Spring 时代我们一般通过 XML 文件来配置,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。使用配置可以告诉 Spring 容器如何创建对象、如何管理对象的生命周期。
总结来说,Spring 的 IOC 容器是一个中央化的、负责管理应用中所有对象生命周期的强大工具。
Bean 的作用域
在 Spring 中,那些组成应用程序的主体及由 Spring IOC 容器所管理的对象,被称之为 Bean。而 Bean 的作用域定义了在应用程序中创建的 Bean 实例的生命周期和可见范围,主要有以下几种。
- 单例:这是默认的作用域,当一个 Bean 的作用域为 Singleton,那么 Spring IoC 容器中只会存在一个共享的 Bean 实例,并且所有对 Bean 的请求,只要 id 与该 bean 定义相匹配,则只会返回 bean 的同一实例。
- 原型:当一个 bean 的作用域为 prototype,表示一个 bean 定义对应多个对象实例。prototype 作用域的 bean 会导致在每次对该 bean 请求时都会创建一个新的 bean 实例。因此,每次请求都会得到一个新的 Bean 实例。
- 请求:一个 HTTP 请求对应一个 Bean 实例,每个请求都有自己的 Bean 实例,且该 Bean 仅在请求期间有效。
- 会话:一个 HTTP 会话对应一个 Bean 实例,Bean 的生命周期与用户的会话周期相同。
- 应用程序:对于定义在 ServletContext 中的 Bean,整个 Web 应用程序共享一个 Bean 实例。
- Websocket:WebSocket生命周期内,每个 WebSocket 会话拥有一个 Bean 实例。
Bean 的生命周期
Spring Bean 的生命周期,其实就是 Spring 容器从创建 Bean 到销毁 Bean 的整个过程。这里面有几个关键步骤:
- 实例化 Bean:Spring 容器通过构造器或工厂方法创建 Bean 实例。
- 设置属性:容器会注入 Bean 的属性,这些属性可能是其他 Bean 的引用,也可能是简单的配置值。
- 检查 Aware 接口并设置相关依赖:如果 Bean 实现了 BeanNameAware 或 BeanFactoryAware 接口,容器会调用相应的 setBeanName 或 setBeanFactory 方法。
- BeanPostProcessor:在 Bean 的属性设置之后,Spring 会调用所有注册的 BeanPostProcessor 的 postProcessBeforeInitialization 方法。
- 初始化 Bean:如果 Bean 实现了 InitializingBean 接口,容器会调用其 afterPropertiesSet 方法。同时,如果 Bean 定义了 init-method,容器也会调用这个方法。
- BeanPostProcessor 的第二次调用:容器会再次调用所有注册的 BeanPostProcessor 的 postProcessAfterInitialization 方法,这次是在 Bean 初始化之后。
- 使用 Bean:此时,Bean 已经准备好了,可以被应用程序使用了。
- 处理 DisposableBean 和 destroy-method:当容器关闭时,如果 Bean 实现了 DisposableBean 接口,容器会调用其 destroy 方法。如果 Bean 定义了 destroy-method,容器也会调用这个方法。
- Bean 销毁:最后,Bean 被 Spring 容器销毁,结束了它的生命周期。
Bean 循环依赖是怎么解决的
Spring 默认只处理单例作用域(singleton scope)Bean 的循环依赖,对于原型作用域(prototype scope)的 Bean,Spring 不会自动解决循环依赖。
Spring 使用了三级缓存(三级缓存机制)来解决单例 Bean 的循环依赖问题。在 Bean 的创建过程中,Spring 通过提前暴露 Bean 的引用来打破循环依赖。
三级缓存机制:
- 一级缓存(singletonObjects):存放完全初始化好的单例 Bean。
- 二级缓存(earlySingletonObjects):存放提前暴露的尚未完全初始化的 Bean,用于解决循环依赖问题。
- 三级缓存(singletonFactories):存放可以创建 Bean 的工厂对象(ObjectFactory),在必要时通过工厂生成 Bean 实例并放入二级缓存。
具体过程:
1. 创建 Bean A 的过程:
- Spring 开始创建 Bean A,发现它依赖于 Bean B。
- A 还未初始化完成,因此暂时将 A 的工厂放入三级缓存。
2. 创建 Bean B 的过程:
- 在创建 B 的过程中,发现 B 依赖 A。
- Spring 检查缓存,发现 A 正在创建(在三级缓存中),此时将 A 提前暴露到二级缓存中(即通过代理或部分初始化的方式暴露)。
- 这样,B 可以依赖 A 并继续完成它的创建。
3. 完成 Bean A 的创建:
- Bean B 创建完成后,A 也可以继续初始化,最终创建完成。
这种提前暴露 Bean 的部分引用并放入缓存的方式,允许 Spring 在依赖的过程中使用尚未完全初始化的 Bean,解决了循环依赖的问题。
Spring Bean 循环依赖问题是如何解决的?_springbean 循环依赖怎么解决-CSDN博客
一文告诉你 Spring 是如何利用"三级缓存"巧妙解决 Bean 的循环依赖问题的【享学 Spring】-腾讯云开发者社区-腾讯云
Spring 中用到了哪些设计模式
- 工厂设计模式:Spring 使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
- 代理设计模式:Spring AOP 功能的实现。
- 单例设计模式:Spring 中的 Bean 默认都是单例的。
- 模板方法模式:Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
- 包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访 问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
- 观察者模式:Spring 事件驱动模型就是观察者模式很经典的一个应用。
- 适配器模式:Spring AOP 的增强或通知(Advice)使用到了适配器模式、Spring MVC 中也是用到了适配器模式适配 Controller。
Spring Boot Starter 有什么用
Spring Boot Starter 的作用是简化和加速项目的配置和依赖管理。
- Spring Boot Starter 可以理解为一种预配置的模块,它封装了特定功能的依赖项和配置,开发者只需引入相关的 Starter 依赖,无需手动配置大量的参数和依赖项。常用的启动器包括 spring-boot-starter-web(用于 Web 应用)、spring-boot-starter-data-jpa(用于数据库访问)等。 引入这些启动器后,Spring Boot 会自动配置所需的组件和 Bean,无需开发者手动添加大量配置。
- Starter 还管理了相关功能的依赖项,包括其他 Starter 和第三方库,确保它们能够良好地协同工作,避免版本冲突和依赖问题。
- Spring Boot Starter 的设计使得应用可以通过引入不同的 Starter 来实现模块化的开发。每个 Starter 都关注一个特定的功能领域,如数据库访问、消息队列、Web 开发等。
- 开发者可以创建自定义的 Starter,以便在项目中共享和重用特定功能的配置和依赖项。
Spring Boot 的常用注解
- @SpringBootApplication:用于标识主应用程序类,通常位于项目的顶级包中。这个注解包含了 @Configuration、@EnableAutoConfiguration 和 @ComponentScan。
- @Controller:用于标识类作为 Spring MVC 的 Controller。
- @RestController:类似于 @Controller,但它是专门用于 RESTful Web 服务的。它包含了 @Controller 和 @ResponseBody。
- @RequestMapping:用于将 HTTP 请求映射到 Controller 的处理方法。可以用在类级别和方法级别。
- @Autowired:用于自动注入 Spring 容器中的 Bean,可以用在构造方法、字段、Setter 方法上。
- @Service:用于标识类作为服务层的 Bean。
- @Repository:用于标识类作为数据访问层的 Bean,通常用于与数据库交互。
- @Component:通用的组件注解,用于标识任何 Spring 托管的 Bean。
- @Configuration:用于定义配置类,类中可能包含一些 @Bean 注解用于定义 Bean。
- @EnableAutoConfiguration:用于启用 Spring Boot 的自动配置机制,根据项目的依赖和配置自动配置 Spring 应用程序。
- @Value:用于从属性文件或配置中读取值,将值注入到成员变量中。
- @Qualifier: 与 @Autowired 一起使用,指定注入时使用的 Bean 名称。
- @ConfigurationProperties:用于将配置文件中的属性映射到 Java Bean。
- @Profile:用于定义不同环境下的配置,可以标识在类或方法上。
- @Async:用于将方法标记为异步执行。
- @Aspect:用于标记切面类。