关于java:JVM完整详解内存分配运行原理回收算法GC参数等

8次阅读

共计 6269 个字符,预计需要花费 16 分钟才能阅读完成。

不论是 BAT 面试,还是工作实际中的 JVM 调优以及参数设置,或者内存溢出检测等,都须要波及到 Java 虚拟机的内存模型、内存调配,以及回收算法机制等,这些都是必考、必会技能。

JVM 内存模型

JVM 内存模型能够分为两个局部,如下图所示,堆和办法区是所有线程共有的,而虚拟机栈,本地办法栈和程序计数器则是线程公有的。


1. 堆(Heap)
堆内存是所有线程共有的,能够分为两个局部:年老代和老年代。下图中的 Perm 代表的是永恒代,然而留神永恒代并不属于堆内存中的一部分,同时 jdk1.8 之后永恒代也将被移除。


堆是 java 虚拟机所治理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,该内存区域寄存了对象实例及数组(但不是所有的对象实例都在堆中)。

其大小通过 -Xms(最小值)和 -Xmx(最大值)参数设置(最大最小值都要小于 1G),前者为启动时申请的最小内存,默认为操作系统物理内存的 1 /64,后者为 JVM 可申请的最大内存, 默认为物理内存的 1 /4,默认当空余堆内存小于 40% 时,JVM 会增大堆内存到 -Xmx 指定的大小,可通过 -XX:MinHeapFreeRation= 来指定这个比列;当空余堆内存大于 70% 时,JVM 会减小堆内存的大小到 -Xms 指定的大小,可通过 XX:MaxHeapFreeRation= 来指定这个比列,当然为了防止在运行时频繁调整 Heap 的大小,通常 -Xms 与 -Xmx 的值设成一样。堆内存 = 新生代 + 老生代 + 长久代。

在咱们垃圾回收的时候,咱们往往将堆内存分成新生代和老生代(大小比例 1:2),新生代中由 Eden 和 Survivor0,Survivor1 组成,三者的比例是 8:1:1,新生代的回收机制采纳复制算法,在 Minor GC 的时候,咱们都留一个存活区用来寄存存活的对象,真正进行的区域是 Eden+ 其中一个存活区,当咱们的对象时长超过肯定年龄时(默认 15,能够通过参数设置),将会把对象放入老生代,当然大的对象会间接进入老生代。老生代采纳的回收算法是标记整顿算法。

2. 办法区(Method Area)
办法区也称”永恒代“,它用于存储虚拟机加载的类信息、常量、动态变量、是各个线程共享的内存区域。默认最小值为 16MB,最大值为 64MB(64 位 JVM 因为指针收缩,默认是 85M),能够通过 -XX:PermSize 和 -XX:MaxPermSize 参数限度办法区的大小。

它是一片间断的堆空间,永恒代的垃圾收集是和老年代 (old generation) 捆绑在一起的,因而无论谁满了,都会触发永恒代和老年代的垃圾收集。不过,一个显著的问题是,当 JVM 加载的类信息容量超过了参数 -XX:MaxPermSize 设定的值时,利用将会报 OOM 的谬误。参数是通过 -XX:PermSize 和 -XX:MaxPermSize 来设定的。

3. 虚拟机栈 (JVM Stack)
形容的是 java 办法执行的内存模型:每个办法被执行的时候都会创立一个”栈帧”, 用于存储局部变量表(包含参数)、操作栈、办法进口等信息。每个办法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

申明周期与线程雷同,是线程公有的。栈帧由三局部组成:局部变量区、操作数栈、帧数据区。局部变量区被组织为以一个字长为单位、从 0 开始计数的数组,和局部变量区一样,操作数栈也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来拜访的,而是通过入栈和出栈来拜访的,能够看作为长期数据的存储区域。

除了局部变量区和操作数栈外,java 栈帧还须要一些数据来反对常量池解析、失常办法返回以及异样派发机制。这些数据都保留在 java 栈帧的帧数据区中。

局部变量表: 寄存了编译器可知的各种根本数据类型、对象援用(援用指针,并非对象自身),其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量的空间,其余数据类型只占 1 个。

局部变量表所需的内存空间在编译期间实现调配,当进入一个办法时,这个办法须要在栈帧中调配多大的局部变量是齐全确定的,在运行期间栈帧不会扭转局部变量表的大小空间。

4. 本地办法栈 (Native Stack)
与虚拟机栈根本相似,区别在于虚拟机栈为虚拟机执行的 java 办法服务,而本地办法栈则是为 Native 办法服务。(栈的空间大小远远小于堆)

5. 程序计数器(PC Register)
是最小的一块内存区域,它的作用是以后线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过扭转这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、异样解决、线程复原等根底性能都须要依赖计数器实现。

6. 间接内存
间接内存并不是虚拟机内存的一部分,也不是 Java 虚拟机标准中定义的内存区域。jdk1.4 中新退出的 NIO,引入了通道与缓冲区的 IO 形式,它能够调用 Native 办法间接调配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小.

JVM 垃圾回收算法

1. 标记革除

原理:

从根汇合节点进行扫描,标记出所有的存活对象,最初扫描整个内存空间并革除没有标记的对象(即死亡对象)
实用场合:

