首先看一下不同厂商的JVM产品 :
厂商 | JVM |
Oracle-SUN | Hotspot |
Oracle | JRocket |
IBM | J9 JVM |
阿里 | Taobao JVM |
JDK1.8以前Java虚拟机(Java Virtual Machine=JVM)的内存空间分为五个部分,分别是:
- 程序计数器(线程私有-无GC)
- Java虚拟机栈(线程私有-无GC)
- 本地方法栈(线程私有-无GC)
- 堆(线程共享-有GC)
- 方法区(线程共享-有GC)-是个概念不是实现
需要注意的是,方法区又称为永久代(Perm Gen-- JDK1.7以前,是堆中一部分,为了区别于堆,又称非堆),jdk1.8后永久代被元空间(MetaSpace)替代。
MetaSpace使用物理内存,不再使用JVM分配的内存。方法区是规范是概念,而永久代和元空间是实现。
JDK1.8之后JVM内存模型如下图:
元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存
。1.7后,字符串常量池从永久代中剥离出来,存放在堆中。
【1】程序计数器
① 什么是程序计数器
程序计数器是一块较小的内存空间,可以把它看作当前线程正在执行的字节码的行号指示器。也就是说,程序计数器里面记录的是当前线程正在执行的那一条字节码指令的地址。程序计数器是线程私有的。
但是,如果当前线程执行的是一个本地方法,那么此时程序计数器为空。
本地方法为Native Method,即由native 修饰的方法。 在定义一个native method时,并不提供实现体(有些像定义一个java interface),因为其实现体是由非java语言在外面实现的。
② 程序计数器的作用
程序计数器主要有两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用来记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪了。
③ 程序计数器的特点
- 是一块较小的存储空间;
- 线程私有–每条线程都有一个程序计数器;
- 是唯一一个不会出现OutOfMemoryError的内存区域;
- 生命周期随着线程的创建而创建,随着线程的结束而死亡。
【2】Java虚拟机栈(JVM Stack)
① 什么是Java虚拟机栈
Java虚拟机栈是描述Java方法运行过程的内存模型。Java虚拟机栈会为每一个即将运行的Java方法创建一块叫做“栈帧”的区域,这块区域用于存储该方法在运行过程中所需要的一些信息,这些信息包括:
局部变量表
存放基本数据类型变量,引用类型的变量(一个指向对象起始地址的引用指针或一个代表对象的句柄)、Return Address类型的变量(指向一条字节码指令的地址)及方法参数。
操作数栈
动态链接
方法出口信息
其他信息
当一个方法即将被运行时,Java虚拟机栈首先会在Java虚拟机栈中为该方法创建一块"栈帧",栈帧中包含局部变量表,操作数栈,动态链接,方法出口信息等。当方法在运行过程中需要创建局部变量时,就将局部变量的值存入栈帧的局部变量表中。
当这个方法执行完毕后,这个方法所对应的栈帧将会出栈,并释放内存空间。
局部变量表为一个以变量槽(Slot)为单位的数组,每个数组元素对应一个局部变量的值。调用方法时,将方法的局部变量组成一个数组,通过索引来访问。若为非静态方法,则加入一个隐含的引用参数this,该参数指向调用这个方法的对象。而静态方法则没有this参数。因此,对象无法调用静态方法。
.
32位虚拟机中一个Slot可以存放一个32位以内的数据类型(boolean、byte、char、short、int、float、reference和returnAddress八种)。
.
操作数栈是一个以字长为单位数组,但是通过栈操作来访问。所谓操作数是那些被指令操作的数据。当需要对参数操作时如a=b+c,就将即将被操作的参数压栈,如将b 和c 压栈,然后由操作指令将它们弹出,并执行操作。虚拟机将操作数栈作为工作区。
Java栈上的所有数据都是私有的,其他线程都不能访问该线程的栈数据。在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。当在一段代码块中定义一个变量时,Java就会在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
注意:人们常说的Java内存空间分为“栈”和“堆”,栈中存放局部变量,堆中存放对象。
这句话不完全正确!这里的“堆”可以这么理解,但是这里的“栈”只代表了Java虚拟机栈中的局部变量表部分。真正的Java虚拟机栈是由一个个栈帧组成的,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接和方法出口信息等。
② Java虚拟机栈的特点
(1)局部变量表的创建是在方法被执行的时候,随着栈帧的创建而创建。
而且局部变量表的大小在编译时期
就确定下来了,在创建的时候只需分配事先规定好的大小即可。此外在方法运行的过程中局部变量表的大小是不会发生改变的。
(2)Java虚拟机栈会出现两种异常,StackOverFlowError and OutOfMemoryError
。
StackOverFlowError
若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
OutOfMemoryError
若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
(3)Java虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
StackOverFlowError and OutOfMemoryError的异同?
.
StackOverFlowError表示当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。而OutOfMemoryError是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。
(4)每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
(5) 栈中的数据在线程内部是共享的。
假设我们同时定 义: int a=3;int b=3 ;编译器先处理 int a = 3;首先它会在栈中创建一个变量 为 a的引用,然后查找栈中是否有 3这个值,如果没找到就将3存放进来,然后将a指向3,接着处理int b=3;在创建完b的引用变量之后,因为栈中已经有了3这个值,便将b直接指向3这样,就出现了a和b同时均指向3的情况。
这时,如果再令 a=4 ;那么编译器会重新搜索栈中是否有 4值,如果没有则将 4存放进来,并令 a指向 4;如果已经有了,则直接将 a指向这个地址 。因此 a值的改变不会影响到 b的。
要注意这种数据的共享与两个对象引用同 时指向一个对象的这种共享是不同的。因为这种情况 ,a的修改并不会影响到b,它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。
【3】本地方法栈
本地方法栈和Java虚拟机栈实现的功能类似,只不过本地方法栈是本地方法运行的内存模型。区别是虚拟机栈为虚拟机执行java方法服务,而本地方法栈则是为虚拟机用到的Native方法服务
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接以及出口信息等。
方法执行完毕后相应的栈帧也会出栈并释放内存空间。
也会抛出两种异常,StackOverFlowError and OutOfMemoryError异常。
【4】堆(Heap)
堆是用来存放对象(数组也是对象)的内存空间,几乎所有的对象都存储在堆中(实例对象创建后,成员变量也随对象储存在堆中,随垃圾回收机制进行释放)。堆是一个运行时数据区,在程序运行时动态分配内存。
在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组和对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中数组或对象。
如下实例:
public void test(){ User user = new User('jane'); //user 引用变量在栈中,值为创建的User对象在堆中的首地址; //new User();将会在堆中创建一个对象 }
引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域外后被释放。
而数组和对象本身在堆中分配,即使程序运行到使用new产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候才变为垃圾,不能再被使用。但仍然占据空间不放,在随后的一个不确定的时间被垃圾回收期收走。这也是Java比较占内存的原因,实际上,栈中的变量指向堆内存的变量,这就是Java的指针。
Heap是大家最为熟悉的区域,是jvm用来存储对象实例的区域,Heap在32位的系统中最大为2G。其大小通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G;-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G。
.
默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。
.
-XX:PermSize -XX:MaxPermSize:方法区持久代大小:方法区域也是全局共享的,在一定的条件下它也会被 GC,当方法区域需要使用的内存超过其允许的大小时,会抛出 OutOfMemory的错误信息。
.
理论上,堆应该为新生代+老年代+永久代==Xmx+XX:MaxPermSize
堆具有以下特点:
① 线程共享
整个Java虚拟机只有一个堆,所有的线程都访问同一个堆。而程序计数器、Java虚拟机栈、本地方法栈都是一个线程对应一个。
② 在虚拟机启动的时候创建
③ 垃圾回收的主要场所
④ 可以进一步细分为:新生代和老年代
新生代主要存储新创建的对象和尚未进入老年代的对象。老年代存储经过多次新生代GC(Minor GC)任然存活的对象。
新生代
程序新创建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成,可通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及Survivor Space的大小。
因此新生代又可被分为:Eden,From Survior,To Survior。
老年代
用于存放经过多次新生代GC仍然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代。主要有两种情况:①.大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不再在新生代分配,而是直接在老年代分配。②.大的数组对象,且数组中无引用外部对象。
老年代所占的内存大小为-Xmx对应的值减去-Xmn(新生代)对应的值。不同的区域存放具有不同生命周期的对象。这样可以根据不同的区域使用不同的垃圾回收算法,从而更具有针对性,从而更高效。
⑤ 堆的大小既可以固定也可以扩展,但主流的虚拟机堆的大小是可扩展的。
因此当线程请求分配内存,但堆已满,且内存已满无法再扩展时,就抛出OutOfMemoryError。
⑥ jdk1.8堆空间内存分配
老年代 : 三分之二的堆空间
年轻代 : 三分之一的堆空间
- eden区: 8/10 的年轻代空间
- survivor0 : 1/10 的年轻代空间
- survivor1 : 1/10 的年轻代空间
命令行上执行如下命令,查看所有默认的jvm参数
java -XX:+PrintFlagsFinal -version
【5】方法区(这是一个概念)
① 什么是方法区
Java虚拟机规范中定义方法区是堆的一个逻辑部分。方法区中存放已经被虚拟机加载的类信息(class文件以特殊数据结构存储在方法区)、字段信息、方法信息、ClassLoader的引用、class的引用、常量、运行时常量池、静态变量、即时编译器编译后的代码等。
方法区有一个别名Non-Heap(非堆),用于区别于Java堆区。默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。
-XX:PermSize:方法区最小值 -XX:MaxPermSize:方法去最大值
jdk1.7及以前实现是永久代,JDK1.8之后永久代被元空间取代,配置参数如下所示:
-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=50m
② 方法区的特点
线程共享
方法区是堆的一个逻辑部分,因此和堆一样都是线程共享的。整个虚拟机中只有一个方法区。
永久代
方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,我们把方法区称为永久代(方法区是思想、规范,永久代是实现)。
内存回收效率低
方法区中的信息一般需要长期存在,回收一遍内存之后可能只有少量信息无效。对方法区的内存回收的主要目标是:对常量池的回收和对类型的卸载
。
Java虚拟机规范对方法区的要求比较宽松
和堆一样,允许固定大小,也允许可扩展的大小,还允许不实现垃圾回收。
方法区域也是全局共享的,在一定的条件下它也会被 GC,当方法区域需要使用的内存超过其允许的大小时,会抛出 OutOfMemory的错误信息。
③ 运行时常量池
类加载后,Class文件结构中常量池中的数据将被存储在运行时常量池中。我们一般在一个类中通过public static final来声明一个常量或者如String str=“abc”。这个类被编译后便生出Class文件,这个类的所有信息都存储在这个class文件中。当这个类被Java虚拟机加载后,class文件中的常量就存放在方法区的运行时常量池中。而且在运行期间,可以向常量池中添加新的常量。
如String类的intern()方法就能在运行期间向常量池中添加字符串常量。
.
PS:
int age = 21;//age是一个变量,可以被赋值;21就是一个字面值常量,不能被赋值;
int final pai = 3.14;//pai就是一个符号常量,一旦被赋值之后就不能被修改。
String str =“abc”;//str是一个对象引用变量,JVM会在堆中创建一个String(abc)对象,同时将引用放入字符串常量池。
当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就需要垃圾收集器回收。
JVM为每个已加载的类型维护一个常量池,常量池就是这个类型用到的常量的一个有序集合。其包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用。即字面量和符号引用,其中字面量指的是整个类中的字面量。包含成员变量、静态方法、非静态方法等中的字面量。池中的数据和数组一样通过索引访问。
由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在Java的动态链接中起了核心作用。
运行时常量池溢出—异常信息:java.lang.OutOfMemoryError:PermGen space
.
如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。这里需要注意jdk1.6 1.7 1.8不同实现。前面讲述的是jdk1.6实现,1.7 1.8等将字符串常量池从永久代移到了堆中,字符串常量池中放的是对象引用地址,字符串对象不在字符串常量池中。
.
由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。
④ 方法区(永久代|元空间)存放内容总结
类的基本信息:
1.每个类的全限定名
2.每个类的直接超类的全限定名(可约束类型转换)
3.该类是类还是接口
4.该类的访问修饰符
5.直接超接口的全限定名的有序列表
已装载类的详细信息
1.字段信息:字段信息存放类中声明的每一个字段的信息,包括字段的名、类型、修饰符。
2.方法信息:类中声明的每一个方法的信息,包括方法名、返回值类型、参数类型、修饰符、异常、方法的字节码。
3.到类classloader的引用:到该类的类装载器的引用。
4.到类class 的引用:虚拟机为每一个被装载的类型创建一个class实例,用来代表这个被装载的类。
5.静态变量和静态代码块–类加载过程中就会执行。
6.静态方法–只是加载,不会执行。
7…
运行时常量池:在方法区中,每个类型都对应一个常量池,存放该类型所用到的所有常量,常量池中存储了诸如文字字符串、final变量值、类名和方法名常量。
其实,确切的说,在类加载过程中,会首先将编译好的class文件加载进方法区,故而class文件中有什么,方法区中就有什么。
class文件是什么?有的说是指令集,有的按照结构划分;其实都没错,更确切的从计算机角度来说,就是一串二进制字节码。
【6】直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,是jvm外部的内存区域。
在NIO中引入了一种基于通道和缓冲的IO方式。它可以通过调用本地方法(Native)直接分配Java虚拟机之外的内存,然后通过一个存储在Java堆中的DirectByteBuffer对象直接操作该内存,而无需先将外面内存中的数据复制到堆中再操作,从而提升了数据操作的效率。 这样就避免了Java堆和Native堆的相互调用了,提升了访问速度。
直接内存的大小不受Java虚拟机控制,但既然是内存,当内存不足时就会抛出OutOfMemoryError异常。
【7】方法区、永久代(持久代)和元空间
① 永久代和方法区的关系
涉及到内存模型时,往往会提到永久代,那么它和方法区又是什么关系呢?
《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 同时大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。因此,我们得到了结论,永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现。其他的虚拟机实现并没有永久带这一说法。在1.7之前在(JDK1.2 ~ JDK6)的实现中,HotSpot 使用永久代实现方法区,HotSpot 使用 GC分代来实现方法区内存回收,可以使用如下参数来调节方法区的大小:
-XX:PermSize 方法区初始大小 -XX:MaxPermSize 方法区最大大小 超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen
② 元空间
对于Java8, HotSpots取消了永久代,那么是不是也就没有方法区了呢?
当然不是,方法区是一个规范,规范没变,它就一直在。那么取代永久代的就是元空间。它可永久代有什么不同的?
存储位置不同,永久代物理是是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存。
存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。
【8】总结
① Java虚拟机的内存模型一共有两个“栈”,分别是Java虚拟机栈和本地方法栈。
两个“栈”功能类似,都是方法运行过程的内存模型。并且两个“栈”内部构造相同,都是方法私有。
只不过Java虚拟机栈描述的是Java方法运行过程的内存模型,而本地方法栈是描述Java本地方法运行过程的内存模型。
② Java虚拟机的内存模型中一共有两个“堆”,一个是原本的堆,一个是方法区。
方法区本质上是属于堆的一个逻辑部分。堆中存放对象,方法区中存放类信息、常量、静态变量、即时编译器编译后的代码等。
③ 堆是Java虚拟机中最大的一块内存区域,也是垃圾收集器主要的工作区域。
在创建对象的时候,非静态成员会被加载堆内存中,并完成成员变量的赋值初始化。也就是说所有的非静态成员(包括成员变量、成员方法、构造方法、构造代码块、普通代码块)是保存在堆内存中的。
但是方法调用的时候,调用的方法会在栈内存中执行,构造代码块也会在栈内存中执行。
④ 线程私有与共享
程序计数器、Java虚拟机栈、本地方法栈是线程私有的,即每个线程都拥有各自的程序计数器、Java虚拟机栈和本地方法栈。并且他们的生命周期和所属的线程一样。
而堆、方法区是线程共享的,在Java虚拟机中只有一个堆,一个方法区。并在JVM启动的时候就创建,JVM停止的时候才销毁。
⑤ Java中变量(包括常量)可以存放在 栈、堆、方法区三块内存区域,除去方法区的常量池中存放的常量、静态变量之外主要的变量都存放在栈和堆中。
类型/变量 | 局部变量 | 成员变量 |
基本数据类型 | 变量名和值都存放在栈中 | 变量名和值都随实例对象存在在堆中 |
引用数据类型 | 变量名存放在栈中,值存放在堆中 | 变量名和值都存在在堆中 |
这里需要注意一个细节,当JVM只是加载class,并没有实例化对象的时候,此时对象是不存在的。
局部变量是在方法调用的时候创建故而此时不存在,而成员变量(静态、非静态)此时是随Class文件加载到方法区的。且在准备阶段为静态成员变量进行默认初始化,在初始化阶段为静态变量成员进行显示初始化(从而说明了静态成员变量及其值是存储在方法区中的)。
被final修饰的常量如果有初始值,那么在编译阶段就会将初始值存入constantValue属性(class文件的常量池)中,在准备阶段就将constantValue的值赋给该字段。更多内容参考如下示意图: