深入理解Java虚拟机 JVM 内存结构

目录
  • 前言
  • JVM是什么
  • JVM内存结构概览
  • 运行时数据区
    • 程序计数器
    • Java虚拟机栈
    • 本地方法栈
    • 方法区
    • 运行时常量池
    • Java堆
  • 直接内存

前言

JVM是Java中比较难理解和掌握的一部分,也是面试中被问的比较多的,掌握好JVM底层原理有助于我们在开发中写出效率更高的代码,可以让我们面对OutOfMemoryError时不再一脸懵逼,可以用掌握的JVM知识去查找分析问题、去进行JVM的调优、去让我们的应用程序可以支持更高的并发量等。。。。。。总之一句话,学好JVM很重要!

JVM是什么

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的,注意JVM是基于软件的,不是基于硬件的。

Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用模式Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。

比如下图:我们编译后产生的.class文件是二进制的字节码,字节码是不能被机器直接运行的,通过JVM把编译好的字节码转换成对应操作系统平台可以直接识别运行的机器码指令,JVM充当了一个中间转换的桥梁,这样我们编写的Java文件就可以做到 "一次编译,到处运行" 。

JVM内存结构概览

JVM虚拟机规范官方文档地址:https://docs.oracle.com/javase/specs/,JDK8虚拟机参考手册地址:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html,JDK8官方文档地址为https://docs.oracle.com/javase/8/docs/,JDK8中内存结构文档https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

我们先看下下面这张图(这张图非常重要!非常重要!非常重要!),一个Java文件的执行过程为:Hello.java文件通过javac被编译为Hello.class文件,然后类装载子系统将class文件加载到运行时数据区,通过执行引擎去执行生成的机器指令。

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这个数据区域就叫运行时数据区。运行时数据区主要包含了PC寄存器(程序计数器)、Java虚拟机栈、本地方法栈、Java堆、方法区以及运行时常量池,这其中Java堆、方法区跟Java虚拟机栈是学习的重点。

但是,需要注意的是,上面的区域划分只是逻辑区域,对于有些区域的限制是比较松的,所以不同的虚拟机厂商在实现上,甚至是同一款虚拟机的不同版本也是不尽相同的。

运行时数据区

程序计数器

程序计数器(Program counter Register,也叫PC寄存器)是一块较小的内存空间,是线程私有的,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模到,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需耍依赖这个计数器来完成。因为Java是可以多线程执行的,一个线程执行到一半可能因为CPU时间片轮转切换到了另外一个线程,在切换回之前线程的时候,需要回到线程上次的执行位置,所以要线程私有。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。此内存区城是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情祝的区域。

比如在下面代码中test1()中调用了test2(),test2()执行完成后退出,这时候需要回到test1()方法中继续执行,程序计数器记录了下一个需要执行的指令的行号。

public void test1(){
    test2();
    System.out.println("test1");
}

public void test2(){
    System.out.println("test2");
}

Java虚拟机栈

Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同到都会创建一个栈帧(Stack Frame)用于存储局部量表、操作数栈、动态链接、方法出口等信息。栈帧是Java方法运行时的基础数据结构,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟栈中从入栈到出栈的过程(说人话就是要执行一个方法,将该方法的栈帧压入栈顶,方法执行完成其栈帧出栈)。在JVM里面,栈帧的操作只有两种:出栈和入栈。正在被线程执行的方法称为当前线程方法,而该方法的栈帧就称为当前帧。

局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、long、float、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象始地址的引用指针,也可能是指向一个代表对象的句柄或其地与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

在Java虚拟机规范中,对这个区域定了两种异状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常;一般的虚拟机栈都是可扩展的,如果扩展时无法丰请到足够的内存,就会抛出OutOfMemoryError异常,可以通过-Xss设置每个线程的堆栈大小。

Java虚拟机栈的结构如下图所示:Java虚拟机栈的生命周期与线程一致,一个方法对应一块栈帧内存区域,栈帧中包含局部变量表、操作数栈、动态链接、方法出口等信息。拿下面代码举例,程序执行main(),main()先压入栈顶,然后main()方法中new了一个Math对象,math变量是指向堆中Math对象的引用,math变量就属于局部变量表,创建Math对象之后,调用了其compute(),然后compute()压入栈顶,compute方法执行完成后其栈帧出栈,然后根据程序计数器记录程序执行的行号,继续回到main方法执行,main方法中已经没有其他执行指令了,则main方法退出,main方法对应的栈帧出栈,虚拟机栈中已经没有其他栈帧,main线程生命周期结束。

注意:关于Java虚拟机栈中的栈帧,还有栈帧中的组成部分,这里只是做个简单的概述,后续会单独进行详细讲解,希望继续关注。

public class Math {

	private static final Integer CONSTANT=666;

