java学习——java基础(十二)之内存泄漏、内存溢出及JVM内存调优

来源:互联网 发布:mac电脑flash过期 编辑:程序博客网 时间:2024/06/06 04:55

写在前面:人生中的又一个选择,不知道选的是对还是错。昨天写的很晚了,今天补完剩下的。


1.what is 内存泄漏和内存溢出?

内存泄露:指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用。

内存溢出:指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。

 
从定义上可以看出内存泄露是内存溢出的一种诱因,不是唯一因素。


2.内存泄漏的场景以及解决办法

内存泄露一般是代码设计存在缺陷导致的,通过了解内存泄露的场景,可以避免不必要的内存溢出和提高自己的代码编写水平。


内存泄漏场景

(1) 长生命周期的对象持有短生命周期对象的引用

这是内存泄露最常见的场景,也是代码设计中经常出现的问题。

例如:在全局静态map中缓存局部变量,且没有清空操作,随着时间的推移,这个map会越来越大,造成内存泄露。

(2) 修改HashSet中对象的参数值,且参数是计算哈希值的字段

当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段,否则对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中删除当前对象,造成内存泄露。

(3) 机器的连接数和关闭时间设置

长时间开启非常耗费资源的连接,也会造成内存泄露。


内存泄漏解决方法:

(1) 尽早释放无用对象的引用。

(2) 使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域。

(3) 尽量少用静态变量,因为静态变量存放在永久代(方法区),永久代基本不参与垃圾回收。

(4) 避免在循环中创建对象。

(5) 开启大型文件或从数据库一次拿了太多的数据很容易造成内存溢出,所以在这些地方要大概计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。 


3.内存溢出的场景以及解决办法

通过了解内存溢出的几种常见情况,可以在出现内存溢出的时候快速的定位问题的位置,缩短解决故障的时间。


内存溢出场景:

(1) 堆内存溢出(outOfMemoryError:java heap space)

在jvm规范中,堆中的内存是用来生成对象实例和数组的。如果细分,堆内存还可以分为年轻代和年老代,年轻代包括一个eden区和两个survivor区。当生成新对象时,内存的申请过程如下:

(a) jvm先尝试在eden区分配新建对象所需的内存;

(b) 如果内存大小足够,申请结束,否则下一步;

(c) jvm启动youngGC,试图将eden区中不活跃的对象释放掉,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;

(d) Survivor区被用来作为Eden及old的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;

(e) 当OLD区空间不够时,JVM会在OLD区进行full GC;

(f) full GC后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”:outOfMemoryError:java heap space。

代码举例:

public class MemoryLeak {        private String[] s = new String[1000];      public static void main(String[] args) throws InterruptedException {          Map<String,Object> m =new HashMap<String,Object>();          int i =0;          int j=10000;          while(true){              for(;i<j;i++){                  MemoryLeak memoryLeak = new MemoryLeak();                  m.put(String.valueOf(i), memoryLeak);              }          }      }  }

(2) 方法区内存溢出(outOfMemoryError:permgem space)

在jvm规范中,方法区主要存放的是类信息、常量、静态变量等。所以如果程序加载的类过多,或者使用反射、gclib等这种动态代理生成类的技术,就可能导致该区发生内存溢出,一般该区发生内存溢出时的错误信息为:outOfMemoryError:permgem space

代码举例:将方法区的大小设置很低即可,在启动加载类库时就会出现内存不足的情况。

(3) 线程栈溢出(java.lang.StackOverflowError)

线程栈是线程独有的一块内存结构,所以线程栈发生问题必定是某个线程运行时产生的错误。一般线程栈溢出是由于递归太深或方法调用层级过多导致的。发生栈溢出的错误信息为:java.lang.StackOverflowError。

代码举例:

public class StackOverflowTest {        public static void main(String[] args) {          int i =0;          digui(i);      }      private static void digui(int i){          System.out.println(i++);          String[] s = new String[50];          digui(i);      }  }


内存溢出解决办法:

一个是优化程序代码,如果业务庞大,逻辑复杂,尽量减少全局变量的引用,让程序使用完变量的时候释放该引用能够让垃圾回收器回收,释放资源。

