Java生产环境JVM设置成固定堆大小深层原理

摘要:
也许很多人都知道,Java程序生成后,运维人员会设置JVM堆大小,并将最大值和最小值设置为相同的值。彻底理解设置固定大小堆的基本原理和好处。尽管JVM在启动时已向系统请求1G的固定堆大小空间。然而,当我们实际的Java程序配置为固定的堆大小时,您会发现,一旦内存使用量增加,它就不会下降。否则,即使Java程序GC对象,对象占用的物理内存也不会立即释放。
  可能很多人都知道Java程序上生产后,运维人员都会设定好JVM的堆大小,而且还是把最大最小设置成一样的值。那究竟是为什么呢?一般而言,Java程序如果你不显示设定该值得话,会自动进行初始化设定。
  -Xmx 的默认值为你当前机器最大内存的 1/4
  -Xms 的默认值为你当前机器最大内存的 1/64 
  显然这样配置的意义是希望JVM可以根据当前运行的环境,动态伸缩堆内存大小。之所以生产上设置成固定大小,网上也是说法不一,很多时候都是使用“防止内存抖动”这样的模糊词语给出解释。但是我相信各位读者也很懵,不知道这个词具体表达什么含义。
  所以接下来我打算用这篇文章来着重解释一下这其中的门道。带大家彻底弄懂设置固定大小堆的底层原理和好处。为了能顺利看懂本文,我假设你们已经具备了一定的操作系统基础知识。
最大堆或最小堆,从字面上理解就是JVM在运行Java程序时,为其分配堆内存空间的上限和下限值。我们把最大和最小堆设置成相同值那意思就是分配了固定大小的内存呗。这样不就省去了动态调整内存(申请和释放)以及频繁的用户态和内核态的切换带来的开销吗?。如下图所示。
Java生产环境JVM设置成固定堆大小深层原理第1张
  看上去就是这么回事,简单明了。然而当我们尝试去做个模拟实验,事实却并非如此。比如,随便写个Java程序,使用如下命令启动之。并设置好固定大小堆为1G。
  java -Xmx1024m -Xms1024m -jar demo.jar
  然后我们通过查看进程的内存占用时,发现程序并没有占用1G的空间,而是很小的占用。这个实验结果和我们预期的完全不一致。究竟是什么原因呢?
  问题其实出在我们对内存模型的理解上有问题。很多人可能都是像上面图中那样理解程序分配内存的。实际上是不对的,且也更复杂。首先我们要理解一个重要概念,那就是“进程的虚拟地址空间”,我们用户程序通过malloc这个系统调用申请内存,实际上就是申请了一个虚拟的内存,并不是真正的物理内存。大家要注意,这个虚拟的内存就是指“进程的虚拟地址空间”,而不是我们通常理解的Windows下的虚拟内存或Linux下的swap(分区交换)。如下图所示。