	private int compute() {//一个方法对应一块栈帧内存区域
		int a=3;
		int b=5;
		int c=(a+b)*10;
		return c;
	}

	public static void main(String[] args) {
		Math math=new Math();
		math.compute();
	}
}

本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈非常相似,也是线程私有的,它们的区别不过是虚拟机栈执行的是Java方法(也就是字节码),而本地方法栈用到的是Native方法。与虚拟机战一样。本地方法栈区域也会出现StackOverFlowError和OutOfMemoryError异常。

方法区

方法区(Method Area),是各个线程共享的内存区域,,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non一Heap(非堆),目的就是要和堆分开。这部分存储的是运行时必须的类相关信息,装载进此区域的数据是不会被垃圾收集器回收的,只有关闭Jvm才会释放这块区域占用的内存。

对于Hotspot虚拟机,很多开发者习惯将方法区称之为“永久代(Parmanent Gen)",但严格本质上说两者不同,或者说使用永久代来实现方法区而己,永久代是方法区(相当于是一个接口interface)的一个实现,idkl.7的版本中,己经将原本放在永久代的字符串常量池移走。Jdk1.7中方法区是用永久代实现的,到1.8中是用元空间(MetaSpace)实现的,而元空间使用的是直接内存。

根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时会抛出OutOfMemoryError异常。可以通过-XX:PermSize和 -XX:MaxPermSize来分别设置永久区最小、最大空间。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生产的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。

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

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存人口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

Java语言不要求常量一定只有编译器才能产生,运行时也可能将新的常量放入池中,该特性用的比较多的就是String类的intern()方法。运行时常量池是方法区的一部分,在内存不够时,也会抛出OutOfMemoryError异常。

Java堆

对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是线程共享的,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是着JIT编译器的发展与逸分析技术逐渐成熟,栈上分配、标量替换优化技术会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么"绝对"了。

Java堆是被收集管理的主要区域,因此很多时候也被称做"GC堆"(Garbage Collected Heap)。从内存回收角度来看,由于现在收集器基本都采用分代算法(为什么要采用分代算法,常用的垃圾收集算法有哪些后面会进行介绍),所以堆中还以细分:新生代(Young/New)和老年代(Old/Tenure),新生代又可以划分为Eden(伊甸园)空间、survivor(幸存区,其又可以分为from survivor和to survivor,也就是S0和S1)空间等。从内存分配的角度来看,线程共享的Java堆中可划分出多个程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的是为了更好地回收内存,或更快地分配内存。

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只逻辑上是连续的即可。.Java虚拟机中可以对堆进行扩展,可以通过-Xms 设置起始堆大小、通过-Xmx设置最大堆大小、通过-XX:NewSize设置新生代最小空间大小、通过 -XX:MaxNewSize设置新生代最大空间大小。如果在堆中没有完成实例分配,并且地也无法再扩展时,将会抛OutOfMemoryError异常。

下图是Java7中的Jvm内存划分:

  • 堆(Heap)、永久代(PermGen)
  • 堆(Heap)又分为新生代(NewGen)或者叫年轻代(YoungGen)、老年代(OldGen)
  • 年轻代(YoungGen)又可分为Eden区(伊甸园区)、Survivor区(幸存区)
  • Survivor区(幸存区)又可分为FromSpace(S0)和ToSpace(S1),整个年轻代中默认比例Eden:S0:S1=8:1:1,同一时间内S0跟S1只会有一个区域被占用
  • 年轻代(New):年轻代用来存放JVM刚分配的Java对象
  • 年老代(Old):年轻代中经过垃圾回收没有回收掉的对象将被Copy到年老代
  • 永久代(Perm):永久代存放Class、Method元信息,其大小跟项目的规模、类、方法的量有关
  • 年轻代发生的GC叫Minor GC,老年代发生的GC叫Major GC
  • 另外还有一个Full GC,是清理整个堆空间—包括年轻代和永久代

关于堆的分代、还有对象是如何从年轻代进入老年代等都会在后面的章节中介绍。

我们看下面这张图,在JDK1.8中将永久代去掉了,改由元空间(MetaSpace)去实现方法区,而元空间跟永久代的最大区别就是其不在JVM内存中,而是使用的直接内存。

关于方法区、常量池、永久代在JDK6、7、8中的变动还是挺大的:

  • Jdk1.6及之前:有永久代,常量池1.6在方法区
  • Jdk1.7:有永久代,但己经逐步“去永久代”,常量池1.7在堆
  • Jdk1.8及之后:无永久代,常量池1.8在元空间

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。在JDK1.4中新加入了NlO(New Inpu/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。当各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),会导致动态扩展时出现OutOfMemoryError异常。