二就是物理解决,增大物理内存,然后通过:-Xms256m -Xmx256m -XX:MaxNewSize=256m -XX:MaxPermSize=256m的修改。

(1) Java虚拟机栈与本地方法栈

栈的大小控制参数时 -Xss。

Java虚拟机在栈中定义了两种异常:StackOverFlowError和OutOfMemeryError。当请求栈的深度大于java虚拟机所允许的最大深度则抛出StrackOverFlowError;如果Java虚拟机在栈扩展时,没有申请到足够的空间时,则抛出OutOfMemeryError


StrackOverFlowError:Java虚拟机在运行中,调用方法时,都要创建栈帧,当栈的空间不够时就会产生。

解决办法:只能是调节-Xss参数,或者减少方法的调用,减小栈帧的大小两种方式。


OutOfMemeryError:在栈上出现OOM一般是多线程的情形。拿32位操作系统来举例, 最大内存2G - Xmx(最大堆容量)- MaxPermSize(最大方法区容量)- 虚拟机本身耗费的内存和程序计数器使用的内存。 剩下的内存就是栈可以使用的空间,当Xss配置的参数一定时,那么在不断的创建线程过程中,遇到不能申请到栈空间的时候就会抛出OOM。

解决办法:调节-Xss参数降低栈大小,或者调节-Xmx以及MaxPermSize的大小扩大留给栈的空间。

(2) 方法区内存溢出

方法区的大小通过-PermSize和-MaxPermSize控制。

因为类常量和运行时常量也存储在方法区中,所以运行时常量过多也可导致方法区的OOM,但是没有直接控制常量池大小的参数。

解决办法:只能通过-PermSize和-MaxPermSize来间接控制。

(3) 堆内存的溢出

发生这种问题的原因是java虚拟机创建的对象太多,在进行垃圾回收之间,虚拟机分配的到堆内存空间已经用满了,与Heap space有关。

解决办法:

1)检查程序,看是否有死循环或不必要地重复创建大量对象。找到原因后,修改程序和算法。

2)增加Java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的大小。如:set JAVA_OPTS= -Xms256m -Xmx1024m。


4.内存调优

首先需要注意的是在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。


对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数,过多的GC和Full GC是会占用很多的系统资源(主要是CPU),影响系统的吞吐量。特别要关注Full GC,因为它会对整个堆进行整理,导致Full GC一般由于以下几种情况

(1)旧生代空间不足

调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 

(2)Pemanet Generation空间不足

增大Perm Gen空间,避免太多静态对象 

(3)统计得到的GC后晋升到旧生代的平均大小大于旧生代剩余空间

控制好新生代和旧生代的比例 

(4)System.gc()被显示调用

垃圾回收不要手动触发,尽量依靠JVM自身的机制 


调优手段主要是通过控制堆内存的各个部分的比例和GC策略来实现,下面来看看各部分比例不良设置会导致什么后果:

(1)新生代设置过小

一是新生代GC次数非常频繁,增大系统消耗;

二是导致大对象直接进入旧生代,占据了旧生代剩余空间,诱发Full GC。

(2)新生代设置过大

一是新生代设置过大会导致旧生代过小(堆总量一定),从而诱发Full GC;

二是新生代GC耗时大幅度增加。

一般说来新生代占整个堆1/3比较合适。

(3)Survivor设置过小

导致对象从eden直接到达旧生代,降低了在新生代的存活时间。

(4)Survivor设置过大

导致eden过小,增加了GC频率。

另外,通过-XX:MaxTenuringThreshold=n来控制新生代存活时间,尽量让对象在新生代被回收。


JVM提供两种较为简单的GC策略的设置方式:

(1)吞吐量优先

JVM以吞吐量为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,来达到吞吐量指标。这个值可由-XX:GCTimeRatio=n来设置。

(2)暂停时间优先

JVM以暂停时间为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,尽量保证每次GC造成的应用停止时间都在指定的数值范围内完成。这个值可由-XX:MaxGCPauseRatio=n来设置。


JVM常见配置

堆设置

-Xms:初始堆大小

-Xmx:最大堆大小

-XX:NewSize=n:设置年轻代大小

