介绍JVM
执行Java字节码的抽象机器,主要负责解释执行编译后的Java程序代码,提供一个平台无关的运行环境,“一次编写,到处运行”。
-
内存管理:
-
JVM 负责管理 Java 程序的内存使用。它将内存划分为不同的区域,如堆、栈、方法区等。
-
堆内存用于存储对象实例,是垃圾回收的主要区域。栈用于存储方法调用的局部变量和执行上下文。方法区存储类信息、常量池等。
-
JVM 通过垃圾回收机制自动回收不再使用的对象,以释放内存空间。不同的垃圾回收算法和策略可根据应用需求进行配置。
-
-
-
垃圾回收:
-
垃圾回收是 JVM 的重要功能之一。它通过识别和回收不再使用的对象来释放内存。
-
最新的 Java 版本通常会引入更高效的垃圾回收算法和优化,以提高垃圾回收的性能和效率。
-
常见的垃圾回收器包括串行垃圾回收器、并行垃圾回收器、CMS(并发标记清除)垃圾回收器和 G1 垃圾回收器等。
-
-
-
类加载:
-
JVM 负责加载 Java 类文件,并将其转换为可执行的字节码。
-
类加载过程包括加载、链接和初始化。加载阶段将类文件读取到内存中,链接阶段进行验证、准备和解析,初始化阶段执行类的静态初始化代码。
-
JVM 支持动态类加载,可以在运行时根据需要加载和卸载类。
-
-
-
字节码执行:
-
JVM 执行 Java 字节码,实现 Java 程序的逻辑。
-
字节码执行引擎可以解释执行字节码,也可以将字节码编译为本地机器码以提高执行效率。
-
最新的 Java 版本通常会对字节码执行进行优化,如即时编译(JIT)技术,以提高程序的性能。
-
-
-
安全管理:
-
JVM 提供了安全机制,防止恶意代码的攻击和非法操作。
-
它包括类文件验证、访问控制、安全管理器等功能,确保 Java 程序在安全的环境中运行。
-
Java 的安全模型可以限制程序对系统资源的访问,并提供加密、数字签名等安全功能。
-
-
-
多线程支持:
-
JVM 支持多线程编程,提供了线程同步和线程调度的机制。
-
线程可以在 JVM 中并发执行,共享堆内存和其他资源。
-
Java 的并发包提供了丰富的工具和类,用于实现高效的多线程编程。
-
-
JVM内存区域分布
-
堆(Heap):堆是 JVM 中最大的内存区域,用于存储对象实例和数组。对象的创建和垃圾回收都在堆上进行。堆是被所有线程共享的,它在Java虚拟机启动时被创建。
-
栈(Stack)
-
虚拟机栈(Java Virtual Machine Stacks):线程私有,用于存储方法调用的栈帧。栈帧包含了局部变量、操作数栈、方法返回地址等信息。先进后出FILO,-Xss 10M
-
本地方法栈(Native Method Stacks):与 Java 虚拟机栈类似,但是用于存储本地方法(非 Java 方法)的调用信息。本地方法是使用其他语言(如C、C++)编写的方法,通过JNI(Java Native Interface)调用在Java程序中执行。
-
-
程序计数器(Program Counter Register):较小的内存区域,用于指示当前线程正在执行的字节码指令的地址(分支、 循环、 跳转、 异常、 线程恢复)。每个线程都有自己独立的程序计数器,退出CPU时记录下一指令。
-
方法区(Method Area):方法区用于存储类信息、常量、静态变量等数据。在 Java 8 之前,方法区也被称为永久代(PermGen),但在 Java 8 及之后,方法区被实现为元空间(Metaspace)。
-
运行时常量池(Runtime Constant Pool):运行时常量池是方法区的一部分,用于存储编译期生成的各种字面量和符号引用。
-
-
方法区的永久代改为元空间的原因
-
解决内存溢出问题:永久代的大小是固定的,在某些情况下可能会导致内存溢出问题。而元空间使用本地内存,其大小可以根据系统的实际内存情况进行动态调整,从而避免了由于永久代大小限制而导致的内存溢出问题。
-
提高性能:元空间的内存分配和垃圾回收由操作系统直接管理,而不是由 JVM 进行管理。这可以提高内存的分配和回收效率,从而提高应用程序的性能。
-
更好的内存管理:元空间使用本地内存,与 JVM 堆内存分离,可以更好地进行内存管理。这可以避免由于堆内存不足而导致的方法区内存不足的问题,同时也可以提高内存的利用率。
-
与云环境更好的兼容性:在云环境中,应用程序的内存使用情况可能会受到限制。元空间的动态调整特性可以更好地适应云环境的需求,从而提高应用程序的可扩展性和灵活性。
堆内存分为新生代、老年代
新生代
-
存储新创建对象,先在Eden区
-
Eden满后,触发1次Minor GC
-
仍然存活的对象,复制到survivor区
-
清空Eden区
-
下次满后,Eden+Survivor区,一起复制到另一个Survivor区
-
年龄达到一定阈值,移到老年代(或者对象大小超出了新生代的容量)
-
老年代
-
存储长期存活的对象
-
垃圾回收频率相对较低,通常为
-
Major GC(Major Garbage Collection):
-
触发条件:老年代内存空间不足
-
影响:时间长,程序可能会短暂停顿
-
-
Full GC(Full Garbage Collection):
-
触发条件(多个或)
-
老年代空间不足,且无法通过 Major GC 释放足够的空间时
-
元空间空间不足时
-
显示调用System.gc()
-
-
影响
-
回收整个堆内存,包括新生代、老年代
-
时间更长,程序停顿时间可能更长,性能影响大
-
-
-
垃圾回收器是如何区分垃圾对象和活跃对象
-
引用计数法(Reference Counting)
-
维护每个对象的引用计数,当对象被引用时计数加一,当引用失效时计数减一。当对象的引用计数为零时,回收。
-
无法解决循环引用问题,即两个或多个对象相互引用,没有其他引用指向它们。
-
-
可达性分析法(Reachability Analysis)
-
从根对象(如方法区中的类静态变量、本地方法栈中的引用等)开始,通过对象之间的引用链,逐个遍历对象,并标记可达的对象。未被标记的对象则被认定为垃圾对象。
-
可解决循环引用问题,因为只有从根对象出发能够到达的对象才会被标记为可达。
-
GC ROOT对象
GC ROOT对象是指那些被认为是存活对象的根节点。垃圾回收器通过从这些根节点出发,遍历对象引用链,判断哪些对象是可达的,哪些对象是不可达的,从而进行垃圾回收。
常见的GC ROOT对象包括以下几种:
-
方法区中的类静态属性:类的静态属性存储在方法区中,它们随着类的加载而被创建,并且在整个程序的生命周期内存在。这些静态属性作为GC ROOT对象,因为它们可以被其他对象引用。
-
方法区中的常量引用:方法区中的常量池存储着常量的引用,包括字符串常量、类名、方法名等。这些常量引用也被视为GC ROOT对象,因为它们被其他对象引用。
-
活动线程的本地变量和调用栈:活动线程中的本地变量和调用栈中的对象引用也被认为是GC ROOT对象。这些对象引用可以通过方法调用链追溯到活动线程的栈帧,因此它们是垃圾回收的起点。
-
JNI(Java Native Interface)引用:JNI允许Java程序调用本地代码,而本地代码中可能会创建和引用Java对象。JNI引用的对象也被视为GC ROOT对象,因为它们可能被本地代码持有引用。
垃圾回收算法
-
垃圾收集算法(Garbage Collection Algorithm)——可达性
-
垃圾回收器使用不同的算法来执行垃圾回收操作。
-
常见的算法
-
标记-清除算法(Mark and Sweep):这是最基本的垃圾回收算法。它分为两个阶段:标记阶段和清除阶段。首先,从根节点开始,标记所有被引用的对象。然后,在清除阶段,遍历整个堆,清除所有未被标记的对象。该算法的缺点是会产生内存碎片。
-
复制算法(Copying):该算法将堆内存分为两个区域,通常是相等大小的两块。当一块区域被填满后,将存活的对象复制到另一块区域中,然后清除原来的区域。该算法的优点是不会产生内存碎片,但代价是需要额外的空间。
-
标记-压缩算法(Mark and Compact):该算法结合了标记-清除算法和复制算法的优点。首先,标记所有被引用的对象。然后,将存活的对象向一端移动,并清除边界之外的内存。这样可以解决标记-清除算法的内存碎片问题。
-
分代算法(Generational):该算法基于一个观察:大部分对象的生命周期很短。根据这个观察,堆内存被划分为几个代,通常是新生代和老年代。新生代中的对象经常被回收,而老年代中的对象则存活更久。这样,可以针对不同代使用不同的垃圾回收算法,以提高效率。
-
-
让一个被判定为垃圾的对象不被回收
-
使用强引用:将对象保持在一个强引用中,这样垃圾回收器就不会回收它。强引用是 Java 中最常见的引用类型,只要存在强引用指向对象,对象就不会被回收。
-
使用 finalize() 方法:你可以在对象的类中重写 finalize() 方法。在 finalize() 方法中,你可以执行一些清理操作或重新建立对象的引用,从而防止对象被回收。然而,需要注意的是,finalize() 方法的调用时机是不确定的,可能不执行,并且不建议过度依赖它,因为它可能会导致性能问题和不确定性。
-
使用软引用或弱引用:Java 提供了软引用(SoftReference)和弱引用(WeakReference)来管理对象的引用。软引用和弱引用不会阻止对象被垃圾回收,但它们可以提供一种机制,在对象被回收之前执行一些额外的操作或获取一些通知。
-
避免对象被孤立:确保对象在程序中仍然被其他对象引用,或者将其存储在一个可以被访问到的位置,以避免它被判定为孤立的垃圾对象。
引用类型
强引用(StrongReference):当一个对象被强引用变量引用时,垃圾回收器不会回收该对象,除非该强引用变量不再引用该对象。
软引用(SoftReference):内存充足时不会被垃圾回收器回收,只有在内存不足时才会被回收。常用于实现缓存。
弱引用(WeakReference):垃圾回收时会被立即回收,无论内存是否充足。常用于解决循环引用的问题,避免对象之间的循环引用导致内存泄漏。
虚引用(PhantomReference):放弃。
减少Full GC发生
Full GC 的发生,可以采取以下一些措施:
-
合理调整堆内存的大小,避免内存不足的情况。
-
优化对象的创建和使用,尽量减少不必要的对象创建。
-
避免在代码中显式调用
System.gc()
方法。 -
监控和分析垃圾回收的日志,了解垃圾回收的情况,及时发现和解决可能存在的问题。
STW(Stop-The-World)是什么,以及它在何时发生
是一种暂停所有应用线程的机制,用于执行特定的操作或任务。当STW发生时,所有的应用线程都会被暂停,直到特定的操作或任务完成后才会恢复执行。
-
垃圾回收(Garbage Collection):
-
在STW期间,垃圾回收器会扫描堆中的对象,并标记和回收垃圾对象。
-
一旦垃圾回收完成,应用线程才会被恢复。
-
为了确保对象的一致性和安全性。
-
-
类加载和卸载(Class Loading and Unloading):
-
在STW期间,执行类加载或卸载的操作。
-
确保类加载和卸载的过程是线程安全的。
-
-
JIT编译器优化(Just-In-Time Compiler Optimization):
-
在STW期间,JIT编译器会分析和优化字节码,以提高程序的执行效率。
-
编译为本地机器码。
-
确保字节码的一致性和正确性。
-
-
安全点检查:方法调用、循环迭代等,如正在执行非原子操作、阻塞操作、长时间操作。在垃圾回收前检查。
分享使用过的堆栈分析工具
-
jstack:这是 JDK 自带的工具,可以生成 Java 进程的线程快照,帮助我们分析线程的状态、阻塞情况等。
-
找到要分析的 Java 进程的 PID。
-
在命令行中运行
jstack PID
,其中PID
是进程的 ID。
-
-
VisualVM:这是一个功能强大的 Java 性能分析工具,可以监控 Java 应用程序的内存使用情况、线程状态、类加载情况等,并提供了丰富的分析和调试功能
-
连接到要分析的 Java 应用程序。
-
在 VisualVM 的界面中,可以查看应用程序的内存使用情况、线程状态、类加载情况等。
-
可以使用各种插件进行更深入的分析和调试。
-
-
MAT(Memory Analyzer Tool):这是一款专门用于分析 Java 堆内存的工具,可以帮助我们找出内存泄漏和内存溢出的原因。
-
打开 MAT,并导入要分析的堆转储文件。
-
在 MAT 的界面中,可以查看堆内存的使用情况、对象的引用关系等。
-
VisualVM 发现 OOM(内存溢出)、线程死锁和性能问题
-
OOM(内存溢出):
-
启动 VisualVM 并连接到要分析的 Java 应用程序。
-
在 VisualVM 的界面中,查看“内存”选项卡。
-
观察内存使用情况图表,如果发现内存使用持续增长且接近或超过可用内存限制,可能存在 OOM 风险。
-
可以进一步查看“堆dump”信息,分析对象的分配和引用情况,找出可能导致内存泄漏的对象。
-
-
线程死锁:
-
在 VisualVM 的“线程”选项卡中,查看线程的状态。
-
如果发现线程长时间处于阻塞状态(BLOCKED 或 WAITING),可能存在死锁情况。
-
点击具体的线程,可以查看线程的堆栈跟踪信息,找出可能导致死锁的代码位置。
-
检查线程之间的资源竞争和同步操作,以确定死锁的原因。
-
-
性能问题:
-
关注“CPU”和“内存”选项卡,查看应用程序的 CPU 和内存使用情况。
-
如果 CPU 使用率过高或内存使用波动较大,可能存在性能问题。
-
在“抽样器”中,可以进行 CPU 和内存采样,以获取更详细的性能数据。
-
分析采样结果,找出消耗 CPU 或内存较多的方法或代码段。
-
还可以查看“垃圾回收”选项卡,了解垃圾回收的频率和时间,以评估对性能的影响。
-
JVM 调优方面的经验和实践
-
内存设置:根据应用程序的需求和硬件资源,合理设置 JVM 的内存大小。可以通过调整
-Xmx
(最大堆内存)和-Xms
(初始堆内存)等参数来优化内存使用。 -
垃圾回收器选择:JVM 提供了多种垃圾回收器,如 Serial、Parallel、CMS、G1 等。根据应用程序的特点和性能要求,选择合适的垃圾回收器。
-
垃圾回收参数调整:对于选择的垃圾回收器,可以进一步调整相关参数,如
-XX:SurvivorRatio
、-XX:MaxTenuringThreshold
等,以优化垃圾回收的效率。 -
堆内存分析:使用工具(如 jconsole、jvisualvm 等)分析堆内存的使用情况,找出可能的内存泄漏或内存溢出问题,并进行相应的优化。
-
代码优化:通过优化代码,减少对象的创建和内存分配,提高代码的性能和效率。
-
监控和日志分析:密切关注应用程序的性能指标和日志,及时发现和解决潜在的问题。
-
压力测试:在实际环境中进行压力测试,以评估 JVM 的性能和稳定性,并根据测试结果进行调优。
单例模式是否可能导致内存泄漏
一般不会。但是,如果单例对象,引用了其他大对象,就有可能。
因为大对象被单例对象引用,而单例对象不会被垃圾回收。