likes
comments
collection
share

Java语言 - JVM内存结构

作者站长头像
站长
· 阅读数 15

JVM(Java Virtual Machine)是Java语言的基石,它通过将Java代码编译成字节码来实现跨平台的特性。JVM内存结构是JVM的一个重要组成部分,它对Java应用程序的运行效率和性能产生了直接的影响。本文将详细介绍JVM内存结构,并提供代码示例和思路清晰的解释。

一、JVM内存结构概述

在Java的世界中,一切都是对象。无论是基本类型还是复杂类型,都被看作是对象。在Java应用程序运行期间,JVM会为每个对象分配内存空间,这些内存空间可以分为以下5个部分:

1.方法区(Method Area):存储类的信息、常量池、静态变量等数据。方法区是线程共享的,所有线程都可以访问该区域的数据。

2.堆(Heap):存储对象实例、数组以及其它动态创建的对象。堆是线程共享的,所有线程都可以访问该区域的数据。

3.虚拟机栈(Virtual Machine Stack):每个线程都有自己的虚拟机栈,用于存储局部变量表、操作栈、动态链接、方法出口等数据。

4.本地方法栈(Native Method Stack):用于执行本地方法的栈。

5.程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器。

下面分别对每个部分进行详细介绍。 Java语言 - JVM内存结构

二、方法区

方法区是JVM中的一个重要概念,它存储了类的信息、常量池、静态变量等数据,是被所有线程所共享的。方法区的实现可以采用永久代或元空间的形式。

Java语言 - JVM内存结构

1.永久代

Java 6及之前的版本中采用永久代来实现方法区。永久代是一个JVM内部的特殊区域,用于存储类的信息、静态变量、常量池等数据。永久代的大小是固定的,由JVM启动参数“-XX:PermSize”和“-XX:MaxPermSize”来控制。

代码示例:

public class PermGenTest {
    public static void main(String[] args) {
        for (int i = 0; i < 100000; i++) {
            String str = "I am a string " + i;
            System.out.println(str);
        }
    }
}

在运行上述代码后,使用jvisualvm进行监控,可以看到PermGen的使用情况。

Java语言 - JVM内存结构

2.元空间

Java 8及之后的版本中采用元空间来实现方法区。元空间也是一个JVM内部的特殊区域,用于存储类的信息、静态变量、常量池等数据。与永久代相比,元空间的优点在于可以自动调整大小,而且不容易出现内存泄漏的问题。同时,Java 8以后,Class对象已经从永久代转移到了堆中,这样就避免了永久代内存溢出的风险。

代码示例:

public class MetaspaceTest {
    public static void main(String[] args) {
        for (int i = 0; i < 100000; i++) {
            String str = "I am a string " + i;
            System.out.println(str);
        }
    }
}

在运行上述代码后,使用jvisualvm进行监控,可以看到Metaspace的使用情况。

3、堆

堆是Java虚拟机中用于存储对象实例、数组以及其它动态创建的对象的区域,是被所有线程所共享的。堆是JVM分配内存的最大区域,因此也是最容易产生OutOfMemoryError的地方。堆的大小可以通过JVM启动参数“-Xmx”来设置。

代码示例:

public class HeapTest {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            list.add(i);
        }
        System.out.println(list.size());
    }
}

在运行上述代码后,使用jvisualvm进行监控,可以看到堆的使用情况。

4、虚拟机栈

虚拟机栈是每个线程所对应的,用于存储局部变量表、操作栈、动态链接、方法出口等数据。虚拟机栈的大小可以通过JVM启动参数“-Xss”来设置。当方法被调用时,JVM会在虚拟机栈中为该方法分配一定的空间,当该方法执行完成后,JVM会自动回收该方法所占用的空间。

代码示例:

public class StackTest {
    public static void main(String[] args) {
        int result = add(1, 2);
        System.out.println(result);
    }

    private static int add(int a, int b) {
        return a + b;
    }
}

在运行上述代码后,使用jconsole进行监控,可以看到虚拟机栈的使用情况。

5、本地方法栈

本地方法栈与虚拟机栈类似,不同的是本地方法栈用于执行本地方法。本地方法是指由Java语言以外的语言(如C、C++等)编写的方法。

6、程序计数器

程序计数器是当前线程所执行的字节码的行号指示器。当执行Java方法时,JVM会把该方法的字节码加载到内存中,然后通过程序计数器逐条执行该方法的字节码。程序计数器是每个线程所对应的,它可以看作是该线程所执行的当前方法的“行号指示器”。

Java语言 - JVM内存结构

JVM内存结构是Java应用程序的基础,它直接影响了Java应用程序的性能和运行效率。在开发Java应用程序时,我们需要了解JVM内存结构的相关知识,以便更好地进行优化和调试。本文详细介绍了JVM内存结构的五个部分:方法区、堆、虚拟机栈、本地方法栈和程序计数器。

三、内存分配与回收策略

在Java中,内存分配和回收是由JVM自动进行的。JVM将堆划分为新生代和老年代两个部分,每个部分都有自己的特点和分配回收策略。

  1. 新生代

新生代是指JVM堆中的一部分,用于存储新创建的对象。由于大部分对象的生命周期都很短暂,因此新生代的回收频率比较高。新生代可以进一步分为Eden区、Survivor0区和Survivor1区三个部分。

