Java不需要开发人员来显式的分配内存和回收内存,而是由JVM来自动管理内存的分配以及回收(又称为垃圾回收、Garbage Collection或GC),这对于开发人员来说确实大大降低了编写程序的难度,但带来的副作用是可能在不知不觉中浪费了很多内存,造成JVM花费很多的时间在进行内存的回收;另外可能会带来的副作用是由于不清楚JVM内存的分配和回收机制,造成内存泄露,最终导致JVM内存不够用。
除了内存的分配及回收外,还需掌握的为跟踪分析JVM内存的使用状况,以便更加准确的判断程序的运行状况以及进行性能的调优。
1. 内存空间
Sun JDK在实现时遵照JVM规范,将内存空间划分为了方法区、堆、本地方法栈、PC寄存器以及JVM方法栈,如下图所示:
方法区
方法区存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。
在Sun JDK中这块区域对应的为Permanet Generation,又称为持久代,默认最小值为16M,最大值为64M,可通过-XX:PermSize以及-XX:MaxPermSize来指定其最小和最大值。
堆
堆用于存储对象实例以及数组值,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中对象所占用的内存由GC进行回收,在32位操作系统上最大为2G,在64位的操作系统上则没有限制,其大小可通过-Xms和-Xmx来控制,-Xms为JVM启动时申请的最小Heap内存,默认为物理内存的1/64但小于1G;-Xmx为JVM可申请的最大Heap内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRatio=来指定这个比例;当空余堆内存大于70%时,JVM会减小Heap的大小到-Xms指定的大小,可通过-XX:MaxHeapFreeRatio=来指定这个比例,对于运行系统而言,为避免在运行时频繁调整Heap 的大小,通常都将-Xms和-Xmx的值设成一样。
为了内存回收更加的高效,Sun JDK从1.2开始对堆采用了分代管理的方式,图示如下:
1. 新生代(New Generation)
又称为新生代,大多数情况下Java程序中新建的对象都从新生代分配内存,新生代由Eden Space和两块相等大小的Survivor Space(通常又称为S0和S1或From和To)构成,可通过-Xmn参数来指定新生代的大小,可通过-XX:SurvivorRatio来调整Eden Space以及Survivor Space的大小,不同的GC方式会以不同的方式按此值来划分Eden Space和Survivor Space,有些GC方式还会根据运行状况来动态调整Eden、S0、S1的大小。。
2. 旧生代(Old Generation或Tenuring Generation)
又称为旧生代,用于存放新生代中经过多次垃圾回收仍然存活的对象,例如缓存对象,新建的对象也有可能在旧生代上直接分配内存,主要有两种状况(由不同的GC实现来决定):一种为大对象,可通过在启动参数上设置-XX:PretenureSizeThreshold=1024(单位为字节,默认值为0)来代表当对象超过多大时就不在新生代分配,而是直接在旧生代分配;另一种为大的数组对象,并且数组中无引用外部对象。
旧生代所占用的内存大小为-Xmx对应的值减去-Xmn对应的值。
本地方法栈
本地方法栈用于支持native方法的执行,存储了每个native方法调用的状态,在Sun JDK的实现中本地方法栈和JVM方法栈是同一个。
PC寄存器和JVM方法栈
每个线程均会创建PC寄存器和JVM方法栈, PC寄存器占用的可能为CPU寄存器或操作系统内存,JVM方法栈占用的为操作系统内存,JVM方法栈为线程私有,其在内存分配上非常高效,当方法运行完毕时,其对应的栈帧所占用的内存也会自动释放。
2. 内存分配
Java对象所需占用的内存主要从堆上进行分配,堆是所有线程共享的,因此在堆上分配内存时需要进行加锁,这导致了创建对象开销比较大,当堆上空间不足时,会触发GC,如GC后空间仍然不足,则抛出OutOfMemory错误信息。
Sun JDK为提升内存分配的效率,会为每个新创建的线程在新生代的Eden Space上分配一块独立的空间,这块空间称为TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行情况计算而得,可通过-XX:TLABWasteTargetPercent来设置TLAB可占用的Eden Space的百分比,默认值为1%,JVM将根据这个比率、线程数量以及线程是否频繁分配对象来给每个线程分配合适大小的TLAB空间18,在TLAB上分配内存时不需要加锁,因此JVM在给线程中的对象分配内存时会尽量的在TLAB上分配,如对象过大或TLAB空间已用完则仍然在堆上进行分配,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效,可通过在启动参数上增加-XX:+PrintTLAB来查看TLAB空间的使用情况19。