关于JVM的内存结构本节先做了一个大概的介绍,其中还有很多细节没有介绍:栈帧中的各个组成部分分别是干什么用的,堆内存的划分,对象是如果从新生代到老年代的,为什么要分代收集,垃圾收集算法有哪些,垃圾收集器有哪些。。。。。这些在后面的章节中会慢慢一一介绍,希望继续关注。

文章内容参考了周志明老师的《深入理解Java虚拟机第二版》以及他翻译的《Java虚拟机规范 JavaSE8版》,想学习JVM的话强烈推荐这本《深入理解Java虚拟机第二版》。

到此这篇关于深入理解Java虚拟机 JVM 内存结构的文章就介绍到这了,更多相关Java 虚拟机 JVM 内存结构内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • java虚拟机原理:类加载过程详解

    目录 一.Java 类加载过程 1.字节码编译 2.加载 3.连接 4.初始化 总结 一.Java 类加载过程 1.字节码编译 编写好 Java 源码 Student.java , 使用 javac 将上述 Java 源码编译成 Class 字节码文件 Student.class , 2.加载 加载 : 通过 " 类加载子系统 " 将该字节码文件 , 加载到 Java 虚拟机内存中 的 方法区 , 然后开始执行 " 连接 " 操作 , 类加载时机 : Java 程序

  • java虚拟机JVM类加载机制原理(面试必问)

    目录 1.类加载的过程. 1)加载 2)验证 3)准备 4)解析 5)初始化 2.Java 虚拟机中有哪些类加载器? 1)启动类加载器(Bootstrap ClassLoader): 2)扩展类加载器(Extension ClassLoader): 3)应用程序类加载器(Application ClassLoader): 3.什么是双亲委派模型? 4.为什么使用双亲委派模式? 5.有哪些场景破坏了双亲委派模型? 1)线程上下文类加载器 2)Tomcat 的多 Web 应用程序 3)OSGI 实现

  • java虚拟机原理:Class字节码二进制文件分析

    目录 一.字节码文件 与 JVM 二.字节码文件示例 三.字节码文件二进制结构分析 1.魔数 2.次版本号 3.主版本号 4.常量池个数 总结 一.字节码文件 与 JVM Java 源码编译成 Class 字节码 ; Java 虚拟机 可以被认为是一个 解释器 , 解释编译后的 Class 字节码文件 , 最后在不同的操作系统中运行 ; Android 虚拟机 不是 Java 规范的 虚拟机 , 有一些根据嵌入式设备进行的定制的实现 ; Class 字节码 本质上就是 二进制数据 , 运行时 ,

  • java内存模型jvm虚拟机简要分析

    目录 主内存和工作内存 内存间的交互操作 原子性.可见性.有序性 原子性 可见性 有序性 主内存和工作内存 Java 内存模型规定了所有的变量都存储在主内存中, 每条线程有自己的工作内存 线程的工作内存中保存了被该线程使用的变量的主内存副本, 线程对变量的所有操作 (读取.赋值等) 都必须在工作内存中进行, 而不能直接读写主内存中的数据 不同的线程之间也无法直接访问对方工作内存中的变量, 线程间变量值的传递均需要通过主内存来完成 内存间的交互操作 原子性.可见性.有序性 Java 内存模型是围绕

  • Java虚拟机JVM类加载机制(从类文件到虚拟机)

    一.类加载机制简介 什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构.类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口. 类加载机制:所谓的类加载机制就是虚拟机将class文件加载到内存,并对数据进行验证,转换解析和初始化,形成虚拟机可以直接使用的jav

  • 深入理解Java虚拟机 JVM 内存结构

    目录 前言 JVM是什么 JVM内存结构概览 运行时数据区 程序计数器 Java虚拟机栈 本地方法栈 方法区 运行时常量池 Java堆 直接内存 前言 JVM是Java中比较难理解和掌握的一部分,也是面试中被问的比较多的,掌握好JVM底层原理有助于我们在开发中写出效率更高的代码,可以让我们面对OutOfMemoryError时不再一脸懵逼,可以用掌握的JVM知识去查找分析问题.去进行JVM的调优.去让我们的应用程序可以支持更高的并发量等......总之一句话,学好JVM很重要! JVM是什么 J

  • JVM内存结构相关知识解析

    最近在看< JAVA并发编程实践 >这本书,里面涉及到了 Java 内存模型,通过 Java 内存模型顺理成章的来到的 JVM 内存结构,关于 JVM 内存结构的认知还停留在上大学那会的课堂上,一直没有系统的学习这一块的知识,所以这一次我把< 深入理解Java虚拟机JVM高级特性与最佳实践 >.< Java虚拟机规范 Java SE 8版 >这两本书中关于 JVM 内存结构的部分都看了一遍,算是对 JVM 内存结构有了新的认识.JVM 内存结构是指:Java 虚拟机定义

  • 深入理解Java虚拟机_动力节点Java学院整理

    什么是Java虚拟机 Java程序必须在虚拟机上运行.那么虚拟机到底是什么呢?先看网上搜索到的比较靠谱的解释: 虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的.Java虚拟机有自己完善的硬体架构,如处理器.堆栈.寄存器等,还具有相应的指令系统.JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行. 这种解释应该算是正确的,但是只描述了虚拟机的外部行为和功能,并没有针对内部原理

  • Java虚拟机JVM性能优化(三):垃圾收集详解

    Java平台的垃圾收集机制显著提高了开发者的效率,但是一个实现糟糕的垃圾收集器可能过多地消耗应用程序的资源.在Java虚拟机性能优化系列的第三部分,Eva Andreasson向Java初学者介绍了Java平台的内存模型和垃圾收集机制.她解释了为什么碎片化(而不是垃圾收集)是Java应用程序性能的主要问题所在,以及为什么分代垃圾收集和压缩是目前处理Java应用程序碎片化的主要办法(但不是最有新意的). 垃圾收集(GC)的目的是释放那些不再被任何活动对象引用的Java对象所占用的内存,它是Java

  • 深入理解Java虚拟机体系结构

    1概述 众所周知,Java支持平台无关性.安全性和网络移动性.而Java平台由Java虚拟机和Java核心类所构成,它为纯Java程序提供了统一的编程接口,而不管下层操作系统是什么.正是得益于Java虚拟机,它号称的"一次编译,到处运行"才能有所保障. 1.1Java程序执行流程 Java程序的执行依赖于编译环境和运行环境.源码代码转变成可执行的机器代码,由下面的流程完成: Java技术的核心就是Java虚拟机,因为所有的Java程序都在虚拟机上运行.Java程序的运行需要Java虚拟

  • JVM内存结构划分实例解析

    这篇文章主要介绍了JVM内存结构划分实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 数据区域划分 运行时内存区域划分:程序计数器.虚拟机栈.本地方法栈.堆.方法区 程序计数器 线程私有 通过寄存器实现 不会存在运行溢出 当前线程所执行的行号指示器,记住下一条JVM指令的执行地址 虚拟机栈 垃圾回收不涉及栈内存 栈内存是线程私有的,可以理解为线程运行需要的内存空间 栈由栈帧组成,每个栈帧代表一个方法执行时需要的内存(参数,局部变量,返回地

  • 深入理解Java之jvm启动流程

    jvm是java的核心运行平台,自然是个非常复杂的系统.当然了,说jvm是个平台,实际上也是个泛称.准确的说,它是一个java虚拟机的统称,它并不指具体的某个虚拟机.所以,谈到java虚拟机时,往往我们通常说的都是一些规范性质的东西. 那么,如果想要研究jvm是如何工作的,就不能是泛泛而谈了.我们必须要具体到某个指定的虚拟机实现,以便说清其过程. 1. 说说openjdk 因为java实际上已经被oracle控制,而oracle本身是个商业公司,所以从某种程度上说,这里的java并不是完全开源的

  • JVM入门之JVM内存结构内容详解

    一.java代码编译执行过程 源码编译:通过Java源码编译器将Java代码编译成JVM字节码(.class文件) 类加载:通过ClassLoader及其子类来完成JVM的类加载 类执行:字节码被装入内存,进入JVM虚拟机,被解释器解释执行   注:Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,   用Java语言编写并编译的程序可以运行在这个平台上 二.JVM简介 1.java程序经过一次编译之后,将java代码编译为字节码也就是class文件,然

  • 详解Java虚拟机(JVM)运行时

    JVM(Java虚拟机)是一个抽象的计算模型.就如同一台真实的机器,它有自己的指令集和执行引擎,可以在运行时操控内存区域.目的是为构建在其上运行的应用程序提供一个运行环境.JVM可以解读指令代码并与底层进行交互:包括操作系统平台和执行指令并管理资源的硬件体系结构.本文主要介绍Java虚拟机(JVM)运行时详解. 我们知道的JVM内存区域有:堆和栈,这是一种泛的分法,也是按运行时区域的一种分法,堆是所有线程共享的一块区域,而栈是线程隔离的,每个线程互不共享. 线程不共享区域 每个线程的数据区域包括

  • java虚拟机jvm方法区实例讲解

    和java堆一样,方法区是一块所有线程共享的内存区域,用于保存系统的类信息,类的信息有哪些呢.字段.方法.常量池.方法区也有一块内存区域所以方法区的内存大小,决定了系统可以包含多少个类,如果系统类太多,方法区内存不够肯定会导致方法区溢出,虚拟机同样会抛出内存溢出信息.(内存溢出后面相关文章给大家总结) jdk6和jdk7中,方法区可以理解为永久区(Perm).永久区可以使用参数-XX:PermSize和-XX:MaxPermSize制定.默认情况下-XX:MaxPermSize为64MB.如果你

随机推荐