初涉JVM——1 内存区域

初涉JVM——1内存区域

JVM内存模型

在这里插入图片描述

五大内存模型

程序计数器

用于存放下一条指令所在单元的地址

属于线程私有区域

是计算机处理器中的寄存器,包含当前正在执行的指令地址,当每个指令被获取,程序计数器地址加一,程序计数器指向顺序中的下一条指令,当计算机重启或复位时,程序计数器置零

特点

  • 又称为指令计数器,程序开始时,必须是他的起始地址
  • 当执行进行非顺序跳转时,后续指令从指令寄存器中取得,
  • 程序计数器具备寄存信息和计数两总功能的结构
  • 该块内存是虚拟机中唯一没有OutOfMemoryError的区域

Java虚拟机栈

与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。

每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。

在这里插入图片描述

Java虚拟机栈中含有以下部分:

局部变量表(局部变量表实操可见https://www.cnblogs.com/kesan/p/11368934.html)

操作数栈

动态链接

方法出口

Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。

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

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

Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。

本地方法栈

本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。

可能出现的异常
StackOverflowError 栈溢出
OutOfMemory 内存溢出

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

所有线程共享,存放对象实例,同时也是GC管理的主要区域,通常称为GC堆

当前主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)

(其中的垃圾回收请看下回分解)

方法区

方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。

特殊区域

常量池

使用final修饰的成员变量,值一旦给定无法改变

静态常量池

.class文件中的常量池

常量池主要存放两大常量:字面量和符号引用。字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

常量池的好处:常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。

举个例子:字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。

  1. 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
  2. 节省运行时间:比较字符串时,比equals()快。对于两个引用变量,只用判断引用是否相等,也就可以判断实际值是否相等。

静态常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。其中符号引用其实引用的就是常量池里面的字符串,但符号引用不是直接存储字符串,而是存储字符串在常量池里的索引

当Class文件被加载完成后,java虚拟机会将静态常量池里的内容转移到运行时常量池里,在静态常量池的符号引用有一部分是会被转变为直接引用的,比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用,而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量池还有个更重要的的特征:动态性。Java要求,编译期的常量池的内容可以进入运行时常量池,运行时产生的常量也可以放入池中。常用的是String类的intern()方法。

既然运行时常量池是方法区的一部分自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

字符串常量池

字符串常量池存在运行时常量池之中(在JDK7之前存在运行时常量池之中,在JDK7已经将其转移到堆中)。

字符串常量池的存在使JVM提高了性能和减少了内存开销。

  1. 每当我们使用字面量(String s=“1”;)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就将此字符串对象的地址赋值给引用s(引用s在Java栈。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,并将此字符串对象的地址赋值给引用s(引用s在Java栈中)。
  2. 每当我们使用关键字new(String s=new String(”1”);)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接堆中创建该对象的副本,然后将堆中对象的地址赋值给引用s,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中创建该对象的副本,然后将堆中对象的地址赋值给引用s。

三个常量池之间的关系

JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。

静态常量池用于存放编译期生成的各种字面量和符号引用,而当类加载到内存中后,jvm就会将静态常量池中的内容存放到运行时常量池中。而字符串常量池存的是引用值,其存在于运行时常量池之中。

使用常量池的优点

1、节省内存空间,常量池中所有相同的字符串常量被合并,只占用一个空间
2、节省运行时间

三个常量池在不同JDK版本中的变化

1.6
  • 静态常量池在Class文件中。

  • 运行时常量池在Perm Gen区(也就是方法区)中。(所谓的方法区是在Java堆的一个逻辑部分,为了与Java堆区别开来,也称其为非堆(Non-Heap),那么Perm Gen(永久代)区也被视为方法区的一种实现。)

  • 字符串常量池在运行时常量池中。

1.7
  • 静态常量池在Class文件中。
  • 运行时常量池依然在Perm Gen区(也就是方法区)中。在JDK7版本中,永久代的转移工作就已经开始了,将譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。但是运行时常量池依然还存在,只是很多内容被转移,其只存着这些被转移的引用。网上流传的一些测试运行时常量池转移的方式或者代码,其实是对字符串常量池转移的测试。
  • 字符串常量池被分配到了Java堆的主要部分(known as the young and old generations)。也就是字符串常量池从运行时常量池分离出来了。
1.8
  • 静态常量池在Class文件中。
  • JVM已经将运行时常量池从方法区中移了出来,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。同时永久代被移除,以元空间代替。元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。其主要用于存放一些元数据。
  • 字符串常量池存在于Java堆中。

直接内存

  • 堆外内存,元数据去,栈空间,直接内存,jvm作为c++程序,mmap的数据等
  • 直接内存是有jvm参数控制的-XX:MaxDirectMemorySize
  • 本身不受GC影响,但有对象在堆中引用这块内存,受到GC的间接影响,典型的是java代码 system.g
  • 是由jvm定义的空间,和堆,元数据去处于同一层次

PermGen(永久代)

  • java 8 将永久代移除了hotspot vm 将其原有的数据迁移至Java Heap 或Metaspace中

移除原因
1、由于Permanent Generation内存经常不够用或发生内存泄露,引发java.lang.OutOfMemoryError: PermGen (在Java Web开发中非常常见)
2、移除Permanent Generation可以促进HotSpot JVM与JRockit VM的融合,因为JRockit没有永久代。

Metaspace(元空间)

从jdk1.8开始,使用元空间取代永久代
永久代:这个区域会存储包括类定义、结构、字段、方法(数据及代码)以及常量在内的类相关数据。它可以通过(以下两个是非堆区配置参数)-XX:PermSize及-XX:MaxPermSize来进行调节。若永久代(Perm Gen)空间用完,会导致java.lang.OutOfMemoryError: PermGenspace的异常。
存放数据

方法区存储的是每个class的信息:
(1)类加载器引用(ClassLoader)
(2)运行时常量池:包含所有常量、字段引用、方法引用、属性
(3)字段数据:每个字段的名字、类型(如类的全路径名、类型或接口) 、修饰符(如public、abstract、final)、属性
(4)方法数据:每个方法的名字、返回类型、参数类型(按顺序)、修饰符、属性
方法区存储的是每个class的信息:
(1)类加载器引用(ClassLoader)
(2)运行时常量池:包含所有常量、字段引用、方法引用、属性
(3)字段数据:每个字段的名字、类型(如类的全路径名、类型或接口) 、修饰符(如public、abstract、final)、属性
(4)方法数据:每个方法的名字、返回类型、参数类型(按顺序)、修饰符、属性
(5)方法代码:每个方法的字节码、操作数栈大小、局部变量大小、局部变量表、异常表和每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

猜你喜欢

转载自blog.csdn.net/weixin_43876186/article/details/108489840