存活对象较多的状况下比拟高效
实用于年轻代(即旧生代)
毛病:

标记革除算法带来的一个问题是会存在大量的空间碎片,因为回收后的空间是不间断的,这样给大对象分配内存的时候可能会提前触发 full gc。

2. 复制算法

原理:

从根汇合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)下来,之后将原来的那一块儿内存(图中上边的那一块儿内存)全副回收掉
实用场合:

存活对象较少的状况下比拟高效
扫描了整个空间一次(标记存活对象并复制挪动)
实用于年老代(即新生代):基本上 98% 的对象是”朝生夕死”的,存活下来的会很少
毛病:

须要一块儿空的内存空间
须要复制挪动对象

3. 标记整顿

原理:

从根汇合节点进行扫描,标记出所有的存活对象,最初扫描整个内存空间并革除没有标记的对象(即死亡对象)(能够发现前边这些就是标记 - 革除算法的原理),革除完之后,将所有的存活对象左移到一起。
实用场合:

用于年轻代(即旧生代)
毛病:

须要挪动对象,若对象十分多而且标记回收后的内存十分不残缺,可能挪动这个动作也会消耗肯定工夫
扫描了整个空间两次(第一次:标记存活对象;第二次:革除没有标记的对象)
长处:

不会产生内存碎片

4. 分代收集算法
以后商业虚拟机的垃圾收集都采纳“分代收集”(Generational Collection)算法,这种算法并没有什么新的思维,只是依据对象存活周期的不同将内存划分为几块。个别是把 Java 堆分为新生代和老年代,这样就能够依据各个年代的特点采纳最适当的收集算法。

专门钻研表明,新生代中的对象 98% 是“朝生夕死”的,所以并不需要依照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次应用 Eden 和其中一块 Survivor[1]。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最初清理掉 Eden 和方才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10% 的内存会被“节约”。当然,98% 的对象可回收只是个别场景下的数据,咱们没有方法保障每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,须要依赖其余内存(这里指老年代)进行调配担保(Handle Promotion)。

在新生代中,每次垃圾收集时都发现有少量对象死去,只有大量存活,那就选用复制算法,只须要付出大量存活对象的复制老本就能够实现收集。而老年代中因为对象存活率高、没有额定空间对它进行调配担保,就必须应用“标记—清理”或者“标记—整顿”算法来进行回收。

垃圾回收器

1.Serial 收集器
Serial 收集器是最古老的收集器,它的毛病是当 Serial 收集器想进行垃圾回收的时候,必须暂停用户的所有过程,即 stop the world。到当初为止,它仍然是虚拟机运行在 client 模式下的默认新生代收集器,与其余收集器相比,对于限定在单个 CPU 的运行环境来说,Serial 收集器因为没有线程交互的开销,分心做垃圾回收天然能够取得最高的单线程收集效率。

2.ParNew 收集器
ParNew 收集器是 Serial 收集器新生代的多线程实现,留神在进行垃圾回收的时候仍然会 stop the world,只是相比拟 Serial 收集器而言它会运行多条过程进行垃圾回收。

ParNew 收集器在单 CPU 的环境中相对不会有比 Serial 收集器更好的成果,甚至因为存在线程交互的开销,该收集器在通过超线程技术实现的两个 CPU 的环境中都不能百分之百的保障能超过 Serial 收集器。当然,随着能够应用的 CPU 的数量减少,它对于 GC 时系统资源的利用还是很有益处的。它默认开启的收集线程数与 CPU 的数量雷同,在 CPU 十分多(譬如 32 个,当初 CPU 动辄 4 核加超线程,服务器超过 32 个逻辑 CPU 的状况越来越多了)的环境下,能够应用 -XX:ParallelGCThreads 参数来限度垃圾收集的线程数。

3.Parallel Scavenge 收集器
Parallel 是采纳复制算法的多线程新生代垃圾回收器,仿佛和 ParNew 收集器有很多的类似的中央。然而 Parallel Scanvenge 收集器的一个特点是它所关注的指标是吞吐量(Throughput)。所谓吞吐量就是 CPU 用于运行用户代码的工夫与 CPU 总耗费工夫的比值,即吞吐量 = 运行用户代码工夫 / (运行用户代码工夫 + 垃圾收集工夫)。进展工夫越短就越适宜须要与用户交互的程序,良好的响应速度可能晋升用户的体验;而高吞吐量则能够最高效率地利用 CPU 工夫,尽快地实现程序的运算工作,次要适宜在后盾运算而不须要太多交互的工作。

4.CMS 收集器
CMS(Concurrent Mark Swep) 收集器是一个比拟重要的回收器,当初利用十分宽泛,咱们重点来看一下,CMS 一种获取最短回收进展工夫为指标的收集器,这使得它很适宜用于和用户交互的业务。从名字 (Mark Swep) 就能够看出,CMS 收集器是基于标记革除算法实现的。它的收集过程分为四个步骤:

  • 初始标记(initial mark)
  • 并发标记(concurrent mark)
  • 从新标记(remark)
  • 并发革除(concurrent sweep)

