JVM内存管理

来源:互联网 发布:华为机顶盒 mac地址 编辑:程序博客网 时间:2024/05/17 05:16

JVM是按照运行时数据(Runtime Data)的存储结构来划分内存结构的。在Java虚拟机规范中将Java运行时数据划分为6种。

内存结构:

程序技术器,Java栈,堆,方法区,本地方法区。运行时常量池。
程序计数器
当前线程所执行的字节码的行号指示器。用于保存当前正常执行的程序的内存地址。这很好理解,它就像一个记事员一样记录下哪个线程当前执行到哪条指令了。
JVM规范只定义了Java方法需要记录指针信息,而对于Native方法,并没有要记录执行的指针地址。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
Java栈:
Java栈总是和线程关联在一起,每当创建一个线程时,JVM就会为这个线程创建一个对应的Java栈,在这个Java栈中又会创建一个栈帧,这些栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,每个栈帧会含有一些内部变量,操作栈和方法返回值等信息。
由于Java栈是与Java线程对应起来,这个数据不是线程共享的,所以我们不用关心它的数据一致性问题,也不会存在同步锁的问题。

此内存的唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存。
堆是存储Java对象的地方,它是jvm管理Java对象的核心存储区域,堆是Java程序员最应该关心的。
每一个存储在堆中的Java对象都会是这个对象的类的一个副本,它会复制包括自它父类的所有非静态属性。
堆是被所有Java线程所共享的,所以对它的访问需要注意同步问题,方法和对应的属性都需要保证一致性。
方法区:
jvm方法区是用于存储类信息,常量,静态变量,方法数据,方法体,构造函数,包括类中专用方法,实例初始化,接口初始化都存储在这个区域。不像Java堆一样会频繁的被GC回收器回收,它存储的信息相对比较稳定。仍然会被垃圾回收器管理。
运行时常量池
是方法区的一部分。用于存放编译期生成的各种字面量和符号引用。如编译期的数字常量,方法或域的引用。它的存储受方法区的规范约束,如果常量池无法分配,同样会抛出OutOfMemoryError
本地方法栈:
本地方法栈是为jvm运行Native方法准备的空间,为Native方法服务。本地方法栈也会抛出OutOfMemoryError和StackOverflowError

JVM内存分配:

jvm内存分配主要基于两种,分别是堆和栈。

Java栈的分配是和线程绑定在一起的,当我们创建一个线程时,很显然,这个线程创建一个新的Java栈,一个线程的方法的调用和返回对应与这个栈的压栈和出栈。当线程激活一个Java方法时,JVM就会在线程的Java堆栈中新压入一个帧,这个栈用来保存参数,局部变量,中间计算过程和其他数据。
栈中主要存放一些基本数据类型的变量数据(int short long byte float double boolean)和对象句柄。存取速度比堆要快,仅次于寄存器,栈数据可以共享。缺点是存在栈中的数据大小与生存期必须是确定的,这也导致了其灵活性。

堆:
每个Java应用都唯一对应一个jvm实例,每个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用程序所有的线程共享。在Java中堆内存是自动初始化的,所有对象的存储空间都是在堆中分配,这个对象的引用却是在堆栈中分配。也就是说在建立一个对象时两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。
堆的优势是可以动态地分配内存的大小,生存期也不必事先告诉编译期,因为它是在运行时动态分配内存的。但缺点是,由于是要在运行时动态分配内存,存取速度较慢。

JVM内存回收策略:
显式的内存申请有两种:一种是静态内存分配,另一种是动态内存分配。
静态内存分配:是指在Java被编译时就已经能够确定需要的内存空间,当程序被加载时系统把内存一次性分配给它。这些内存不会在程序执行时发生变化。在Java类和方法中的局部变量包括原生数据类型(int, long ,char等)和对象的引用都是静态分配内存的。原生数据类型存储在栈中。

动态分配内存:是在程序执行时才知道要分配的存储空间大小,而不是在编译时就能够确定的。对象类型存储在堆中,如Integer,String,Object.

