Java 启动后也作为一个进程运行在操作系统中,那么这个进程有哪些部分需要分配内
存空间呢?
?
Java 堆
Java 堆是用于存储Java 对象的内存区域,堆的大小在JVM 启动时就一次向操作系统
申请完成,通过 -Xmx 和 -Xms 两个选项来控制大小,Xmx 表示堆的最大大小,Xms 表示
初始大小。一旦分配完成,堆的大小就将固定,不能在内存不够时再向操作系统重新申请,
同时当内存空闲时也不能将多余的空间交还给操作系统。
?
在 Java 堆中内存空间的管理由JVM 来控制,对象创建由Java 应用程序控制,但是对
象所占的空间释放由管理堆内存的垃圾收集器来完成。根据垃圾收集(GC)算法的不同,
内存回收的方式和时机也会不同。
?
?
?线程
JVM 运行实际程序的实体是线程,当然线程需要内存空间来存储一些必要的数据。每
个线程创建时JVM 都会为它创建一个堆栈,堆栈的大小根据不同的JVM 实现而不同,通
常在256KB~756KB 之间。
?
线程所占空间相比堆空间来说比较小。但是如果线程过多,线程堆栈的总内存使用量
可能也非常大。当前有很多应用程序根据CPU 的核数来分配创建的线程数,如果运行的
应用程序的线程数量比可用于处理它们的处理器数量多,效率通常很低,并且可能导致比
较差的性能和更高的内存占用率。
?
?
类和类加载器
在 Java 中的类和加载类的类加载器本身同样需要存储空间,在Sun JDK 中它们也被
存储在堆中,这个区域叫做永久代(PermGen 区)。
?
需要注意的一点是JVM 是按需来加载类的,曾经有个疑问:JVM 如果要加载一个jar
包是否把这个jar 包中的所有类都加载到内存中?显然不是的。JVM 只会加载那些在你的
应用程序中明确使用的类到内存中。要查看JVM 到底加载了哪些类,可以在启动参数上
加上-verbose:class。
?
理论上使用的Java 类越多,需要占用的内存也会越多,还有一种情况是可能会重复
加载同一个类。通常情况下JVM 只会加载一个类到内存一次,但是如果是自己实现的类
加载器会出现重复加载的情况,如果PermGen 区不能对已经失效的类做卸载,可能会导致
PermGen 区内存泄漏。所以需要注意PermGen 区的内存回收问题。通常一个类能够被卸载,
有如下条件需要被满足。
?
◎ 在Java 堆中没有对表示该类加载器的java.lang.ClassLoader 对象的引用。
◎ Java 堆没有对表示类加载器加载的类的任何java.lang.Class 对象的引用。
◎ 在Java 堆上该类加载器加载的任何类的所有对象都不再存活(被引用)。
需要注意的是,JVM所创建的3 个默认类加载器Bootstrap ClassLoader、ExtClassLoader
和AppClassLoader 都不可能满足这些条件,因此,任何系统类(如java.lang.String)或通
过应用程序类加载器加载的任何应用程序类都不能在运行时释放。
?
?
NIO
Java 在1.4 版本以后添加了新I/O(NIO)类库,引入了一种基于通道和缓冲区来执行
I/O 的新方式。就像在Java 堆上的内存支持I/O 缓冲区一样,NIO 使用java.nio.ByteBuffer.
allocateDirect() 方法分配内存, 这种方式也就是通常所说的NIO direct memory 。
?
ByteBuffer.allocateDirect() 分配的内存使用的是本机内存而不是Java 堆上的内存,这也进
一步说明每次分配内存时会调用操作系统的os::malloc()函数。另外一方面直接ByteBuffer
产生的数据如果和网络或者磁盘交互都在操作系统的内核空间中发生,不需要将数据复制
到Java 内存中,很显然执行这种I/O 操作要比一般的从操作系统的内核空间到Java 堆上
的切换操作快得多,因为它们可以避免在Java 堆与本机堆之间复制数据。如果你的I/O 频
繁地发送很小的数据,这种系统调用的开销可能会抵消数据在内核空间和用户空间复制带
来的好处。
?
直接 ByteBuffer 对象会自动清理本机缓冲区,但这个过程只能作为Java 堆GC 的一部
分来执行,因此它们不会自动响应施加在本机堆上的压力。GC 仅在Java 堆被填满,以至
于无法为堆分配请求提供服务时发生,或者在Java 应用程序中显示请求时发生。当前在
很多NIO 框架中都在代码中显式地调用System.gc()来释放NIO 持有的内存。但是这种方
式会影响应用程序的性能, 因为会增加GC 的次数, 一般情况下通过设置 -XX:+
DisableExplicitGC 来控制System.gc()的影响,但是又会导致NIO direct memory 内存泄漏
问题。
?
JNI
JNI 技术使得本机代码(如C 语言程序)可以调用Java 方法,也就是通常所说的native
memory。实际上Java 运行时本身也依赖于JNI 代码来实现类库功能,如文件操作、网络
I/O 操作或者其他系统调用。所以JNI 也会增加Java 运行时的本机内存占用。