留神初始标记和从新标记还是会 stop the world,然而在消耗工夫更长的并发标记和并发革除两个阶段都能够和用户过程同时工作。

不过因为 CMS 收集器是基于标记革除算法实现的,会导致有大量的空间碎片产生,在为大对象分配内存的时候,往往会呈现老年代还有很大的空间残余,然而无奈找到足够大的间断空间来调配以后对象,不得不提前开启一次 Full GC。

为了解决这个问题,CMS 收集器默认提供了一个 -XX:+UseCMSCompactAtFullCollection 收集开关参数(默认就是开启的),用于在 CMS 收集器进行 FullGC 完开启内存碎片的合并整顿过程,内存整理的过程是无奈并发的,这样内存碎片问题倒是没有了,不过进展工夫不得不变长。虚拟机设计者还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction 参数用于设置执行多少次不压缩的 FULL GC 后跟着来一次带压缩的(默认值为 0,示意每次进入 Full GC 时都进行碎片整顿)。

可怜的是,它作为老年代的收集器,却无奈与 jdk1.4 中曾经存在的新生代收集器 Parallel Scavenge 配合工作,所以在 jdk1.5 中应用 cms 来收集老年代的时候,新生代只能抉择 ParNew 或 Serial 收集器中的一个。

ParNew 收集器是应用 -XX:+UseConcMarkSweepGC 选项启用 CMS 收集器之后的默认新生代收集器,也能够应用 -XX:+UseParNewGC 选项来强制指定它。

5.G1 收集器
G1 收集器是一款面向服务端利用的垃圾收集器。HotSpot 团队赋予它的使命是在将来替换掉 JDK1.5 中公布的 CMS 收集器。与其余 GC 收集器相比,G1 具备如下特点:

并行与并发:G1 能更充沛的利用 CPU,多核环境下的硬件劣势来缩短 stop the world 的进展工夫。
分代收集:和其余收集器一样,分代的概念在 G1 中仍然存在,不过 G1 不须要其余的垃圾回收器的配合就能够单独治理整个 GC 堆。
空间整合:G1 收集器有利于程序长时间运行,调配大对象时不会无奈失去间断的空间而提前触发一次 GC。
可预测的非进展:这是 G1 绝对于 CMS 的另一大劣势,升高进展工夫是 G1 和 CMS 独特的关注点,能让使用者明确指定在一个长度为 M 毫秒的工夫片段内,耗费在垃圾收集上的工夫不得超过 N 毫秒。
在应用 G1 收集器时,Java 堆的内存布局和其余收集器有很大的差异,它将这个 Java 堆分为多个大小相等的独立区域,尽管还保留新生代和老年代的概念,然而新生代和老年代不再是物理隔离的了,它们都是一部分 Region(不须要间断)的汇合。

尽管 G1 看起来有很多长处,实际上 CMS 还是支流。

与 GC 相干的罕用参数
除了下面提及的一些参数,上面补充一些和 GC 相干的罕用参数:

-Xmx: 设置堆内存的最大值。
-Xms: 设置堆内存的初始值。
-Xmn: 设置新生代的大小。
-Xss: 设置栈的大小。
-PretenureSizeThreshold: 间接降职到老年代的对象大小,设置这个参数后,大于这个参数的对象将间接在老年代调配。
-MaxTenuringThrehold: 降职到老年代的对象年龄。每个对象在保持过一次 Minor GC 之后,年龄就会加 1,当超过这个参数值时就进入老年代。
-UseAdaptiveSizePolicy: 在这种模式下,新生代的大小、eden 和 survivor 的比例、降职老年代的对象年龄等参数会被主动调整,以达到在堆大小、吞吐量和进展工夫之间的平衡点。在手工调优比拟艰难的场合,能够间接应用这种自适应的形式,仅指定虚拟机的最大堆、指标的吞吐量 (GCTimeRatio) 和进展工夫 (MaxGCPauseMills),让虚拟机本人实现调优工作。
-SurvivorRattio: 新生代 Eden 区域与 Survivor 区域的容量比值,默认为 8,代表 Eden: Suvivor= 8: 1。
-XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常状况下能够和 CPU 数量相等。但在 CPU 数量比拟多的状况下,设置绝对较小的数值也是正当的。
-XX:MaxGCPauseMills:设置最大垃圾收集进展工夫。它的值是一个大于 0 的整数。收集器在工作时,会调整 Java 堆大小或者其余一些参数,尽可能地把进展工夫管制在 MaxGCPauseMills 以内。
-XX:GCTimeRatio: 设置吞吐量大小,它的值是一个 0-100 之间的整数。假如 GCTimeRatio 的值为 n,那么零碎将破费不超过 1/(1+n) 的工夫用于垃圾收集。

对于作者:mikechen,十余年 BAT 架构教训,资深技术专家,曾任职阿里、淘宝、百度。

关注集体公众号:mikechen 的互联网架构 ,十余年 BAT 架构教训倾囊相授!
在公众号菜单栏对话框回复 【架构】 关键词,即可查看我 原创的 300 期 +BAT 架构技术系列文章与 1000+ 大厂面试题答案合集

正文完
 0