当前位置: 首页>后端>正文

深入理解Java的内存管理和垃圾回收机制

当涉及到深入理解Java的内存管理时,下面是一个可能的博客大纲:

1. 引言

了解和掌握Java的内存管理技术对于开发高性能、稳定和安全的Java应用程序至关重要。良好的内存管理实践可以提高应用程序的效率、稳定性和安全性,同时也能减少资源消耗和维护成本。

2. Java虚拟机内存结构

2.1 Java虚拟机的内存划分

  • 永久代(Perm Generation):该区域保存的是Java类的元数据信息,例如类名、方法信息、常量池等,JDK8以后被元空间(Meta Space)替代。
  • 堆内存(Heap Memory):用于存储对象实例,也是Java程序中最大的一块内存空间。堆内存由年轻代(Young Generation)和老年代(Old Generation)组成。
  • 栈内存(Stack Memory):栈内存用于存储每个线程的本地变量以及方法调用堆栈信息。
  • 程序计数器(Program Counter Register):程序计数器用于记录线程执行的字节码指令地址,也就是当前线程所执行的代码行号。
  • 本地方法栈(Native Method Stack):本地方法栈用于保存Java应用调用本地方法(Native Method)时的参数和局部变量,与栈内存不同的是,本地方法栈中不存储Java方法。

什么是本地方法?

在Java中,本地方法(Native Method)是指使用其他语言(如C、C++等)编写的方法或函数,它们通过Java Native Interface(JNI)与Java应用程序进行交互和调用。

在Java中,使用本地方法可以实现与底层系统或特定硬件交互的功能,或者利用其他语言的优势来提高性能或访问特定的系统资源。常见的本地方法应用包括与操作系统进行交互、访问硬件设备、使用底层库等。

使用本地方法需要进行一些特殊的操作和配置。首先,需要在Java代码中进行声明和定义本地方法,并使用native关键字标记。然后,在Java代码中调用本地方法时,JVM会通过JNI将控制权转移到底层编写的本地方法,再由本地方法实现相应的功能。本地方法的具体实现在底层使用其他语言编写,在编译时生成相应的动态链接库(如.so文件或.dll文件),然后在Java应用程序中加载和调用。

需要注意的是,使用本地方法可以提高性能和访问特定资源,但同时也引入了一些安全和可移植性的问题,因此在使用本地方法时需要谨慎操作,并确保遵循相应的安全措施和最佳实践。

2.2 Java内存模型

Java内存模型(Java Memory Model,JMM)是一种规范,其定义了Java虚拟机在执行Java程序时的内存管理方式和规则,以确保多线程程序在各种平台上执行时都能正确、可靠地工作。

在Java中,不同的线程可以访问共享的变量和对象。为了确保这些共享数据的正确性和一致性,Java内存模型定义了以下三个主要概念:

  1. 主内存(Main Memory):主内存是Java虚拟机中的共享内存区域,所有线程都可以访问其中的变量和对象。主内存中保存的是所有共享变量的真实值。

  2. 工作内存(Working Memory):工作内存是线程私有的内存区域,每个线程都有自己的工作内存。工作内存中保存了线程需要访问的变量和对象的本地副本。每个线程不能直接访问主内存中的变量和对象,而是通过工作内存进行访问和修改。

  3. 内存交互(Memory Interaction):内存交互指的是线程之间对共享变量的传递和交换。线程之间通过主内存进行通信,每个线程可以将变量从工作内存刷新到主内存,也可以从主内存获取最新的变量值到工作内存。

在Java内存模型中,共享变量的访问原则是“先读取后写入”,即一个线程在修改共享变量之前,必须先从主内存中读取最新的变量值,然后将其拷贝到工作内存中进行修改,最后再将修改后的值刷新到主内存中,以便其他线程可以看到修改的结果。同时,Java内存模型还规定了一系列同步操作的具体实现方式,如synchronized关键字、volatile关键字、Lock等。

需要注意的是,Java内存模型是一种抽象的规范,具体的内存管理和同步方式会因不同的虚拟机、操作系统、硬件平台等而有所不同,因此对于多线程程序的开发和调试,需要特别注意内存的可见性、原子性和有序性等问题,并遵循相应的最佳实践。

2.3 Java 内存区域和 JMM 有何区别?

这是一个比较常见的问题,很多初学者非常容易搞混。 Java 内存区域和内存模型是完全不一样的两个东西

  • JVM 内存结构和 Java 虚拟机的运行时区域相关,定义了 JVM 在运行时如何分区存储程序数据,就比如说堆主要用于存放对象实例。
  • Java 内存模型和 Java 的并发编程相关,抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中,规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。

2.4 理解happens-before

为什么需要 happens-before 原则?

