抱歉,您的瀏覽器無法訪問本站
本頁面需要瀏覽器支持(啟用)JavaScript
了解詳情 >

待学

  • JVM运行时数据区

  • 对象的创建

JVM运行时数据区

简介

image-20210129211305011

JVM在执行Java程序的过程中会将他所管理的内存划分为诺干个不同的数据区域,如上图。

程序计数器

程序计数器是一个较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,线程私有。

由于JVM中的多线程是通过线程轮流切换并分配处理器的执行时间的方式来实现的,所以在任何一个时候,一个处理器都只会执行一个线程中的指令。所以为了保证线程切换后能够恢复到正确的执行位置,每一个线程都要有私有的程序计数器。

PS: 程序计数器是唯一一个不会出现内存溢出(OutOfMemoryError)的内存区域,它的生命周期随着线程的创建而创建,线程的销毁而销毁

虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的时候都会创建一个栈帧(Stack Frame)用于存储 局部变量表操作数栈动态链接方法出口等信息。每一个方法从调用到执行完成的过程,都代表一个栈帧在虚拟机栈中从入栈到出栈的过程

局部变量表

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,在编译成Class文件时,就已经确认了该方法所需的分配的局部变量表的最大容量。

局部变量表存放了编译时期可知的的各种基本数据类型:boolean、byte、char、short、int、long、float、double、对象引用(reference类型)、returnAddress(指向字节码指令的地址)。

变量槽

局部变量表中的最小单位是变量槽(Variable Slot),每个变量槽能够存储32位长度的内存空间。对于64位长度的数据类型,虚拟机会采用高位对齐的方式为其分配两个连续的Slot空间,也就是将64位长度的数据类型分割成两次的32位读写。

32位长度:boolean、byte、short、char、int、float、reference。

64位长度:double、long

PS:Java虚拟机栈中会出现两种错误:StackOverFlowError和OutOfMemoryError

本地方法栈

和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

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

方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowErrorOutOfMemoryError 两种错误。

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

Java世界中“几乎”所有的对象都在堆中分配,但是,随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从jdk 1.7开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

方法区(元空间)

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

《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。

在JDK1.8的时候方法区已经被彻底移除了,取而代之的是元空间,元空间使用的是直接内存

在方法区没有被废弃的时候方法区常用的参数如下

1
2
-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen

JDK1.8元空间常用参数如下

1
2
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小

为啥将方法区替换为元空间?

  1. 整个方法区有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
  2. 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。

运行时常量池

运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用),当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。

  1. JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
  2. JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
  3. JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。

JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据

本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。