JVM内存区域和GC策略-学习笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013963380/article/details/77838301

对于从事C、C++程序开发的开发人员来说,在内存管理领域,他们拥有每一个对象的“所有权”,也担负着每一个对象生命开始到终结的维护责任。

对于java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每一个对象的new操作去写匹配的delete和free代码,不容易出现内存泄露和内存溢出问题,由虚拟机管理内存看起来都很美好。但是如果不理解虚拟机是怎么样使用内存的,一旦出现内存泄露和内存溢出的问题,那么排查错误将会是一项异常艰难的工作。

JVM中的内存使用方式,包括虚拟机内存区域的划分,Java对象分配时的处理原则和逻辑,以及我们日常开发中最需要关心的GC回收的策略和算法,是开发出拥有出色而稳定的Java软件产品所必须深刻理解的。

接下来,按照自己的理解整理,供自己学习使用。

1.JVM运行时数据区域

java虚拟机在执行java程序过程中会将其管理的内存划分为若干不同的数据区域。每个区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。

其中,JVM下所有线程共享的区域有:方法区和堆;而每个独立运行的线程,各自又享有各自的:虚拟机栈、程序计数器、本地方法栈。接下来重点介绍方法区和堆。

(1)方法区:其实就是平时我们常说的永久代(PermGen),在Java程序调试时候会碰到的”java.lang.OutOfMemoryError: PermGen Space” 就发生在这个区域。这个永久代是各个线程共享的JVM内存区域,用于存储虚拟机已加载的类信息,常量,静态变量,即使编译器编译后的代码等等数据,还会包括一些跟类有关的对象数组和类型数组,JVM使用的内部对象,以及编译器优化的使用信息等。在过去的JVM版本中,常量池也是放在永久代的。但在HotSpot 的JDK 1.7开始,字符串常量池已经从永久代中移出了。方法区的大小,在JVM启动参数里可以用-XX:MaxPermSize=XXXM来设置

(2)Java堆:这是JVM所管理的最大的一块内存区域,被所有线程共享。该区域唯一的用途就是用来存放对象实例。所有的Java对象实例及数组都要在堆上分配内存。于是堆也成了垃圾收集器管理的最主要区域,一般对象的分配以及垃圾的回收策略主要就是针对这个区域。JVM启动参数-Xmx及-Xms可用来控制堆内存大小。
(-Xms 初始堆大小,为jvm启动时分配的内存;-Xmx 最大堆大小为jvm运行过程中分配的最大内存)

(3)程序计数器:是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。

(4)虚拟机栈:描述的是java方法执行的内存模型,用于存放 局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表:存放了编译器可知的各种基本数据类型、对象的引用类型等。

(5)本地方法栈:类似于虚拟机栈发挥的作用,虚拟机栈为虚拟机执行java方法服务,本地方法栈则为虚拟机用到的Native方法服务。

2.对象的分配策略和原则

普通Java对象的创建,通常是以new关键字来调用一个类的构造方法开始的。程序执行到此时,在Java堆内存中的对象分配工作便开始了。
(tips:通过new语句创建一个对象;通过反射机制创建对象;通过clone()方法创建对象;通过反序列化的方式创建对象)

(1)第一步,检查该new指令的参数,是否在常量池中能够定位到一个类的引用。如String对象,如果字符串常量池中已有该String的引用,则直接返回。查找常量池后,则要检查该类是否已被加载,解析及初始化过。如未加载该类,则先要执行相应的类加载过程。在类加载完成之后,该new指令需要分配多大的内存空间就可以被确定了。

(2)第二步,就是给该对象分配内存了。通常情况下,对象在新生代的Eden区进行分配。如果Eden区没有足够空间进行分配时,JVM将发起一次MinorGC(年轻代GC)。

大对象,即新生代没有足够的连续空间可供其使用的对象,通常是那种需要大量连续内存空间的长字符串或者数组对象,在其所需空间大于 -XX:PretenureSizeThreshold参数时,直接在老年代分配。手动设置这个值可避免在年轻代的Eden区及Survivor区之间发生大量的内存复制,而复制后依然不够分配。

我们在设计程序时候亦应该尽量避免大对象的分配,尤其是生命周期很短的大对象分配。因为这会导致Eden区还有大量空间的情况下(但空间不连续,没法容纳我们需要的大对象),提前触发GC来获得连续空间。

3.GC策略及算法

(1)GC分代的方式:
JVM为了更有效地管理和回收堆内存,以HotSpot虚拟机为例,其基于以下假设,将堆内存从物理上划分为两个部分,即年轻代(Young Generation)和年老代(Old Generation)。
(tips:默认YG和OG的比例是1:2)
抽象JVM的堆内存分配图如下:
这里写图片描述

这两个假设便是:

