详解JVM的内存管理机制
我们在深入Java核心系列文章中给大家讲过JVM中的栈和局部变量。在做Java开发的时候常用的JVM内存管理有两种,一种是堆内存,一种是栈内存。堆内存主要用来存储程序在运行时创建或实例化的对象与变量,例如:我们通过new MyClass()创建的类MyClass的对象。而栈内存则是用来存储程序代码中声明为静态(或非静态)的方法。下面我给大家举个例子:
代码 public class Test{ static Vector list = new Vector(); static void makeThings(){ Object object = new Object(); list.add(object); } public static void main(){ makeThings(); } }
就拿上面的例子来说,放在栈内存中的有:main,makeThings,放在堆内存中有:Test,list,object。
JVM中对象的生命周期大致可以分为7个阶段:创建阶段、应用阶段、不可视阶段、不可到达阶段、可收集阶段、终结阶段与释放阶段。
1.创建阶段:
(1)为对象分配存储空间。
(2)开始构造对象。
(3)递归调用其超类的构造方法。
(4)进行对象实力初始化与变量初始化。
(5)执行构造方法体。
还有就是你在创建对象的时候需要注意的地方:
(1)避免在循环体中创建对象,即使该对象占用内存空间不大。
(2)尽量及时使对象符合垃圾回收标准。
(3)不要采用过深的继承层次。
(4)访问本地变量优于访问类中的变量。
2.应用阶段:
在应用阶段涉及到4个引用:
(1)强引用:是指JVM内存管理器从根引用集合出发遍寻堆中所有到达对象的路径。
(2)软引用:是具有较强的引用功能,只有当内存不够的时候,才回收这类内存,因此内存足够的时候,不会被回收。
(3)弱引用:弱引用与软引用对象的最大不同在于:GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用来说,GC总是进行回收。
(4)虚引用:主要用于辅助finalize函数的使用。虚引用主要适用于以某种比Java终结机制更灵活的方式调度pre-mortem清除操作。
3.不可视阶段:
先看一段代码:
代码 public void process(){ try{ Object obj = new Object(); obj.doSomething(); } catch(Exception e){ e.printStackTrace(); } while(isLoop){ //这个区域对于obj对象来说已经是不可视的了 //因此下面的代码在编译时会引发错误 obj.doSomething(); } }
如果一个对象已使用完了,应该主动将其设置为null,可以在上面的代码行obj.doSomething();下添加代码行obj=null;这样一行代码强制将obj对象置为空值,这样做的意义就是帮助JVM及时的发现这个垃圾对象,并且可以及时的回收该对象占用的系统资源。
4.不可到达阶段:
处于不可到达阶段的对象,在虚拟机所管理的对象引用根集合中再也找不到直接或间接的强引用,这些对象通常是指多有线程栈中的临时变量,所有已装载的类的静态变量或者对本地代码接口(JNI)引用。
5.可收集阶段、终结阶段与释放阶段:
当对象处于这个阶段的时候,可能处于下面三种情况:
(1)垃圾回收器发现该对象已经不可到达。
(2)finalize方法已经被执行。
(3)对象空间已被重用。
当对象处于上面三种清空的时候,虚拟机就可以直接将该对象回收了。
析构方法finalize
前面我们说了JVM的垃圾回收机制和JVM中对象的生命周期,今天给大家讲个方法,叫做析构方法finalize,我想搞过C++的人都知道,而且是内存管理技术中相当重要的一部分。但是,在Java中好像没有这个概念,这是因为,理论上JVM负责对象的析构(销毁与回收)工作,finalize是Object类中的一个方法,并且是protected,由于所有的类都继承了Object对象,因此,就都隐式的继承了改方法,不过可以重写这个方法,如果重写此方法,最后一句必须写上super.finalize()语句,因为finalize方法没有自动实现递归调用。那我们在什么时候要重写它呢?当有一些不容易控制并且非常重要的资源时,要放到finalize方法中,例如:一些I/O的操作,数据的连接等等,这些资源的释放对整个应用程序是非常关键的。
我先让大家看一段代码:
public class TestA{ Object obj = null; public TestA(){ obj = new Object(); System.out.println("创建obj对象"); } protected void destroy(){ System.out.println("释放obj对象"); obj = null; //释放自身所占用的资源 } protected void finalize() throws java.long.Throwable{ destroy(); //递归调用超类中的finalize方法 super.finalize(); } }
finalize方法最终是由JVM中的垃圾回收器调用的,由于垃圾回收器调用finalize的时间是不确定或者不及时的,调用时机对我们来说是不可控的,因此我们可以在自己的类中声明一个destory()方法,在这个方法中添加释放系统资源的处理代码,但是还是建议你将对destroy()方法的调用放入当前类的finalize()方法体中,因为这样做更保险,更安全。
静态变量
我们知道类中的静态变量在程序运行期间,其内存空间对所有该类的对象实例而言是共享的,为了节省系统内存开销、共享资源,应该将一些变量声明为静态变量。通过下面的例子,你就会发现有什么不同。
代码一:
public class MemoryTest { static class Data{ private int week; private String name; Data(int i, String s){ week = i; name = s; } } Data weeks[] = { new Data(1,"monday"), new Data(2,"Tuesday"), new Data(3,"Wednesday"), new Data(4,"Thursday"), new Data(5,"Friday"), new Data(6,"Saturday"), new Data(7,"Sunday") }; public static void main(String[] args) { final int N = 20000; MemoryTest test = null; for (int i = 0; i <=N; i++) { test = new MemoryTest(); } System.out.println(test.weeks.length); } }
代码二:
public class MemoryTest { static class Data{ private int week; private String name; Data(int i, String s){ week = i; name = s; } } static Data weeks[] = { new Data(1,"monday"), new Data(2,"Tuesday"), new Data(3,"Wednesday"), new Data(4,"Thursday"), new Data(5,"Friday"), new Data(6,"Saturday"), new Data(7,"Sunday") }; public static void main(String[] args) { final int N = 20000; MemoryTest test = null; for (int i = 0; i <=N; i++) { test = new MemoryTest(); } System.out.println(test.weeks.length); } }
我想大家应该发现上面那两个类的区别了吧!
代码一会在内存中保存20000个weeks的副本,而代码二则在内存中保存1个weeks的副本,然后共享该副本,这样的话就不会造成内存的浪费。
虽然静态的变量能节约大量的内存,但是并不是所有的地方都适合用,建议大家在下列条件都符合的情况下,尽量用静态变量:
(1)变量所包含的对象体积较大,占用内存较多。
(2)变量所包含的对象生命周期较长。
(3)变量所包含的对象数据稳定。
(4)该类的对象实例有对该变量所包含的对象的共享需求。
如果变量不具备上述特点,建议不要轻易使用静态变量,以免弄巧成拙。
最后,再提一点内存的优化,就是有关对象的重用,比如:对象池和数据库连接池等。那样的话,是很节约内存空间的,不过,在用的时候要考虑各个方面,比如:运行环境的内存资源的限制等。为了防止对象池中的对象过多,要记得清除。
内存管理有许多技巧和方式
其实内存管理有许多技巧和方式,在这,我给大家介绍一下。
(1)要尽早的释放无用对象的引用。如果,该对象不用了,你可以把它设置为null。但要注意,如果该对象是某方法的返回值,千万不要这样处理,否则你从该方法中得到的返回值永远为空,而且这种错误不易被发现,因此这时很难及时抓住、排除NullPointerException异常。
(2)尽量少用finalize函数。因为它会加大GC的工作量,因此尽量少用finalize方式回收资源。
(3)如果需要使用经常用到的图片,可以使用soft应用类型(也就是转换为软引用类型),它可以尽可能将图片保存在内存中,供程序调用,而不引起OutOfMemory。
(4)注意集合数据类型,包括数组、树、图、链表等数据结构,这些数据结构对于GC来说,回收更为复杂。另外,要注意那些全局变量,静态变量,这些对象往往容易引起悬挂对象,造成内存浪费。
(5)尽量避免在类的默认构造器中创建、初始化大量的对象,防止在调用其子类的构造器时造成不必要的内存资源浪费。
(6)尽量避免强制系统做垃圾内存回收(通过显式调用方法System.gc()),增长系统做垃圾回收的最终时间,降低系统性能。
(7)尽量避免显式申请数组空间,当不得不显式申请数组空间时尽量准确的估计出其合理值,以免造成不必要的系统内存开销。
(8)尽量在做远程方法调用(RMI)类应用开发时使用瞬间值变量,除非远程调用端需要获取该瞬间值变量的值。