java内存管理的原理_第1页
java内存管理的原理_第2页
java内存管理的原理_第3页
java内存管理的原理_第4页
java内存管理的原理_第5页
已阅读5页,还剩12页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、总述作为java开发人员,很少会去关心内存是如何分配与回收,java虚拟机为我们做好了一切,但并不代表开发人员可以对java内存管理的原理一无所知,java应用程序是很消耗内存的,特别是对Sever模式的应用程序,当并发高,运行时间长的时候,代码中内存的浪费也会导致应用程序的停顿、事务执行的失败。因此,了解java内存的管理对于编码的习惯,jvm垃圾回收器的选择等有助于应用程序效率的提高有很大的帮助。基础知识java内存块简述Java虚拟机的内存主要分为以下几块堆内存堆内存是java内存块中最大的一块内存,也是java虚拟机内存管理的主要内存块,它存放对象的实例,线程共享。想想看,我们应用程序

2、中有成千上万个对象,每个对象可能被实例化成几十个乃至上百个,可能还更多。我们来做一个简单的计算:一个bean对象有7个string型属性,对象是简单的getter/setter方法,大小是187byte。通过如下代码可以判断一个对象的大小:如果一个应用程序有5W个这样的对象,实例化100个。那么它占据堆内存大小为:(187*50000*100)/(1024*1024)=891.69M这个数据可以简单的想象下,当创建大量string对象,那么内存的消耗相当可观,因此在程序当中,要注意对象的创建,能重用的就重用,如String的对象最好用stringbuffer来代替等等。那么堆内存是如何管理的呢

3、,先从堆内存的结构说起:内存结构堆内存可以分为新生代和老年代,新生代又可以分为Eden区、From Survivor区、To Survivor区。异常会抛出OutOfMemoryError并进一步提示Java heap space.抛出异常原理及例子请看“管理与回收原理”.管理与回收原理Jvm对新生代和老年代有不同的管理方式和算法,也有不同的垃圾回收器进行回收。新生代这个区的大小可以通过-XX:NewRatio来设置,例如:-XX:NewRatio=4表示新生代和老代的比例是1:4。新生代的垃圾回收管理一般采用复制算法。这个算法是将内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内

4、存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。基于这种算法,JVM把新生代分为一块较大的Eden空间和两块较小的Survivor空间.1) 实例化一个对象时,首先会优先在Eden区域分配内存,但当这个对象很大的时候则直接进入老年代。a) 对象大到什么程度才会进入老年代呢?默认的大小是Eden的大小,但是可以通过-XX:PretenureSizeThreshold参数设置,如 -XX:PretenureSizeThreshold=3145728,这个参数不能与-Xmx之类的参数一样直接写3MB.看如下代码./* 参数:-Xms20M -Xmx20M -Xm

5、n10M -XX:+PrintGCDetails -XX:MaxTenuringThreshold=15 -XX:SurvivorRatio=8 -XX:+PrintTenuringDistribution -XX:PretenureSizeThreshold=3145728B */private void bigObject()int _1M=1024*1024;byte allo1;/分配3M,直接进入老年代.allo1=new byte3*_1M;b) 如何设置Eden的大小?通过比例Eden:Survivor的比例来设置,默认是8:1.例如-XX:SurvivorRatio=8,表示1