1).大多数对象的生命周期都不会很长。也就意味着这些对象的引用会很快变得不可达, 只有很少的由老对象(创建时间较长的对象)指向新生对象的引用。对于年轻代,绝大多数新分配对象会在这块区域被创建;

2).年老代,占用空间会比年轻代多,年轻代中进行minor GC时存活下来的对象最终会进入这里。年老代中发生GC(即Full GC)的次数要少得多;

年轻代(Young Generation)又会被划分为三个区域:Eden、两个Survivor区,其分配的比例为8:1:1。

Eden区无需多说,对象分配时首先从这里分配空间。而两个Survivor的作用在于,Eden中经过一次minor GC后存活下来的对象,会进入其中一个Survivor区,而另一个Survivor区域则留作当前Survivor区的备份空间。当Survivor A区域中的空间饱和的时候,此时发生的Minor GC会将Survivor A中依然存活的对象,以及Eden中存活的对象,都复制到Survivor B,然后清空Survivor A。就这样循环往复,两个Survivor之间互相复制。

注:如何判断对象的老年代?
由于GC采用分代收集思想,于是就有了对象年龄计数这一概念。出生在Eden的对象,经过一次Minor GC仍存活,年龄+1。如果其能被Survivor区容纳,将进入Survivor区域。也就是说只要其活过一次minorGC,就可进入survivor区了。随后每一次minorGC, 活下来的对象年龄都+1,达到一定年龄的对象将进入老年代(默认是15岁)。该阈值可通过 -XX:MaxTenuringThreshold设置。

同时,如果survivor区内很多年龄不太大的对象怎么办呢,大家年龄都不足以进入老年代,但数量太多,survivor也吃不消。于是还有一条规则,就是survivor区内所有年龄相同的对象大小总和如果超过survivor区空间的一半,年龄大于等于该年龄的对象都直接进入老年代,不受参数MaxTenuringThreshold参数的限制了。

TipsA.
Minor GC:新生代GC。发生相对频繁,回收速度也较快。
Major GC/Full GC:老年代GC。通常会伴随一次Minor GC,速度较慢,通常比Minor GC耗时多10倍以上。

Minor GC的触发条件:一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Minor GC。
Full GC的触发条件:年老代被写满;持久代(Perm)被写满 ;System.gc()被显示调用 ;

(2)GC策略
JVM回收的对象,是那些已经不再被使用的对象。但是如何判断一个对象是否可用呢?有两种方法:

引用计数算法:给对象添加一个引用计数器,每当有一个地方引用他时,计数器就加1;每当引用失效时,计数器就减1.任何时刻计数器为0的对象不可能再被使用。

可达性分析算法:就是说从GC Root出发,对象之间的引用链没有指向的对象,我们称之为不可到达。

可作为GC Root的对象包括:
*方法区中加载的类的静态字段引用的对象,常量引用的对象;
*每个线程对象的虚拟机栈中引用的对象;
*本地方法栈中引用的本地对象或常量。

(3)GC算法
由于垃圾回收算法的实现涉及大量的程序细节,而且每个平台的虚拟机操作内存的方法又各步相同,接下来简单介绍下几种算法的思想。

标记-清除算法:
算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

主要不足:一个是效率的问题,标记和清除两个过程的效率都不高;另一方面是空间的问题,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多会导致以后在程序运行过程中需要分配大对象时,无法找到足够的连续内存空间而不得不提前触发另一次的GC动作。

复制算法:
为了解决效率的问题,复制算法出现了,它将可用内存按容量划分为大小相等的两块,每一次都只使用其中的一块。当一块内存使用完了,就将还存活的对象复制到另一块上面,然后再把使用过的内存空间一次性清理掉。

标记-整理算法:
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率会变低。一般老年代不适用复制算法,有人提出了另一种标记-整理算法,标记过程没什么区别,后续的步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端界以外的内存。

分代收集算法:
一般的虚拟机都会使用分代收集策略,一般在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率比较高、没有额外空间对它进行分配担保,就必须使用标记-整理或者标记-清除算法。

(4)JVM的简单调优

xms/xmx:定义YOUNG+OLD段的总尺寸,xms为JVM启动时YOUNG+OLD的内存大小;xmx为最大可占用的YOUNG+OLD内存大小。在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销。

NewSize/MaxNewSize:定义YOUNG段的尺寸,NewSize为JVM启动时YOUNG的内存大小;MaxNewSize为最大可占用的YOUNG内存大小。在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销。

PermSize/MaxPermSize:定义Perm段的尺寸,PermSize为JVM启动时Perm的内存大小;MaxPermSize为最大可占用的Perm内存大小。在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销。

SurvivorRatio:设置Survivor空间和Eden空间的比例

猜你喜欢

转载自blog.csdn.net/u013963380/article/details/77838301