JVM虚拟机在执行Java程序过程中,会把所管理的内存划分为几个不同的数据区域(程序计数器、虚拟机堆、虚拟机栈、方法去、本地方法栈),统称为运行时数据区域。下图是JDK1.7及以前版本的图解:

Java内存区域概况(一)

程序计数器(Program Counter Register

当前线程执行代码的行号指示器。字节码解释器通过改变程序计数器的值选取下一条要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机多线程是通过随机切换线程分配处理器执行时间的方式实现的,所以在任一确定时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。为了切换后能正确恢复到原本的执行位置,因此每条线程都需要独立的程序计数器,所以这类区域为“线程私有”的内存。

 

Ps:此区域是唯一在Java虚拟机规范中没有规定任何OutOfMemoryError的区域。

注意

1)如果当前线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。

2)如果当前线程执行的是Native方法,这个计数器则为空

问题:Java多线程执行Native方法时程序计数器为空,那么线程切换后如何找到之前执行到哪里了?

这里的“程序计数器”是在抽象的JVM层面上的概念——当执行Java方法时,这个抽象的“程序计数器”存的是Java字节码的地址。实现上可能有两种形式,一种是相对该方法字节码开始处的偏移量,叫做bytecode index,简称bci;另一种是该Java字节码指令在内存里的地址,叫做bytecode pointer,简称bcp。对Native方法而言,它的方法体并不是由Java字节码构成的,自然无法应用上述的“Java字节码地址”的概念。所以JVM规范规定,如果当前执行的方法是Native的,那么PC寄存器的值"未定义"——是什么值都可以。

虚拟机栈(Java Virtual Machine Stacks)

与程序计数器一样,Java虚拟机栈也是“线程私有”的,生命周期同线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表操作数栈动态链接方法出口等信息。每个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。程序员说的栈内存,一般指虚拟机栈,或者说是虚拟机栈中的局部变量表局部变量表存放了编译器可知的各种基本数据类型(8种基本数据类型)、对象引用(reference类型,指向堆中对象起始地址的引用指针,也可能是指向代表对象的句柄或对象想关的位置),returnAddress类型基本废用。

       其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余基本类型只占用1个局部变量表所需内存空间在编译期完成分配。当进入一个方法时,这个方法需要在帧中分配多大的变量空间是完全确定的,在方法运行期间不会改变局部变量表大小

 

Ps:在Java虚拟机规范中,对这个区域规定了两种异常状况:

1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。

2)如果虚拟机栈可以动态扩展,但是扩展时无法申请到足够内存,会抛OutOfMemoryError异常。

 

对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。

堆(Java Heap)

堆是Java虚拟机所管理的内存中最大的一块。也是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例。几乎所有的对象实例都在这里分配内存。在Java虚拟机规范中描述是:所有的对象以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟(可能导致部分对象可在栈上分配,而随栈帧出栈随即回收内存),栈上分配对象成为了可能性,所有对象都分配在堆上也不是那么“绝对”了。

    Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC”堆(Garbage Collected Heap)。现在内存回收基本都采用分代收集算法,所以Java可以细分为:新生代(三分之一堆空间)和老年代(三分之二),再细分新生代可分为:Eden、From Survivor、To Survivor,比例为8:1:1。根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的空间中,只要逻辑连续即可。

 

PS:如果在堆中没有内存完成实例分配,并且堆无法扩展时,将会抛出OutOfMemoryError异常。

注意JDK1.7后,字符串常量池从方法区(永久代)中剥离出来,存放在堆中

方法区(Method Area)

与Java堆一样,是各个线程共享的区域,它用于存储已经被虚拟机加载类信息常量、静态变量、即时编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap非堆),目的是为了与堆区分开。对于习惯在HotSpot虚拟机上开发、部署的开发者来说,很多人把方法区称为“永久代(Permanent Generation)”。本质上,两者不等价。仅仅是因为HotSpot虚拟机的设计团队把GC分代收集扩展至方法区,或者说用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。对于其他虚拟机(JRockit、J9)等,不存在永久代概念。由于使用“永久代”来实现方法区可能导致内存溢出,所以HotSpot虚拟机官方发布的信息,现在也已放弃永久代,改为采用Native Memory来实现方法区的规划。现目前发布的1.7的HotSpot虚拟机中,已经把原本放在永久代(方法区)的字符串常量池移出

JDK1.8和JDK1.7的区别:

在JDK1.8中,永久代已经不存在w,存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。下图是JDK1.8的图解:

Java内存区域概况(一)

 

Ps:方法区可选择不进行垃圾收集,垃圾收集在该区域也较少出现,但并非数据进入了方法区就不会被回收,该区域回收主要针对常量池回收和类型的卸载。回收的可能性是存在的。当方法区无法满足内存分配是将抛出OutOfMemoryError异常。

运行时常量池(Runtime Constant Pool)

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

符号引用包括:1.类的全限定名 2.字段名和属性 3.方法名和属性

字符串常量池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到String Pool中(String Pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。在HotSpot虚拟机里实现的String Pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot虚拟机的实例只有一份,被所有的类共享

 

1.字符串池常量池在每个虚拟机中只有一份,存放的是字符串常量的引用值,存放在堆中。

2Class文件常量池是在编译的时候每个Class都有的,在编译阶段,存放的是常量的符号引用。

  1. 运行时常量池是在类加载完成之后,将每个Class常量池中的符号引用值转存到运行时常量池中,也就是说,每个Class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

 

Ps:因为是方法区一部分,所以会抛出OutOfMemoryError。

本地方法栈(Native Method Stack)

与虚拟机栈所发挥的作用相同,区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机执行Native方法服务。Java虚拟机规范并未强制规定其具体实现,如HotSpot虚拟机把虚拟机栈和本地方法栈合二为一。

 

Ps:本地方法栈和虚拟机栈一样会抛出StackOverflowError异常。

直接内存

并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域。

在JDK1.4中新加入了NIO(New Input/OutPut)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式。

JVM内存的参数配置与调优

如何通过参数来控制各区域的内存大小

Java内存区域概况(一)

堆内存的接口和内存限制参数:

Java内存区域概况(一)

 

-Xms设置堆的最小空间大小。

-Xmx设置堆的最大空间大小。

-XX:NewSize设置新生代最小空间大小。

-XX:MaxNewSize设置新生代最大空间大小。

-XX:PermSize设置永久代最小空间大小。

-XX:MaxPermSize设置永久代最大空间大小。

-Xss设置每个线程的堆栈大小。

JDK1.8之后JVM没有方法区概念,所以没有PermSize参数设置取而代之是Matespace,参数设置为

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不> 超过MaxMetaspaceSize时,适当提高该值。

-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集

-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。

老年代空间大小=堆空间大小-年轻代大空间大小

 

相关文章:

  • 2021-12-12
  • 2021-09-27
猜你喜欢
  • 2021-09-09
  • 2021-08-26
  • 2021-08-26
  • 2021-08-10
  • 2022-12-23
  • 2021-09-26
相关资源
相似解决方案