happens-before 原则的诞生是为了程序员和编译器、处理器之间的平衡。程序员追求的是易于理解和编程的强内存模型,遵守既定规则编码即可。编译器和处理器追求的是较少约束的弱内存模型,让它们尽己所能地去优化性能,让性能最大化。happens-before 原则的设计思想其实非常简单:为了对编译器和处理器的约束尽可能少,只要不改变程序的执行结果(单线程程序和正确执行的多线程程序),编译器和处理器怎么进行重排序优化都行。对于会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序。

happens-before 常见规则有哪些?谈谈你的理解?

happens-before 的规则就 8 条,说多不多,重点了解下面列举的 5 条即可。全记是不可能的,很快就忘记了,意义不大,随时查阅即可。

  1. 程序顺序规则:一个线程内,按照代码顺序,书写在前面的操作 happens-before 于书写在后面的操作;
  2. 解锁规则:解锁 happens-before 于加锁;
  3. volatile 变量规则:对一个 volatile 变量的写操作 happens-before 于后面对这个 volatile 变量的读操作。说白了就是对 volatile 变量的写操作的结果对于发生于其后的任何操作都是可见的。
  4. 传递规则:如果 A happens-before B,且 B happens-before C,那么 A happens-before C;
  5. 线程启动规则:Thread 对象的 start()方法 happens-before 于此线程的每一个动作。

如果两个操作不满足上述任意一个 happens-before 规则,那么这两个操作就没有顺序的保障,JVM 可以对这两个操作进行重排序。

3. 垃圾回收机制

3.1垃圾回收的目的

垃圾回收的目的是为了方便开发人员管理内存、避免内存泄漏、提高程序的性能和可靠性,并优化内存的利用,从而使得Java程序更加高效、健壮和可持续发展。

3.2 常见的垃圾回收策略

3.2.1. 引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,这个计数器的值就加一,当引用失效时,这个计数器的值就减一。当引用计数器的值变为0的时候,说明这个对象已经不能再被使用了。
缺点:很难解决对象之间循环引用的问题。
3.2.2 可达性分析算法

3.3 垃圾回收的主要区域

JVM(Java Virtual Machine)的垃圾回收主要涉及两个主要区域:年轻代(Young Generation)和老年代(Old Generation)。其中,年轻代又分为三部分:Eden区、SurvivorFrom区和SurvivorTo区。

  1. 年轻代(Young Generation):年轻代是用于存放新创建的对象的区域。在年轻代中,对象更常见地产生和被回收。年轻代一般被划分为一个较大的Eden区和两个较小的Survivor区(SurvivorFrom和SurvivorTo)。当创建一个新对象时,它首先会被分配到Eden区。当Eden区满时,会触发Minor GC(年轻代垃圾回收),将存活的对象复制到Survivor区。经过多次GC后,仍然存活的对象会被晋升到老年代。

  2. 老年代(Old Generation):老年代用于存放长时间存活的对象。在老年代中,对象的生命周期较长,不会被频繁回收。当对象经过一定次数的复制(一般是15次)后,它会被晋升到老年代。当老年代空间不足时,会触发Major GC(老年代垃圾回收),进行全局的垃圾回收。

除了年轻代和老年代,还有一个永久代(Permanent Generation),用于存放JVM的元数据和类信息。但在Java 8及以后的版本中,永久代被元数据区(Metaspace)所取代。

3.4 垃圾回收的算法

标记-清除算法(Mark-Sweep):最早的垃圾回收算法,分为两个阶段,先标记所有存活的对象,再清除掉所有未被标记的对象。这个算法的缺点是会产生大量不连续的内存碎片,导致内存利用率下降。

复制算法(Copying):将内存划分为两个区域,一边用于存放对象,另一边留着备份。当存活的对象被复制到备份区域后,再清空原区域。这个算法解决了内存碎片问题,但将内存利用率降低了一半。

标记-整理算法(Mark-Compact):也是两个阶段,先标记所有存活的对象,再将存活的对象移动到内存的一端,然后清空端边界以后的所有内存。这个算法没有复制算法占用空间大,也没有标记-清除算法造成内存碎片。

分代收集算法(Generational):将堆内存划分为年轻代和老年代两部分,年轻代中存放生存周期较短的对象,老年代中存放生存周期较长的对象。在年轻代中采用复制算法,在老年代中采用标记-整理算法。分代收集算法更能利用每个阶段的特点,提高垃圾回收效率。

4. 垃圾收集器

