2.垃圾收集算法
2.1 标记-清除算法
标记阶段:先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程其实就是上述的可达性分析算法中的标记过程。它是最基础的算法,后续的收集算法都是基于这种思路并对其不足进行改进而得到的。
不足之处主要有两个:
1.效率问题,标记和清除两个过程效率都不高。
2.空间问题,标记清除之后会产生大量不连续的碎片,可能会导致后续程序需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
2.2 复制算法
在上述标记-清除算法的基础上,为了解决效率问题,复制算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动对顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点就是,将内存缩小为原来的一半,代价较高;当对象存活率较高时就要进行较多的复制操作,效率将会变低。
2.3 标记-整理算法
复制算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以老年代一般不能直接选用这种算法。标记-整理算法(Mark-Compact)的标记过程与”标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
3.与清理相关的方法
3.1 gc()
对于程序员来说,GC基本是透明的,不可见的。运行GC的函数是System.gc(),调用后启动垃圾回收器开始清理。
但是根据Java语言规范定义, 该函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低。
JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。
3.2 finalize()
finalize()是Object类中的方法。
了解C++的都知道有个析构函数,但是注意,finalize()绝不等于C++中的析构函数。
Java编程思想中是这么解释的:一旦GC准备好释放对象所占用的的存储空间,将先调用其finalize()方法,并在下一次GC回收动作发生时,才会真正回收对象占用的内存,所以一些清理工作,我们可以放到finalize()中。
该方法的一个重要的用途是:当在java中调用非java代码(如c和c++)时,在这些非java代码中可能会用到相应的申请内存的操作(如c的malloc()函数),而在这些非java代码中并没有有效的释放这些内存,就可以使用finalize()方法,并在里面调用本地方法的free()等函数。
所以finalize()并不适合用作普通的清理工作。
但是从很多方面了解,该方法都是被推荐不要使用的,并被认为是多余的。
六、内存溢出和内存泄露分析
1.内存溢出
内存溢出:OOM(OutOfMemoryError)异常,即程序需要的内存超出了虚拟机可以分配内存的最大范围。在Java 虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他区域都可能发生OOM异常。
2.内存溢出区域
2.1 Java 堆溢出
Java 堆用于存储对象实例,只要不断地创建对象,并且保证垃圾回收机制清除这些对象,那么在对象数量达到最大堆限制就会产生内存溢出异常。
测试方案:无限循环new对象实例出来,在List中保存引用,防止GC回收,最终会产生OOM ,异常堆栈信息并提示Java heap space。
2.2 虚拟机栈和本地方法栈溢出
关于虚拟机栈和本地方法栈,Java虚拟机规范中定义了两种异常:
a.如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError 异常。 b.如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
StackOverflowError异常:
单线程条件下,通过不断递归调用方法,如不断累加的方法,如下所示
public class JavaVMStackSOF{ private int stackLength=1; public void stackLeak(){ stackLength++;//累加变量 stackLeak();//调用自身 } }
最终会产生StackOverflowError栈溢出异常;
OutOfMemoryError异常:
多线程条件下,无限循环地创建线程,并为每个线程无限循环的增加内存,最终会导致OutOfMemoryError异常。
2.3 方法区和运行时常量池溢出
运行时常量池是方法区的一部分。方法区用于存放Class的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等。
测试方法:
1.对于非常量池部分,运行时生成大量的动态类填满方法区; 2.对于常量池部分,无限循环调用String的intern()方法产生不同的String对象实例,并在List中保存其引用,以防止被GC回收,最终会产生溢出。
2.4 本机直接内存溢出
此类内存溢出一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,可以考虑一下是不是这方面原因。
3 内存泄露
内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你OOM。
Java内存泄漏的根本原因是长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收。
3.1 静态集合类引起内存泄漏
像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
3.2 集合里面的对象属性被修改,再调用remove()方法不生效
例如:
public static void main(String[] args){ Set<Person> set = new HashSet<Person>(); Person p1 = new Person("唐僧","pwd1",25); Person p2 = new Person("孙悟空","pwd2",26); Person p3 = new Person("猪八戒","pwd3",27); set.add(p1); set.add(p2); set.add(p3); System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素! p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变 set.remove(p3); //此时remove不掉,造成内存泄漏 set.add(p3); //重新添加,居然添加成功 System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素! for (Person person : set) { System.out.println(person); } }
3.3 监听器
在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
3.4 各种连接
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。
3.5 单例模式
不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏
七、引用类型
如果一个内存中的对象没有任何引用的话,就说明这个对象已经不再被使用了,从而可以成为被垃圾回收的候选。不过由于垃圾回收器的运行时间不确定,可被垃圾回收的对象的实际被回收时间是不确定的。对于一个对象来说,只要有引用的存在,它就会一直存在于内存中。如果这样的对象越来越多,超出了JVM中的内存总数,JVM就会抛出OutOfMemory错误。虽然垃圾回收的具体运行是由JVM来控制的,但是开发人员仍然可以在一定程度上与垃圾回收器进行交互,其目的在于更好的帮助垃圾回收器管理好应用的内存。这种交互方式就是使用JDK 1.2引入的java.lang.ref包,下图是JDK1.7中ref包的结构层次图。
7.1 强引用(StrongReference)
在一般的Java程序中,见到最多的就是强引用。如Date date = new Date(),date就是一个对象的强引用。对象的强引用可以在程序中到处传递。很多情况下,会同时有多个引用指向同一个对象。强引用的存在限制了对象在内存中的存活时间。假如对象A中包含了一个对象B的强引用,那么一般情况下,对象B的存活时间就不会短于对象A。如果对象A没有显式的把对象B的引用设为null的话,就只有当对象A被垃圾回收之后,对象B才不再有引用指向它,才可能获得被垃圾回收的机会。
7.2 软引用(SoftReference)
软引用在强度上弱于强引用,通过类SoftReference来表示。它的作用是告诉垃圾回收器,程序中的哪些对象是不那么重要,当内存不足的时候是可以被暂时回收的。当JVM中的内存不足的时候,垃圾回收器会释放那些只被软引用所指向的对象。如果全部释放完这些对象之后,内存还不足,才会抛出OutOfMemory错误。软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。比如考虑一个图像编辑器的程序。该程序会把图像文件的全部内容都读取到内存中,以方便进行处理。而用户也可以同时打开多个文件。当同时打开的文件过多的时候,就可能造成内存不足。如果使用软引用来指向图像文件内容的话,垃圾回收器就可以在必要的时候回收掉这些内存。
public class ImageData { private String path; private SoftReference<byte[]> dataRef; public ImageData(String path) { this.path = path; dataRef = new SoftReference<byte[]>(new byte[0]); } private byte[] readImage() { return new byte[1024 * 1024]; //省略了读取文件的操作 } public byte[] getData() { byte[] dataArray = dataRef.get(); if (dataArray == null || dataArray.length == 0) { dataArray = readImage(); dataRef = new SoftReference<byte[]>(dataArray); } return dataArray; } }
在运行上面程序的时候,可以使用 -Xmx 参数来限制JVM可用的内存。由于软引用所指向的对象可能被回收掉,在通过get方法来获取软引用所实际指向的对象的时候,总是要检查该对象是否还存活。
7.3 弱引用(WeakReference)
弱引用在强度上弱于软引用,通过类WeakReference来表示。它的作用是引用一个对象,但是并不阻止该对象被回收。如果使用一个强引用的话,只要该引用存在,那么被引用的对象是不能被回收的。弱引用则没有这个问题。在垃圾回收器运行的时候,如果一个对象的所有引用都是弱引用的话,该对象会被回收。弱引用的作用在于解决强引用所带来的对象之间在存活时间上的耦合关系。
弱引用最常见的用处是在集合类中,尤其在哈希表中。哈希表的接口允许使用任何Java对象作为键来使用。当一个键值对被放入到哈希表中之后,哈希表对象本身就有了对这些键和值对象的引用。如果这种引用是强引用的话,那么只要哈希表对象本身还存活,其中所包含的键和值对象是不会被回收的。如果某个存活时间很长的哈希表中包含的键值对很多,最终就有可能消耗掉JVM中全部的内存。
对于这种情况的解决办法就是使用弱引用来引用这些对象,这样哈希表中的键和值对象都能被垃圾回收。Java中提供了WeakHashMap来满足这一常见需求。
7.4 虚引用(PhantomReference)
在介绍虚引用之前,要先介绍Java提供的对象终止化机制(finalization)。在Object类里面有个finalize方法,其设计的初衷是在一个对象被真正回收之前,可以用来执行一些清理的工作。因为Java并没有提供类似C++的析构函数一样的机制,就通过 finalize方法来实现。但是问题在于垃圾回收器的运行时间是不固定的,所以这些清理工作的实际运行时间也是不能预知的。虚引用(phantom reference)可以解决这个问题。在创建虚引用PhantomReference的时候必须要指定一个引用队列。当一个对象的finalize方法已经被调用了之后,这个对象的虚引用会被加入到队列中。通过检查该队列里面的内容就知道一个对象是不是已经准备要被回收了。
八、java内存模型
8.1 主内存和工作内存
Java内存模型规定了所有的变量都存储在主内存中(Main Memory)中(此处的主内存和介绍物理硬件时的主内存名字一样,两者也可以互相类比,但此处仅是虚拟机内存的一部分)。每条线程还有自己的工作内存(Working Memory,可与前面所讲的处理器高速缓存类比),线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程也无法直接访问对方工作内存中的变量,线程间变量的传递需要通过主内存来完成。
线程、主内存、工作内存的交互关系如下图所示:
九、JVM的生命周期
(1)JVM实例的诞生:当启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。
(2)JVM实例的运行:main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程。
(3)JVM实例的消亡:当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。