二、Java内存
1. 简述一下堆和栈?
堆(Heap)和栈(Stack)是程序在运行时用于存储数据的两个内存区域,它们在内存管理方面各有特点和用途:
堆(Heap)
- 动态内存分配:堆是程序运行时动态分配内存的区域,其生命周期由垃圾收集器管理。
- 用途:主要用于存储Java对象实例,包括通过new关键字创建的对象和数组。
- 管理方式:堆内存的分配和清理是动态的、手动的(通过代码的创建和垃圾回收)。
- 内存大小:堆的大小远比栈大,可达到GB级别,取决于系统内存。
- 性能:相比栈,在存取速度上稍慢,因为涉及更复杂的内存管理。
- 错误处理:当堆内存不足时,Java会抛出OutOfMemoryError异常。
栈(Stack)
- 静态内存分配:栈是线程私有的内存区域,每个线程创建时都会分配一个栈,生命周期与线程相同。
- 用途:主要用于存储方法执行时的局部变量和方法调用记录(栈帧)。
- 管理方式:栈内存的分配和清理是自动的,由系统完成。方法调用结束时,对应的栈帧会被自动弹出栈。
- 内存大小:栈的大小相比堆小很多,通常在MB级别,可以通过JVM参数(如-Xss)进行配置。
- 性能:相比堆,在存取速度上更快,因为涉及到的内存管理相对简单。
- 错误处理:当栈内存不足时,例如递归调用太深,Java会抛出StackOverflowError异常。
2. Java中新建的对象存放在哪里?
在Java中,新建的对象根据其具体类型和上下文环境存放在不同的内存区域:
-
使用static final修饰的常量通常存放在常量池中。在JVM中,常量池是元数据区(Metaspace)的一部分,而不是永久代(Permanent Generation)的一部分(JDK 8之前)。
-
方法中的局部变量存储在虚拟机栈(Java Stacks)中的方法栈帧的局部变量表中。局部变量可以是基本数据类型的变量,也可以是对象的引用变量。基本类型的变量直接存储值,而对象的引用变量存储指向实际对象内存地址的引用。
-
类中的普通成员变量存储在堆内存(Heap)中。当使用new关键字创建一个对象时,该对象会被分配在堆内存中。成员变量如果是基本数据类型,则直接存储值;如果是引用类型,则存储指向实际对象的引用。
-
类信息和类中的静态变量(无论是否被final修饰)都存放在元数据区(Metaspace)。这包括类的成员方法、构造函数和静态变量等。
需要注意的是,对象的引用变量本身存储在栈上,它包含了指向堆内存中实际对象的指针。当引用变量的作用域结束后,引用本身会被释放,但被引用的对象不会立即被销毁,除非没有任何引用指向它。此时,对象将成为垃圾,等待Java垃圾回收器(Garbage Collector)清理。
此外,Java使用双亲委派模型进行类的加载,以确保类的全局唯一性,防止类被重复加载。类的加载过程发生在元数据区,而对象实例的创建发生在堆内存中。
3. 简述一下JVM内存模型?
JMM,即Java内存模型(Java Memory Model),是一个抽象的概念,它描述了Java虚拟机(JVM)在运行Java程序时,如何处理内存中的变量,以及线程如何通过内存进行交互。
JMM的关键特性包括:
主内存和工作内存
- JMM定义了主内存和工作内存的概念。主内存是所有线程共享的内存区域,工作内存是每个线程私有的内存缓冲区。线程对变量的操作(读取、赋值等)都必须在工作内存中进行,然后同步回主内存。
内存屏障
- JMM通过插入特定类型的内存屏障来禁止特定类型的处理器重排序,确保特定的内存读写操作的顺序。
happens-before原则
- JMM通过这个原则来阐述操作之间的内存可见性。如果操作A happens-before 操作B,那么A操作的执行结果对B可见,而且A操作的执行顺序排在B操作之前。
锁的内存语义
- JMM定义了synchronized关键字在获取和释放锁时的内存语义。包括:加锁时,清空工作内存中的共享变量值,使用共享变量的值必须从主内存中重新读取;解锁时,将工作内存中的共享变量的最新值刷新回主内存。
volatile的内存语义
- volatile变量保证了不同线程对该变量的读写操作的可见性,并且禁止指令重排序优化。
final的内存语义
- final变量一旦初始化完成,其值就被写入主内存,并且之后不可更改。final变量可以确保初始化后的可见性。
JMM的主要目的是解决多线程环境下的可见性、原子性、有序性问题,确保Java程序在多线程环境下能够正确运行。
4. 简述一下G1?
G1(Garbage-First)是一款面向服务器的垃圾回收器,旨在满足具有大内存需求的应用程序,同时尽可能减少垃圾回收的停顿时间。G1将堆内存分割成多个大小相等的独立区域(Region),并根据每个区域垃圾回收的价值来优先回收那些价值最大的区域,这也是其名字“Garbage-First”的由来。
G1的主要特点如下:
-
分代收集:G1依然保留了分代的概念,将对象分为年轻代和老年代。
-
并行与并发:G1在年轻代使用并行回收,而在老年代则使用并发标记清除,以减少停顿时间。
-
可控的停顿时间:G1允许用户设定垃圾回收的最大停顿时间,以此来控制垃圾回收对应用响应性的影响。
-
整理算法:与CMS的清除算法不同,G1在回收时会进行整理,以减少内存碎片。
-
增量回收:G1通过每次只回收一部分区域,而不是整个堆,实现了增量化垃圾回收,这样可以进一步降低单次垃圾回收的停顿时间。
-
适应性:G1能够根据应用程序的行为和垃圾回收的效率动态调整区域的大小和垃圾回收策略。
由于上述特点,G1适用于那些需要低延迟和大内存容量的应用场景。但是,相对于其他GC算法,G1可能会消耗更多的CPU资源,因为它需要在垃圾回收过程中维护更多的内存区域和数据结构。
选择G1还是其他垃圾回收器,需要根据具体的应用场景和性能要求来决定。例如,如果应用程序对延迟非常敏感,且运行在多核服务器上,G1可能是一个很好的选择。
5. G1中Young GC、Mixed GC和Full GC的区别?
Young GC:年轻代垃圾回收。CSet(收集集合)中只包括年轻代的分区。
Mixed GC:混合垃圾回收。CSet中包括年轻代分区和老年代分区。
Full GC:期间会暂停应用,对所有分区进行垃圾回收和压缩。
在G1垃圾回收器中,Young GC(年轻代垃圾回收)、Mixed GC(混合垃圾回收)和Full GC(完全垃圾回收)是三种不同的垃圾回收模式,它们有着不同的时机、目标和行为:
- Young GC:
- 时机:当年轻代(Eden区)被占满时触发。
- 目标:回收年轻代中的垃圾对象。
- 行为:采用并行复制算法,将存活的对象从Eden区复制到存活区(Survivor区),可能晋升一些对象到老年代。是一个STW事件,暂停所有应用线程。
- 特点:频率较高,持续时间较短。
- Mixed GC:
- 时机:当堆内存使用率达到一定阈值或全局并发标记结束时触发。
- 目标:回收年轻代和部分老年代中的垃圾对象。
- 行为:选择部分老年代区域进行回收,是一个STW事件。
- 特点:旨在在控制停顿时间的同时,回收更多老年代垃圾。
- Full GC:
- 时机:当Mixed GC无法跟上内存分配速度或显式调用System.gc()时触发。
- 目标:回收整个堆内存中的所有垃圾对象。
- 行为:进行一次完整的堆内存清理,包括年轻代和老年代,是一个长时间的STW事件。
- 特点:停顿时间通常比其他两种GC要长,对性能影响较大。
G1垃圾回收器的设计目标是通过合理管理Young GC和Mixed GC来避免Full GC的频繁发生,以减少停顿时间并保持较高的吞吐量。
6.G1配置项ParallelGCThreads和ConcGCThreads的区别?
ParallelGCThreads:配置在停滞阶段(STW)并发执行垃圾回收操作的线程数。一般等于逻辑CPU核数。
ConcGCThreads:配置并发标记阶段,并发执行标记的线程数。一般配置为ParallelGCThreads的四分之一。
ParallelGCThreads 和 ConcGCThreads 是 G1 垃圾回收器中的两个重要配置参数,它们分别控制着不同类型的垃圾回收过程中的线程数量。
ParallelGCThreads:
- 用途:设置并行垃圾回收(Young GC)时的线程数量,用于加快垃圾回收过程。
- 默认值:通常等于物理处理器的数量(不包括超线程)。
- 适用场景:适用于需要快速回收年轻代的应用程序。
ConcGCThreads:
- 用途:设置并发垃圾回收(Mixed GC 和标记阶段)时的线程数量,用于减少停顿时间并允许与应用程序线程竞争CPU资源。
- 默认值:通常较小,大约是物理处理器数量的 1/4 或更少。
- 适用场景:适用于需要减少停顿时间并且能够容忍垃圾回收线程与应用程序线程竞争CPU资源的场景。
这两个参数的关键区别在于工作模式、影响范围和性能考量:
- 工作模式:ParallelGCThreads 控制的是并行工作模式下的线程数,应用程序会被暂停;ConcGCThreads 控制的是并发工作模式下的线程数,应用程序不会被完全暂停。
- 影响范围:ParallelGCThreads 主要影响 Young GC 的性能;ConcGCThreads 影响 Mixed GC 和并发标记阶段的性能。
- 性能考量:增加 ParallelGCThreads 可能提高垃圾回收的效率,但可能会增加停顿时间;增加 ConcGCThreads 可能减少停顿时间,但可能会增加应用程序的响应时间。
在配置这两个参数时,需要根据具体的应用程序需求、硬件资源以及性能目标进行调优,以找到合适的平衡点,最小化停顿时间同时保持较高的吞吐量。
7.G1垃圾回收在并发标记时,如果发生对象引用修改会如何?
如果在初始标记中的对象引用发生变化时,会被记录到一个SATB缓冲区中,当这个缓冲区满了后会被加载到全局列表中。并发标记阶段还会有线程去处理这个全局列表。
在G1垃圾回收器的并发标记阶段,对象引用的修改是正常现象,因为这一阶段允许应用程序的线程与垃圾回收器线程同时运行。为了确保标记的准确性,G1采用了“snapshot-at-the-beginning”(SATB)技术:
-
SATB日志缓冲区:G1会记录并发标记阶段发生的对象引用修改到SATB日志缓冲区中,这些变化会被暂时记录下来。
-
处理SATB缓冲区:当SATB缓冲区满时,G1将这些变化刷新到全局数据结构中,以便后续的标记阶段考虑这些变化。
-
维护标记的正确性:专门的线程负责处理全局列表,确保所有在标记过程中发生变化的对象引用都被正确处理,以确保标记的准确性。
-
增量更新:处理全局列表时,G1会更新对堆内存中对象图的了解,以确保在最终标记阶段之前,所有存活的对象都被正确标记。
-
最终标记阶段的修正:在并发标记阶段结束后进入最终标记阶段,暂停应用程序线程,再次扫描根对象并根据SATB记录修正标记,确保所有存活对象都被正确标记。
通过这种方式,G1垃圾回收器能够有效处理并发标记阶段对象引用的修改,保持垃圾回收的高效性和准确性,也是G1在减少停顿时间方面的优势之一。
8.简述一下CMS?
CMS(Concurrent Mark Sweep)垃圾回收器适用于对响应时间要求高的应用程序,如Web服务器、电信网络服务等。它通过初始标记、并发标记、重新标记和并发清理等阶段来降低停顿时间,但也存在一些缺点:
-
CPU敏感:CMS在并发标记和并发清理阶段需要大量CPU资源,如果服务器CPU资源有限可能影响性能。
-
浮动垃圾:由于并发清理阶段应用程序线程仍在运行,可能会产生新的垃圾,称为“浮动垃圾”,导致下次回收周期需提前触发。
-
空间碎片:CMS使用标记-清除算法,回收过程中不整理内存碎片,可能导致碎片化,影响新对象分配。
-
停顿时间预测性差:由于并发执行,停顿时间可能因堆内存大小、对象复杂度等因素变得难以预测。
综合考虑这些因素,对于低延迟和高吞吐量场景,可能需要考虑其他垃圾回收器,如G1垃圾回收器。
9.如何排查OOM问题?
OOM(Out Of Memory)指的是内存溢出,即应用程序请求的内存超过了JVM可以提供的最大内存限制。
排查OOM(Out Of Memory)问题通常需要一系列的系统性和细致的分析步骤。以下是一些常用的排查方法:
-
问题原因识别:
- 内存泄露:应用程序未能释放申请的内存,导致内存持续占用。
- 内存溢出:应用程序申请的内存超出了JVM设置的内存限制。
-
情况分类:
- Java Heap Space:堆内存溢出。
- PermGen/Metaspace Space:永久代/元空间溢出。
- StackOverflowError:虚拟机栈溢出。
-
排查过程:
- 生成堆转储文件(Heap Dump)。
- 使用内存分析工具如MAT(Memory Analyzer Tool)。
- 分析堆栈跟踪和垃圾回收日志。
- 代码审查,查找可能存在的内存管理问题。
- 动态监控JVM实时内存使用情况。
- 压力测试模拟生产环境负载。
通过以上步骤的综合分析,可以逐步定位并解决OOM问题,需要耐心和细致地处理,有时可能需要多次迭代才能找到准确的原因并解决问题。
10. 如何评估对象占用内存大小?
评估对象在Java虚拟机中占用的内存大小是重要的性能考量因素,特别是在处理大量对象或进行内存敏感型应用开发时。有两种评估对象占用内存大小的方法:
-
手动评估:
- 对象头:包括MarkWord(通常占8字节)、类指针(占4字节,64位系统可能更大)、数组长度字段(对数组对象,通常占4字节)。
- 实例数据:根据不同基本类型和引用类型的大小来计算。
- 填充部分(Padding):为满足JVM要求对象大小为8字节整数倍而填充的字节。
-
利用工具评估:
- ObjectSizeCalculator:使用sun.misc.Unsafe类计算对象大小,需谨慎使用。
- Apache Lucene’s RamUsageEstimator:递归计算对象及其引用链上所有对象的大小。
- jol(Java Object Layout):帮助分析和理解Java对象布局,提供丰富的打印选项展示对象内部结构、大小和对齐情况。
在使用工具评估时要注意:
- 工具准确性受JVM版本和垃圾收集器类型影响。
- 某些工具可能无法考虑对象间引用关系,给出的是对象自身大小而非整个对象图大小。
- 在内存评估中理解这些细微差异尤为重要,尤其在性能调优和内存泄漏检测时。
11. 如何进行JVM调优?
JVM调优目的:减少GC频率和Full GC次数,从而提高应用吞吐量,即提高应用运行的有效时间在应用运行的总时间中的占比。因为GC时会导致应用停滞。
JVM(Java虚拟机)调优是一个复杂的过程,旨在优化Java应用程序的性能和资源利用率。以下是进行JVM调优的一些基本步骤和考虑因素:
-
确定调优目标:
- 性能提升:提高应用程序的响应速度和处理能力。
- 资源优化:更高效地利用CPU、内存和磁盘I/O。
- 稳定性增强:减少延迟、避免内存泄漏和过度的垃圾回收。
-
性能监控与分析:
- 使用工具监控JVM的运行状态,包括内存使用、垃圾回收、CPU使用情况等。
- 分析应用程序的运行日志,查找瓶颈和性能问题。
-
堆内存设置:
-Xms:设置初始堆大小。-Xmx:设置最大堆大小。-Xmn:设置年轻代大小。
-
垃圾收集器选择:
- 根据应用程序特点选择合适的垃圾收集器。
- 使用
-XX:+Use[GC名称]参数指定垃圾收集器。
-
调整垃圾收集器参数:
- 根据监控数据调整GC参数。
- 使用
-XX:MaxGCPauseMillis设置GC暂停时间。
-
性能监控参数:
-XX:PrintGCDetails:打印详细的GC日志。-XX:PrintGCDateStamps:打印GC时间戳。
-
优化JVM选项:
- 逃逸分析、标量替换、代码缓存等优化选项。
-
运行期优化:
- 利用JVM代码缓存、即时编译(JIT)和垃圾收集调优。
- 分析和优化热点代码。
-
持续调优:
- 定期审查和更新JVM参数。
- 根据应用程序运行情况不断调整。
-
性能测试:
- 在调优前后进行性能测试,确保调优效果符合预期。
- 使用基准测试工具进行全面的性能评估。
-
注意事项:
- 避免过度调优,可能导致系统不稳定。
- 任何调优变更都应该逐步进行,以便可以回滚到之前的配置。
- 确保理解每个参数的影响,不要盲目应用推荐的设置。
JVM调优需要深入理解JVM内部原理、应用程序行为和系统架构,正确的调优可以显著提升应用程序性能,但需要谨慎和系统化的方法。
2786



被折叠的 条评论
为什么被折叠?