-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4

-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5

-XX:MaxPermSize=n:设置持久代大小


收集器设置

-XX:+UseSerialGC:设置串行收集器

-XX:+UseParallelGC:设置并行收集器

-XX:+UseParalledlOldGC:设置并行年老代收集器

-XX:+UseConcMarkSweepGC:设置并发收集器


垃圾回收统计信息

-XX:+PrintGC

-XX:+PrintGCDetails

-XX:+PrintGCTimeStamps

-Xloggc:filename


并行收集器设置

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。

-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间

-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)


并发收集器设置

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。

-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。


5.垃圾回收及内存相关面试题总结

(1) 什么叫垃圾回收机制? 

垃圾回收是一种动态存储管理技术,它自动地释放不再被程序引用的对象,按照特定的垃圾收集算法来实现资源自动回收的功能。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用,以免造成内存泄露。


(2) java的垃圾回收有什么特点?

JAVA语言不允许程序员直接控制内存空间的使用。内存空间的分配和回收都是由JRE负责在后台自动进行的,尤其是无用内存空间的回收操作(garbagecollection,也称垃圾回收),只能由运行环境提供的一个超级线程进行监测和控制。


(3) 垃圾回收器什么时候会运行? 

一般是在CPU空闲或空间不足时自动进行垃圾回收,而程序员无法精确控制垃圾回收的时机和顺序等。


(4) 什么样的对象符合垃圾回收条件? 

当没有任何获得线程能访问一个对象时,该对象就符合垃圾回收条件。


(5) 垃圾回收器是怎样工作的? 

垃圾回收器如果发现一个对象不能被任何活线程访问时,他将认为该对象符合删除条件,就将其加入回收队列,但不是立即销毁对象,何时销毁并释放内存是无法预知的。垃圾回收不能强制执行,然而java提供了一些方法(如:System.gc()方法),允许你请求JVM执行垃圾回收,而不是要求,虚拟机会尽其所能满足请求,但是不能保证JVM从内存中删除所有不用的对象。


(6) 一个java程序能够耗尽内存吗? 

可以。垃圾收集系统尝试在对象不被使用时把他们从内存中删除。然而,如果保持太多活的对象,系统则可能会耗尽内存。垃圾回收器不能保证有足够的内存,只能保证可用内存尽可能的得到高效的管理。


(7) 如何显示的使对象符合垃圾回收条件? 

1) 空引用:当对象没有对他可到达引用时,他就符合垃圾回收的条件。也就是说如果没有对他的引用,删除对象的引用就可以达到目的,因此我们可以把引用变量设置为null,来符合垃圾回收的条件。

StringBuffer sb = new StringBuffer("hello");System.out.println(sb);sb= null;

2) 重新为引用变量赋值:可以通过设置引用变量引用另一个对象来解除该引用变量与一个对象间的引用关系。 

StringBuffer sb1 = new StringBuffer(“hello”); StringBuffer sb2 = new StringBuffer(“goodbye”); System.out.println(sb1); sb1=sb2;//此时”hello”符合回收条件 

3) 方法内创建的对象:所创建的局部变量仅在该方法的作用期间内存在。一旦该方法返回,在这个方法内创建的对象就符合垃圾收集条件。有一种明显的例外情况,就是方法的返回对象。

public static void main(String[] args) {    Date d = getDate();    System.out.println("d="+d);}private static Date getDate() {    Date d2 = new Date();    StringBuffer now = new StringBuffer(d2.toString());    System.out.println(now);    return d2;}

4) 隔离引用:这种情况中,被回收的对象仍具有引用,这种情况称作隔离岛。若存在这两个实例,他们互相引用,并且这两个对象的所有其他引用都删除,其他任何线程无法访问这两个对象中的任意一个。也可以符合垃圾回收条件。

public class Island {    Island i;    public static void main(String[] args) {        Island i2 = new Island();        Island i3 = new Island();        Island i4 = new Island();        i2. i =i3;        i3. i =i4;        i4. i =i2;        i2= null;        i3= null;        i4= null;    }}

(8) 垃圾收集前进行清理——finalize()方法 