6、个Survivor和Eden的比值为1:82) 在Eden分配内存,首先要判断空间是否足够,如果不够则会进行一次Minor GC,以释放内存。释放内存后还是不够分配,则直接进入老年代。Minor GC中内存的调配,发生了以下动作:a) JVM会进行一次复制算法,把不能回收的对象,也就是还有引用的对象复制到第二块Survivor,这时要注意当第二块Survivor空间不足以存放复制对象时,则复制对象会晋升到老年代。请看如下代码:/* * 新生代不够分配,就直接分配到老年代,老年代还是不够直接报错 * 参数:-Xms30M -Xmx30M -Xmn10M -XX:+PrintGCDetails *

7、/private void notEnoughEdenAndOld()int _1M=1024*1024;/1:分配6M到新生代:Eden和一个Survivorbyte _6M=new byte6*_1M;/2:欲分配7M空间。/3:发现新生代空间不够,触发一次Minor GC,/先前分配的6M空间对象还有引用,故不能GC掉,基于复制的算法,/把6M的对象复制到第二块Survivor,第二块Survivor只有1M空间,不够存放6M的内容,/则6M对象直接进入老年代。/4:此时新生代够存放7M对象,7M对象分配给新生代/5:这时候新生代7M,老年代6Mbyte _7M=new byte7*_1

8、M;_6M=null;/6:欲在新生代分配6M空间,不够(见第3点描述)。/7:发生一次MinorGC。/8:在GC的过程中,7M的空间晋升老年代,6M分配给新生代。/9:这时候新生代6M,老年代7+6=13M,其中6M可以回收。byte _6M2=new byte6*_1M;_7M=null;/10:欲在新生代分配8M空间,不够(见第3点描述)/11:发生一次MinorGC/12:在GC的过程中,6M的空间晋升老年代。老年代6+13=19M,其中6+7=13M可以回收/13:此时新生代没有对象,老年代8M+7M=15Mbyte _8=new byte8*_1M;b) 长期存活的对象将进入老年

9、代。长期存活的对象默认是15岁,也就是经过了15次Minor GC还存在的对象,可以通过-XX:MaxTenuringThreshold来设置,例如-XX:MaxTenuringThreshold=1,意思是当MinorGC一次后,就直接把改对象放入老年代。参考如下代码:/* 参数:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:SurvivorRatio=8 */private void notEnoughEdenAndOld1()int _1M=1024*1024;byte allo1,a

10、llo2,allo3;/1:分配256到新生代:Eden和一个Survivorallo1=new byte_1M/4;/2:欲分配4M空间。/3:发现新生代空间足够,直接分配到新生代。/4:这时候新生代4.25M。allo2=new byte4*_1M;/5:欲分配5M空间。/6:发现新生代空间不够,触发一次Minor GC,/先前分配的4.25M空间对象还有引用,故不能GC掉,基于复制的算法,/把4.25M的对象复制到第二块Survivor。/7:此时新生代不够存放7M对象,7M对象分配给老年代/8:这时候老年代4.25M,新生代5M,并且标记为1岁allo3=new byte5*_1M;/

11、9:欲分配4M空间。/10:发现新生代空间不够,触发一次Minor GC,/新生代中5M的对象已经被标记为1岁,直接进入老年代,5524K>0K(9216K)完美清0,如果是-XX:MaxTenuringThreshold=15的话则不会清0/11:这时候老年代5+4=9M,新生代4.25Mallo3=new byte4*_1M;c) 动态年龄对象晋升到老年代:描述如下:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄.参考如下代码:/* 参

12、数:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:MaxTenuringThreshold=15 -XX:SurvivorRatio=8 */private void notEnoughEdenAndOld2()int _1M=1024*1024;byte allo1,allo2,allo3,allo4;/1:分配256到新生代:Eden和一个Survivorallo1=new byte_1M/4;allo2=new byte_1M/4;allo3=new byte4*_1M;/触发一次MinorGC,allo1、allo2、allo3被标记为

13、1岁/晋升到老年代的是allo3对象/这时候老年代的大小差不多是4M-256*2K(剩余allo1、allo2和部分allo3对象在另一块Survivor空间),新生代的大小是4M+256*2Kallo4=new byte4*_1M;allo4=null;/触发一次MinorGC,allo1、allo2的等于Survivor空间的一半,因为allo1、allo2和部分allo3晋升到老年代/allo4被回收/新生代大小是4M,老年代大小是4M-256*2M+256*2M+部分的allo3,结果是4Mallo4=new byte4*_1M;d) 什么样的对象是不能回收的对象呢?JVM是使用根搜索

14、算法(GC RootsTracing)判断对象是否存活的。这个算法的基本思路是通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。老年代老年代的垃圾回收与管理效率不高,大概是新生代的十分之一左右,这跟算法有关,老年代不能采用复制的算法,原因有2个:1:复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低.而老年代存放的大部分是长久存活的对象,会倒是较多的复制操作.2

