JVM系列:一、JVM内存模型

前言

前段时间生产上遇到一些JVM参数设置不恰当导致内存溢出的问题,按照网上文章的思路解决了问题。但目前为止只知道这样设置可以解决问题,却不知道问题产生的深层原因是什么?以后应该怎么防止类似的问题出现?知其然不知其所以然,于是就有了这一系列文章。

参考资料:
《深入理解Java虚拟机第二版》 周志明


一、JVM是什么?

我们ୃ知道Java 源文件通过编译器能够编译成相应的.Class 文件,也就是字节码文件,而字节码文件又通过Java 虚拟机中的解释器,解释编译成特定机器上的机器码,让其能够在各个平台运行。事实上JVM 就是一种规范,各个供应商都可以实现自己 JVM 虚拟机,包括我们自己也可以手写一个。目前我们常用的就是Oracle的Hotspot,其他的还有IBM的J9、BEA公司 JRockit等等。
在这里插入图片描述
对于Java来说,JVM是它能够跨平台运行的关键。JVM就像一个虚拟计算机,介于底层操作系统和Java程序之间,屏蔽了底层硬件和不同操作系统的复杂性。为Java程序提供了一个不依赖于底层操作系统和机器硬件结构的运行环境,使Java摆脱了硬件的束缚,实现了“一次编写,到处运行”。并且还提供了一个相对安全的内存管理和访问机制,避免了绝大部分的内存泄露和指针越界问题。实现了热点代码监测和运行时编译和优化,这使得Java应用能随着运行时间的增加而获得更高性能。
在这里插入图片描述

二、JVM内存区域

1. JDK1.6、JDK1.7、JDK1.8 内存模型演变

在这里插入图片描述上图是参考网上一个大神的,通过上图我们可以很清晰的看到jdk这几个版本的不同之处。(我发现我画的图都花花绿绿的 emmmm…)
按照我的理解简单解释下,首先解释一下方法区,方法区实际上是JVM规范中的一个概念性东西。不同的虚拟机对方法区有不同的实现。就比如Hotspot对方法区的实现就是永久代。
那jdk1.7相对于jdk1.6,主要的变化就是将永久代中的字符串常量池移到堆内存中,交由堆管理。我们知道堆是JVM内存管理的主要区域,那么将字符串常量池放到堆内存中更方便高效的对字符串常量进行管理和垃圾回收。
而jdk1.8相对于jdk1.7来说,主要区别有两点,一是将虚拟机栈和本地方法栈合二为一了,二是移除永久代,增加了元数据区,元数据区使用本地内存,只受计算机内存大小的限制。而永久代使用的还是堆内存空间,受堆内存大小的限制。

2. 内存模型各区域介绍

2.1 程序计数器

  • 一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 为了线程切换后能恢复到正确的位置,每个线程都需要一个独立的程序计数器,各个线程之间互不影响,独立存储,这也就是所谓的“线程私有区域”。
  • 如果线程执行的是一个Java方法,那么程序计数器记录的是正在执行的虚拟机字节码指令的地址。如果执行的是一个本地方法,那么程序计数器的值则为空(Undefined)
  • 虚拟机规范中唯一没有定义OOM异常的区域

2.2 虚拟机栈

  • 线程私有
  • 虚拟机栈描述的是方法执行的内存模型:每个方法在执行时都会创建一个栈帧,每个栈帧存放的是局部变量表,操作数栈,动态链接,方法出口等信息,方法被调用到执行完成对应的是一个栈帧从入栈到出栈的过程。
  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。如果虚拟机栈动态扩展时没法申请到足够内存,就会抛出OutOfMemoryError异常。

2.3 本地方法栈

  • 本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。比如: Object类中的clone / wait 等方法就是本地方法。
  • 在HotSpot虚拟机中直接就把本地方法栈和虚拟机栈合二为一了。

2.4 堆

  • 线程共享的一块内存区域,几乎所有对象实例以及数组都在堆上分配内存
  • Java堆是垃圾收集器管理的主要区域,因此也叫GC堆。Java堆还可以细分为:新生代和老年代,新生代还可以进一步分为Eden空间,From Survivor空间、To Survivor空间。主要目的还是为了更好的回收内存,或者更快的分配内存。
  • Java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)
  • 字符串常量池:jdk1.7之后字符串常量池是堆中的一块内存区域。我们可以利用串池的机制,来避免重复创建字符串对象。达到高效使用堆空间的目的。一般通过String的intern方法实现让字符串入池。

2.5 方法区

  • 一块线程共享的内存区域
  • 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
  • 对于HotSpot来说,设计团队使用永久代来实现方法区,把GC分代手机扩展至方法区,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。对于其他虚拟机来说是不存在永久代的概念的。
  • 但是使用永久代来实现方法区,更容易内存溢出问题(永久代 -XX:MaxPermSize的上限),Java虚拟机规范对方法区的限制非常宽松,甚至可以选择不是先垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
  • 在jdk1.8中已经去除了永久代,改用只受计算机本地内存大小限制的元空间来实现方法区,元空间参数(-XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=1024M)

2.6 运行时常量池

  • 运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
  • 运行时常量池既然是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OOM异常。

2.7 直接内存

  • 直接内存分配不会受到Java堆大小的限制,但是会受到本机总内存大小以及处理器寻址空间的限制。
  • 直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域。
  • JDK1.4后新加入了NIO类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回赋值数据。

总结

对于JVM,它是一个很成熟的产品了,我们大部分时候是不需要每次都要对它进行调优的。我觉得更重要的是出现问题时,快速定位问题,分析问题产生的原因,确定解决方案。是代码的问题去优化代码,是参数设置的问题再去调整参数。但是,前提是你对JVM非常了解,然后才能很自信的确定该如何解决问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值