java提供了一种机制,使你能够在对象刚要被垃圾回收之前运行一些代码。这段代码位于名为finalize()的方法内,所有类从Object类继承这个方法。由于不能保证垃圾回收器会删除某个对象。因此放在finalize()中的代码无法保证运行。因此建议不要重写finalize()。


(9) GC用的引用可达性分析算法中,哪些对象可作为GC Roots对象? 

常说的GC(Garbage Collector) roots,特指的是垃圾收集器(Garbage Collector)的对象,GC会收集那些不是GC roots且没有被GC roots引用的对象。

Java虚拟机栈中的对象 

方法区中的静态成员 

方法区中的常量引用对象 

本地方法区中的JNI(Native方法)引用对象。 


(10) 用什么工具可以查出内存泄漏 ?

MemoryAnalyzer:一个功能丰富的 JAVA 堆转储文件分析工具,可以帮助你发现内存漏洞和减少内存消耗。

EclipseMAT:是一款开源的JAVA内存分析软件,查找内存泄漏,能容易找到大块内存并验证谁在一直占用它,它是基于Eclipse RCP(Rich Client Platform),可以下载RCP的独立版本或者Eclipse的插件。

JProbe:分析Java的内存泄漏。 


(11) JVM线程死锁,你该如何判断是因为什么?如果用VisualVM,dump线程信息出来,会有哪些信息?

常常需要在隔两分钟后再次收集一次thread dump,如果得到的输出相同,仍然是大量thread都在等待给同一个地址上锁,那么肯定是死锁了。


(12) 如何把程序写得更健壮?

(1)尽早释放无用对象的引用。 好的办法是使用临时变量的时候,让引用变量在退出活动域后,自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄露。对于仍然有指针指向的实例,jvm就不会回收该资源,因为垃圾回收会将值为null的对象作为垃圾,提高GC回收机制效率;

(2)定义字符串应该尽量使用String str=”hello”;的形式,避免使用String str = new String(“hello”);的形式。因为要使用内容相同的字符串,不必每次都new一个String。

(3)我们的程序里不可避免大量使用字符串处理,避免使用String,应大量使用StringBuffer,因为String被设计成不可变(immutable)类,所以它的所有对象都是不可变对象。

(4)尽量少用静态变量,因为静态变量是全局的,GC不会回收的;

(5)尽量避免在类的构造函数里创建、初始化大量的对象,防止在调用其自身类的构造器时造成不必要的内存资源浪费,尤其是大对象,JVM会突然需要大量内存,这时必然会触发GC优化系统内存环境;显示的声明数组空间,而且申请数量还极大。

(6)尽量在合适的场景下使用对象池技术以提高系统性能,缩减开销,但是要注意对象池的尺寸不宜过大,及时清除无效对象释放内存资源,综合考虑应用运行环境的内存资源限制,避免过高估计运行环境所提供内存资源的数量。

(7)大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理,然后解决一块释放一块的策略。

(8)不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象。可以适当的使用hashtable,vector创建一组对象容器,然后从容器中去取那些对象,而不用每次new之后又丢弃。

(9)一般都是发生在开启大型文件或跟数据库一次拿了太多的数据,造成Out Of Memory Error的状况,这时就大概要计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。

(10)尽量少用finalize函数,因为finalize()会加大GC的工作量,而GC相当于耗费系统的计算能力。

(11)不要过滥使用哈希表,有一定开发经验的开发人员经常会使用hash表(hash表在JDK中的一个实现就是HashMap)来缓存一些数据,从而提高系统的运行速度。比如使用HashMap缓存一些物料信息、人员信息等基础资料,这在提高系统速度的同时也加大了系统的内存占用,特别是当缓存的资料比较多的时候。其实我们可以使用操作系统中的缓存的概念来解决这个问题,也就是给被缓存的分配一个一定大小的缓存容器,按照一定的算法淘汰不需要继续缓存的对象,这样一方面会因为进行了对象缓存而提高了系统的运行效率,同时由于缓存容器不是无限制扩大,从而也减少了系统的内存占用。现在有很多开源的缓存实现项目,比如ehcache、oscache等,这些项目都实现了FIFO 、MRU等常见的缓存算法。

原创粉丝点击