JVM系列之内存结构
JVM区域划分
程序计数器、Java 堆、Java 虚拟机栈、元数据区和本地方法栈
JVM内存结构区域划分.png
程序计数器
程序计数器是当前线程所执行的字节码的行号指示器,它会指出下一条将要执行的指令的地址,字节码解释器就是通过改变计数器的值来选取程序接下来执行的操作;
程序计数器是线程私有的一小块内存,每条线程都要有一个独立的程序计数器,以使线程切换后恢复到正确的执行位置;
- 执行的如果是是Native方法,则为空
- 线程正在执行 Java 方法,则计数器记录的是正在执行的虚拟机字节码指令的地址
它也是唯一一个不会出现 OutOfMemoryError 的内存区域
本地方法栈
本地方法栈类似于虚拟机栈,只不过本地方法栈使用的是本地方法,为虚拟机执行 Native 方法提供服务;
可能会抛出 StackOverflowError 和 OutOfMemoryError 异常
Java虚拟机栈
和程序计数器一样,Java 虚拟机栈也是线程私有的,在线程创建时 Java 栈会被创建,每个方法在执行的同时都会创建一个栈帧,用于存放局部变量表,操作数栈,动态链接,方法出口等信息;
每一个方法从调用直至执行完成,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程,入栈出栈的方式是先进后出
;
- 局部变量表:
是存放方法参数和局部变量的区域,存放了各种基本数据类型( 8 种),对象引用(reference 类型) 和 returnAddress 类型,局部变量表所需的空间在编译期就已经确定并完成分配,在方法运行期间不会被改变 - 操作栈:
是个初始状态为空的桶式结构栈 - 动态链接:
每个栈帧中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态连接 - 方法出口(两种退出情况):
① 正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等;
② 异常退出
- StackOverflowError:如果线程中的计算需要比允许的更大的 Java 虚拟机堆栈,则 Java 虚拟机会抛出一个
- OutOfMemoryError:如果 Java 虚拟机堆栈可以动态扩展,并且尝试扩展,但无法提供足够的内存来实现扩展,或者如果无法提供足够的内存来为新线程创建初始 Java 虚拟机堆栈,则 Java 虚拟机会抛出一个
Java堆
Java 堆是是虚拟机中最主要的内存区域,几乎所有的对象实例都存储在 Java 堆中,在虚拟机启动时创建,所有线程共享;
GC回收
GC分区.png
- JAVA对象优先在Eden区分配,当Eden区没有足够的空间时触发一次Minor GC ,触发Minor GC时,Eden和from区中的存活对象会被复制到to区,然后from和to交换指针,以保证下次Minor GC时,to区还是空的,如果survival区无法容纳的对象将通过分配担保机制直接进入老年区
- 分配担保机制可以通过HandlePromotionFailure配置,如果不允许的话,则直接发生FULL GC
- 新生代(Young Generation)的最大大小将根据总堆的最大大小和NewRatio参数的值来计算。参数的“不受限制”默认值MaxNewSize意味着计算值不受限制,MaxNewSize除非MaxNewSize在命令行中指定了值
- 一般情况下,不允许-XX:Newratio值小于1,即Old要比Young大
- 大对象直接进入老年区的判断是根据PretenureSizeThreshold设置的阈值,所谓大对象时指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组
6.发生full GC的条件
- 调用System.gc时,系统建议执行Full GC,但是不必然执行
- 老年代空间不足
- 方法去空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
- 对象存活判断
- 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题
- 可达性分析:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象
- GC Roots对象包括
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
- 已启动且未停止的java线程
- TLAB(Thread Local Allocation Buffer)
线程本地分配缓存区,这是一个线程专用的内存分配区域,可以使用参数 -XX:+UseTLAB,默认开启,这个是用于解决多线程竞争堆内存分配问题,核心原理是每个线程可以向JAVA虚拟机申请一段连续的内存,作为线程私有的TLAB,这个操作需要加锁
注意:
老年代:2/3的堆空间
年轻代:1/3的堆空间
eden区:8/10 的年轻代
survivor0: 1/10 的年轻代
survivor1:1/10的年轻代
GC参数
参数 | 默认值 | 作用 |
---|---|---|
MinHeapFreeRatio | 40 | GC后,如果发现空闲堆内存小于整个预估堆内存的40%,则放大堆内存的预估最大值,但不超过固定最大值 |
MaxHeapFreeRatio | 70 | GC后,如果发现空闲堆内存占到整个预估堆内存的70%,则收缩堆内存预估最大值 |
Xms | 物理内存的1/64(<1GB) | 初始堆大小 |
Xmx | 物理内存的1/4(<1GB) | 最大堆大小 |
NewRatio | 2 | 年轻代(包括Eden和两个Survivor区)与年老代的比值 |
NewSize | 1310M | 设置年轻代大小 |
MaxNewSize | 不限 | 设置年轻代大小最大值 |
SurvivorRatio | 8 | Eden区与Survivor区的大小比值 |
MaxTenuringThreshold | 15 | 垃圾最大年龄 |
PretenureSizeThreshold | 0 | 超过这个值直接在old区分配,默认值是0,意思是不管多大都是先在eden中分配 |
元数据区
类元信息(Klass Metaspace)就是用来存类元(klass)的,类元(klass)是class文件在jvm里的运行时数据结构,不过有点要提的是我们看到的类似A.class其实是存在heap里的,是java.lang.Class的一个对象实例。这块内存是紧接着Heap的,和我们之前的perm一样,这块内存大小可通过-XX:CompressedClassSpaceSize参数来控制,这个参数前面提到了默认是1G,但是这块内存也可以没有,假如没有开启压缩指针就不会有这块内存,这种情况下klass都会存在NoKlass Metaspace里,另外如果我们把-Xmx设置大于32G的话,其实也是没有这块内存的,因为会这么大内存会关闭压缩指针开关。还有就是这块内存最多只会存在一块。
非类元信息(NoKlass Metaspace)专门来存类元(klass)相关的其他的内容,比如方法区,常量池等,这块内存是由多块内存组合起来的,所以可以认为是不连续的内存块组成的。这块内存是必须的,虽然叫做非类元信息,但是也其实可以存类元的内容,上面已经提到了对应场景。
类元信息(Klass Metaspace)和非类元信息(NoKlass Mestaspace)都是所有类加载器共享的,所以类加载器们要分配内存,但是每个类加载器都有一个空间管理(SpaceManager),来管理属于这个类加载的内存小块。如果类元信息(Klass Metaspace)用完了,就会OOM异常,不过一般情况下不会,非类元信息(NoKlass Metaspace)是由一块块内存慢慢组合起来的,在没有达到限制条件的情况下,会不断加长这条链,让它可以持续工作。
元空间的特点
- 充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致。
- 每个加载器有专门的存储空间
- 只进行线性分配
- 不会单独回收某个类
- 省掉了GC扫描及压缩的时间
- 元空间里的对象的位置是固定的
- 如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉
元空间的内存分配模型
- 绝大多数的类元数据的空间都从本地内存中分配
- 用来描述类元数据的类(klasses)也被删除了
- 分元数据分配了多个虚拟内存空间
- 给每个类加载器分配一个内存块的列表。块的大小取决于类加载器的类型; sun/反射/代理对应的类加载器的块会小一些
- 归还内存块,释放内存块列表
- 一旦元空间的数据被清空了,虚拟内存的空间会被回收掉
- 减少碎片的策略
方法区
方法区与 Java 堆一样,为线程共享。用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。也叫作 Non-Heap(非堆)。
如果方法区无法满足内存分配需求,会抛出 OutOfMemoryError 异常
运行时常量池
运行时常量池时方法区中的一部分,用于存放编译期生成的各种字面量和符号引用,并不是只有编译期才能产生常量,运行期间也有可能将新的常量放入常量池,因此也会有可能抛出OutOfMemoryError异常
直接内存
直接内存并不由 JVM 管理,它是利用 Native 函数库在 Java 堆外申请分配的内存区域,可以避免在 Java 堆和 Native 堆中复制数据以提高性能