Java中我们基本上不会显式地调用分配内存的函数,分配内存和回收内存都由JVM自动完成了。
所谓物理内存就是我们通常说的RAM(随机存储器),计算机中还有一个存储单元叫做寄存器,用于存储计算单元执行指定的中间结果。寄存器的大小决定了一次计算可使用的最大数值。
不管是在Windows系统还是在Linux系统下,运行程序都要向操作系统先申请内存地址。通常操作系统管理内存的申请空间是按照进程来管理的,每个进程拥有一段独立的地址空间,每个进程之间不会相互重合,操作系统也会保证每个进程只能访问自己的内存空间(逻辑间独立,操作系统保证)。随着程序越来越庞大和设计的多任务性,物理内存无法满足程序需要,就有了虚拟内存的出现。
一个计算机有一定的内存空间,程序并不能完全使用这些地址空间,因为这些地址空间被划分为内核空间和用户空间。内核空间主要是指操作系统运行时所使用的用户程序调度,虚拟内存的使用或者连接硬件资源等的程序逻辑,为了保证操作系统运行的稳定性,运行在操作系统中的用户程序不能访问操作系统所使用的内存空间。
Java中哪些组件需要使用内存
1. Java堆
Java堆用于存储Java对象的内存区域,堆的大小在JVM启动时就一次向操作系统申请完成,通过-Xmx和-Xms两个选项来控制大小。一旦分配完成,堆的大小就将会固定,不能在内存不够时再向操作系统重新申请内存,同时当内存空闲时也不能将多余的空间还给操作系统。
2. 线程
JVM运行实际程序的实体是线程,线程同时也需要一定的内存空间来存储一些必要的数据,每个线程创建时JVM都会为它创建一个堆栈,堆栈的大小根据不同的JVM实现而不同,通常在256K至768K之间。
3. 类和类加载器
Java中的类核加载类的类加载器本身同样需要存储空间,在Sun JDK中也被存储在堆中,这个区域叫做永久代(PermGen)。
JVM是按需来加载类的,JVM只会加载那些在你的应用程序中明确使用的类到内存中,如果需要查看JVM到底加载了哪些类,可以在启动参数中加上-verbose:class。
通常一个类能够被卸载,有如下条件需要被满足:
- 在Java堆中没有对表示该类加载器的java.lang.ClassLoader对象的引用;
- Java堆中没有对表示类加载器加载的类的任何java.lang.Class对象的引用;
- 在Java堆上该类加载器加载的任何类的所有对象不再存活(被引用);
注意JVM所创建的3个默认类加载器Bootstrap ClassLoader, ExtClassLoader, AppClassLoader都不可能满足这些条件,因此任何系统类或通过应用程序类加载器加载的任何应用程序类都不可能在运行时释放。
4. Java NIO
Java在1.4版本中添加了NIO相关类库,引入了一种基于通道(Channel)和缓冲区(Buffer)来执行IO的新方式。NIO使用ByteBuffer.allocateDirect()方式分配内存,这种方式也就是通常所说的NIO direct memory,其分配的内存使用的是本机内存而不是Java堆上的内存,这也进一步说明每次分配内存会调用操作系统的os::malloc()函数。
直接ByteBuffer对象会自动清理本机缓冲区,但此过程只能作为Java堆GC的一部分来执行,它们不会自动响应施加在本机堆上的压力。GC仅在Java堆被占满,以至于无法为堆分配请求提供服务时发生。当前在很多NIO框架中都在代码中显式地调用System.gc()来释放NIO持有的内存,但这样会影响应用程序的性能,增加GC的次数。如果设置-XX: +DisableExplicitGC来控制System.gc()的影响,但是又会导致NIO direct memory内存泄漏的问题。
5. JNI
JNI技术使得本机代码可以调用Java方法,也就是通常所说的native memory,实际上Java运行本身也依赖JNI代码来实现类库的功能,如文件操作,网络IO,JNI也会增加Java运行时的本机内存占用。
Java内存问题分析
有时候Java内存溢出发生时,我们并不知道原因,在JVM启动时可以加上一些参数来控制,当JVM出问题能够记下一些当时的情况,就是记录下来的GC日志,可以观察GC的频度以及每次GC都回收了哪些内存。
1. GC的日志
日志输出有大概下面的参数:
-verbose:gc | 输出一些详细的GC信息 |
-XX:+PrintGCDetails | 输出GC的详细信息 |
-XX:+PrintGCApplicationStoppedTime | 输出GC造成应用程序暂停时间 |
-XX:+PrintGCDateStamps | GC发生的时间信息 |
-XX:+PrintHeapAtGC | GC前后输出堆中各个区域的大小 |
-Xloggc:[file] | 将GC信息输出到单独的文件中 |
曾经分析过GC的日志,具体过程可以参考:http://brandnewuser.iteye.com/blog/2101501
可以根据日志来判断是否有内存在泄漏,如果在每次GC执行完成后,进入Old区或者Perm区的大小,ending occupancy的值一直在增长,而且GC非常频繁,很可能就是内存泄漏。
除去日志文件分析后,可以直接通过JVM自带的一些工具分析,如jstat -gcutil [pid] [interval] [count]。(这些工具的具体使用,也曾经总结过:http://brandnewuser.iteye.com/blog/2042528)
2. 堆快照文件分析
可以通过jmap -dump:format=b,file=[filename] [pid] 来记录下堆的内存快照,利用第三方工具(mat,jhat)来分析整个Heap的对象关联情况。
如果内存耗尽可能导致JVM直接Crash掉,可以通过参数-XX:+HeapDumpOnOutOfMemoryError来配置当内存耗尽时记录下内存快照,可以通过-XX: HeapDumpPath来指定文件的路径,这个文件的命名格式如java_[pid].hprof。
3. JVM Crash日志分析
JVM有时也会因为一些原因而直接垮掉,因为JVM本身也是一个正在运行的程序,是程序就会有bug,也会导致JVM异常退出。JVM退出一般会在工作目录下产生一个日志文件,通过JVM参数来设定,如果-XX:ErrorFile=/tmp/log/hs_error_%p.log
JVM退出一般有三种主要的原因:
EXCEPTION_ACCESS_VIOLATION
正在运行JVM自己的代码,而不是外部的Java代码或其他类库代码,这种情况可能是JVM自己的bug,遇到这种错误可以根据出错信息oracle官网搜索一下发行的bug。大部分情况下是由于JVM的内存回收导致的,查看堆的内存占用情况。
SIGSEGV
JVM正在执行本地或JNI的代码,出这种错误很可能是第三方的本地库有问题,可以通过gbd和core文件来分析出错原因。
我们曾经遇到过一个这种类型的问题:http://brandnewuser.iteye.com/blog/2144456, 分析的过程比较复杂,但最终的原因却是无意中替换JNI包导致的。
EXCEPTION_STACK_OVERFLOW
这是个栈错误,注意JVM在执行Java线程出现的栈溢出通常不会导致JVM退出,而是抛出java.lang.*Error。但是在Java虚拟机中,Java的代码和本地C或C++代码共用相同的栈,这时如果出现栈溢出的话,就可能直接导致JVM退出,建议将JVM的栈尺寸调大,主要涉及两个参数: -Xss和-XX:StackShadowPages=n。
日志文件的Thread部分信息对排查问题的原因有很大的帮助,包括Machine Instructions和Thread Stack(虽然本人真的看不懂...)。