JVM本身是硬件的一层软件抽象,在这之上才能够运行Java程序,也才有了我们所吹嘘的平台独立性以及“一次编写,处处运行”。
在理解Java垃圾回收机制之前需要我们对于JVM的内存结构能够有一个充分的理解,便于后面对于Java垃圾回收机制的理解。
1. JVM内存区域
JVM内存区域结构如上图所示。
1. 程序计数器:
- 程序计数器是一块较小的内存区域,可以看作是当前线程所执行的字节码的行号指示器。
- 每个线程拥有一个PC寄存器,在线程创建时创建并指向下一条指令的地址。执行本地方法时,PC的值为undefined。
2. Java虚拟机栈
- 线程私有,生命周期与线程相同。
- 描述的是Java方法执行的内存模型:每一个方法执行的同时都会创建一个栈帧(Stack Frame),由于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法的执行就对应着栈帧在虚拟机栈中的入栈,出栈过程。
- 局部变量表、操作数栈、方法返回地址、动态连接
3. 本地栈方法
- 与虚拟机栈所发挥的作用非常相似,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码服务),而本地方法栈则为虚拟机使用到的Native方法服务。
4. Java堆
- 对于大多数的应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。
- Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
- 其唯一目的就是存放对象实例。
- Java堆是垃圾收集器管理的主要区域,被称作是GC堆
- 要注意,这个“堆”并不是数据结构意义上的堆,而是动态内存分配意义上的堆——用于管理动态生命周期的内存区域。
5. 方法区
- 是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
基于上述的内容,可以较为详细的理解JVM内存机制。
在此需要详细叙述一下Java堆的问题。
从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为新生代和老年代;新生代还可以分为Eden、From Survivor、To Survivor。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。进一步划分的目的是为了更好地回收内存、更快地分配内存。
2. Java垃圾回收机制
2.1 意义
Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。
2.2 垃圾回收的一些方法
Mark and Sweep
算法分为“标记”和“清楚”两个阶段:
- 首先标记出所需要回收的对象;
- 在标记完成后统一回收所有被标记的对象。
缺点:
- 效率问题:标记和清楚两个过程的效率都不高;
- 空间问题:标记清楚之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
Copy
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完后,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉。
优点:
- 不用考虑内存碎片等复杂情况
- 实现简单,运行高效
缺点:
- 空间缩小
Compact
采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。
优点:
- 解决了碎片的问题
缺点:
- 代价高
在实际的回收机制中,对于不同的情况其实是使用不同的回收算法来应对不同的状况的。其基本假设是大部分对象只存在很短的时间,对于新生代使用copy算法,对于老年代使用compact算法。
2.3 Generation
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。
新生代
- 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
- 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
- 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收
- 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)
老年代
- 在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年嗲中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
- 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
持久代
- 用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。
- Java 1.8后,使用Metaspace替代,其好处是不舍限制,杜绝了OutOfMemoryError,使用系统内存
2.4 垃圾收集器
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。
- Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
- Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
- ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
- Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
- Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
- CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
- G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
3. 内存分配与回收策略
Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存 以及 回收分配给对象的内存。一般而言,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓存(TLAB),将按线程优先在TLAB上分配。少数情况下也可能直接分配在老年代中。总的来说,内存分配规则并不是一层不变的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。
- 对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。现在的商业虚拟机一般都采用复制算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。 当进行垃圾回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后处理掉Eden和刚才的Survivor空间。(HotSpot虚拟机默认Eden和Survivor的大小比例是8:1)当Survivor空间不够用时,需要依赖老年代进行分配担保。
- 大对象直接进入老年代。所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。
- 长期存活的对象将进入老年代。当对象在新生代中经历过一定次数(默认为15)的Minor GC后,就会被晋升到老年代中。
- 动态对象年龄判定。为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
需要注意的是,Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间。也就是说,垃圾收集器回收的是无任何引用的对象占据的内存空间而不是对象本身。
4. 总结
- 哪些内存需要回收?(对象是否可以被回收的两种经典算法: 引用计数法 和 可达性分析算法)
- 什么时候回收? (堆的新生代、老年代、永久代的垃圾回收时机,MinorGC 和 FullGC,分配内存失败时运行)
- 如何回收?(三种经典垃圾回收算法(标记清除算法、复制算法、标记整理算法)及分代收集算法 和 七种垃圾收集器,扫描根节点,扫描到的是需要回收的)
网友评论