jvm内存模型
JVM内存模型
Java虚拟机运行时数据区
- 堆
- 存放对象的实例,数组等
- 线程共享,空间最大,JVM虚拟机启动时创建
- 堆上内存可完成实例分配,当无法扩展时,抛出OutOfMemeoryError
- 通过-Xmx,-Xms设置堆大小
方法区(永久代),JDK7将常量池移出,jdk8真正开始废弃永久代,而使用元空间(Metaspace)- 存放虚拟机加载类的信息,如常量,静态变量等(即存放编译后需要存放的数据)
- 无法满足内存分配需求时,抛出OutOfMemeoryError
- 虚拟机栈
- 存放基本数据类型,对象的引用,指向字节码指令的地址(堆内存地址)
- 线程请求深度大于虚拟机所允许深度时,抛出AtackOverflowError
- 无法申请足够大的内存,抛出OutOfMemeoryError
- 本地方法栈
- 与虚拟机栈类似,区别是为本地方法服务
- 程序计数器
- 记录正在执行的虚拟机字节码指令地址
- 执行本地方法(由C/C++写的,由系统调用)时,计数器为空(undefined)
- 线程私有,唯一一个不会出现OutOfMemeoryError的区域,生命周期跟随线程
- 运行时常量池
- 方法区的一部分,存放编译期生成的字面量和符号引用
- 字面量就是我们所说的常量概念,用final修饰的常量值
- 符号引用是一组符号来描述所引用的目标,(例如:String str = “this is String”)
- 常量池内存不足时,抛出OutOfMemeoryError
- 方法区的一部分,存放编译期生成的字面量和符号引用
元空间相对于方法区(永久代)的优势
持久代的大小
- 它的上限是MaxPermSize,默认是64M
- Java堆中的连续区域 : 如果存储在非连续的堆空间中的话,要定位出持久代到新对象的引用非常复杂并且耗时。卡表(card table),是一种记忆集(Remembered Set),它用来记录某个内存代中普通对象指针(oops)的修改。
- 持久代用完后,会抛出OutOfMemoryError "PermGen space"异常。解决方案:应用程序清理引用来触发类卸载;增加MaxPermSize的大小。
- 需要多大的持久代空间取决于类的数量,方法的大小,以及常量池的大小。
为什么移除持久代
- 它的大小是在启动时固定好的——很难进行调优。-XX:MaxPermSize,设置成多少好呢?
- HotSpot的内部类型也是Java对象:它可能会在Full GC中被移动,同时它对应用不透明,且是非强类型的,难以跟踪调试,还需要存储元数据的元数据信息(meta-metadata)。
- 简化Full GC:每一个回收器有专门的元数据迭代器。
- 可以在GC不进行暂停的情况下并发地释放类数据。
- 使得原来受限于持久代的一些改进未来有可能实现
元空间的特点:
- 充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致。
- 每个加载器有专门的存储空间
- 只进行线性分配
- 不会单独回收某个类
- 省掉了GC扫描及压缩的时间
- 元空间里的对象的位置是固定的
- 如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉
元空间的内存分配模型
- 绝大多数的类元数据的空间都从本地内存中分配
- 用来描述类元数据的类也被删除了
- 分元数据分配了多个虚拟内存空间
- 给每个类加载器分配一个内存块的列表。块的大小取决于类加载器的类型; sun/反射/代理对应的类加载器的块会小一些
- 归还内存块,释放内存块列表
- 一旦元空间的数据被清空了,虚拟内存的空间会被回收掉
- 减少碎片的策略
堆的划分
- 新生代
- 分为三个区,一个Eden(伊甸)、两个Survivor(幸存者),比例8:1:1
- 新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代)
- Survivor分为To Survivor、From Survivor每次GC,两个区的名字都会交换
- GC时,将Eden,和From Surivivor区存活对象复制到To Survivor
- 每一次GC,年龄值加1,到达15(默认,通过**-XX:MaxTenuringThreshold**来设定)是移至老年代中
- 老年代
- 多次GC后仍然存活下来的对象会进入老年代中
- 对象生命周期较长,存活率比较高
- GC的频率相对而言较低,而且回收的速度也比较慢。
- 永久代
- 存储类信息、常量、静态变量、即时编译器编译后的代码等数据
- 一般不会进行垃圾回收。