如何检测垃圾:
垃圾收集器必须能够完成两件事情:一件是能够正确检测出垃圾对象,另一件是能释放垃圾对象占用的内存空间。
什么东西会被回收?只要是某个对象不再被其他活动对象引用,那么这个对象就可以被回收。这里的活动对象指的是能够被一个根对象集合到达的对象。

基于分类的垃圾收集算法:

JVM将堆划分为young区,old区,perm区,分别存放不同年龄的对象。
Young区:
Young区又分为Eden区和两个Survivor区,其中所有新创建的对象都在Eden区,当Eden区满后会触发minor GC将Eden区仍然存活的对象复制到其中一个Survivor区中,另外一个Survivor区中的存活对象也复制到这个Survivor中,以保证始终有一个Survivor区是空的。

Old区
存放的是Young区的Survivor满后触发 minor GC后仍然存活的对象,当Eden区满后会将对象存放到Survivor区中,如果Survivor仍然存放不下这些对象,GC收集器会将这些对象直接存放到Old区,如果在Survivor区中的对象足够老,也直接存放到Old区。如果Old区也满了,将会触发full GC,回收整个堆内存。
Perm区
存放的主要是类的Class对象,如果一个类被频繁地加载,也可能会导致Perm区满,Perm区的垃圾回收也是由Full GC 触发的。
一般建议Young区的大小为整个堆的1/4,而Young区中Survivor一般设置为整个Young区的1/8.

Serial Collector串行单线程收集器
我们指定所有的对象都在Young区的Eden中创建,但是如果创建的对象超过了Eden区的大小,或者超过了PretenureSizeThreshold配置参数配置的大小,就只能在Old区分配了。

当Eden空间不足就触发了Minor GC,触发Minor GC时首先会检查之前每次Minor GC时晋升到Old区的平均对象大小是否大于Old区的剩余空间,如果大于,则将直接出发Full GC,如果小于,则要看HandlePromotionFailure参数(-XX:-handlePromotionFailure)的值。如果为true,仅触发Minor GC,否则再触发一次Full GC,其实这个规则很好理解,如果每次晋升的对象大小都超过了Old区的剩余空间,那么说明当前的Old区的空间已经不能满足新对象所占空间的大小,只有触发Full GC才能获得更多的内存空间。

当Minor GC时,除了将Eden区的非活动对象回收以外,还会把一些老对象也复制到Old区。这个老对象的定义是通过配置参数MaxTenuringThreshold来控制的,如-XX:MaxTenuringThreshold=10,则如果这个对象已经被Minor GC回收过10次后仍然存活,那么这个对象在这次Minor GC后直接放入Old区。还有一种情况,当这次Minor GC时Survivor区中的To Space放不下这些对象是,这些对象也将直接放入Old区。如果Old区或者Perm区空间不足,将会触发Full GC,Full GC会检查Heap堆中所有对象,清除所有垃圾对象,如果Perm区,会清楚已经被卸载的classloader中加载的类的信息。

JVM在做GC时由于是串行的,所以这些动作都是单线程完成的,在JVM中的其他应用程序会全部停止。

Parallel Collector
Parallel GC根据Minor GC和Full GC的不同分为三种,分别是ParNewGC,ParallelGC和ParallelOleGC
1.ParNewGC
它的对象分配和回收策略与Serial Collector类似。只是回收的不是单线程,而是多线程并行回收。
2.ParallelGC
挡在Eden区申请内存空间时,如果Eden区不够,那么看当前申请的空间是否大于等于Eden区的一半,如果大于则这次申请的空间直接在Old区分配,如果小于则触发Minor GC.在触发GC之前首先会检查每次晋升到Old区的平均大小是否大于Old区的剩余空间,如大于则再触发Full GC。在这次触发GC之后仍然会按照这个规则重新检查一次。也就是满足上面这个规则,Full GC会执行两次。
3.ParallenOldGC
它与ParallelGC有何不同之处,其实不同之处在Full GC上,前者Full GC进行的动作为清空整个Heap堆中的垃圾对象,清除Perm区中已经被卸载的类信息,并进行压缩,而后者是清除Heap堆中的部分垃圾对象,并进行部分的空间压缩。
GC垃圾回收都是以多线程方式进行的,同样也将暂停所有的应用程序。

