Java内存模型(Java Memory Model ,JMM)
JMM是jvm的一种规范,定义了jvm的内存模型。它屏蔽了各种硬件和 *** 作系统的访问差异,不像c那样直接访问硬件内存,相对安全很多,它的主要目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。可以保证并发编程场景中的原子性、可见性和有序性。保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
Java数据区域分为五大数据区域。这些区域各有各的用途,创建及销毁时间:
1.2 、程序计数器程序计数器是一块很小的内存空间,它是线程私有的,记录当前线程执行到代码多少行
之所以需要程序计数器,是因为对于一个处理器(如果是多核cpu,那对应每一核),在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。
如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域
1.3、Heap(堆)Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。当前主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)
所有对象实例及数组都要在堆上分配内存,(但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对) 堆是java虚拟机管理内存最大的一块内存区域,堆存放的对象是线程共享的。
1.3.1. java堆空间分配比列即时编译器:可以把把Java的字节码,包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序)
逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者其其它线程所引用。
在 Java中,堆被划分成新生代 ( Young )、老年代 ( Old )。
新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。
堆空间默认占比为(可以通过参数调整):
堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。
其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即:Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
1.3.2 对象分配规则对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC
1.4、方法区方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。
用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。
运行时常量池
是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。
在jdk1.6及以前常量池分配在永久代中。可通过 -XX:PermSize和-XX:MaxPermSize限制方法区大小,从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。
永久代是Hotspot虚拟机特有的概念,是方法区的一种实现,别的JVM都没有这个东西。在Java 8中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间(metaspace)
1.5 、Java栈(虚拟机栈)Java栈(虚拟机栈)和计数器一样为线程私有。每个方法被执行的时候都会创建一个栈帧用于存储局部变量表, *** 作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。【栈先进后出】
平时说的栈一般指局部变量表部分:
局部变量表:一片连续的内存空间,用来存放方法参数,以及方法内定义的局部变量,存放着编译期间已知的数据类型(八大基本类型和对象引用(reference类型),returnAddress类型。
它的最小的局部变量表空间单位为Slot,虚拟机没有指明Slot的大小,但在jvm中,long和double类型数据明确规定为64位,这两个类型占2个Slot,其它基本类型固定占用1个Slot。
reference类型:与基本类型不同的是它不等同本身,即使是String,内部也是char数组组成,它可能是指向一个对象起始位置指针,也可能指向一个代表对象的句柄或其他与该对象有关的位置。
returnAddress类型:指向一条字节码指令的地址【深入理解Java虚拟机】怎么理解returnAddress
需要注意的是,局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。
1.6、Java栈(虚拟机栈)Java虚拟机栈可能出现两种类型的异常:
线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。
虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。
本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。
1.7、Java对象的构成Java对象在内存中由3部分组成:
1.7.1. 对象头【markword】在32位系统下,对象头8字节,64位则是16个字节【未开启压缩指针,开启后12字节】
在32位系统下和64位系统下对象头构成会略有差异,以下是64位系统对象头的构成:
Java锁的状态,会记录在在Java对象头中。锁的状态分为四种无锁状态、偏向锁、轻量级锁和重量级锁。不同锁状态锁标志位不同。
如果需要理解锁升级,Synchronized底层原理,Java对象头构成是我们需要掌握的必备知识。(后面我会专门写文章介绍Java锁等原理知识。“关注我”)
1.7.2. 实列数据存放对象程序中各种类型的字段类型,不管是从父类中继承下来的还是在子类中定义的。
分配策略:相同宽度的字段总是放在一起,比如double和long
1.7.3. 对象填充这部分没有特殊的含义,仅仅起到占位符的作用满足JVM要求。
由于HotSpot规定对象的大小必须是8的整数倍,对象头刚好是整数倍,如果实例数据不是的话,就需要占位符对齐填充。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)