Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配,堆内存(Heap Memory)是在 Java 虚拟机启动时创建,是Java代码可及的内存,留给开发人员使用的;默认空余堆内存小于40%时,JVM 就会增大堆直到-Xmx 的最大限制,可以由 -XX:MinHeapFreeRatio 指定;默认空余堆内存大于70%时,JVM 会减少堆直到-Xms的最小限制,可以由 -XX:MaxHeapFreeRatio 指定。
非堆内存(Non-heap Memory)是在JVM堆之外的内存,非堆是JVM留给自己用的,包含方法区、JVM内部处理或优化所需的内存(如 JITCompiler,Just-in-time Compiler,即时编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码。
2、Java对象的状态
GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"可恢复的",哪些对象是"不可达的"。当一个对象存在一个以上引用的时候,该对象为“可达状态”;当一个对象不存在任何引用的时候,该对象为”可恢复状态“;当对象调用finalize()方法后仍然不存在任何引用,该对象则为”不可达状态“,当GC确定一些对象为"不可达"时,GC就会回收这些内存空间。
3、Java内存分配
a) JVM 会试图为相关Java对象在Eden中初始化一块内存区域
b) 当Eden空间足够时,内存申请结束;否则到下一步
c) JVM 试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区
d) Survivor区被用来作为Eden及OLD的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区
e) 当OLD区空间不够时,JVM 会在OLD区进行完全的垃圾收集(0级)
f) 完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory”错误
4、Java分代GC
Java应用程序分配的内存均来自堆内存(Heap Memory)。如上图,堆内存被划分为三代,分别为Young,Old,Perm。
a) 在Young Generation中,有一个叫Eden Space的空间,主要是用来存放新生的对象,还有两个Survivor Spaces(from、to),它们的大小总是一样,它们用来存放每次垃圾回收后存活下来的对象。其垃圾回收一般用Copying的算法,速度快。每次GC的时候,存活下来的对象首先由Eden拷贝到某个SurvivorSpace,当Survivor Space空间满了后,剩下的live对象就被直接拷贝到OldGeneration中去。因此,每次GC后,Eden内存块会被清空。
b) 在Old Generation中,主要存放应用程序中生命周期长的内存对象。其垃圾回收一般用mark-compact的算法,速度慢些,但减少内存要求。
c) 垃圾回收分多级,0级为全部(Full)的垃圾回收,会回收OLD段中的垃圾;1级或以上为部分垃圾回收,只会回收Young中的垃圾;”out of memory“通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。
d) 造成Full GC的原因。New了很多对象,没有即时主动释放 -> Eden内存不够用 -> 不断把对象往Old迁移 -> Old满了 -> Full GC
5、增量式GC
增量式GC(Incremental GC),是GC在JVM中通常是由一个或一组进程来实现的,它本身也和用户程序一样占用heap空间,运行时也占用CPU。当GC进程运行时,应用程序停止运行。因此,当GC运行时间较长时,用户能够感到Java程序的停顿,另外一方面,如果GC运行时间太短,则可能对象回收率太低,这意味着还有很多应该回收的对象没有被回收,仍然占用大量内存。因此,在设计GC的时候,就必须在停顿时间和回收率之间进行权衡。一个好的GC实现允许用户定义自己所需要的设置,例如有些内存有限的设备,对内存的使用量非常敏感,希望GC能够准确的回收内存,它并不在意程序速度的快慢。另外一些实时网络游戏,就不能够允许程序有长时间的中断。
增量式GC就是通过一定的回收算法,把一个长时间的中断,划分为很多个小的中断,通过这种方式减少GC对用户程序的影响。虽然,增量式GC在整体性能上可能不如普通GC的效率高,但是它能够减少程序的最长停顿时间。
Sun JDK提供的HotSpot JVM就能支持增量式GC。HotSpot JVM缺省GC方式为不使用增量GC,为了启动增量GC,我们必须在运行Java程序时增加-Xincgc的参数。
HotSpot JVM增量式GC的实现是采用Train GC算法,它的基本想法就是:将堆中的所有对象按照创建和使用情况进行分组(分层),将使用频繁高和具有相关性的对象放在一队中,随着程序的运行,不断对组进行调整。当GC运行时,它总是先回收最老的(最近很少访问的)的对象,如果整组都为可回收对象,GC将整组回收。这样,每次GC运行只回收一定比例的不可达对象,保证程序的顺畅运行。
6、关于对象的finalize方法
finalize方法是Object对象中定义的。在一个对象所占用的内存被回收之前,如果它实现了finalize方法,则该方法一定会被调用。同时,该方法抛出的未捕获异常只会导致该对象的finalize执行退出。关于finalize方法的使用建议:
a) 最重要的,尽量不要用finalize,太复杂了,还是让系统照管比较好。可以定义其它的方法来释放非内存资源。
b) 如果用,尽量简单。
c) 如果用,避免对象再生,这个是自己给自己找麻烦。
d) 可以用来保护非内存资源被释放。即使我们定义了其它的方法来释放非内存资源,但是其它人未必会调用该方法来释放。在finalize里面可以检查一下,如果没有释放就释放好了,晚释放总比不释放好。
e) 即使对象的finalize已经运行了,不能保证该对象被销毁。要实现一些保证对象彻底被销毁时的动作,只能依赖于java.lang.ref里面的类和GC交互了。
f)在finalize运行完成之后,该对象可能变成可达的,GC还要再检查一次该对象是否是可达的,因此使用 finalize会降低GC的运行性能。
g)由于GC调用finalize的时间是不确定的,因此如果通过这种方式释放资源也是不确定的。
7、GC的程序交互
Java 2 平台引入了 java.lang.ref 包,其中包括的类可以让您引用对象,而不将它们留在内存中。这些类还提供了与垃圾收集器(garbage collector)之间有限的交互。
a) StrongReference是那种你通常建立的reference,这个reference就是强可及的,这个不会被垃圾回收器自动回收。
b) SoftReference也类似于可有可无的东西。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
d) WeakReference类似于可有可无的东西。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
e) PhantomReference为"虚引用",与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收,也就是说其get方法任何时间都会返回null。虚引用主要用来跟踪对象被垃圾回收的活动。其必须和引用队列(ReferenceQueue)联合使用,这是与弱引用和软引用最大的不同。
8、优化建议
a) 要是对象被监听,必须先移除监听器在设置为null
b) 尽早将不用的对象设置为null
c) 缓存经常使用的对象
d) 使用reference交互
e) 尽早释放无用对象的引用。好的办法是使用临时变量的时候,让引用变量在退出活动域后,自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄露。对于仍然有指针指向的实例,jvm就不会回收该资源,因为垃圾回收会将值为null的对象作为垃圾,提高GC回收机制效率;
e) 我们的程序里不可避免大量使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域;
g) 尽量少用静态变量,因为静态变量是全局的,GC不会回收的;
h) 避免集中创建对象尤其是大对象,JVM会突然需要大量内存,这时必然会触发GC优化系统内存环境;显示的声明数组空间,而且申请数量还极大。
i) 尽量运用对象池技术以提高系统性能;生命周期长的对象拥有生命周期短的对象时容易引发内存泄漏,例如大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理,