jvm之内存管理机制分享

jvm之内存管理机制分享

我们都知道,对于从事c和c++程序开发的人员来说,在内存管理领域,他们拥有每一个对象的生杀大权。但对于java程序员来说,在虚拟机的自动内存管理机制的帮助下,不再需要为每一个new操作去写配对的delete/free代码,而且也不容易出现内存泄漏和内存溢出问题,这一切都交由虚拟机管理,但是一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机怎样使用内存,排查错误将成为一项非常艰难的工作。今天我就分享下java虚拟机内存机制、垃圾回收机制、排查问题及案例分析。

1.java内存区域——运行时数据区

Java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,主要包括以下几个运行时数据区。

 
 
 

 

1.1 程序计数器

程序计数器是一块较小的内存空间,他的作用是指向下一条即将执行的指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。每条线程都有一个独立的程序计数器,各线程之间的计数器互不影响,独立存储,这类内存区域为“线程私有”的内存。此内存区域是唯一一个在java虚拟机规范中没有规定任何outofmemoryerror情况的区域。

1.2java虚拟机栈

也是线程私有的。虚拟机栈描述的是java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。Java虚拟机规范中,对此区域规定了两种异常情况:StackOverflowError异常(线程请求的栈深度大于虚拟机所允许的深度)与OutOfMemoryError异常(动态扩展时无法申请到足够的内存)

1.3 本地方法栈

与虚拟机栈类似,不过虚拟机栈为虚拟机执行java方法服务,而本地方法栈为虚拟机使用本地方法服务,与java虚拟机栈一样,本地方法栈区也会抛出StackOverflowError异常与OutOfMemoryError异常。

1.4java堆

是java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域。在java虚拟机规范中,所有的对象实例及数组都在这里分配内存。Java堆是垃圾收集器管理的书要区域,很多时候也被称作GC堆。从内存回收的角度看,java堆还可以细分为新生代与老年带。Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。通过-Xmx和-Xms来控制是否可以扩展。如果在堆中没有内存来完成实例分配,且堆也无法扩展时,就会抛出OutOfMemoryError异常。

1.5 方法区

与java堆一样,是各个线程共享的内存区域。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。很多程序员习惯把方法区称为“永久代”,因为垃圾收集在这个区域比较少出现。当这个方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

2.对象访问

介绍完java虚拟机的运行时数据区后,我们来介绍下在java语言中,对象访问是如何进行的?举个简单的例子。

Object obj=new Object();

Object obj反应到java栈的本地变量中,作为一个引用类型数据出现。

new Object()反应到java堆中,引用类型早java虚拟机中通过哪种方式去定位?目前主流的有两种:使用句柄与直接指针,具体如下

使用句柄:java堆中划分出一块内存来存储句柄池,reference中存储的是对象的句柄地址,句柄中包含对象实例数据和类型数据的具体地址。如下图

 

 

好处:在对象移动时,不需要改变reference

坏处:速度慢

直接指针方式:java堆中为访问对象的相关信息,reference中直接存放对象地址。如下图

 
 
 

好处:速度快,节省了一次指针定位的时间开销

坏处:对象移动时,要修改reference

3.垃圾回收机制

3.1如何判断对象已死

垃圾收集器在对堆进行回收前,第一件事情就是要确定对象是否已死。下面说下如何判断对象是否存活。

3.1.1 引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值减1.任何时候,计数器值为0的对象就是死亡的对象,可以被回收。Java语言中没有选用该方法来管理内存,因为它很难解决对象之间的循环引用。

3.1.2根搜索算法

通过一系列的名为“GC Roots“的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用连相连时,则证明此对象是可以回收的。在根搜索中不可达的对象,也并非是”非死不可“的,这时候它们暂时处于”缓刑“阶段,要真正宣告一个对象死亡,至少要经历两次标记过程,这个不细说了。

3.2 垃圾收集算法

3.2.1 标记-清除算法

标记出所有需要回收的对象,标记完成后统一回收掉。

 

 

 

 

 

 
 

缺点:

(1)效率问题

(2)会产生大量不连续的内存碎片

3.2.2 复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后把第一块的内存一次清理掉。

 
 
 


优点:简单高效,不会产生内存碎片

缺点:以缩小一半内存为代价

目前商业虚拟机都采用这种收集算法来回收新生代,但是新生代中的对象98%都是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间。每次使用eden和其中的一块Survivor,当回收时,将eden和Survivor中还存活的对象一次性拷贝到另外一块Survivor中,最后清理掉eden和第一块Survivor。默认eden和Survivor的大小比例是8:1。我们不能保证每次回收都只有不多于10%的对象存活,当第二块Survivor空间不足时,需要依赖老年代。

3.2.3 标记-整理算法

老年代适合这种算法。标记过程与标记-清除算法一样,但是不是直接对标记的对象进行清除,而是让未标记的存活对象都向一端移动,然后直接清理掉边界以外的内存。算法如下:

 

