Java 的内存结构

简介: Java源码文件被编译为字节码文件,字节码文件在程序运行中被读入jvm中,jvm中的每个线程都会有一个属于自己的程序计数器(PC),程序都是由CPU逐条进行执行的,PC记录的就是当前线程执行到的代码的位置。

Java 的内存结构

1. 程序计数器

Java源码文件被编译为字节码文件,字节码文件在程序运行中被读入jvm中,jvm中的每个线程都会有一个属于自己的程序计数器(PC),程序都是由CPU逐条进行执行的,PC记录的就是当前线程执行到的代码的位置。每个线程都有属于自己的PC,因此PC是线程私有的,并不是共享的。

如果当前线程执行的是Java中的方法,那么当前线程的PC就是记录的方法所执行到的位置;而如果当前线程执行的并不是Java中的方法,而是使用Native修饰的方法,则PC则是未被定义的。

综上所述,PC是不会引起OOM的。

2. 方法区

img

img

方法区主要存放的就是如上图所示的这些东西。Java8使用metaspace来代替方法区。

这里的主要问题就是字符串的一个拼接问题,Integer的初始化问题等。

3. 栈

栈是一种有条件的线性数据结构,FOLO的数据结构。根据栈的的特性,在程序执行时,保留现场数据,过程数据很适合采用栈这种数据结构。JVM中使用到栈的地方主要有两个地方,第一个是JVM栈,另一个是本地方法栈,本地方法栈主要是在执行Native方法时用到的,所以这里不做具体的讨论。主要来讲JVM栈,里面保存的主要是Java方法的执行过程和中间变量。

3.1 虚拟机栈

img

Java程序在执行时,每一个线程分配一个方法栈,所以说,方法栈的生命周期是与线程同步的。

如上图所示,内存中分配了方法栈,每个栈是由一个个的栈帧组成的,那么,我们肯定会好奇,那么栈帧中存储的都是些什么呢?我们接着往下看:

img

如上图所示,我们在前面也提到了,一个线程对应一个JVM栈,而这个线程中的每个方法则对应一个栈帧,那么这样一一对应起来之后,就能够很容易的猜想到每个栈帧中都存储着哪些东西。

既然每个栈帧对应的是该线程中的的一个方法,那么栈帧中存储的肯定有该方法中的临时变量,对应的操作数动态链接方法出口等信息。

3.1.1 临时变量表

临时变量表就是用来存储方法中的临时变量的,其中包括在方法中出现的局部变量(基本数据类型:boolean,byte,short,char,int,float,long,double),入参和对象的引用,这里需要注意的有两点:

一:局部变量表的基本单位是 slot,32位,因此,只此64位的基本数据类型比如long,double这些数据需要占用两个slot。

二:slot中存储的对象的引用,并不是对象本身的值。

局部变量表在编译期间就已经分配好了,在运行期间不会改变它的大小

还有一点就是静态方法和非静态方法的一个区别,静态方法所有的对象公用资源,因此第一位slot存储的就是局部变量,而非静态方法,每个对象都用自己的资源,因此,第一个slot存储的是该对象的引用。

img

slot的复用

为了更好的去利用空间,在一个局部变量的生命周期结束之后,该局部变量对应的slot槽应该是空出来了,如果后面再有局部变量来声明,可以采用该slot槽,就不用重新开辟新的slot来存储后面的局部变量。

