Java 虚拟机内存结构
不同虚拟机的运行时数据区可能略微有所不同,但都会遵从 Java 虚拟机规范, Java 虚拟机规范规定的区域分为以下 5 个部分:
方法区 程序计数器 虚拟机栈 本地方法栈 堆
线程私有
程序计数器 记录的图就是 0 1 3 4 5 ,为什么013 中间没有2 能,因为jvm可能会执行私密的东西不方便给程序员看,所以行号就隐藏起来了,行号是递增的.如果还有行号被隐藏了(就是行号不连续),那就是jvm还是在执行一些私密的东西.
线程私有的(每启动一个线程就必定有下面三个线程私有的东西)
线程私有是运行指令的
1.程序计数器
2.虚拟机栈
3.本地方法栈
线程私有的区域不用考虑垃圾回收问题,因为线程跑的时候不可能让线程中断,不可能跑完一半儿就关掉,线程跑完了,线程私有的这些区域就消亡了,线程开启了,线程私有区域就有产生了.
线程私有区域生命周期是跟线程来的
程序计数器
指向当前线程正在执行的字节码指令。线程私有的。内存空间小, 该内存区域是唯一一个 java 虚拟机规范没有规定任何 OOM 情况的区域。
Java语言是多线程的,所以必须要有一个指向当前线程正在执行的字节码指令的地址(行号).程序计数器是随时记录行号的,因为线程是操作系统的东西,Java是管不了的,所以只能是在执行的时候随时去记录.为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,是线程私有的.各线程之间独立存储,互不影响(面试可能问到为什么需要).
如果线程正在执行的是一个Java方法,则指明当前线程执行的代字节码行数,如果正在执行的是Natvie方法,这个计数器值则为空(Undefined).
注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
为什么需要程序计数器 ?参考:
https://www.yuque.com/docs/share/417db60c-c1eb-4256-bf54-846b901382a3?# 《为什么需要程序计数器 ?》
虚拟机栈
虚拟机栈是Java执行方法的内存模型。每个方法被执行的时候,都会创建一个栈帧,把栈帧压人栈,当方法正常返回或者抛出未捕获的异常时,栈帧就会出栈。
栈的结构是栈帧组成的,调用一个方法就压入一帧 帧上面存储局部变量表,操作数栈,动态链接, 方法出口等信息, 局部变量表存放的是 8 大基础类型加上一个引用类型,所以还是一个指向地址的指针
虚拟机栈平时成为栈内存, 是线程私有的, 生命周期和线程相同.
每个方法在执行的时候都会创建一个栈帧, 用于存储局部变量表、操作数栈、动态链接和方法出口等信息,把栈帧压人栈,当方法正常返回或者抛出未捕获的异常时,栈帧就会出栈。
栈帧
概述:
栈帧存储方法的相关信息,包含局部变量数表、返回值、操作数栈、动态链接
详解:
虚拟机栈是存储当前线程运行方法所需要的数据,指令和返回地址的东西.以栈帧的方式存储方法调用的过程.并存储方法调用过程中基本数据类型的变量(int、short、long、byte、float、double、boolean、char等)以及对象的引用变量(reference),其内存分配在栈上,变量出了作用域就会自动释放;
当前线程可以跑多个方法.Java虚拟机为了区分多个方法就用栈帧这个东西.(一个虚拟机栈可以有多个栈帧)
每个线程私有的,线程在运行时,在执行每个方法的时候都会打包成一个栈帧,存储了局部变量表,操作数栈,动态链接,方法出口等信息,然后放入栈。
栈帧就是虚拟机栈的进一步划分.
每个时刻正在执行的当前方法就是虚拟机栈顶的栈桢。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程。
一个虚拟机栈有1~无数个栈帧.
虚拟机栈大小一般是固定的,默认是1M, 可以通过参数来设置,如果线程数量非常多,比如海量,每一个线程分配1M大小,假如有10W个线程进来,可能也会发生内存溢出,当然这个有点极端了. 当然很少会跑10W个线程,一般差不多1000个100个线程就差不多了,超过10000个的都很少,一万个线程才10个G内存.
在栈帧里面还会划分为:
1. 局部变量表 (就是存放局部变量的表)
2. 操作数栈(计算的临时数据存储区)
3. 动态连接(如果方法有多态的表现,就需要动态连接)
4. 返回地址(方法要执行完了返回到哪个地方)
5. 等等……(其它东西不重要,不用去了解,就上面四个重要)
如果图片显示不好看,可以访问下面的笔记看,有更清晰的图片
文档:[和笔记对接]jvm运行时数据区图片.note
链接:http://note.youdao.com/noteshare?id=17ab262f84f37a1adb40f3e2cc7dd305
虽然两个栈帧是独立的,但是两个栈帧中间有方法调用的话,方法调用方法可能会传递参数,虚拟机会采取数据重叠的优化,局表变量表和操作数栈占据的都是一样的,都是32位的.
所以,操作数栈和局部变量表可能会把它中间的一部分进行重叠,比如说栈帧N执行完以后,还存在局部变量表里面的数据,送入栈帧N-1 里面方法的入参,比如说需要调用另一个方法,就可能会使用栈帧 n -1 ,然后就会把局部变量表和操作数栈对应的东西进行重叠,这样可以去节约我们的空间.
局部变量表
局部变量表是一组局部变量值存储空间, 存放编译期间可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double),对象的引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
局部变量空间,对于 long 和 double 占用两个 Slot,其余的基本类型占用一个。
局部变量表的所需要的内存空间,会在编译期间完成分配。
因为运行过程都走的是方法,那么进入一个方法的时候,这个方法需要在帧中分配多大的局部变量空间是完全确定的。在方法运行期间不会改变局部变量表的大小。
操作数栈
操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区。
操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式
操作数栈就是用栈的结构去完成代码的操作的,操作数栈的最大深度在编译的时候就已经确认了.
操作数栈也常被称为操作栈,它是一个后入先出栈。JVM底层字节码指令集是基于栈类型的,所有的操作码都是对操作数栈上的数据进行操作,对于每一个方法的调用,JVM会建立一个操作数栈,以供计算使用。
和局部变量一样。操作数栈的最大深度也是编译的时候写入到方法表的code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long、double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。
在操作数栈如果进行操作的时候没有八大基本数据类型,很多数据类型都会转成int类型的.
操作数栈是操作变量的内存模型。操作数栈的最大深度在编译的时候已经确定(写入方法区 code 属性的 max_stacks 项中)。操作数栈的的元素可以是任意 Java 类型,包括 long 和 double,32 位数据占用栈空间为 1,64 位数据占用 2。方法刚开始执行的时候,栈是空的,当方法执行过程中,各种字节码指令往栈中存取数据。
这也是为什么 java 性能低的原因,因为基于栈的指令集系统做的平台无关性,但是也降低了性能
编译期就已经确定,存在方法 code 属性中,栈的特点后进先出,存放表达式。
因为就我们熟知的 X86 和 ARM 指令集,对数据的操作都是基于寄存器,比如对两个数进行加法操作,那么会把两个数送到
两个寄存器中,在执行加法操作。
基于栈的设计模式,是将数放在栈中,我们需要使用的时候,将栈顶的数据出栈,并执行相应的操作。
举例来说,在 JVM 中 执行 a = b + c 的字节码执行过程中操作数栈以及局部变量表的变化如下图所示。
局部变量表中存储着 a、b、c 三个局部变量,首先将 b 和 c 分别入栈
将栈顶的两个数出栈执行加法操作,并将结果保存至栈顶,之后将栈顶的数出栈赋值给 a
动态连接
如果方法有多态的表现,就需要动态连接
多态是Java语言特性多态(需要类加载、运行时才能确定具体的方法)
既然是执行方法,那么我们需要知道当前栈帧执行的是哪个方法,栈帧中会持有一个引用(符号引用),该引用指向某个具体方法。
符号引用是一个地址位置的代号,在编译的时候我们是不知道某个方法在运行的时候是放到哪里的,这时我用代号com/enjoy/pojo/User.Say:()V指代某个类的方法,将来可以把符号引用转换成直接引用进行真实的调用。
用符号引用转化成直接引用的解析时机,把解析分为两大类
静态解析:符号引用在类加载阶段或者第一次使用的时候就直接转换成直接引用。
动态连接:符号引用在每次运行期间转换为直接引用,即每次运行都重新转换。
返回地址
记录方法最终会返回到哪里去,方法返回的东西记录在栈帧里面的返回地址里面,就是方法要执行完了有 return.
方法退出方式有:正常退出与异常退出两种退出方式
理论上,执行完当前栈帧的方法,需要返回到当前方法被调用的位置,所以栈帧需要记录一些信息,用来恢复上层方法的执行状态。正常退出,上层方法的PC计数器可以做为当前方法的返回地址,被保存在当前栈帧中。”异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息”
方法退出时会做的操作:恢复上次方法的局部变量表、操作数栈,把当前方法的返回值,压入调用者栈帧的操作数栈中,使用当前栈帧保存的返回地址调整PC计数器的值,当前栈帧出栈,随后,执行PC计数器指向的指令。
本地方法栈
本地方法栈保存的是native方法的信息,当一个jvm创建的线程调用native方法后,jvm不再为其在虚拟机栈中创建栈帧,jvm只是简单地动态链接比直接调用native方法.
本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点以及都能抛出StackOverflowError和OutOfMemoryError异常。
同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法,其他基本上一致.
如何去服务native方法?native方法使用什么语言实现?怎么组织像栈帧这种为了服务方法的数据结构?虚拟机规范并未给出强制规定,因此不同的虚拟机实可以进行自由实现,我们常用的HotSpot虚拟机选择合并了虚拟机栈和本地方法栈。
线程共享
线程共享是不管中间有多个线程,方法区和堆和直接内存只有一个,线程共享是存储和处理数据的.
1.堆
2.方法区
3.直接内存(非运行时数据区的一部分)
java堆
栈是运行时的单位,而堆是存储的单位。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
堆内存:保存每一个对象的属性内容,堆内存需要用关键字new才可以开辟,堆内存主要用于存放对象和数组.
在类里面定义的八大基础数据类型成员变量(成员变量也叫实例变量)并且没有final和static修饰的变量就存在堆里面,堆是需要重点关注的一块区域,因为涉及到内存的分配(new 关键字, 反射等)与回收(回收算法,收集器等)
它是JVM管理的内存中最大的一块区域,堆内存和方法区都被所有线程共享,在虚拟机启动时创建。在垃圾收集的层面上来看,由于现在收集器基本上都采用分代收集算法,因此堆还可以分为新生代(YoungGeneration)和老年代(OldGeneration),新生代还可以分为Eden、From Survivor、To Survivor。
几乎所有对象都分配在这里,也是垃圾回收发生的主要区域,可用以下参数调整:
-Xms:堆的最小值;
-Xmx:堆的最大值;
-Xmn:新生代的大小;
-XX:NewSize;新生代最小值;
-XX:MaxNewSize:新生代最大值;
例如- Xmx256m
方法区(永久代,元空间)
jdk1.7和以前方法区叫永久代,jdk1.8以后方法区叫元空间.
方法区主要用于存储虚拟机加载的类信息、常量、静态变量,以及编译器编译后的代码等数据。
1. 类信息: .Java文件编译后的的.class文件里面会有类信息
2. 常量 : 在类里面成员变量同时final修饰的八大基础数据类型
3. 静态变量: 在类里面成员变量同时static修饰的八大基础数据类型
4. 即时编译期编译后的代码: 我们编写的Java语言都是.java编程成.class再来跑,但是Java支持动态语言,有可能运行的时候是动态的编译的,所以即时编译的代码也会放在方法区的,比如JS就是即时编译语言.
方法区是不会被GC回收的,因为类信息和即时编译后的代码是需要随时用的,同时类信息和常量都是后面随时可能会用到的.
运行时常量池
运行时常量池(Runtime Constant Pool),它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),
用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到常量池中。
①符号引用
比如A类Main方法需要调用Tools工具类里面的A方法,A方法还没跑起来的时候你不知道实际地址,然后就定义一个符号引用,如果Main方法再去调用Tools工具类的A方法的时候再把Tools方法new出来就知道了A类的Main方法要指向Tools工具类的A方法的实际地址.
一个java类(假设为People类)被编译成一个class文件时,如果People类引用了Tool类,但是在编译时People类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。
而在类装载器装载People类时,此时可以通过虚拟机获取Tool类的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址,及直接引用地址。
即在编译时用符号引用来代替引用类,在加载时再通过虚拟机获取该引用类的实际地址.
以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局是无关的,引用的目标不一定已经加载到内存中。
②字面量:
文本字符串 String a = “abc”,这个abc就是字面量
八种基本类型int a = 1; 这个1就是字面量
声明为final的常量
比如类里面的成员变量都是字面量
③常量池到好处
常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
类和接口的全限定名
字段名称和描述符
方法名称和描述符
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。
JDK1.6 运行时常量池在方法区中
JDK1.7运行时常量池在堆中
JDK1.8去永久代,使用元空间(空间大小只受制于服务器的内存了)代替永久代
永久代为什么叫元空间
https://www.yuque.com/docs/share/aca97988-bf44-42d9-9395-d91e2d1f9129?# 《Java8之后对内存分代做了什么改进》
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中农定义的内存区域。直接内存是JVM直接管理不了的,可以这样理解,直接内存,就是 JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区 (Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。 由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。
但是如果使用NIO的话,直接内存就会被使用,NIO调用的是操作系统,操作系统不受制于虚拟机,直接使用直接内存.
如果NIO使用Java堆的话,我会发现我最终与操作系统的我会经常做堆和直接内存之间的COPY,因为进行NIO操作的话,就是进行输入和输出操作,进行数据的交换,如果我把这个交换的数据放到堆或者方法区的话,那么我会在我们的直接内存和jvm管理的堆或者方法去,进行反复的操作.
所以我们为了提高效率,我们用NIO用直接内存,使用直接内存避免了在Java 堆和Native 堆中来回复制数据,能够提高效率
本机直接内存的分配不会受到Java 堆大小的限制,受到服务器本机总内存大小限制,也可以进行参数的设置.
如果使用了NIO,直接内存这块区域会被频繁使用, 可以通过MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常;
NIO避免了在Java 堆和Native 堆(本地内存)中来回复制数据,能够提高效率
直接内存(堆外内存)与堆内存比较
直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显