本文已参与「新人创作礼」活动,一起开启掘金创作之路
jvm内存结构
方法区和堆是所有线程共享的内存区域;而java栈、本地方法栈和程序计数器是运行是线程私有的内存区域。
- Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
- 方法区(Method Area),方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量等数据。
-
- 在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代。
-
- 在JDK1.7字符串常量池和静态变量被从方法区拿到了堆中,运行时常量池剩下的还在方法区, 也就是hotspot中的永久代。
-
- 在JDK8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆,运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间(Metaspace,本地内存),类的元数据保存在本地内存中,元空间的最大可分配空间就是系统可用内存空间,可以避免永久代的内存溢出问题
区别:
方法区只是个概念,这两个是它的实现
Perm Space:FGC不会清理,大小启动的时候根据指令指定,虚拟机一旦启动不能改变,所以经常因为内存不够造成溢出
metaSpace:会触发FGC清理,不设定的话最大就是物理内存,可以避免内存溢出
\
- 程序计数器(Program Counter Register),执行Java程序的时候,会创建一个进程,JVM就是一个进程。一个进程由多个线程组成,在任何一个时候,Java虚拟机只能执行一条线程中的指令,Java虚拟机通过读取某一个线程中的程序计数器决定该线程该执行哪个基础功能,例如循环、读取数据库、异常恢复等。因此每个线程的程序计数器是相互独立,互不影响的。
-
- 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指向的地址;如果正在执行的是Native方法,这个计数器值为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
- JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。在执行方法的时候,会创建一个栈帧(一个方法就一个栈桢),用于存储局部变量表、操作数栈(操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式)、dynamic Linking(动态连接:指向运行时常量池中该栈帧所属的方法的引用)、return address(方法a调用方法b,方法b返回值存放的位置)等信息。
-
- 局部变量表 最基本的存储单元是Slot(变量槽) ,
- 局部变量表大小在代码编译期间就已经确定。
- 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
- 在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot
- 如果当前帧是由构造方法或者实例方法创建的(意思是当前帧所对应的方法是构造器方法或者是普通的实例方法),那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序排列。
- 静态方法中不能引用this,是因为静态方法所对应的栈帧当中的局部变量表中不存在this
- java栈也是线程私有。创建线程时同步创建java栈,线程结束,java栈也同时销毁,释放占用的内存。
- 本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
\
\
堆区详细介绍
- 堆区又分为新生代和老年代,其中新生代又分为一块(eden)区和两块(youngGC)区。
- 新new出来的对象往往会分配在eden区,一旦eden区被塞满就会触发一次youngGC(就是年轻代的垃圾回收),这时候就会将没有用的数据通过垃圾回收算法回收掉,再将存活的对象移动到Survivor区
- 之所以Survivor分为两部分,就是为了清除垃圾时能够做内存整理,防止内存碎片
- 每次触发youngGC的时候,对象都会在两个Survivor区交替整理,直到达到阀值对象进入老年区
对象分配规则
- 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC(youngGC)存活下来的放入Survivor1/2中交替。
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,直到达到阀值对象进入老年区。
- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存),尽量避免new一些会快速释放的大对象,不然频繁触发老年代的fullGC。
- 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
- 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
如何通过参数来控制个各个内存区域
参考此文章: jvm系列(二):JVM内存结构