Java生产环境JVM设置成固定堆大小深层原理第2张
  用户程序申请的虚拟内存(虚拟地址空间),也就是通过malloc系统调用,本质就是在进程的虚拟地址空间里分配了一块地址范围而已。32位系统理论上最大4G,每个进程都有自己的虚拟地址空间,都能申请到最大4G内存。但是申请了的内存,如果没有实际使用(写入数据),则操作系统不会给这块虚拟空间分配实际的物理内存。其实原因很简单,物理内存一直属于紧缺资源,所以现代操作系统都设计为由内核程序统一管理,用户程序无权直接干涉。不是说你申请多少就真的给你多少,而是你实际使用多少才会给你多少。
  回到上面那个小实验,你发现启动后程序内存占用很小就是这个原因。尽管JVM已经在你启动时向系统申请了1G的固定堆大小空间。但是由于你这个程序只是一个简单的测试,里面并没有实际的代码操作业务。所以你实际上只用到了很小的物理内存空间。但是如果你的程序真有业务逻辑,随着系统的运行,实际占用物理内存就会越来越多,直到达到申请的上限值1G。运行期间,你的程序同时也会释放一些对象(通过GC),并在适当的时机归还一些物理内存给操作系统。所以占用的物理内存大小,也会动态有所调整。这样操作系统就可以给其他程序使用,提高了内存利用效率。这样的设计也没什么不好的。
  如上图所示,操作系统对内存管理是以页为基本单位的,一个页代表了一个固定大小的地址范围。用户程序给某个变量比如byte[]赋值时,此时该变量对应的进程虚拟地址空间所在的页在物理内存上找不到对应的页映射时,就会触发了一个缺页中断异常,操作系统就会重新将虚拟地址的页映射到物理内存中的页,此时才是真正实现了内存分配,会占用实际的物理内存空间。假如Java程序的GC把这个byte[]变量收回了,也就是不需要占用内存空间了,用户进程的堆管理器会适当的归还一些物理内存给操作系统,以便下次可以给其他任何程序使用。需要注意的是用户程序调用的malloc和free两个系统调用,都是针对用户进程的虚拟地址空间而言的,并不是实际操作物理内存。只有操作系统才拥有对实际物理内存的管理权限。操作系统可以使用有效的各种算法,来独立高效的管理物理内存。这里面的细节,我这里不详细说了,有兴趣的可以去看些操作系统的资料深入了解下。
  然而我们实际的Java程序,配置成固定堆大小后,你会发现,内存占用一旦上去了就下不来了。即使当前程序处于比较空闲的状态下。这又是为什么呢?难道Java的GC没有回收内存?
  其实并不是GC没有回收内存,而是我们这里存在理解问题。GC回收内存并不是指物理内存,而是指当前进程的虚拟内存(虚拟地址空间)。一般而言,回收的虚拟内存并不会立即归还给操作系统,从而操作系统也就无法回收它了。至于何时归还物理内存,这取决于一个叫glibc的堆管理器。它根据一定的策略和算法适当的释放真实的物理内存。否则即便Java程序GC了对象,该对象占用的物理内存也不会立即释放出来。由于这里我们是设置了固定大小的堆空间,实际上GC回收的虚拟内存,也不会被释放归还给操作系统。故Java进程内存占用一旦增长,内存占用几乎都不会再下降了,这样也是出于对象再分配的效率考虑的。这样显然可以避免操作系统反复把进程的虚拟地址页复映射物理内存页(缺页中断异常)操作,导致频繁的用户态和内核态切换造成的性能问题。
 

免责声明:文章转载自《Java生产环境JVM设置成固定堆大小深层原理》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇H3C使用tracert命令--用户视图Django多级评论下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Java 堆内存 新生代 (转)

Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括...

ubuntu 安装jdk7总结

ubuntu 安装jdk7,现在来总结一下: 第一步:下载jdk-7u25-linux-x64.tar.gz 直接在ORACLE的官网中下载就可以: http://download.oracle.com/otn-pub/java/jdk/7u25-b15/jdk-7u25-linux-x64.tar.gz?AuthParam=13724178...

linux free 命令

free 命令显示系统内存的使用情况,包括物理内存、交换内存(swap)和内核缓冲区内存。 如果加上 -h 选项,输出的结果会友好很多: 有时我们需要持续的观察内存的状况,此时可以使用 -s 选项并指定间隔的秒数: $ free -h -s 3 上面的命令每隔 3 秒输出一次内存的使用情况,直到你按下 ctrl + c。(Ubuntu 16.04 中...

JVM启动参数详解 (转)

非标准参数 非标准参数又称为扩展参数,其列表如下:-Xint设置jvm以解释模式运行,所有的字节码将被直接执行,而不会编译成本地码。-Xbatch关闭后台代码编译,强制在前台编译,编译完成之后才能进行代码执行;默认情况下,jvm在后台进行编译,若没有编译完成,则前台运行代码时以解释模式运行。-Xbootclasspath:bootclasspath让jv...

Java命令学习系列(三)——Jmap

jmap是JDK自带的工具软件,主要用于打印指定Java进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节。可以使用jmap生成Heap Dump。在Java命令学习系列(零)——常见命令及Java Dump介绍和Java命令学习系列(二)——Jstack中分别有关于Java Dump以及线程 Dump的介绍。这篇文章主要介绍Java的堆Du...

Jprofile解析dump文件使用详解

1 Jprofile简介 官网 下载对应的系统版本即可 性能查看工具JProfiler,可用于查看java执行效率,查看线程状态,查看内存占用与内存对象,还可以分析dump日志. 2 功能简介 选择attach to a locally running jvm 选择需要查看运行的jvm,双击或者点击start 等待进度完成,弹出模式选择 I...