CMS Collector
1.总体介绍:
CMS(Concurrent Mark-Sweep)是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动JVM参数加上-XX:+UseConcMarkSweepGC ,这个参数表示对于老年代的回收采用CMS。CMS采用的基础算法是:标记—清除。
2.CMS过程:
初始标记(STW initial mark)
并发标记(Concurrent marking)
并发预清理(Concurrent precleaning)
重新标记(STW remark)
并发清理(Concurrent sweeping)
并发重置(Concurrent reset)

初始标记 :在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的”根对象”开始,只扫描到能够和”根对象”直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。
并发标记 :这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。
并发预清理 :并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段”重新标记”的工作,因为下一个阶段会Stop The World。
重新标记 :这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从”跟对象”开始向下追溯,并处理对象关联。
并发清理 :清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。
并发重置 :这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收。

3.CMS缺点
CMS回收器采用的基础算法是Mark-Sweep。所有CMS不会整理、压缩堆空间。这样就会有一个问题:经过CMS收集的堆会产生空间碎片。 CMS不对堆空间整理压缩节约了垃圾回收的停顿时间,但也带来的堆空间的浪费。为了解决堆空间浪费问题,CMS回收器不再采用简单的指针指向一块可用堆空 间来为下次对象分配使用。而是把一些未分配的空间汇总成一个列表,当JVM分配对象空间的时候,会搜索这个列表找到足够大的空间来hold住这个对象。
需要更多的CPU资源。为了让应用程序不停顿,CMS线程和应用程序线程并发执行,这样就需要有更多的CPU,单纯靠线程切 换是不靠谱的。并且,重新标记阶段,为空保证STW快速完成,也要用到更多的甚至所有的CPU资源。当然,多核多CPU也是未来的趋势!
CMS的另一个缺点是它需要更大的堆空间。因为CMS标记阶段应用程序的线程还是在执行的,那么就会有堆空间继续分配的情况,为了保证在CMS回 收完堆之前还有空间分配给正在运行的应用程序,必须预留一部分空间。也就是说,CMS不会在老年代满的时候才开始收集。相反,它会尝试更早的开始收集,已 避免上面提到的情况:在回收完成之前,堆没有足够空间分配!默认当老年代使用68%的时候,CMS就开始行动了。 – XX:CMSInitiatingOccupancyFraction =n 来设置这个阀值。
总得来说,CMS回收器减少了回收的停顿时间,但是降低了堆空间的利用率。

4.啥时候用CMS
如果你的应用程序对停顿比较敏感,并且在应用程序运行的时候可以提供更大的内存和更多的CPU(也就是硬件牛逼),那么使用CMS来收集会给你带来好处。还有,如果在JVM中,有相对较多存活时间较长的对象(老年代比较大)会更适合使用CMS。

CMS GC与上面讨论的GC不太一样,它既不是上面所说的Minor GC,也不是Full GC,它是基于这两种GC之间的一种GC。它的触发规则是检查Old去或者Perm区的使用率,当达到一定比例时就会触发CMS GC,触发时会回收Old区中的内存空间。这个比例可以通过CMSInitiatingOccupancyFraction参数来指定,默认是92%。
触发CMS GC时回收的只是Old区或者Perm区的垃圾对象,在回收时和前面所说的Minor GC和Full GC基本没有关系。

三种GC优缺点比较
优点 缺点
串行 在适合内存有限的情况下 回收慢
并行 效率高 当Heap过大时,应用程序暂停时间较长
CMS并发 Old区回收暂停时间短 产生内存碎片,整个GC耗时长,比较耗CPU。


递归时没有退出条件会发生栈溢出


永久代不属于堆内存,堆内存只包含新生代和老年代。
其次,永久代内存溢出为OutOfMemoryError:Pergen space