在一些数学显存为8g的服务器上,主要运行一个Java服务linux软件下载,系统显存分配如下:Java服务的JVM堆大小设置为6g,一个监控进程占用大概600m,Linux自身使用大概800m。
从表面上,数学显存应当是足够使用的;但实际运行的情况是,会发生大量使用SWAP(说明化学显存不够使用了),如右图所示。因为SWAP和GC同时发生会导致JVM严重卡顿,所以我们要追问:显存到底去哪里了?
要剖析这个问题,理解JVM和操作系统之间的显存关系十分重要。接出来主要就Linux与JVM之间的显存关系进行一些剖析。
一、Linux与进程显存模型
JVM以一个进程(Process)的身分运行在Linux系统上,了解Linux与进程的显存关系,是理解JVM与Linux显存的关系的基础。右图给出了硬件、系统、进程三个层面的显存之间的概要关系。
从硬件上看,Linux系统的显存空间由两个部份构成:化学显存和SWAP(坐落c盘)。化学显存是Linux活动时使用的主要显存区域;当化学显存不够使用时,Linux会把一部份暂时不用的显存数据放在c盘上的SWAP中去,便于腾出更多的可用显存空间;而当须要使用坐落SWAP的数据时,必须先将其换回到显存中。JVM运行时区域解读,推荐你们看下。
从Linux系统上看,不仅引导系统的BIN区,整个显存空间主要被分成两个部份:内核显存(Kernelspace)、用户显存(Userspace)。
内核显存是Linux自身使用的显存空间,主要提供给程序调度、内存分配、连接硬件资源等程序逻辑使用。
用户显存是提供给各个进程主要空间,Linux给各个进程提供相同的虚拟显存空间;这促使进程之间互相独立,互不干扰。实现的方式是采用虚拟显存技术:给每一个进程一定虚拟显存空间,而只有当虚拟显存实际被使用时,才分配化学显存。
如右图所示,对于32的Linux系统来说,通常将0~3G的虚拟显存空间分配做为用户空间,将3~4G的虚拟显存空间分配为内核空间;64位系统的界定情况是类似的。
从进程的角度来看,进程能直接访问的用户显存(虚拟显存空间)被界定为5个部份:代码区、数据区、堆区、栈区、未使用区。
代码区中储存应用程序的机器代码,运行过程中代码不能被更改,具有只读和固定大小的特征。
数据区中储存了应用程序中的全局数据,静态数据和一些常量字符串等,其大小也是固定的。
堆是运行时程序动态申请的空间,属于程序运行时直接申请、释放的显存资源。
栈区拿来储存函数的传入参数、临时变量,以及返回地址等数据。
未使用区是分配新内存空间的预备区域。
二、进程与JVM显存空间
JVM本质就是一个进程,因而其显存空间(俗称之为运行时数据区,注意与JMM的区别)也有进程的通常特征。深入浅出Java中JVM显存管理,这篇参考下。
然而,JVM又不是一个普通的进程,其在显存空间上有许多崭新的特征,主要缘由有两个:
1.JVM将许多原本属于操作系统管理范畴的东西,移植到了JVM内部linux 给用户分配空间,目的在于降低系统调用的次数;
2.JavaNIO,目的在于降低用于读写IO的系统调用的开支。JVM进程与普通进程显存模型比较如右图:
须要说明的是,这个模型的并不是JVM显存使用的精确模型,更注重于从操作系统的角度而省略了一些JVM的内部细节(虽然也很重要)。下边从用户显存和内核显存两个方面讲解JVM进程的显存特征。
1.用户显存
上图非常指出了JVM进程模型的代码区和数据区指的是JVM自身的,而非Java程序的。普通进程栈区,在JVM通常仅仅用做线程栈。JVM的堆区和普通进程的差异是最大的,下边具体详尽说明:
首先是永久代。永久代本质上是Java程序的代码区和数据区。Java程序中类(class),会被加载到整个区域的不同数据结构中去,包括常量池、域、方法数据、方法体、构造函数、以及类中的专用方式、实例初始化、接口初始化等。这个区域对于操作系统来说,是堆的一个部份;而对于Java程序来说,这是容纳程序本身及静态资源的空间,致使JVM还能解释执行Java程序。
其次是新生代和老年代。新生代和老年代才是Java程序真正使用的堆空间,主要用于显存对象的储存;并且其管理方法和普通进程有本质的区别。
普通进程在运行时给显存对象分配空间时,例如C++执行new操作时,会触发一次分配显存空间的系统调用,由操作系统的线程按照对象的大小分配好空间后返回;同时,程序释放对象时,例如C++执行delete操作时,也会触发一次系统调用,通知操作系统对象所占用的空间早已可以回收。
JVM对显存的使用和通常进程不同。JVM向操作系统申请一整段显存区域(具体大小可以在JVM参数调节)作为Java程序的堆(分为新生代和老年代);当Java程序申请显存空间,例如执行new操作,JVM将在这段空间中按所需大小分配给Java程序,但是Java程序不负责通知JVM何时可以释放这个对象的空间,垃圾对象显存空间的回收由JVM进行。
JVM的显存管理方法的优点是显而易见的,包括:第一,降低系统调用的次数,JVM在给Java程序分配显存空间时不须要操作系统干预,仅仅在Java堆大小变化时须要向操作系统申请显存或通知回收,而普通程序每次显存空间的分配回收都须要系统调用参与;第二,降低显存泄露,普通程序没有(或则没有及时)通知操作系统显存空间的释放是显存泄露的重要诱因之一,而由JVM统一管理,可以防止程序员带来的显存泄露问题。
最后是未使用区,未使用区是分配新显存空间的预备区域。对于普通进程来说,这个区域被可用于堆和栈空间的申请及释放,每次堆显存分配就会使用这个区域,因而大小变动频繁;对于JVM进程来说,调整堆大小及线程栈时会使用该区域,而堆大小通常较少调整,因而大小相对稳定。操作系统会动态调整这个区域的大小,但是这个区域一般并没有被分配实际的化学显存,只是准许进程在这个区域申请堆或栈空间。
2.内核显存
应用程序一般不直接和内核显存打交道,内核显存由操作系统进行管理和使用;不过随着Linux对性能的关注及改进,一些新的特点促使应用程序可以使用内核显存,或则是映射到内核空间。JavaNIO正是在这些背景下诞生的,其充分借助了Linux系统的新特点,提高了Java程序的IO性能。
上图给出了JavaNIO使用的内核显存在linux系统中的分布情况。niobuffer主要包括:nio使用各类channel时所使用的ByteBuffer、Java程序主动使用ByteBuffer.allocateDirector申请分配的Buffer。
而在PageCache上面,nio使用的显存主要包括:FileChannel.map方法打开文件占用mapped、FileChannel.transferTo和FileChannel.transferFrom所须要的Cache(图中标识niofile)。
通过JMX可以监控到NIOBuffer和mapped的使用情况,如右图所示。不过,FileChannel的实现是通过系统调用使用原生的PageCache,过程对于Java是透明的,难以监控到这部份显存的使用大小。
Linux和JavaNIO在内核显存上开辟空间给程序使用,主要是降低不要的复制,以减轻IO操作系统调用的开支。诸如,将c盘文件的数据发送网卡,使用普通方式和NIO时,数据流动比较右图所示:
将数据在内核显存和用户显存之间拷贝是比较消耗资源和时间的事情,而从上图我们可以看见,通过NIO的形式降低了2次内核显存和用户显存之间的数据拷贝。这是JavaNIO高性能的重要机制之一(另一个是异步非阻塞)。
从里面可以看出,内核显存对于Java程序性能也十分重要,因而,在界定系统显存使用时侯,一定要给内核留出一定可用空间。
三、案例剖析
1.显存分配问题
通过前面的剖析,省略比较小的区域,可以总结JVM占用的显存:
JVM显存≈Java永久代+Java堆(新生代和老年代)+线程栈+JavaNIO
回到文章开头提出的问题,原先的显存分配是:6g(java堆)+600m(监控)+800m(系统),剩余大概600m显存未分配。
如今剖析这600m显存的分配情况:
Linux保留大概200m,这部份是Linux正常运行的须要,Java服务的线程数目是160个,JVM默认的线程栈大小是1m,因而使用160m显存,JavaNIObuffer,通过JMX查到最多占用了200m,Java服务使用NIO大量读写文件,须要使用PageCache,正如上面剖析,这个暂时不好定量计算大小。
前三项加上去早已560m,因而可以推断Linux化学显存不够使用。
悉心的人会发觉,序言中给出两个服务器,一个SWAP最多占用了2.16g,另外一个SWAP最多占用了871m;并且,虽然我们的显存缺口没有这么大。事实上,这是因为SWAP和GC同时进行导致的,从右图可以看见,SWAP的使用和长时间的GC在同一时刻发生。
SWAP和GC同时发生会造成GC时间很长,JVM严重卡顿,极端的情况下会造成服务崩溃。缘由如下:JVM进行GC时,时须要对相应堆分区的已用显存进行遍历;如果GC的时侯,有堆的一部份内容被交换到SWAP中,遍历到这部份的时侯就须要将其交换回显存,同时因为显存空间不足,就须要把显存中堆的另外一部份换到SWAP中去;于是在遍历堆分区的过程中,(极端情况下)会把整个堆分区轮流往SWAP写一遍。Linux对SWAP的回收是滞后的,我们都会看见大量SWAP占用。上述问题linux下载工具,可以通过降低堆大小,或则降低化学显存解决。
因而,我们得出一个推论:布署Java服务的Linux系统,在显存分配上,须要防止SWAP的使用;具体怎么分配须要综合考虑不同场景下JVM对Java永久代、Java堆(新生代和老年代)、线程栈、JavaNIO所使用显存的需求。
2.显存泄露问题
另一个案例是,8g显存的服务器,Linux使用800m,监控进程使用600m,堆大小设置4g;系统可用显存有2.5g左右,而且也发生了大量的SWAP占用。
剖析这个问题如下:
1在这个场景中,Java永久代、Java堆(新生代和老年代)、线程栈所用显存基本是固定的,因而,占用显存过多的缘由就定位在JavaNIO上。
2按照上面的模型,JavaNIO使用的显存主要分布在Linux内核显存的System区和PageCache区。查看监控的记录,如右图,我们可以看见发生SWAP之前linux 给用户分配空间,也就是化学显存不够使用的时侯,PageCache随之缩小。因而,可以定位在System区的JavaNIOBuffer发生显存泄露。
3因为NIO的DirectByteBuffer须要在GC的后期被回收,因而连续申请DirectByteBuffer的程序,一般须要调用System.gc(),防止长时间不发生FullGC造成引用在old区的DirectByteBuffer显存泄露。剖析到此,可以推测有两种可能的诱因:第一,Java程序没有在必要的时侯调用System.gc();第二,System.gc()被禁用。
4最后是要排查JVM启动参数和Java程序的DirectByteBuffer使用情况。在本例中,查看JVM启动参数,发觉启用了-XX:+DisableExplicitGC造成System.gc()被禁用。
四、总结
本文详尽剖析了Linux与JVM的显存关系,比较了通常进程与JVM进程使用显存的优缺点,理解这种特点将对Linux系统显存分配、JVM调优、Java程序优化有帮助。限于篇幅关系仅仅列出两个案例,希望起到抛砖引玉的作用。