当对象被创建时,JVM会将其分配到Eden区中。当Eden区满了之后,JVM会将其中存活的对象复制到Survivor0区或Survivor1区中,同时清空Eden区。JVM会不断地重复这个过程,直到Survivor0区或Survivor1区也满了。当Survivor0区或Survivor1区满了之后,JVM会将其中存活的对象复制到另一个空闲的区域中。最终,当对象经过多次复制仍然存活下来时,它会被移动到老年代中。

  1. 老年代

老年代是指JVM堆中存放长期存活的对象的部分,它的容量比新生代大很多,使用的是标记-清除算法进行垃圾回收。由于老年代中存放的对象生命周期较长,因此GC的频率相对较低,但每次回收都需要耗费较长的时间。

  1. 永久代/元空间

永久代/元空间主要用于存储类的信息、常量池、静态变量等数据。在Java 8及之前,永久代是一个固定大小的区域,无法自动调整大小,容易产生内存溢出问题。而在Java 8以后,永久代被替换为了元空间,元空间采用的是本地内存存储,可以自动调整大小,不容易出现内存溢出的问题。

垃圾回收算法

Java中的垃圾回收算法主要有标记-清除算法、复制算法、标记-整理算法和分代算法等。

垃圾收集器类型作用域特点适用场景
Serial串行回收新生代响应速度优先适用于单核CPU环境下的Client模式
Serial Old串行回收老年代响应速度优先适用于单核CPU环境下的Client模式
ParNew并行回收新生代响应速度优先多核CPU环境中Server模式下与CMS配合使用
Parallel Scavenge并行回收新生代吞吐量优先适用于后台运算,而交互少的场景
Parallel Old并行回收老年代吞吐量优先适用于后台运算,而交互少的场景
CMS (Concurrent Mark-Sweep)并发回收老年代响应速度优先适用于B/S业务,也就是交互多的场景
G1 (Garbage-First)并发/并行回收新生代 & 老年代(整堆)响应速度优先面向服务端的应用
  1. 标记-清除算法

标记-清除算法是一种最基础的垃圾回收算法。它将堆分成两个部分,一部分是存储对象的区域,另一部分是空闲区域。当出现内存不足时,JVM会先将存储对象的区域进行标记,标记所有仍然存活的对象,然后把未标记的对象从堆中清除。

标记-清除算法 (Mark-Sweep)是最早出现也是最基础的垃圾收集算法,分为“标记”和“清除”两个阶段从根集合(GC Roots) 进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的死亡对象进行回收标记回收。

主要缺点:第一执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低。第二内存空间碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

Java语言 - JVM内存结构

  1. 复制算法

复制算法是将堆空间分为大小相等的两块,每次只使用其中一块。当该块内存用完了之后,剩余的对象将复制到另一块空闲的区域中,并且清空该块空间。复制算法的优点是实现简单,且不容易产生碎片,但缺点是需要额外的空间来存放复制的对象。

标记-复制算法(Semispace Copying): 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这-块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉.主要缺点: 第-Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。第二将可用内存缩小为了原来的一半,空间浪费。

Java语言 - JVM内存结构

  1. 标记-整理算法

标记-整理算法是在标记-清除算法的基础上进行改进得到的。标记-整理算法也是将堆区分为存储对象的区域和空闲区域,但是它会先将所有存活的对象移动到空闲区域中,然后把存储对象的区域清空,使得存储对象的区域变成一个大的连续的空闲区域。这种算法可以解决标记-清除算法中出现的内存碎片问题。

标记-整理算法(Mark-Compact) 标记过程仍然与“标记-清除”算法一样,但后续步不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存主要缺点: 进过标记后如果有大量存活对象,移动存活对象,并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行 (Stop The World) 。

Java语言 - JVM内存结构

  1. 分代算法

分代算法是目前应用最广泛的垃圾回收算法。它根据对象的生命周期将堆分为新生代和老年代两个部分,对不同的区域采用不同的垃圾回收算法。对于新生代,采用复制算法;对于老年代,采用标记-清除或标记-整理算法。

分代收集算法 Generational Collection (分代收集)是目前大部分JVM的垃圾收集器采用的算法。根据对象存活的生命周期将内存划分为若千个不同的区域。

四、常见的内存问题

  1. 内存泄漏

内存泄漏是指程序在运行过程中,由于某些原因未能及时释放不再使用的内存,导致内存空间被占用,最终耗尽了可用的内存资源。内存泄漏可能会导致应用程序的性能下降、系统崩溃等严重后果。

  1. 内存溢出

内存溢出是指在程序运行期间,申请内存时没有足够的可用内存,导致程序无法正常运行。常见的内存溢出错误包括OutOfMemoryError和StackOverflowError。

  1. 内存碎片

内存碎片是指内存空间被分割成了不连续的小块,导致无法分配一块足够大的内存满足需要。内存碎片可能会导致程序崩溃或者减缓程序的运行速度。

五、优化内存使用的方法

  1. 减小对象的创建和销毁

频繁地创建和销毁对象会增加GC的负担,降低程序性能。因此可以通过对象池、缓存等技术减少对象的创建和销毁次数。

  1. 合理设计对象的生命周期

合理设计对象的生命周期,尽量使对象在较短的时间内存活,可以减轻GC的压力。例如使用局部变量而不是成员变量,及时释放资源等。

  1. 控制数据集合大小

对于大量数据的操作,可以采用分批处理、分页查询等方式,避免一次性处理过多的数据。

  1. 合理设置内存参数

可以根据应用程序的需求和硬件配置,设置合适的JVM内存参数,以优化程序的性能。

总之,优化内存使用是提高Java应用程序性能的重要手段。需要结合具体应用场景和问题进行优化,以达到最佳效果。