public void test(boolean flag)
{
   
   
    if(flag)
    {
   
   
        int a = 66;
    }

    int b = 55;

我们来看上面这个例子,在方法test中,初始的局部变量表应该是如下所示的:

image-20201106101341480

当局部变量a的生命周期结束后,b开始声明的时候,a的slot槽是空着的,因此,此时声明b的时候就可以使用a的slot槽了。

我们来看一个这个题目:

public class TestDemo {
   
   
    public static void main(String[] args) {
   
   

        byte[] bbbb = new byte[64 * 1024 * 1024];


        System.gc();
    }
}

我们声明一个64M的数组,在最后执行gc的时候能不能够回收呢?在 JVM运行参数中加入 -verbose:gc

会得到如下的打印信息:

[GC (System.gc())  69468K->66104K(251392K), 0.0012261 secs]
[Full GC (System.gc())  66104K->65946K(251392K), 0.0053029 secs]

Process finished with exit code 0

理论上来说不会回收,因为他的生命周期并没有严格的结束,所以并不能够回收。

下面我们来给它规定一个生命周期:

public class TestDemo {
   
   
    public static void main(String[] args) {
   
   
        {
   
   
            byte[] bbbb = new byte[64 * 1024 * 1024];
        }
        System.gc();
    }
}

那这样的话应该能够gc回收了吧,然而运行后我们发现他还是没有进行回收:

[GC (System.gc())  69468K->66104K(251392K), 0.0007805 secs]
[Full GC (System.gc())  66104K->65946K(251392K), 0.0052613 secs]

Process finished with exit code 0

这是为什么呢?

原因就是slot的复用导致的,虽然这个数组的生命周期结束了,但是整个方法运行完,局部变量表并没有进行改变,GC ROOT 也没有变过,因此并没有进行gc回收。

我们继续改变:

public class TestDemo {
   
   
    public static void main(String[] args) {
   
   
        {
   
   
            byte[] b = new byte[64 * 1024 * 1024];
        }
        int aaa = 0;
        System.gc();
    }
}

我们定义了一个变量,根据slot的复用规则,bbbb的生命周期结束后,又来了一个aaa的局部变量,这样就会把bbbb的slot槽复用了,引用的那个地址就被丢弃了,这样的话gc就会回收。

[GC (System.gc())  69468K->66088K(251392K), 0.0017629 secs]
[Full GC (System.gc())  66088K->410K(251392K), 0.0053990 secs]

Process finished with exit code 0
3.1.2 操作数栈

操作数栈也是一种栈结构,操作数栈,主要是用来进行运算的时候来进行保存中间量和结果的。

我们先来看一段代码:

public class OperandStack{
   
   
  public static int add(int a, int b){
   
   
      int c = a + b;
      return c;
  }

  public static void main(String[] args){
   
   
      add(100, 98);
  }
}

上面的代码,在进行运算add(100,98)的过程是,首先开始的时候,操作数栈为空,开始计算的时候,把第一个操作数也就是100压入栈中,然后把第二个操作数98压入栈中,然后开始计算,依次弹出两个操作数,把98和100弹出计算成198,然后把198压入栈,最后弹出198,完成。

img

综上所述,我们可以知道,操作数栈是线程私有的,方法的递归调用也有使操作数栈的深度增加,因此在递归调用的时候很容易出现栈溢出的异常。操作数栈所需要的空间 > JVM中允许分配的最大空间。

当操作数栈无法申请到足够的所需要的内存空间,就会引起 OOM。

3.2 本地方法栈

本地方法栈的原理和JVM栈的原理类似,因此,参考上面即可,二者最大的区别就是JVM栈中保存的是Java的方法执行的过程,本地方法栈执行的是系统本地的方法。

4. 堆

堆是JVM中内存共享的区域,主要是用来存放对象的一个内存空间,当然,堆也是所有的内存空间中最大的一个。堆的机构如图所示:

在这里插入图片描述

在Java8之后,永久代就改名为元空间了,metaspace:

在这里插入图片描述

然后我们可一从纵向看一下:

img

对主要分为新生代和老年代,新生代中分为伊甸区和幸存区,幸存区分为from和to,一般来说,eden:from:to = 8:1:1的内存大小分配。

一个对象刚被创建,会被分配到eden区,当eden区满了之后,gc会进行一次 minor gc,清空eden和from区,把幸存下来的放到to

下一次满的时候,就会清空eden和to,把幸存下来的放入到 from中。当对象的年龄超过一定的阈值之后,就会转到老年区,老年区满了之后,就会触发一次fullGC,这是会把整个堆的无用对象给清除。如果剩下来的内存还不足以分配给新来的对象,就会触发OOM。

相关文章
|
1月前
|
存储 缓存 安全
Java内存模型深度解析:从理论到实践####
【10月更文挑战第21天】 本文深入探讨了Java内存模型(JMM)的核心概念与底层机制,通过剖析其设计原理、内存可见性问题及其解决方案,结合具体代码示例,帮助读者构建对JMM的全面理解。不同于传统的摘要概述,我们将直接以故事化手法引入,让读者在轻松的情境中领略JMM的精髓。 ####
39 6
|
24天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
26 0
|
1月前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
1月前
|
存储 算法 Java
Java内存管理深度剖析与优化策略####
本文深入探讨了Java虚拟机(JVM)的内存管理机制,重点分析了堆内存的分配策略、垃圾回收算法以及如何通过调优提升应用性能。通过案例驱动的方式,揭示了常见内存泄漏的根源与解决策略,旨在为开发者提供实用的内存管理技巧,确保应用程序既高效又稳定地运行。 ####
|
26天前
|
存储 监控 算法
Java内存管理深度剖析:从垃圾收集到内存泄漏的全面指南####
本文深入探讨了Java虚拟机(JVM)中的内存管理机制,特别是垃圾收集(GC)的工作原理及其调优策略。不同于传统的摘要概述,本文将通过实际案例分析,揭示内存泄漏的根源与预防措施,为开发者提供实战中的优化建议,旨在帮助读者构建高效、稳定的Java应用。 ####
37 8
|
24天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
28天前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
55 5
|
26天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
26天前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
|
1月前
|
JSON Java 程序员
Java|如何用一个统一结构接收成员名称不固定的数据
本文介绍了一种 Java 中如何用一个统一结构接收成员名称不固定的数据的方法。
26 3

热门文章

最新文章