Java开发有个很基础的问题,虽然我们平时接触的不多,但是了解它却成为Java开发的必备基础——这就是JVM。在C++中我们需要手动申请内存然后释放内存,否则就会出现对象已经不再使用内存却仍被占用的情况。在Java中JVM内置了垃圾回收的机制,帮助开发者承担对象的创建和释放的工作,极大的减轻了开发的负担。那是不是我们就不需要了解JVM了,显然在做一些优化或者深入研究应用性能的时候,JVM还是起了很关键的作用的。因此本篇就总结性的描述下JVM的内存模型与垃圾回收相关的知识。
本文的主要内容如下:
- 内存模型
- 垃圾回收
- 参考文章
内存模型
各部分的功能
这几个存储区最主要的就是栈区和堆区,那么什么是栈什么是堆呢?说的简单点,栈里面存放的是基本的数据类型和引用,而堆里面则是存放各种对象实例的。
堆与栈分开设计是为什么呢?
- 栈存储了处理逻辑、堆存储了具体的数据,这样隔离设计更为清晰
- 堆与栈分离,使得堆可以被多个栈共享。
- 栈保存了上下文的信息,因此只能向上增长;而堆是动态分配
栈的大小可以通过-XSs设置,如果不足的话,会引起java.lang.*Error的异常
栈区
线程私有,生命周期与线程相同。每个方法执行的时候都会创建一个栈帧(stack frame)用于存放 局部变量表、操作栈、动态链接、方法出口。
堆
存放对象实例,所有的对象的内存都在这里分配。垃圾回收主要就是作用于这里的。
- 堆得内存由-Xms指定,默认是物理内存的1/64;最大的内存由-Xmx指定,默认是物理内存的1/4。
- 默认空余的堆内存小于40%时,就会增大,直到-Xmx设置的内存。具体的比例可以由-XX:MinHeapFreeRatio指定
- 空余的内存大于70%时,就会减少内存,直到-Xms设置的大小。具体由-XX:MaxHeapFreeRatio指定。
因此一般都建议把这两个参数设置成一样大,可以避免JVM在不断调整大小。
程序计数器
这里记录了线程执行的字节码的行号,在分支、循环、跳转、异常、线程恢复等都依赖这个计数器。
方法区
类型信息、字段信息、方法信息、其他信息
总结
名称 | 特征 | 作用 | 配置 | 异常 |
---|---|---|---|---|
栈区 | 线程私有,使用一段连续的内存空间 | 存放局部变量表、操作栈、动态链接、方法出口 | -XSs | *Error OutOfMemoryError |
堆 | 线程共享,生命周期与虚拟机相同 | 保存对象实例 | -Xms -Xmx -Xmn | OutOfMemoryError |
程序计数器 | 线程私有、占用内存小 | 字节码行号 | 无 | 无 |
方法区 | 线程共享 | 存储类加载信息、常量、静态变量等 | -XX:PermSize -XX:MaxPermSize | OutOfMemoryError |
垃圾回收
如何定义垃圾
有两种方式,一种是引用计数(但是无法解决循环引用的问题);另一种就是可达性分析。
判断对象可以回收的情况:
- 显示的把某个引用置位NULL或者指向别的对象
- 局部引用指向的对象
- 弱引用关联的对象
垃圾回收的方法
Mark-Sweep标记-清除算法
这种方法优点就是减少停顿时间,但是缺点是会造成内存碎片。
Copying复制算法
这种方法不涉及到对象的删除,只是把可用的对象从一个地方拷贝到另一个地方,因此适合大量对象回收的场景,比如新生代的回收。
Mark-Compact标记-整理算法
这种方法可以解决内存碎片问题,但是会增加停顿时间。
Generational Collection 分代收集
最后的这种方法是前面几种的合体,即目前JVM主要采取的一种方法,思想就是把JVM分成不同的区域。每种区域使用不同的垃圾回收方法。
上面可以看到堆分成三个区域:
- 新生代(Young Generation):用于存放新创建的对象,采用复制回收方法,如果在s0和s1之间复制一定次数后,转移到年老代中。这里的垃圾回收叫做minor GC;
- 年老代(Old Generation):这些对象垃圾回收的频率较低,采用的标记整理方法,这里的垃圾回收叫做 major GC。
- 永久代(Permanent Generation):存放Java本身的一些数据,当类不再使用时,也会被回收。
这里可以详细的说一下新生代复制回收的算法流程:
在新生代中,分为三个区:Eden, from survivor, to survior。
- 当触发minor GC时,会先把Eden中存活的对象复制到to Survivor中;
- 然后再看from survivor,如果次数达到年老代的标准,就复制到年老代中;如果没有达到则复制到to survivor中,如果to survivor满了,则复制到年老代中。
- 然后调换from survivor 和 to survivor的名字,保证每次to survivor都是空的等待对象复制到那里的。
垃圾回收器
串行收集器 Serial
这种收集器就是以单线程的方式收集,垃圾回收的时候其他线程也不能工作。
并行收集器 Parallel
以多线程的方式进行收集
并发标记清除收集器 Concurrent Mark Sweep Collector, CMS
大致的流程为:初始标记--并发标记--重新标记--并发清除
G1收集器 Garbage First Collector
大致的流程为:初始标记--并发标记--最终标记--筛选回收
JVM里的GC(Garbage Collection)的算法有很多种,如标记清除收集器,压缩收集器,分代收集器等等,详见HotSpot VM GC 的种类
现在比较常用的是分代收集(generational collection,也是SUN VM使用的,J2SE1.2之后引入),即将内存分为几个区域,将不同生命周期的对象放在不同区域里:young generation,tenured generation和permanet generation。绝大部分的objec被分配在young generation(生命周期短),并且大部分的object在这里die。当young generation满了之后,将引发minor collection(YGC)。在minor collection后存活的object会被移动到tenured generation(生命周期比较长)。最后,tenured generation满之后触发major collection。major collection(Full gc)会触发整个heap的回收,包括回收young generation。permanet generation区域比较稳定,主要存放classloader信息。
young generation有eden、2个survivor 区域组成。其中一个survivor区域一直是空的,是eden区域和另一个survivor区域在下一次copy collection后活着的objecy的目的地。object在survivo区域被复制直到转移到tenured区。
我们要尽量减少 Full gc 的次数(tenured generation 一般比较大,收集的时间较长,频繁的Full gc会导致应用的性能收到严重的影响)。
堆内存GC
JVM(采用分代回收的策略),用较高的频率对年轻的对象(young generation)进行YGC,而对老对象(tenured generation)较少(tenured generation 满了后才进行)进行Full GC。这样就不需要每次GC都将内存中所有对象都检查一遍。
非堆内存不GC
GC不会在主程序运行期对PermGen Space进行清理,所以如果你的应用中有很多CLASS(特别是动态生成类,当然permgen space存放的内容不仅限于类)的话,就很可能出现PermGen Space错误。
内存申请、对象衰老过程
一、内存申请过程
- JVM会试图为相关Java对象在Eden中初始化一块内存区域;
- 当Eden空间足够时,内存申请结束。否则到下一步;
- JVM试图释放在Eden中所有不活跃的对象(minor collection),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
- Survivor区被用来作为Eden及old的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
- 当old区空间不够时,JVM会在old区进行major collection;
- 完全垃圾收集后,若Survivor及old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现"Out of memory错误";
二、对象衰老过程
- 新创建的对象的内存都分配自eden。Minor collection的过程就是将eden和在用survivor space中的活对象copy到空闲survivor space中。对象在young generation里经历了一定次数(可以通过参数配置)的minor collection后,就会被移到old generation中,称为tenuring。
- GC触发条件
GC类型 触发条件 触发时发生了什么 注意 查看方式 YGC eden空间不足 清空Eden+from survivor中所有no ref的对象占用的内存
将eden+from sur中所有存活的对象copy到to sur中
一些对象将晋升到old中:
to sur放不下的
存活次数超过turning threshold中的
重新计算tenuring threshold(serial parallel GC会触发此项)重新调整Eden 和from的大小(parallel GC会触发此项)
全过程暂停应用
是否为多线程处理由具体的GC决定jstat –gcutil
gc logFGC old空间不足
perm空间不足
显示调用System.GC, RMI等的定时触发
YGC时的悲观策略
dump live的内存信息时(jmap –dump:live)清空heap中no ref的对象
permgen中已经被卸载的classloader中加载的class信息
如配置了CollectGenOFirst,则先触发YGC(针对serial GC)
如配置了ScavengeBeforeFullGC,则先触发YGC(针对serial GC)
全过程暂停应用
是否为多线程处理由具体的GC决定
是否压缩需要看配置的具体GCjstat –gcutil
gc log
参考 https://www.cnblogs.com/xing901022/p/7725961.html
http://www.cnblogs.com/redcreen/archive/2011/05/04/2037056.html