根据JVM 规范,jvm内存划分为下面的几个区域。
1.方法区(Method Area)
2.堆区(Heap)
3.虚拟机栈(VM Stack)
4.本地方法栈(Native Method Stack)
5.程序计数器(Program Counter Register)
1.方法区(Method Area)
方法区主要用于存储JVM 加载的类信息(类的版本、字段、方法、接口),常量,静态变量,还有 即时编译器JIT(Just in Time)编译后的的代码,以及动态代理等生产的字节码。
方法区是一种逻辑概念上的区域,并没有规定这个区域在何处,所以JDK版本不一样,实现也不一样。比如在 JDK1.7中实现是 永久代(PermGen)、JDK1.8实现叫做 元空间(Metaspace) 。
1.8 相对于 1.7 的变化
移除了永久代(PermGen),替换为元空间(Metaspace);
永久代中的 类信息 转移到了 本地内存,不在是 虚拟机中;
永久代的字面量(interned Strings) 和 类的静态变量(class static variables) 转移到了java 堆中;
-
方法区运行时大小 设置参数 由 永久代参数 PermSize MaxPermSize 改为 元空间参数 MetaspaceSize MaxMetaspaceSize;
1.7的PermSize 和 1.8的MetaSpaceSize 参数,设置的不是初始化的空间大小,而是 方法区占用的内存达到该值,就会触发垃圾回收进行类卸载,gc 结束后,如果方法区中,不能分配内存,同样会抛出 OutOfMemoryError。
JVM 1.8 配置建议:
MetaspaceSize和 MaxMetaspaceSize设置一样大;
具体设置多大,建议稳定运行一段时间后通过jstat -gc pid确认且这个值大一些,对于大部分项目256m即可
2.堆区 ( Heap )
堆是被所有线程共享的一片内存区域,在虚拟机启动的时候创建,主要存储的是对象,当然也包括数组, 堆的大小可以用 -Xms -Xmx 指定。程序运行时生产的对象,都会在堆区生成,所以堆内存是非常活跃的、gc 也是非常频繁的。为了提升gc的效率,区分堆中的活跃对象与非活跃对象,堆内存是分代存储的。
堆内存分为 年轻代 和 老年代,默认的比例是 3:1( 可以用 -XX:NewSize=1G 设置)。
年轻代:年轻代内部又分为 Eden区 和 2块 surivor区。默认比例是 8:1:1(可以用 -SurvivorRatio=8 设置)。新创建的对象一般都会放到Eden,当eden 空闲的大小,不足以分配新的对象的时候,就会触发新生代GC,把年轻代和 一个Surivor 区的存活对象,复制到另外一个Surivor区,然后清空,所以两块Surivor区,都会有一个为空。
tips:
为了提升对象分配的效率, 对象分配内存使用 TLAB (Thread Local Allocation Buffer), 线程本地分配缓存区。
若虚拟机设置了参数 -XX:UseTLAB,在线程初始化的时候,也会申请一块指定大小的内存,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。
TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1% (可以用-XX:TLABWasteTargetPercent 设置TLAB空间所占用Eden空间的百分比)。
当新生代发生垃圾回收通常称为 Minor GC。一般采用 复制算法,性能比较好,STW时间比较短。
老年代:主要存放历经多次年轻代GC 的对象,和 占用内存比较大的对象 (大对象的阈值 可以用PretenureSizeThreshold 参数设置,注意此参数只生效与 Serial/ParNew 年轻代回收器)。
当老年代的发生垃圾回收通常称为 Major GC,一般也会同时触发年轻代GC,所以也称为 Full GC。一般情况下,老年代GC使用标记清理、标记压缩 算法,存活的对象比较多,所以GC 的停顿时间比 年轻代GC要大的的多,所以生产服务器 Full GC比较多的话,比较影响用户的体验。
3.虚拟机栈 ( VM Stack)
虚拟机栈也是在操作系统内存里分配的区域,JVM进程下,每创建一个线程,都会对应一个虚拟机栈。所以它是线程私有的,生命周期也和线程一样。JAVA1.8 默认线程栈是1M,可以通过-Xss256K 设置大小。
虚拟机栈内部是一个个的栈帧(Stack Frame),线程执行中调用的一个方法,就会创建一个栈帧并入栈,当方法执行结束,栈帧就会出站,栈顶的栈帧代表当前正常执行的方法。
栈帧的内部分为 局部变量表、操作数栈、动态连接、返回地址等信息。
局部变量表(Local Variable Table):主要包含方法参数 和 局部变量。在Java 代码编译为Class 文件上的时候,就在方法上标注 max_locals 数据,明确了该方法执行时候局部变量表所需要的分配最大容量;
操作数栈(Operand Stack):也称作操作栈,代码的运行计算主要基于操作数栈,在方法的执行过程中,会有各种的指令和计算结果进行出栈和入栈。
动态连接 (Dynamic Linking) :每个栈帧都包含一个指向运行时常量池中所属方法的引用,这个引用是为了支持方法调用过程中的动态连接。
Class 文件存放了大量的符号引用,字节码中的方法调用指令,就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类解析阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
返回地址(Return Address): 一个方法执行的时候,只有两种方法退出。方法退出实际上就是将栈帧从栈里弹出,恢复上一层的栈帧的本地变量表和操作数栈,并将并将返回值压入上一栈帧的操作数栈。
第一种是执行中遇到返回的字节码指令(return), 这个时候会将返回值传递给上层的调用者,这种方式也称为正常完成出口。
第二种是在执行的过程中遇到了异常,且这个异常在当前方法体内没有得到处理,就会导致方法退出,这种情况是不会将返回值传递给它的上层调用者的,这种方式也叫做异常完成出口。
无论采用哪种方式退出,在方法退出之后,都需要返回到被调用的位置,程序才能继续执行。一般来说,正常退出时返回调用者的pc计数器的值,而异常退出时,返回地址是需要异常处理器确定的。
4.本地方法栈 (Native Method Stack)
本地方法栈用于支持native方法的执行,它也是每个线程独享的,存储了每个native方法的执行状态。
本地方法栈和虚拟机栈他们的运行机制一致,唯一的区别是,虚拟机栈执行Java方法,本地方法栈执行native方法。在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将虚拟机栈和本地方法栈一起使用。
5.程序计数器 (Program Counter Register)
程序计数器是线程执行的字节码的行号,所以占用的内存非常小,基本可以忽略不计。
线程之间是隔离的。
如果程序当前正在执行的是一个java方法,则程序计数器记录的是正在执行的虚拟机字节码指令地址。
如果执行的是native方法,则计数器的值为空,原因是native方法是java通过jni调用本地C/C++库来实现,非java字节码实现,所以无法统计行号。
程序计数器是java虚拟机规范中唯一一个没有规定任何OutofMemeryError的区域。
6.其它
还有一个区域是不属于JVM的,叫做堆外内存,可以通过 NIO中的 allocationDirect () API和 Unsafe包来操作堆外内存。 可以直接在堆外分配空间,然后通过 DirectByteBuffer引用可以直接操作。
堆外内存是不属于jvm管理的,但是堆外内存是当前jvm进程的空间,而不是有的网络上所说的内核空间,内核地址是供操作系统的地址空间,不会给应用的,他的地址空间与jni的空间应该是一致的。
堆外内存优势:
1 减少了垃圾回收的工作,因为使用堆外内存减少了JVM的堆内存的占用;
2 加快了copy的速度。因为Java的io(file, socket)的操作都需要堆外内存和JVM内存进行相互copy,而堆外内存跳过了这一次内存复制;
堆外内存劣势:
1 堆外内存难以控制,如果内存泄漏,那么很难排查;
2 堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合;
可以使用-XX:MaxDirectMemorySize 限制最大的堆外内存,堆外内存也会溢出,并且其垃圾回收依赖于代码显式调用System.gc()。(JVM参数:-XX:+DisableExplicitGC,禁止代码中显式调用System.gc())。