【JVM】内存模型和垃圾回收
内存模型
各部分的功能:
这几个存储区最主要的部分就是堆区和栈区,那么什么是栈什么是堆呢?说的简单点,栈存放的是基本数据类型和引用类型的引用,而堆里边则是存放各种对象的实例。
栈于堆分开设计的好处:
- 栈存储了处理逻辑、堆存储了具体数据,这样隔离设计提高了清晰度和可用性
- 堆与栈分离,使得堆可以被多个栈共享
- 栈保存了上下文的信息,因此只能向上增长;而堆则是动态分配
栈的大小可以通过 -XSs
设置,如果不足的话,会引起java.lang.StackOverFlowError异常
– 栈区 –
线程私有,生命周期与线程相同。每个方法执行的时候都会创建一个栈帧(stack frame)用于存放 局部变量表、操作栈、动态链接、方法出口
– 堆 –
存放对象实例,所有的对象的内存都在这里分配。垃圾回收的主要部分就是在这里。
- 堆的内存由
-Xms
指定,默认是物理内存的1/64; 最大的内存由-Xmx
指定,默认是物理内存的1/4。 - 默认空余的堆内存小于40%时,就会增大,直到填充到
-Xmx
设置的指定内存。具体比例可以由-XX:MinHeapFreeRatio
指定。 - 空余的内存大于70%时,就会减少内存,直到
Xms
设置的大小。具体由-XX:maxHeapFreeRatio
指定。
因此一般都建议把这两个参数设置成一样大,可以避免JVM在不断调整大小。
– 程序计数器 –
这里记录了线程执行的字节码行号,在分支、循环、跳转、异常、线程恢复等都依赖程序计数器。也是垃圾回收的一部分。
– 方法区 –
类型信息、字段信息、方法信息、其他信息,也被称为静态方法区。
– 总结 –
名称 | 特征 | 作用 | 配置 | 异常 |
---|---|---|---|---|
栈区 (stack) | 线程私有,生命周期与线程相同,使用一段连续的内存空间 | 存放局部变量表、操作栈、动态链接、方法出口 | -XSs | StackOverFlowError OutOfMemoryError |
堆 (heap) | 线程共享,生命周期与虚拟机相同 | 保存对象实例 | -Xms -Xmx -Xmn | StackOverFlowError OutOfMemoryError |
程序计数器 | 线程私有、占用内存小 | 字节码行号 | 无 | 无 |
方法区 | 线程共享 | 存储类加载信息、常量、静态变量等 | -XX:PermSize -XX:MaxPermSize | OutOfMemoryError(OOM) |
本地方法栈 | 线程私有、占用内存小 | 与虚拟机栈的作用相似,虚拟机栈为虚拟机执行执行java方法服务,而本地方法栈则为虚拟机使用到的本地方法服务 | 无 |
垃圾回收
– 如何定义垃圾回收 –
有两种方式,一种是引用计数(但是无法解决循环引用问题);另一种就是可达性分析。
判断对象可以回收的情况:
- 显示的把某个引用置为NULL或者指向别的对象
- 局部引用指向的对象
- 弱引用关联的对象
下面针对垃圾回收两种方式写了一篇帖子: 【JVM】垃圾回收方式
– 分代收集器 –
分代收集器主要结合了复制算法Copying
Mark-Compact标记-整理算法
Mark-Sweep标记-清除算法
的优点,其思想就是把JVM分成不同的区域。每种区域使用不同的垃圾回收方法。
从上图可以看出堆区分为三个区域,这种分代方式可以坚守垃圾回收的停顿时间及大范围对象的回收成本,三个区域分别如下:
- 新生代(Young Generation)
用于存放新创建的对象,采用复制回收方法,如果s0和s1之间复制一定次数后,转移到年老代中。这里的垃圾回收器叫做minor GC;
新生代-复制算法:
该算法的核心就是 将可用的内存按容量划分为大小相等的两块,每次只用其中一块,当这一块内存用完,就将还存活的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉。
这使得每次只对其中一块内存进行回收,分配也就不用考虑内存碎片等复杂情况,实现简单且运行高效。
现代商用VM的新生代均采用复制算法,但由于新生代中的98%的对象都是生命周期极短的,因此并不需要完全按照1:1的比例划分新生代空间,而是将新生代划分为一块较大的Eden区和两块较小的Survivor区(HotSpot默认Eden和Survivor的大小比例为8:1),每次只用Eden和其中一块Survivor,当发生Minor GC时,将Eden和Survivor中还存活的对象一次性地copy到另一块survivor上,最后清理掉Eden和刚才用过的Survivor空间,当Survivor空间不够用(不足以保持尚存活的对象)时,需要依赖老年代进行空间分配担保机制,这部分内存直接进入老年代
- 年老代(Old Generation)
这些对象垃圾回收的频率较低,采用标记清除方法,这里的垃圾回收器叫做major GC
老年代-标记清除算法:
该算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象(可达性分析),在标记完成后统一清理掉所有被标记的对象。
该算法会有以下两个问题:
1、效率问题:标签和清除过程的效率都不高
2、空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集
- 持久代(Permanent Generation)
存放Java本身的一些数据,当类不再使用时,也会被回收。
这里可以详细的说一下新生代复制回收的算法流程:
在新生代中,分为三个区:Eden,from survivor, to survivor。
- 当触发 minor GC时,会先把Eden中存活的对象复制到to survivor中;
- 然后再看from survivor,如果次数打到年老代的标准,则复制到年老代中;如果没有打到则复制到to survivor中,如果to survivor满了,则复制到年老代中。
- 然后调换from survivor 和 to survivor的名字,保证每次to survivor都是空的等待对象复制到那里的。