在首先,须要注意的是,Java内存区域与Java内存模型是不一样的概念:html
ava虚拟机在运行程序时会把其自动管理的内存划分为区域,这些区域就被称为 Java内存区域。java
而Java内存模型(即Java Memory Model,简称JMM)自己是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,经过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。linux
而在Java内存区域与Java内存模型惟一一点类似之处,都存在共享数据区域和私有数据区域, 但二者的内存区域又不能等同,属于不一样层级的概念。算法
在查阅资料的时候,发现有人将这二者混为一谈,让我颇感困惑,特意在这里提醒一下。windows
而Java内存区域的概念比起JMM来讲要浅显易懂。数组
而Java内存区域则划分为: 方法区,堆(这二者是被全部线程共享的内存区域),虚拟机栈,本地方法栈,程序计数器。(这三者属于线程隔离区域)缓存
但对于不一样的虚拟机,实现可能有所区别。数据结构
在这里的程序计数器,做用与计算机中的程序计数器十分相似,不过处理的对象有所不一样,在计算机中,程序计数器指向的是下一条计算机指令所在的地址,而Java的程序计数器是一块内存空间,能够被看作是当前线程
所执行的字节码文件的行号指示器。并发
当所执行的方法为 Native时,这个程序计数器的值为空。jvm
对于每个线程,都须要维护其独有的程序计数器。
Java虚拟机栈也是线程所私有的,生命周期与线程相同。它描述的是Java方法执行的内存模型:在每一个方法执行的同时,都会建立一个栈帧,每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈出栈的过程。
通俗意义上讲,就是当一个方法被调用的时候,表明这个方法的栈帧入栈,当一个方法返回的时候,表明这个方法的栈帧出栈。
栈帧(stack frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操做数栈、动态链接和方法返回地址等信息。
若是线程所请求的栈深度大于虚拟机的最大栈深度,会抛出 StackOverflowError异常,而若是虚拟的栈自己能够动态扩展,在扩展时没法申请到足够内存,则抛出 OutOfMemoryError异常。
查看字节码的命令:
javap -verbose ClassName.class
局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件时,就在方法的Code属性max_locals数据项中肯定了该方法所须要分配的最大局部变量表的容量。
局部变量表中的空间基本单位是 slot, 对于32位以内的数据,用一个 slot 来存放,如 int,short 等;对于64位的数据用连续的两个 slot 来存放,如 long,double 等。引用类型的变量 JVM 并无规定其长度,它多是 32 位,也有多是 64 位的,因此既有可能占一个 slot,也有可能占两个 slot。而且须要注意到的是,在这里所提到的数据类型,仅仅是可以与java的数据类型类比,事实上依然分属不一样的概念。 JVM并不只仅只支持java语言,天然数据类型也不可能和java强相关。
虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,若是是实例方法(非static),那么局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中经过this访问。咱们知道,静态方法只须要和类关联便可,实例方法必须和对象相关联。
而之因此在实例方法中能够访问其余实例方法,是由于在实例方法中第一个参数为this,指向当前对象,天然能够调用其余方法,而静态方法没有 this 引用,没法给实例方法提供指向方法接收者的隐含参数,所以不能调用实例方法。
在局部变量表中存储参数(索引从零开始)的顺序为:
this(若是是实例方法)=> 参数(若是有的话)=> 定义的局部变量(若是有的话)。
同时,局部变量表中的Slot是可重用的,方法体中定义的变量,其做用域并不必定会覆盖整个方法体,若是当前字节码PC计数器的值已经超出了某个变量的做用域,那么这个变量对应的Slot就能够交给其余变量使用。这样的设计不只仅是为了节省栈空间,在某些状况下Slot的复用会直接影响到系统的垃圾收集行为。
而这一点参考:
操做数栈
在这以前,首先须要提到一个概念。
Java虚拟机的解释执行引擎被称为"基于栈的执行引擎"。
Java的一大特色是,一次编译,处处运行,而这里的编译并不是编译成机器码,而仅仅是指编译成字节码文件,在不一样的机器上都有相应的JVM执行这些字节码文件,以求在不一样的机器上会有相同的表现形式。
类装载器装载负责装载编译后的字节码,并加载到运行时数据区(Runtime Data Area),而后执行引擎执行会执行这些经过类装载器装载的字节码,被分配到JVM的运行时数据区的字节码会被执行引擎执行。
这里所说的基于栈执行是区别于基于寄存器执行的。
而这里的执行引擎在我理解来就是读取字节码,解释并执行相应代码的功能。
但这种解释不可避免的会致使效率低下,所以在Java中又使用了,即时编译(JIT),将热点代码编译成本地代码,放入本地缓存中,在使用时直接读取相应的本地代码去执行便可,使得速度很是快。当再也不是热点代码,则从缓存中移除,恢复成解释型执行。
在看过Java的执行引擎以后,再来看,对应的操做数栈。
和局部变量区同样,操做数栈也是被组织成一个以slot为单位的数组。可是和前者不一样的是,它不是经过索引来访问,而是经过标准的栈操做—压栈和出栈—来访问的。好比,若是某个指令把一个值压入到操做数栈中,稍后另外一个指令就能够弹出这个值来使用。
看下面这段代码,简单的将两个数相加:
begin iload_0 // push the int in local variable 0 onto the stack iload_1 // push the int in local variable 1 onto the stack iadd // pop two ints, add them, push result istore_2 // pop int, store into local variable 2 end
则是分别将操做数压入栈,这里从0开始,是由于静态方法的参数,正是从索引0开始的, 将索引0,1的位置数据分别压入栈,然后弹出两个数,相加以后,再度压入栈,存在索引2的位置。不难理解,在最终,操做数栈存有的即是方法自己的返回值。
动态链接
每一个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程当中的动态链接。咱们知道Class文件的常量池有存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期间转化为直接引用,这部分称为动态链接。
本地方法栈是区别于虚拟机方法栈的,其区别不过是虚拟机方法栈是为执行Java方法服务,而本地方法栈则是为 Native方法服务,在HotSpot中,则是采起将二者合二为一。
Java堆是被全部线程共享的一块内存区域,在虚拟机启动时建立。这个内存区域惟一的做用就是用来存放Java实例对象,几乎全部的实例对象都要在当前区域分配,数组是一种特殊类型的对象,也被分配在当前区域。
Java堆是垃圾收集器管理的主要区域,所以不少时候也被称为“GC堆”(Garbage Collected Heap)。
从内存回收的角度来看,因为如今收集器基本上都是采用分代收集算法回收内存,因此Java堆中还能够细分为:新生代和老年代;再细分一点的有:Eden空间,From Survivor空间,To Survivor空间等。
从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer ,TLAB),不管如何划分,都与存放内容无关,不管哪一个区域,存放的都仍然是对象实例。进一步划分是为了更好地回收内存,更快地分配内存。
根据Java虚拟机规范的规定,Java堆能够处于物理上不连续的内存空间中,只要逻辑上是连续便可,就像咱们的磁盘空间同样。当前主流的虚拟机都是按照可扩展来实现的,若是在堆中没有内存完成实例分配,而且堆也没法在扩展时,将会抛出OutOfMemoryError异常。
至于更详细的就不在这里多作介绍,放在下一篇博客进行详细介绍。
参考:
方法区被全部线程所共享,存储已经被虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据。
在HotSpot中用永久代的方式来实现方法区,而且将GC的分代收集扩展至方法区。至于其中缺点稍后再谈。
须要注意到的是:
方法区的大小没必要是固定的,jvm能够根据应用的须要动态调整。一样方法区也没必要是连续的。方法区能够在堆(甚至是虚拟机本身的堆)中分配。jvm能够容许用户和程序指定方法区的初始大小,最小和最大尺寸。
方法区一样存在垃圾收集,由于经过用户定义的类加载器能够动态扩展Java程序,一些类也会成为垃圾。jvm能够回收一个未被引用类所占的空间,以使方法区的空间最小。
接下来分别看一下,在方法区存储的这几种信息。
类信息
对于每一个已经被虚拟机加载的类,jvm在方法区中存储如下类型信息:
这个类型的完整有效名称(全限定名 包名+类名)
这个类型的直接父类的完整有效名称(Interface 或者 java.lang.Object 没有父类)
这个类型的修饰符(public abstract final )
这个类型直接接口的一个有序列表
类型的常量池( constant pool)
域(Field)信息
方法(Method)信息
除了常量外的全部静态(static)变量
常量池
而常量则是存储在常量池中,jvm为每个被虚拟机加载的类型维护一个常量池,常量池就是这个类型里用到的常量的一个有序集合:
包括了实际的常量(被final修饰的, 广泛意义上的常量),对类、属性、方法的符号引用。池中数据相似数组项,能够经过索引访问。
这个常量池,和运行时常量池是相同的概念,在运行时,当类被加载以后,相应内存才存放在运行时常量池中。
而就在这里,有一个让我以前混淆的概念问题,就是 import * 与import单个Java类的区别,我一直觉得,import * 会引入全部Java类的常量数据,相关的final信息,致使占用没必要要的内存,由于这样的缘由才致使须要引入特定类。
但事实显然并不是如此,import自己只能影响编译,对运行时无能为力,所以只会带来编译时的压力,而不会引发其余问题。
但仍是推崇引入单个Java类这种方式:
域信息
jvm在方法区中保存类的全部域的相关信息以及声明顺序。
域的相关信息:
域名
域的类型
域的修饰符
在Java中域是指:
field,域是一种属性,能够是一个类变量,一个对象变量,一个对象方法变量或者是一个函数的参数。
因此这个类自身的几乎全部信息都可以在方法区中找到。
方法信息:
jvm在方法区中保存类的全部方法的相关信息以及声明顺序方法的相关信息:
方法的返回值(或者void)
方法的参数列表
方法的修饰符
方法的字节码操做数栈和局部变量表大小
异常表
在这里主要了解一下,在Java堆中,一个对象是如何被分配,布局,访问,这整个过程的。
当使用一个方法,且在方法中定义一个变量时,这个变量的引用/变量,便会被存储在Java虚拟机栈中的 栈帧 中的 局部变量表中,Java栈是线程私有的,当变量指向一个 new 出来的对象时,会在共享区域,也就是方法区中的常量池中去查找相应类的符号引用,并检测类是否被加载。
而在类被加载以后,须要给新生对象分配相应的内存,若是在堆中,内存划分工整有序,被分为空闲的和已使用的内存,中间经过一个指针来进行分割,那么分配内存就是移动指针便可,而若是内存混乱无序,就须要维护相应的列表,记录内存的使用状况,前者被称为“指针碰撞”,后者被曾为“空闲列表”,而Java虚拟机究竟采起哪一种方式的关键因素之一则是: 垃圾收集器是否具备内存压缩功能。
而当内存已经指定,在划份内存区域的时候一样会遇到相应的并发情况,这时候就须要采起CAS的方式, 或是采起将分配动做按照线程划分不一样的区域,保证不会冲突。
而在内存分配完毕以后,虚拟机须要将分配到的空间初始化为零值(不包含对象头),这一操做保证了对象的实例字段在Java代码中即便不进行初始化也可以直接使用。
接下来就是须要设置对象头,如对象是哪一个类的实例(与多态息息相关),如何可以找到对象的元信息等其余信息。
而这时,以Java虚拟的角度来看,对象的建立已经完成。但从Java程序的角度来看,对象的建立才刚刚开始,进行相应的初始化操做等。
而一个Java对象分为三块区域:对象头,实例数据,对齐填充。
对象头主要分为两部分:其一是对象的运行时数据,如哈希码,GC分代年龄,锁状态,等其余信息。
而另外一部分则是类型指针,即对象指向它的类元数据的指针,虚拟机经过这个来肯定到底是哪一个类的实例,若是为数组对象,则还须要记录数组长度的数据。
而实例数据则是对象真正存储的有效信息。
对齐填充的真正目的在于由于在hotSpot内存管理中要求对象起始地址必须为8字节的整数倍。
在对象的访问中,主流存在两种方式:
句柄:在Java栈中存储的是对象的句柄,在对象的句柄中包含对象的实例数据,对象类型信息的地址。 对象的实例数据地址再指向堆中的实例数据。对象类型指针则是指向方法区中的对象类型信息。
直接指针:在栈中存储的就是对象地址,对象地址中包含对象的实例数据以及到对象类型数据的指针。
以上就是堆中对象的相关信息。
来源于:《深刻理解Java虚拟机》
-Xms -Xmx
用来分配和设置进程对堆内存的最小最大内存大小。
-Xmn
-Xmn用来设置堆内新生代的大小。经过这个值咱们也能够获得老生代的大小:-Xmx减去-Xmn,在JDK1.8之前,HotSpot中还要减去永久代的大小
-Xss
-Xss设置每一个线程可以使用的内存大小。在相同物理内存下,减少这个值能生成更多的线程。所以对于具备多个线程的应用而言,须要将这个值设置的尽可能小,以支持更多的线程。
而关于内存的限制,不得不提到的一个点是:
32位程序的寻址能力是2^32,也就是4G。对于32位程序只能申请到4G的内存。并且这4G内存中,在windows下有2G,linux下有1G是保留给内核态使用,用户态没法访问。故只能分配2G、3G的内存使用(对单个进程而言)。
参考:
Java8内存模型—永久代(PermGen)和元空间(Metaspace)
之因此要将永久代单独提出来, 是由于在 java8中完全移除了一个概念,永久代。而其缘由除了永久代自己的设计不合理因素以外, 则是Oracle可能要将 HotSpot 与 JRocket进行合并,所以这种特性上的,且设计并不是很合理的东西就被删除掉了。
那么永久代的概念是什么呢?
其实在以前已经提到过这个概念了,也就是方法区,不过有所不一样的是,方法区是Jvm的一种规范,而永久代则是方法区在HotSpot中的实现方式。而最初采起这种设计方式的目的,则是由于,在方法区一样须要实现类的卸载,运行时常量池的回收工做。(须要了解的是,字符串常量就被存储在永久代中。)
所以就将GC的分代收集扩展至方法区,使用永久代来实现方法区。
java.lang.OutOfMemoryError: PermGen space "这个异常正是因为永久代所致使的。最典型的场景就是,在 jsp 页面比较多的状况,容易出现永久代内存溢出。由于jsp页面在其所对应的servlet第一次被访问时,就会建立相应的class文件。而且加载到方法区中。
而原先的运行时常量则是存储到元空间。
字符串池里的内容是在类加载完成,通过验证,准备阶段以后在堆中生成字符串对象实例,而后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是咱们常说的用双引号括起来的)的引用(而不是驻留字符串实例自己),也就是说在堆中的某些字符串实例被这个StringTable引用以后就等同被赋予了”驻留字符串”的身份。这个StringTable在每一个HotSpot VM的实例只有一份,被全部的类共享。
对于字符串常量池:
而元空间:
元空间与永久代相似,存放的数据差异不大,最大的区别则在于存放的位置,元空间并不在虚拟机中,而是使用本地内存。
相关配置参数:
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:若是释放了大量的空间,就适当下降该值;若是释放了不多的空间,那么在不超过MaxMetaspaceSize时,适当提升该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的(仅取决于自身机器内存影响)。
除了上面两个指定大小的选项之外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC以后,最小的Metaspace剩余空间容量的百分比,减小为分配空间所致使的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC以后,最大的Metaspace剩余空间容量的百分比,减小为释放空间所致使的垃圾收集。
所以在无限加载String时, Java Heap Space
而加载大量Java类时: MetaSpace。
JVM内存区域相关的介绍,就到这里。