认真学,JVM内存模型

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/J080624/article/details/82109357

Java虚拟机(Java Virtual Machine=JVM)的内存空间分为五个部分,分别是:

  • 程序计数器
  • Java虚拟机栈
  • 本地方法栈
  • 方法区
    这里写图片描述

【1】程序计数器

① 什么是程序计数器

程序计数器是一块较小的内存空间,可以把它看作当前线程正在执行的字节码的行号指示器。也就是说,程序计数器里面记录的是当前线程正在执行的那一条字节码指令的地址。

但是,如果当前线程执行的是一个本地方法,那么此时程序计数器为空。

本地方法为Native Method,即由native 修饰的方法。 在定义一个native method时,并不提供实现体(有些像定义一个java interface),因为其实现体是由非java语言在外面实现的。


② 程序计数器的作用

程序计数器主要有两个作用:

1.字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如顺序执行、选择、循环、异常处理。

2.在多线程的情况下,程序计数器用来记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪了。


③ 程序计数器的特点

  • 是一块较小的存储空间;
  • 线程私有–每条线程都有一个程序计数器;
  • 是唯一一个不会出现OutOfMemoryError的内存区域;
  • 生命周期随着线程的创建而创建,随着线程的结束而死亡。

【2】Java虚拟机栈(JVM Stack)

① 什么是Java虚拟机栈

Java虚拟机栈是描述Java方法运行过程的内存模型。

Java虚拟机栈会为每一个即将运行的Java方法创建一块叫做“栈帧”的区域,这块区域用于存储该方法在运行过程中所需要的一些信息,这些信息包括:

  • 局部变量表
    存放基本数据类型变量,引用类型的变量、Return Address类型的变量及方法参数。
  • 操作数栈
  • 动态链接
  • 方法出口信息等

当一个方法即将被运行时,Java虚拟机栈首先会在Java虚拟机栈中为该方法创建一块”栈帧”,栈帧中包含局部变量表,操作数栈,动态链接,方法出口信息等。当方法在运行过程中需要创建局部变量时,就将局部变量的值存入栈帧的局部变量表中。

当这个方法执行完毕后,这个方法所对应的栈帧将会出栈,并释放内存空间。

局部变量表为一个以变量槽(Slot)为单位的数组,每个数组元素对应一个局部变量的值。调用方法时,将方法的局部变量组成一个数组,通过索引来访问。若为非静态方法,则加入一个隐含的引用参数this,该参数指向调用这个方法的对象。而静态方法则没有this参数。因此,对象无法调用静态方法。
32位虚拟机中一个Slot可以存放一个32位以内的数据类型(boolean、byte、char、short、int、float、reference和returnAddress八种)。
.
操作数栈是一个以字长为单位数组,但是通过栈操作来访问。所谓操作数是那些被指令操作的数据。当需要对参数操作时如a=b+c,就将即将被操作的参数压栈,如将b 和c 压栈,然后由操作指令将它们弹出,并执行操作。虚拟机将操作数栈作为工作区。

注意:人们常说的Java内存空间分为“栈”和“堆”,栈中存放局部变量,堆中存放对象。

这句话不完全正确!这里的“堆”可以这么理解,但是这里的“栈”只代表了Java虚拟机栈中的局部变量表部分。真正的Java虚拟机栈是由一个个栈帧组成的,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接和方法出口信息等。

这里写图片描述


② Java虚拟机栈的特点

(1)局部变量表的创建是在方法被执行的时候,随着栈帧的创建而创建。

而且局部变量表的大小在编译时期就确定下来了,在创建的时候只需分配事先规定好的大小即可。此外在方法运行的过程中局部变量表的大小是不会发生改变的。

(2)Java虚拟机栈会出现两种异常,StackOverFlowError and OutOfMemoryError。

  • StackOverFlowError
    若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。

  • OutOfMemoryError
    若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。

(3)Java虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。

StackOverFlowError and OutOfMemoryError的异同?
StackOverFlowError表示当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。而OutOfMemoryError是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。

(4)每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问.


【3】本地方法栈

本地方法栈和Java虚拟机栈实现的功能类似,只不过本地方法栈是本地方法运行的内存模型。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接以及出口信息等。

方法执行完毕后相应的栈帧也会出栈并释放内存空间。

也会抛出两种异常,StackOverFlowError and OutOfMemoryError异常。


【4】堆(Heap)

堆是用来存放对象(数组也是对象)的内存空间,几乎所有的对象都存储在堆中(成员变量也随对象储存在堆中,随垃圾回收机制进行释放)。

在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。

其大小通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G;-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G。
默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。

堆具有以下特点:

① 线程共享

整个Java虚拟机只有一个堆,所有的线程都访问同一个堆。而程序计数器、Java虚拟机栈、本地方法栈都是一个线程对应一个。

② 在虚拟机启动的时候创建

③ 垃圾回收的主要场所


④ 可以进一步细分为:新生代和老年代

新生代主要存储新创建的对象和尚未进入老年代的对象。老年代存储经过多次新生代GC(Minor GC)任然存活的对象。

  • 新生代

程序新创建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成,可通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及Survivor Space的大小。
因此新生代又可被分为:Eden,From Survior,To Survior。

  • 老年代

用于存放经过多次新生代GC仍然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代。

主要有两种情况:①.大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。②.大的数组对象,且数组中无引用外部对象。

老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。

不同的区域存放具有不同生命周期的对象。这样可以根据不同的区域使用不同的垃圾回收算法,从而更具有针对性,从而更高效。


⑤ 堆的大小既可以固定也可以扩展,但主流的虚拟机堆的大小是可扩展的。

因此当线程请求分配内存,但堆已满,且内存已满无法再扩展时,就抛出OutOfMemoryError。


