JVM内存模型详解与GC策略

JVM内存模型详解与GC策略

JVM内存模型总览

首先看一下JVM内存模型

程序计数器Program Counter Register

程序计数器是一块较小的内存区域,可以看做当前线程执行字节码的行号指示器;如果当前线程正在执行一个Java方法,这个计数器记录的正是虚拟机执行字节码指令的地址,如果执行的是native方法,这个计数器的值是空(Undefined),此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError的区域。
【注】当线程执行native方法时,则重新启动一个新的线程,那么新线程的计数器为空不影响当前线程的计数器,相互独立

虚拟机栈 VM Stack

Java代码执行的内存模型,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量,操作数栈,动态链接,方法接口等信息;局部变量表存储了编译器可知的各种基本数据类型,如byte,boolean,char,int,float,double,long和对象引用如 reference类型,returnAddres类型;线程请求的栈深度不够会抛出StackOverFlowError,栈动态扩容内存不足会抛出OOM错误。

本地方法栈

类似于虚拟机栈,只不过本地方法栈使用的是本地方法

堆 Heap

几乎所有的对象实例都存储于堆上
堆结构

  1. java对象优先分配于Eden区,当Eden区没有足够的内存时会触发一次Minor GC;触发Minor GC时,Eden和From区的活动对象会复制到To区,然后From和To交换指针以保证下次交换时To区是空的;如果Survival区无法容纳的对象通过分配担保机制直接进入到老年区。
  2. 分配担保机制可以通过HandlePromotionFailure配置,如果不允许的话直接触发FULL GC。
  3. 新生代的最大值将根据堆的最大值和newRatio参数值计算。
  4. 一般情况下,不允许-XX:NewRatio小于1,即Old要比yang大。
  5. 大对象直接进入老年代的判断依据是PretenureThreshold设置的阈值,所谓大对象是指需要大量连续存储空间的对象,最典型的大对象就是很长的字符串和数组。
  6. 发生GC的条件:
    • 调用System.gc时,系统建议指向FULL GC,但不必然执行
    • 老年代空间不足
    • 方法区空间不足
    • 通过Minor GC后进入老年代的平均大小大于老年代剩余的内存
    • 由Eden区、From区向To区复制时,对象大小大于To区的可用内存则直接把该对象复制到老年代且老年代的可用内存小于对象占用的内存。
  7. 对象存活判断
    • 引用计数:每个对象有一个引用计数属性,新增引用时计数加1,释放引用时计数减1,计数为0时可以释放;此方法比较简单,但是无法解决对象循环引用的问题。
    • 可达性分析:从GC的root对象开始往下搜索,搜索所走的路径为引用链。当一个对象到GC Root对象没有任何引用链时,则证明对象时不可引用的不可达对象。
  8. GC Roots对象
    • 虚拟机栈(栈帧中的本地变量表)中引用对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法中JNI引用对象
    • 已启动且未停止的JAVA线程
  9. TLAB(Thread Local Allocation Buffer-线程本地分配缓存区),这是一个线程专用的内存分配区,可以使用参数-XX:+UseTLAB,默认开启状态,这是个解决多线程竞争分配堆内存问题,核心原理是每个线程可以向堆申请连续的内存空间,作为私有的TLAB,这个操作需要加锁。
  10. 堆常用的内存参数:
参数默认值作用
MinHeapRatio40GC后,如果发现空闲堆内存小于整个堆的内存的40%,则放大堆内存最大值但不超过固定最大值
MaxHeapRatio70GC后没如果发现空闲堆内存大于整个堆内存的70%,则会缩小堆内存最大值
Xms物理内存的1/64(<1GB)初始堆大小
Xmx物理内存的1/4(<1GB)最大堆大小
NewRatio2年轻代(包括Eden和Suviror)与老年代的比值
NewSize1310M设置年轻代大小
MaxNewSize不限设置年轻代大小最大值
SurvivorRatio8Eden区和Survivor区比值
MaxTenuringThreshold15垃圾最大年龄
Pretenure0超过这个值的对象直接分配到老年区,默认值是0意思是不管多大都分配到Eden区

方法区

虚拟机规范中把方法区描述为堆的一个逻辑部分,但它却有一个Non-Heap别名,用于存储已被加载的类信息、常量、静态变量;即编译后的代码数据。
在JDK8中永久代被删除,类元数据在本机内存红分配。默认情况下用于存储类的元数据,默认情况是无限量,可以用MaxMetaSpaceSize设置类元数据内存量上线。

metaspace其实由两大部分组成
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相关的其他的内容,比如method,constantPool等,这块内存是由多块内存组合起来的,所以可以认为是不连续的内存块组成的。这块内存是必须的,虽然叫做NoKlass Metaspace,但是也其实可以存klass的内容,上面已经提到了对应场景。
Klass Metaspace和NoKlass Mestaspace都是所有classloader共享的,所以类加载器们要分配内存,但是每个类加载器都有一个SpaceManager,来管理属于这个类加载的内存小块。如果Klass Metaspace用完了,那就会OOM了,不过一般情况下不会,NoKlass Mestaspace是由一块块内存慢慢组合起来的,在没有达到限制条件的情况下,会不断加长这条链,让它可以持续工作。

元空间的特点
  • 充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致。
  • 每个加载器有专门的存储空间
  • 只进行线性分配
  • 不会单独回收某个类
  • 省掉了GC扫描及压缩的时间
  • 元空间里的对象的位置是固定的
  • 如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉
元空间的内存分配模型:
  • 绝大多数的类元数据的空间都从本地内存中分配
  • 用来描述类元数据的类(klasses)也被删除了
  • 分元数据分配了多个虚拟内存空间
  • 给每个类加载器分配一个内存块的列表。块的大小取决于类加载器的类型; sun/反射/代理对应的类加载器的块会小一些
  • 归还内存块,释放内存块列表
  • 一旦元空间的数据被清空了,虚拟内存的空间会被回收掉
  • 减少碎片的策略

运行时常量池 Runtime Constraint Pool

运行时常量池时方法区的一部分,用于存放编译期生成的各种字面量和符号引用,并不是编译的时候才能产生变常量,运行时也可能生成常量放入常量池,有可能抛出OOM错误。常见的字符串常量池.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值