15、:当对象100%存活的情况下,第二块Survivor空间将很大可能放不下,需要额外的空间担保.老年代以外已经没有额外的空间担保了.因此老年代一般采用采用标记-清除算法或者是标记整理算法.1) 标记-清除:分为标记和清楚两个阶段:首先标记处所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象.它有2个缺点:a) 效率问题,标记和清除过程的效率都不高.b) 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作.2) 标记-整理:该算法的标记过程和标记-清除的标记一致,但后续

16、步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存.效率会比标记-清除算法来的高.晋升到老年代的对象,当碰到如下情况会发生FULL GC:1:老年代空间不足,看如下代码:/* * 参数:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails */private void old()int _1M=1024*1024;byte allo1,allo2,allo3;/大对象直接进入老年代allo1=new byte8*_1M;/新生代足够存放2Mallo2=new byte2*_1M;/分配7M给新生代,但是不够分配,

17、触发一次Minor GC,/新生代的2M晋升到老年代,此时老年代空间不够,触发一次FULL GC,对象没有释放,空间不够,直接报错allo3=new byte7*_1M;2:老年代空间足够,但是不允许担保失败,看如下代码:/* -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:-HandlePromotionFailure */private void old()int _1M=1024*1024;byte allo1,allo2,allo3,allo4,allo5,allo6,allo7;/2M进入新生代allo1=new byte2*_1M;/

18、2M进入新生代,总共有4Mallo2=new byte2*_1M;/触发一次MinoGC,4M进入老年代/这时候新生代4M,老年代4Mallo3=new byte4*_1M;/3M进入新生代/这时候新生代7M,老年代4Mallo4=new byte3*_1M;allo3=null;allo2=null;/触发一次MinoGC,回收6M空间(allo3+allo2),另外2M空间进入老年代,参数设置不允许担保/2M<老年代剩余空间,因此会触发一次FULL GCallo5=new byte3*_1M;3:不够空间存放大对象,看如下代码:/* * 参数:-Xms20M -Xmx20M -Xmn

19、10M -XX:+PrintGCDetails */private void old()int _1M=1024*1024;byte allo1,allo2,allo3;/新生代和老年代都不够存放,触发FULL GC,并且报错allo1=new byte20*_1M;堆内存分配过程图方法区方法区内存块存放的是被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它是线程共享的。这块内存会导致OutOfMemoryError:PermGen space。以下的操作为导致内存溢出:1:使用CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的class可以加载到内