3.2.4 分代收集算法

当前商业虚拟机的垃圾回收都采用“分代收集“算法,该算法根据对象的存活周期将不同内存划分为几块,一般将java堆分为新生代与老年代,在新生代采用复制算法,在老年代采用标记-清理算法。

3.3 垃圾收集器

垃圾收集算法是方法论,真正实现要靠垃圾收集器。针对不同的代,垃圾收集器主要有以下几种

 

 
 

(1)Serial收集器

在进行垃圾收集时,必须停止其他所有的工作线程。

(2)ParNew收集器

是Serial收集器的多线程版本。使用多线程进行垃圾收集。默认开启的收集器线程跟CPU的数量一样。对于多CPU,可以减少当机时间。

(3) ParallelScavenge收集器

也是新生代收集器,也是多线程收集器。他的关注点不是尽可能地缩短垃圾收集时用户线程的停顿时间,他的关注点在于达到一个可控制的吞吐量。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。

(4) Serial Old收集器

是Serial收集器的老年代版。

(5) ParNew Old收集器

ParallelScavenge收集器的老年代版。

(6)CMS收集器

并发收集,低停顿。默认情况下,CMS收集器在老年代使用了60%的空间后就会被激活,如果在应用中,老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高出发百分比,以降低内存回收次数获取更好的性能。

(7)G1收集器

目前最好的收集器,且新生代、老年代都适用。

3.4 内存分配与回收策略

(1)对象优先在Eden分配

见样例Minor_GC.

(2)大对象直接进入老年代

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代中分配。见样例Minor_GC_1

(3)长期存活的对象将进入老年代

对象在Survivor区中,每熬过一次MinorGC,年龄就增加1岁,当年龄增加到一定程度时,默认为15岁,就会被晋升到老年代中,晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。分别设置-XX:MaxTenuringThreshold=1与-XX:MaxTenuringThreshold=15来执行下代码Minor_GC_2。如果在Survivor空间中相同的年龄所有对象大小总和大于Survivor空间的一半时,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。(注:试了下,JDK6满足,JDK7不满足)

(4)Full GC

在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure的设置,如果HandlePromotionFailure=true,只进行Minor GC,否则,则进行FullGC。

4. 虚拟机监控与故障分析工具

4.1jdk工具

监控虚拟机常用的JDK命令行工具有以下几种

(1)jps:虚拟机进程状况工具

 
 
 

 


(2)jstat:虚拟机统计信息监视工具

 
 
 


(3)jinfo:java配置信息工具

实时查看和调整虚拟机的各项参数。

如:

 
 
 

(4)jmap:java内存映像工具

很多功能在windows下受限!

(5)jhat:虚拟机堆转储快照分析工具

与jmap搭配使用,分析jmap生成的堆转储快照。

(6)jstack:java堆栈跟踪工具

用于生成虚拟机当前时刻的线程快照。

 
 
 

 

4.2可视化工具

Jconsole与jvisualvm

5.内存溢出异常

在java虚拟机规范中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有可能发生OutOfMemoryError异常。

5.1 java堆溢出

Java堆内存异常是最常见的内存异常,出现java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“java heapspace”。

-XX:+HeapDumpOnOutOfMemoryError当前内存溢出异常时,dump出内存堆转储快照。

例如HeapOMM。

根据dump文件分析是内存泄漏还是内存溢出?

使用mat插件,具体分析方法见

http://www.blogjava.net/rosen/archive/2010/05/21/321575.html

http://www.blogjava.net/rosen/archive/2010/06/13/323522.html

http://chiyx.iteye.com/blog/1528782

5.2虚拟机栈与本地方法栈溢出

操作系统分配给每个进程的内存是有限的,32位的windows限制为2GB,虚拟机提供参数来控制java堆和方法区这两部分内存的最大值,剩余的2GB(操作系统限制)-Xmx(最大堆容量)-MaxPermSize(最大方法区容量),程序计数器消耗内存可以忽略。这些内存就由虚拟机栈与本地方法栈瓜分了。我们可以通过使用-Xss参数减少虚拟机栈内存容量,制造栈溢出实例,抛出StackOverflowError异常。见用例JavaVMStackSOF。

5.3运行时常量池溢出

通过-XX:PermSize=10m-XX:MaxPermSize=10m限制方法区的大小,从而间接限制常量池的容量(因为常量池在方法区中)。见样例RuntimeConstantPoolOOM。运行时常量池溢出,在OutOfMemoryError后跟随的提示信息是“PermGen space”。

5.4 方法区溢出

方法区用于存放Class的相关信息,我们可以想办法时运行时产生大量的类(即大量的字节码)去填满方法区,直到溢出。

方法区溢出与运行时常量池溢出提示信息都是在OutOfMemoryError后跟随的提示信息是“PermGenspace”。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值