【5】方法区

① 什么是方法区

Java虚拟机规范中定义方法区是堆的一个逻辑部分。

方法区中存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。

方法区有一个别名Non-Heap(非堆),用于区别于Java堆区。默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。


② 方法区的特点

  • 线程共享
    方法区是堆的一个逻辑部分,因此和堆一样都是线程共享的。整个虚拟机中只有一个方法区。

  • 永久代
    方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,我们把方法区称为老年代。

  • 内存回收效率低
    方法区中的信息一般需要长期存在,回收一遍内存之后可能只有少量信息无效。对方法区的内存回收的主要目标是:对常量池的回收和对类型的卸载。

  • Java虚拟机规范对方法区的要求比较宽松
    和堆一样,允许固定大小,也允许可扩展的大小,还允许不实现垃圾回收。


③ 运行时常量池

方法区中存放的几种数据:类信息、常量、静态变量、即时编译器编译后的代码。其中,常量存储在运行时常量池中。

我们一般在一个类中通过public static final来声明一个常量或者如String str=”abc”。

这个类被编译后便生出Class文件,这个类的所有信息都存储在这个class文件中。

当这个类被Java虚拟机加载后,class文件中的常量就存放在方法区的运行时常量池中。

而且在运行期间,可以向常量池中添加新的常量。

如String类的intern()方法就能在运行期间向常量池中添加字符串常量。
PS:
int age = 21;//age是一个变量,可以被赋值;21就是一个字面值常量,不能被赋值;
int final pai = 3.14;//pai就是一个符号常量,一旦被赋值之后就不能被修改。
String str =”abc”;//str是一个对象引用变量,JVM会在堆中创建一个String对象,同时将abc放入常量池。

当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就需要垃圾收集器回收。

JVM为每个已加载的类型维护一个常量池,常量池就是这个类型用到的常量的一个有序集合。其包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用。

池中的数据和数组一样通过索引访问。

由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在Java的动态链接中起了核心作用。


④ 方法区存放内容总结

  • 类的基本信息:

    1.每个类的全限定名

    2.每个类的直接超类的全限定名(可约束类型转换)

    3.该类是类还是接口

    4.该类的访问修饰符

    5.直接超接口的全限定名的有序列表


  • 已装载类的详细信息

    1.运行时常量池:在方法区中,每个类型都对应一个常量池,存放该类型所用到的所有常量,常量池中存储了诸如文字字符串、final变量值、类名和方法名常量。

    2.字段信息:字段信息存放类中声明的每一个字段的信息,包括字段的名、类型、修饰符。

    3.方法信息:类中声明的每一个方法的信息,包括方法名、返回值类型、参数类型、修饰符、异常、方法的字节码。

    4.到类classloader的引用:到该类的类装载器的引用。

    5.到类class 的引用:虚拟机为每一个被装载的类型创建一个class实例,用来代表这个被装载的类。

    6.静态变量和静态代码块。


【6】直接内存

直接内存是除Java虚拟机之外的内存,但也有可能被Java使用。

在NIO中引入了一种基于通道和缓冲的IO方式。它可以通过调用本地方法直接分配Java虚拟机之外的内存,然后通过一个存储在Java堆中的DirectByteBuffer对象直接操作该内存,而无需先将外面内存中的数据复制到堆中再操作,从而提升了数据操作的效率。

直接内存的大小不受Java虚拟机控制,但既然是内存,当内存不足时就会抛出OutOfMemoryError异常。


【7】总结

① Java虚拟机的内存模型一共有两个“栈”,分别是Java虚拟机栈和本地方法栈。

两个“栈”功能类似,都是方法运行过程的内存模型。并且两个“栈”内部构造相同,都是方法私有。

只不过Java虚拟机栈描述的是Java方法运行过程的内存模型,而本地方法栈是描述Java本地方法运行过程的内存模型。


② Java虚拟机的内存模型中一共有两个“堆”,一个是原本的堆,一个是方法区。

方法区本质上是属于堆的一个逻辑部分。堆中存放对象,方法区中存放类信息、常量、静态变量、即时编译器编译后的代码等。


③ 堆是Java虚拟机中最大的一块内存区域,也是垃圾收集器主要的工作区域。

在创建对象的时候,非静态成员会被加载堆内存中,并完成成员变量的赋值初始化。也就是说所有的非静态成员(包括成员变量、成员方法、构造方法、构造代码块、普通代码块)是保存在堆内存中的。

但是方法调用的时候,调用的方法会在栈内存中执行,构造代码块也会在栈内存中执行。


④ 线程私有与共享

程序计数器、Java虚拟机栈、本地方法栈是线程私有的,即每个线程都拥有各自的程序计数器、Java虚拟机栈和本地方法栈。并且他们的生命周期和所属的线程一样。

而堆、方法区是线程共享的,在Java虚拟机中只有一个堆,一个方法区。并在JVM启动的时候就创建,JVM停止的时候才销毁。


⑤ Java中变量(包括常量)可以存放在 栈、堆、方法区三块内存区域,除去方法区的常量池中存放的常量、静态变量之外主要的变量都存放在栈和堆中。

类型/变量 局部变量 成员变量
基本数据类型 变量名和值都存放在栈中 变量名和值都存在在堆中
引用数据类型 变量名存放在栈中,值存放在堆中 变量名和值都存在在堆中

这里需要注意一个细节,当JVM只是加载class,并没有实例化对象的时候,此时对象是不存在的。局部变量是在方法调用的时候创建故而此时不存在,而成员变量(静态、非静态)此时是加载到方法区的。

猜你喜欢

转载自blog.csdn.net/J080624/article/details/82109357