20、存。2:大量JSP或动态产生JSP文件的应用。3:常量池的溢出。请看如下代码:/* * 常量池溢出模拟 * 参数-XX:PermSize=10M -XX:MaxPermSize=10M -XX:+PrintGCDetails */private void changliangchi()List<String> list=new ArrayList<String>();int i=0;while(true)list.add(String.valueOf(i+).intern();Sern这个方法的意思是:如果池已经包含一个等于此 String 对象的字符

21、串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。如果不用intern这个方法上述代码将抛出堆内存溢出的错误。垃圾回收分为2部分内容:1:废弃常量。没有地方引用的常量.2:无用的类。1:该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。2:加载该类的ClassLoader已经被回收。3:该类对于的java.lang.Class对象没有任何地方被引用,无法再任何地方通过反射访问该类的方法栈Java虚拟机栈是线程私有的,它的生命周期与线程相同。每个方法被执行的时候都会同时创建一个栈帧

22、用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。所以不会出现栈帧内存溢出的异常。局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(返回对象的类型)。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这

23、个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。栈会抛出2中异常:1:StackOverflowError。如果线程请求的栈深度大于虚拟机所允许的最大深度,抛出异常。2:OutOfMemoryError如果虚拟机在扩展时无法申请到足够的内存空间,抛出异常。可以通过-Xss参数来模拟这2中异常的抛出。-Xss128K:减少栈内存大小,减少128K。那么通过上面的描述可以知道,所有线程的栈是固定的,那么当每个线程的栈比较大(也就是说-Xss设置的比较小),那么能分配到的线程就比较少。这时候可能抛出第二种异常。如下代码:/* * 栈异常测试 * 参数-Xm

24、s1024M -Xmx1024M -XX:PermSize=256M -XX:MaxPermSize=256M -Xss83201K */public static void main(String args) throws Throwable/不会打印12,因为栈内存设为0,则线程请求不到内存分配,直接抛出异常。System.out.println(12);JvmErrorTest jvmErrorTest=new JvmErrorTest();try jvmErrorTest.methodStack(); catch (Throwable e) System.out.println(jvm

25、ErrorTest.stackLength);throw e;private void methodStack()stackLength+;methodStack();如果-Xss设置的比较大,则每个线程栈内存就比较小,那么线程分配的多,这种时候就可以尽量避免第二种错误,但这种分配栈内存小,也就意味着每个栈的栈深度比较小,就有可能出现第一种情况异常,如下代码:/* * 栈异常测试 * 参数-Xms1024M -Xmx1024M -XX:PermSize=256M -XX:MaxPermSize=256M -Xss128K */public static void main(String arg

26、s) throws ThrowableJvmErrorTest jvmErrorTest=new JvmErrorTest();try jvmErrorTest.methodStack(); catch (Throwable e) / TODO: handle exceptionSystem.out.println(jvmErrorTest.stackLength);throw e;private void methodStack()stackLength+;/递归调用,当达到最大栈深度,则报错methodStack();实际上第一种异常也可以看出是内存的溢出,只不过JVM把这种内存的溢出转化

27、成栈深度的溢出。程序计数器它是一块比较小的内存空间,作用是当前线程所执行的字节码的行号指示器。也是线程私有的。不会出现内存异常情况。Java虚拟机垃圾回收器的选择新生代垃圾回收器和老年代垃圾回收器的搭配图:单线程垃圾回收器:(单CPU环境下效果比较好,一般应用与Client模式)新生代Serial收集器该收集器采用复制的算法,单线程的意义不仅仅意味着它只会使用一个CPU或者是一条线程去完成垃圾回收工作,更重要的是在它进行垃圾回收时,会暂停其他所有的工作线程,这样会造成应用程序的停顿,带给用户恶劣的体验,俗称“Stop The World”.老年代Serial Old收集器使用标记-整理算法,该

28、收集器也可以用在Server模式下,一个是在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用(Parallel Scavenge收集器本身有PS MarkSweep收集器来进行老年代收集,并没有直接使用Serial Old收集器,但是这个PS MarkSweep收集器是以Serial Old收集器为模板设计的,与Serial Old的实现非常接近);另一个是做为CMS收集器的后备方案,在并发手机发生Concurrent Mode Failure的时候使用。多线程垃圾回收器:(大于1个的CPU效果比较好,一般应用与Server模式)新生代ParNew收集器该收集器跟相

29、差不大,唯一一个区别就是多线程,它是一个并行收集器。并行:指多条垃圾收集线程并行工作,但用户线程仍然处于等待状态。并发:指用户线程与垃圾收集线程同时执行。Parallel Scavenge收集器该收集器使用复制算法,也是并行的多线程收集器,它跟ParNew收集器有什么区别呢?关注点不同,Parallel Scavenge收集器关注吞吐量,所谓的吞吐量指的是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),如虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。老年代Parellel Old收集器该收集器是1.6版本才提供和Parallel Scavenge搭配使用CMS收集器该收集器是一种以获取最短回收停顿时间为目标的收集器。一般用在互联网或者B/S系统的服务端上。它是基于“标记-清楚”算法实现的。默认启动线程是(CPU数量+3)/4。当CPU在4个以上时,并发回收时垃圾收集线程最多占用不超过25%。如果CPU不足4个时,那么CMS对用户程序的影响就可能变得很大。缺点:1:无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生,并且启用备用垃圾回收器Serial Old,这样停顿时间就很长了。浮动垃圾:由

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论