JVM从零开始:JVM运行时内存区结构(四)

原文地址:https://www.sudo.ren/article/83

终于到了JVM内存管理这一章了,相信很多人都望而却步,的确如此,这一章也是我的痛,以前看过两遍关于JVM内存分配,但零零散散的笔记总是把我的大脑弄的七零八落,实在是别无他法了,因为对JVM没有很好的了解,不能整理去一套较全面的笔记。所以这一次我决定一次性搞定所有关于JVM,于是我狠心重新阅读两遍关于JVM的书,然后把所有笔记整理并分享出来。废话不多说了,往下看吧。

我们都知道,JVM具有自动内存管理机制,与C/C++相比,使用Java编程并不需要显示为每个对象进行内存分配和内存回收操作,使得Java开发人员可以从繁琐的体力劳动中解放出来,只需关注自身业务即可,并且从某种意义上来说还降低了内存泄漏和内存溢出的风险。但是作为程序员,我们也不能盲目的由着JVM自动内存分配,如果真的需要我们去进行性能调优呢?是不是就一脸懵了!弄懂JVM还是很有必要的,JVM内部定义了多个程序在运行时需要使用到的内存区:


JVM中的内存区可以根据受访权限的不同定义为线程共享线程私有两大类。

  • 线程共享:就是指可以允许被所有线程共享访问,包括堆区、方法区、运行时常量池。

  • 线程私有:指不允许被所有线程共享访问的,只允许被所属的独立线程进行访问,包括PC寄存器、Java栈及本地方法栈。

 

线程共享内存区

 

Java堆区(Heap)Java堆区在JVM启动的时候被创建,其实际内存空间中可以是不连续的。Java堆区是一块用于存储对象实例的内存区,同时也是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域,也让GC极有可能会在大内存的使用和回收上成为性能瓶颈。

这里不得不说TaoBaoVM,为了解决以上可能的性能瓶颈,基于OpenJDK深度定制的TaoBaoVM,其创新的GCIH(GC invisible heap)技术实现了off-heap,将生命周期较长的Java对象从heap中移至heap之外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的,除此之外逃逸分析与栈上分配等优化技术同样也是降低GC回收频率和提升GC回收效率的方式,这样一来,Java堆区将不再是Java对象内存分配的唯一选择。

 堆内存中的对象可以分为两类:一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速,另一类对象就是生命周期非常长,在某些极端的情况下还能与JVM的生命周期保持一致。对不同生命周期的对象采用不同垃圾收集策略,由此诞生分代收集。几乎所有的GC都是分代收集算法,因此Java堆区还可以细分为新生代(YoungGen)和老年代(OldGen),其中YoungGen又可以划分为Eden空间、From Survivor空间和To Survivor空间,如下图:

参数设置:

  • -Xms:设置在JVM启动时的堆内存初始大小;

  • -Xmx:设置堆区允许的最大内存

如果堆区内存超过-Xmx设置大小,则抛出OutOfMemoryError异常。

 

方法区(Method Area)存储每一个Java类的结构信息,比如:运行时常量池、字段、方法数据、构造函数、普通方法的字节码内容、实例、接口初始化所需的特殊方法等数据。方法区仅仅只是逻辑上的独立,实际上还是包含在Java堆区内,也就是说,方法区在物理上还是属于Java堆区的一部分。方法区在JVM启动时被创建,并且它的实际内存和堆区一样都是不连续的。方法区是一块比较特殊的运行时内存区,被很多程序员称之为永久代(PermanentGen),主要因为方法区并不会想Heap那样频繁被GC执行回收,甚至还可以显示地指定是否需要在程序运行时回收方法区的数据,如果没有显式要求内存回收,GC的回收目标仅针对方法区中的常量池和类型卸载。

参数设置:

  • -XX:Max-PermSize :设置指定的最大内存进行动态扩展。

如果超出设置内存,将会抛出OutOfMemoryError异常。

 

运行时常量池(Runtime Constant Pool):运行时常量池属于方法区中的一部分,一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),那么运行时常量池就是字节码文件中常量池表的运行时表示形式。运行时常量池中包含多种不同的常量,比如编译期就已经明确的数值字面量运行期解析后才能够获得的方法或者字段引用。运行时常量池类似于传统编程语言中的符号表(symbol table),但是它所包含的数据却比符号表要更加丰富一些。

当类装载器成功将一个类或者接口装载进JVM后,就会创建与之对应的运行时常量池。在此大家需要注意,由于每一个运行时常量池所分配的内存来源于方法区,一旦所需要的内存大小超过方法区所能够提供的最大值时,运行时常量池同样也会抛出OutOfMemoryError异常。

 

 

线程私有内存区

 

PC寄存器(Program Counter Register,程序计数器):JVM是基于栈的架构,任何操作都需要经过入栈和出栈来完成。PC寄存器是对物理PC寄存器的一种抽象模拟,生命周期与线程保持一致。当线程所执行的是Java方法,则PC寄存器就会存储正在执行的字节码指令地址;如果是native方法,PC寄存器的值就是空(undefined)。

PC寄存器为什么被设定为线程私有?

我们都知道所谓多线程就是在一个特定的时间内只会执行其中某一个线程的方法,CPU会不定的做任务切换,那么为了记录每一个线程正在执行的当前字节码指令地址,最好的办法就是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,不会出现相互干扰。

JVM的字节码解释器通过改变PC寄存器的值来明确下一条应该执行什么字节码指令。PC寄存器是JVM内存区中唯一一个不会抛出OutOfMemoryError异常。

 

Java栈(JVM Stack):生命周期与线程一致。Java栈用于存储栈帧(Stack Frame),而栈帧中所存储的就是局部变量表、操作数栈、方法出口等信息。其中局部变量表中又存储了各类原始数据类型、对象引用(refreence)以及returnAddress类型。returnAddress类型被定义为JVM内部的原始类型,该类型用于表示一条字节码指定的操作码(opcode)。但returnAddress类型在Java语言中并不存在对应的类型,自然也就无法在运行时更改returnAddress类型的值。尽管开发人员无法在程序中直接使用returnAddress类型,但在Java7之前,该类型却被用于finally字句的实现。

Java栈允许被实现成固定大小的内存或者是可动态扩展的内存大小。

SOF:当线程调用一个方法时JVM会压入一个新的栈帧到这个线程的栈空间中,当方法执行结束时栈帧会出栈,但是在方法执行过程中,这个栈帧会一直存在的。所以如果方法的嵌套调用层次太多,随着栈帧的增加导致总和大于JVM设置的-Xss值(请求栈的深度不足时)就会抛出StackOverFlowError。

OOM:另外,由于每个线程占的内存大概为1M,因此线程的创建也需要内存空间。操作系统可用内存-Xmx-MaxPermSize即是栈可用的内存,如果申请创建的线程比较多超过剩余内存的时候,也会抛出如下类似错误:java.lang.OutofMemoryError: unable to create new native thread

参数设置:

  • -Xmx-MaxPermSize:设置栈可用的内存;

  • -Xss: 每个线程的堆栈大小,JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.
    在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

 

本地方法栈(Native Method Stack):用于支持本地方法(native方法,比如使用C/C++编写的方法)的执行,和Java栈的作用类似,允许被实现成固定或者动态可扩展的内存大小,并且本地方法栈同样会抛出StackOverflowError和OutOfMemoryError异常。


评论区