JVM内存结构

问题:

所有的Java开发人员可能会遇到这样的困惑?我该为堆内存设置多大空间呢?OutOfMemoryError的异常到底涉及到运行时数据的哪块区域?该怎么解决呢?其实如果你经常解决服务器性能问题,那么这些问题就会变的非常常见,了解JVM内存也是为了服务器出现性能问题的时候可以快速的了解那块的内存区域出现问题,以便于快速的解决生产故障。

结构图:

1、方法区

定义:方法区,也称非堆(Non-Heap),是一个被线程共享的内存区域。其中主要存储加载的类字节码、class/method/field 等元数据对象、static-final 常量、static 变量、JIT 编译器编译后的代码等数据。另外,方法区包含了一个特殊的区域 “运行时常量池”

1.1 方法区存放的数据

类型全限定名:全限定名为 package 路径与类名称组合起来的路径;
类型的直接超类的全限定名:父类或超类的全限定名;
类型是类类型还是接口类型:判定当前类是 Class 还是接口 Interface;
类型的访问修饰符:判断修饰符,如 pulic,private 等;
类型的常量池:;
字段信息:类中字段的信息;
方法信息:类中方法的信息;
静态变量:类中的静态变量信息;
一个到类 ClassLoader 的引用:对 ClassLoader 的引用,这个引用指向堆内存;
一个到 Class 类的引用:对对象实例的引用,这个引用指向堆内存。

从图中可以看到,方法区存放了 ClassLoader 对象的引用,也存放了一个到类对象的引用,这两个引用的对象实例会存放到堆内存中。

1.2 运行时常量池

 在 Class 文件结构中,最头的 4 个字节用于存储 Megic Number,用于确定一个文件是否能被 JVM 接受,再接着 4 个字节用于存储版本号,前 2 个字节存储次版本号,后 2 个存储主版本号,再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个 u2 类型的数据 (constant_pool_count) 存储常量池容量计数值。

常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References)。

其实 Class 文件中的常量池与运行时常量池的关系非常容易理解,Class 文件中的常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

简单总结来说,编译期使用 Class 文件中的常量池,运行期使用运行时常量池。

运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是 String 类的 intern() 方法。

常量池的优势
  避免频繁的创建和销毁对象而影响系统性能,实现对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
  节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
  节省运行时间:比较字符串时,== 比 equals () 快。对于两个引用变量,只用 == 判断引用是否相等,也就可以判断实际值是否相等。

1.3 方法区内存变更

2、JVM堆内存

2.1 什么是堆内存

定义:堆内存是JVM启动时,从操作系统获取的一片内存空间,主要用于存放实例对象本身,创建完成的对象会放置到堆内存中。

物理层面:从物理层面(硬件层面)来说,当 Java 程序开始运行时,JVM 会从操作系统获取一些内存,这些内存的一部分就是堆内存。
开发层面:从开发层面来说,堆内存通常在存储地址的底层,向上排列。当一个对象通过 new 关键字或通过其他方式创建后,对象从堆中获得内存。当对象不再使用了,被当做垃圾回收掉后,这些内存又重新回到堆内存中。

2.2 堆内存结构

堆内存从结构上来说,分为年轻代(YoungGen)和老年代(OldGen)两部分;
年轻代(YoungGen)又可以分为生成区(Eden)和幸存者区(Survivor)两部分;
幸存者区(Survivor)又可细分为 S0区(from space)和 S1区 (to space)两部分。

特点

1.JVM 内存划分为堆内存和非堆内存,堆内存分为年轻代(YoungGen)、老年代(OldGen);

2.Eden 区占大容量,Survivor 两个区占小容量,默认比例是 8:1:1;

3.堆内存存放的是对象

2.3 堆内存的分代概念

在这里插入图片描述

分代 :将堆内存从概念层面进行模块划分,总体分为两大部分,年轻代和老年代。从物理层面将堆内存进行内存容量划分,一部分分给年轻代,一部分分给老年代。这就是我们所说的分代。
分代的意义:

1.易于堆内存分类管理,易于垃圾回收:类似于我们经常使用的 Windows 操作系统,我们会将物理磁盘划出一部分存储空间作为用户系统安装盘(如 C 盘),我们还极大可能将剩余的磁盘空间划分为 C, D, E 等磁盘,用于存储同一类型的数据。
2.易于管理:对于堆空间的分代也是如此,比如新创建的对象会进入年轻代(YoungGen)的生成区(Eden),生命周期未结束的且可达的对象,在经历多次垃圾回收之后,会存放入老年代(OldGen),这就是分类管理;
3.易于垃圾回收:将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及 GC 频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。


3、 JVM 中堆的对象转移与年龄判断

3.1对象转移

 从上图中可以看出:新生成的非大对象首先放到年轻代 Eden 区,当 Eden 空间满了,触发 Minor GC,存活下来的对象移动到 Survivor0 区,Survivor0 区满后触发执行 Minor GC,Survivor0 区存活对象移动到 Suvivor1 区,这样保证了一段时间内总有一个 survivor 区为空。经过多次 Minor GC 仍然存活的对象移动到老年代。
如果新生成的是大对象,会直接将该对象存放入老年代。
老年代存储长期存活的对象,GC 期间会停止所有线程等待 GC 完成,所以对响应要求高的应用尽量减少发生 Major GC,避免响应超时。

什么是大对象: 10M 的对象算大吗?100M 的对象呢?什么是大对象?大对象的标准是可以由开发者定义的,我们的 JVM 参数中,能够通过 -XX:PretenureSizeThreshold 这个参数设置大对象的标准,可惜的是这个参数只对 Serial 和 ParNew 两款新生代收集器有效。
  对于不能够设置 -XX:PretenureSizeThreshold 参数的JVM来说, Eden 区容量不够存放的对象就是所谓的大对象。

3.2年龄判断

作用·:JVM 通过判断对象的具体年龄来判别是否该对象应存入老年代,JVM通过对年龄的判断来完成对象从年轻代到老年代的转移。
对象年龄(Age)计数器:HotSpot 虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。
年龄增加:对象通常在 Eden 区里诞生,如果经过第一次 Minor GC 后仍然存活,并且能被Survivor容纳的话,该对象会被移动到 Survivor 空间中,并且将其对象年龄设为 1 岁。对象在Survivor区中每熬过一次 Minor GC,年龄就增加 1 岁。
年龄默认阈值:当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 设置。
 


————————————————
参考:

https://blog.csdn.net/a321123b/article/details/123329127

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值