4.1 常见的垃圾收集器

  1. G1垃圾回收器(Garbage-First Garbage Collector):G1垃圾回收器是Java 7以后引入的一种全新的垃圾回收器。它的主要目标是为了解决应用程序内存占用大、响应时间长的问题。G1回收器将整个堆内存划分为多个大小相等的区域,每个区域可以是Eden区、Survivor区或者老年代区域。它使用并发标记-清理算法,在垃圾回收时能够并发执行标记和清理操作,减少停顿时间。G1回收器还具备自适应的垃圾回收算法,能够根据当前应用程序的情况动态调整回收策略。

  2. ZGC垃圾回收器(Z Garbage Collector):ZGC垃圾回收器是Java 11以后引入的一种新型的低停顿垃圾回收器。它的设计目标是减少长时间的停顿时间,使得应用程序能够更加平滑地响应用户的请求。ZGC回收器使用了可并发的标记-整理(Mark-Compact)算法,通过并发线程完成垃圾的标记和整理工作,减少了垃圾回收时的停顿时间。它还使用了柔性内存模型,可以处理非常大的堆内存,并且能够动态地调整回收的范围,以适应不同的应用场景。

这两个垃圾回收器都是为了提供低停顿时间的垃圾回收而设计的,并且在大内存情况下表现良好。选择哪个垃圾回收器要根据具体的应用场景和需求来决定。其他常见的垃圾回收器还包括CMS回收器、Serial回收器、Parallel回收器等,它们在不同的场景下也有不同的特点和适用性。

4.2 工作原理、特点和性能表现

4.3 配置和选择垃圾收集器的建议

5. 内存泄漏和内存溢出

5.1列举一些常见的内存泄漏和内存溢出情况

5.1.1 thread local导致内存泄漏
5.1.2 线程池死锁导致内存泄漏

5.2提供诊断和解决这些问题的方法和工具

6. 内存性能调优

6.1如何测量和监控Java应用程序的内存使用情况

GUI:使用jConsole、VisualVM
APM: SkyWalking、Prometheus

6.2内存性能调优的技巧和建议

服务器程序部署策略

例如,一个15万PV/天左右的在线文档类型网站最近更换了硬件系统,新的硬件为4个CPU、16GB物理内存,操作系统为64位CentOS 5.4,Resin作为Web服务器。整个服务器暂时没有部署别的应用,所有硬件资源都可以提供给这访问量并不算太大的网站使用。管理员为了尽量利用硬件资源选用了64位的JDK 1.5,并通过-Xmx和-Xms参数将Java堆固定在12GB。使用一段时间后发现使用效果并不理想,网站经常不定期出现长时间失去响应的情况。
监控服务器运行状况后发现网站失去响应是由GC停顿导致的,虚拟机运行在Server模式,默认使用吞吐量优先收集器,回收12GB的堆,一次Full GC的停顿时间高达14秒。并且由于程序设计的关系,访问文档时要把文档从磁盘提取到内存中,导致内存中出现很多由文档序列化产生的大对象,这些大对象很多都进入了老年代,没有在Minor GC中清理掉。这种情况下即使有12GB的堆,内存也很快被消耗殆尽,由此导致每隔十几分钟出现十几秒的停顿,令网站开发人员和管理员感到很沮丧。
这里先不延伸讨论程序代码问题,程序部署上的主要问题显然是过大的堆内存进行回收时带来的长时间的停顿。硬件升级前使用32位系统1.5GB的堆,用户只感觉到使用网站比较缓慢,但不会发生十分明显的停顿,因此才考虑升级硬件以提升程序效能,如果重新缩小给Java堆分配的内存,那么硬件上的投资就显得很浪费。

在高性能硬件上部署程序,目前主要有两种方式:
通过64位JDK来使用大内存。
使用若干个32位虚拟机建立逻辑集群来利用硬件资源。

解决方案

使用若干个32位虚拟机建立逻辑集群来利用硬件资源。具体做法是在一台物理机器上启动多个应用服务器进程,每个服务器进程分配不同端口,然后在前端搭建一个负载均衡器,以反向代理的方式来分配访问请求。读者不需要太过在意均衡器转发所消耗的性能,即使使用64位JDK,许多应用也不止有一台服务器,因此在许多应用中前端的均衡器总是要存在的。
考虑到在一台物理机器上建立逻辑集群的目的仅仅是为了尽可能利用硬件资源,并不需要关心状态保留、热转移之类的高可用性需求,也不需要保证每个虚拟机进程有绝对准确的均衡负载,因此使用无Session复制的亲合式集群是一个相当不错的选择。我们仅仅需要保障集群具备亲合性,也就是均衡器按一定的规则算法(一般根据SessionID分配)将一个固定的用户请求永远分配到固定的一个集群节点进行处理即可,这样程序开发阶段就基本不用为集群环境做什么特别的考虑了。

7. 总结与结论

通过深入理解Java的内存管理和垃圾回收机制,我们可以更好的开发、部署、维护程序。

引用:
《深入理解Java虚拟机》


https://www.xamrdz.com/backend/3b81941871.html

相关文章: