JVM内存结构概述

本节将会介绍一下JVM的内存结构,JVM运行时数据区的各个组成部分:堆,方法区,程序计数器,Java虚拟机栈,本地方法栈,还会对Java堆的分代划分做个简单的介绍。

目录

前言

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虚拟机第二版》。

2019最新深入理解JVM内存结构及运行原理(JVM调优)高级核心课程视频教程下载。JVMJava知识体系中的重要部分,对JVM底层的了解是每一位Java程序员深入Java技术领域的重要因素。本课程试图通过简单易懂的方式,系统的深入讲解JVM相关知识。包括JVM执行过程、虚拟机类加载机制、运行时数据、GC、类加载器、内存分配与回收策略等,全套视频加资料高清无密码  第1讲 说在前面的话 免费 00:05:07  第2讲 整个部分要讲的内容说明 免费 00:06:58  第3讲 环境搭建以及jdk,jre,jvm的关系 免费 00:20:48  第4讲 jvm初体验-内存溢出问题的分析与解决 免费 00:17:59  第5讲 jvm再体验-jvm可视化监控工具 免费 00:21:17  第6讲 杂谈 免费 00:12:37  第7讲 Java的发展历史 00:27:24  第8讲 Java的发展历史续 00:02:27  第9讲 Java技术体系 00:08:46  第10讲 jdk8的新特性 00:07:31  第11讲 lanmbda表达式简介 00:07:02  第12讲 Java虚拟机-classic vm 00:06:06  第13讲 Java虚拟机-ExactVM 00:03:35  第14讲 Java虚拟机-HotSpotVM 00:04:23  第15讲 Java虚拟机-kvm 00:03:04  第16讲 Java虚拟机-JRockit 00:04:12  第17讲 Java虚拟机-j9 00:04:23  第18讲 Java虚拟机-dalvik 00:02:20  第19讲 Java虚拟机-MicrosoftJVM 00:03:57  第20讲 Java虚拟机-高性能Java虚拟机 00:02:58  第21讲 Java虚拟机-TaobaoVM 00:03:06  第22讲 Java内存域-简介 00:07:56  第23讲 Java内存域-Java虚拟机 00:12:04  第24讲 Java内存域-程序计数器 00:12:54  第25讲 Java内存域-本地方法 00:02:39  第26讲 Java内存域-内存 00:05:08  第27讲 Java内存域-方法 00:06:32  第28讲 Java内存域-直接内存运行时常量池 00:15:53  第29讲 对象在内存中的布局-对象的创建 00:21:19  第30讲 探究对象的结构 00:13:47  第31讲 深入理解对象的访问定位 00:08:01  第32讲 垃圾回收-概述 00:06:20  第33讲 垃圾回收-判断对象是否存活算法-引用计数法详解 00:14:08  第34讲 垃圾回收-判断对象是否存活算法-可达性分析法详解 00:07:09  第35讲 垃圾回收算法-标记清除算法 00:04:36  第36讲 垃圾回收算法-复制算法 00:14:35  第37讲 垃圾回收算法-标记整理算法和分代收集算法 00:05:24  第38讲 垃圾收集器-serial收集器详解 00:09:45  第39讲 垃圾收集器-parnew收集器详解 00:04:53  第40讲 垃圾收集器-parallel收集器详解 00:11:02  第41讲 垃圾收集器-cms收集器详解 00:14:58  第42讲 最牛的垃圾收集器-g1收集器详解 00:18:04  第43讲 内存分配-概述 00:04:23  第44讲 内存分配-Eden域 00:22:51  第45讲 内存分配-大对象直接进老年代 00:06:42  第46讲 内存分配-长期存活的对象进入老年代 00:03:40  第47讲 内存分配-空间分配担保 00:04:54  第48讲 内存分配-逃逸分析与上分配 00:10:32  第49讲 虚拟机工具介绍 00:10:27  第50讲 虚拟机工具-jps详解 00:11:20  第51讲 虚拟机工具-jstat详解 00:09:20  第52讲 虚拟机工具-jinfo详解 00:05:03  第53讲 虚拟机工具-jmap详解 00:08:48  第54讲 虚拟机工具-jhat详解 00:08:10  第55讲 虚拟机工具-jstack详解 00:10:19  第56讲 可视化虚拟机工具-Jconsole内存监控 00:07:09  第57讲 可视化虚拟机工具-Jconsole线程监控 00:12:18  第58讲 死锁原理以及可视化虚拟机工具-Jconsole线程死锁监控 00:10:38  第59讲 VisualVM使用详解 00:08:03  第60讲 性能调优概述 00:11:22  第61讲 性能调优-案例1 00:23:28  第62讲 性能调优-案例2 00:10:05  第63讲 性能调优-案例3 00:12:41  第64讲 前半部分内容整体回顾 00:15:41  第65讲 Class文件简介和发展历史 免费 00:11:26  第66讲 Class文件结构概述 免费 00:16:50  第67讲 Class文件设计理念以及意义 免费 00:13:41  第68讲 文件结构-魔数 免费 00:09:49  第69讲 文件结构-常量池 免费 00:23:44  第70讲 文件结构-访问标志 免费 00:11:36  第71讲 文件结构-类索引 00:11:26  第72讲 文件结构-字段表集合 00:13:21  第73讲 文件结构-方法表集合 00:10:06  第74讲 文件结构-属性表集合 00:18:23  第75讲 字节码指令简介 00:09:18  第76讲 字节码与数据类型 00:09:34  第77讲 加载指令 00:09:33  第78讲 运算指令 00:10:24  第79讲 类型转换指令 00:13:42  第80讲 对象创建与访问指令 00:09:38  第81讲 操作树指令 00:03:27  第82讲 控制转移指令 00:11:58  第83讲 方法调用和返回指令 00:06:37  第84讲 异常处理指令 00:09:44  第85讲 同步指令 00:07:34  第86讲 类加载机制概述 00:07:26  第87讲 类加载时机 00:13:15  第88讲 类加载的过程-加载 00:15:15  第89讲 类加载的过程-验证 00:10:24  第90讲 类加载的过程-准备 00:05:40  第91讲 类加载的过程-解析 00:14:04  第92讲 类加载的过程-初始化 00:19:41  第93讲 类加载器 00:22:41  第94讲 双亲委派模型 00:17:03  第95讲 运行时结构 00:08:46  第96讲 局部变量表 00:20:48  第97讲 操作数 00:08:36  第98讲 动态连接 00:02:56  第99讲 方法返回地址和附加信息 00:03:24  第100讲 方法调用-解析调用 00:09:49  第101讲 方法调用-静态分派调用 00:16:21  第102讲 方法调用-动态分派调用 00:09:02  第103讲 动态类型语言支持 00:09:27  第104讲 字节码执行引擎小结 00:03:38  第105讲 总结与回顾 00:10:55  第106讲 happens-before简单概述 00:15:17  第107讲 重排序问题 00:23:19  第108讲 锁的内存语义 00:13:54  第109讲 volatile的内存语义 00:12:04  第110讲 final域内存语义
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值