JVM成神之路-JVM内存结构

 

线程私有

程序计数器

  • 当前线程所执行的字节码的行号指示器
  • 对于 Java 方法,记录正在执行的虚拟机字节码指令的地址;对于 native 方法,记录值为空(Undefined)
  • 唯一一个Java 虚拟机规范中没有规定任何 OutOfMemoryError 的内存区域

Java 虚拟机栈:java方法执行的内存模型

   生命周期与线程执行结果相同

  • 每个线程都有独自的虚拟机栈,并且在栈中的结构是栈帧,当调用一个方法时,会为这个方法创建一个栈帧,每个方法对应的是入栈、出栈的过程;也就是每个方法从调用到执行过程都在一个栈帧中。
  • 栈帧中包括局部变量表、操作数栈、动态链接、方法出口等信息
  1. 局部变量表:被组织成以一个字长为单位的数组,从0开始计数的数组,short,byte,和char的值在每次存入数组前要被转换成int值;long,duble在数组中占连续的两个字长,通过索引来访问。
  2. 操作数栈:被组织成以一个字长为单位的数组,通过出栈和入栈来访问(临时数据存储区域)
  3. 帧数据区:用于支持常量池解析,正常方法返回,异常派发机制。
  • 线程请求的栈深度大于虚拟机规定的栈深度,会抛出 StackOverflowError,当栈空间动态扩展,但无法申请足够的内存,将抛出 OutOfMemoryError

本地方法栈

  • 与虚拟机栈类似,区别在于虚拟机栈执行Java方法,本地方法栈执行 Native 方法

线程共享

堆:JVM启动时创建该区域;Java虚拟机所管理内存最大的一块区域

  • 堆是垃圾回收的主要区域,很多时候被称为 GC堆
  • 堆被分为新生代、老年代:Eden Space(伊甸园)、Survivor Space(幸存者区)、Tenured Gen(老年代)
  • 几乎所有的对象实例及数组都在堆上分配
  • 当内存空间不足时,无法完成实例分配,将抛出 OutOfMemoryError
  • -Xms(最小值),-Xmx(最大值)

方法区:堆的一个逻辑部分:“非堆”

  • 方法区在 HotSpot 中又被称为永久代
  • 存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
  • 该区域的回收目标主要是对常量池的回收以及类的卸载
  • 当内存空间不足时,无法为方法区开辟新空间时,将抛出 OutOfMemoryError
  • 运行时常量池,存储类加载后的常量池信息;编译期,运行时期均可产生新的敞亮于常量池中(String.intern());
  • 默认最小值16MB,最大值64MB;-XX:PermSize,-XX:MaxPeermSize

垃圾回收

why - 为什么要了解垃圾回收

  • 提高系统性能,突破应用性能瓶颈
  • 排查各种内存溢出,内存泄漏的问题

what1 - 哪些内存区域需要回收

  • Java 堆和方法区
  • Java 堆是对无用的对象的回收,方法区是对废弃的常量以及无用的类进行回收(方法区回收效率远低于堆,主要是对常量池回收)

what2 - 哪些对象需要回收(对象存活判定方法)

引用计数法

  • 原理:给对象添加引用计数器,每当有地方引用它,引用计数器加1;引用失效,计数器减1;当计数器的值为 0,对象可被回收
  • 优点:实现简单,判定效率高
  • 缺点:存在循环引用的问题

可达性分析法(GC Roots)

  • 原理:通过一系列的称为「GC Roots」的对象作为起始点,从起始点搜索的路径称为引用链,当一个对象没有与任何引用链相连时,则证明此对象是不可用的

GC Roots 对象种类

  • Java 虚拟机栈中引用的对象
  • 本地方法栈 (Native方法) 中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

when - 什么时候进行回收

  • 达到 Minor GC 或者 Full GC 的触发条件时
  • Minor GC 触发条件

            当新生代(Eden区)空间不足时,发起一次 Minor GC

  • Full GC 触发条件
  1.     调用 System.gc(),系统建议执行 Full GC,但是不必然执行
  2.     当老年代空间不足时
  3.     方法区空间不足时
  4.     历次通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存时
  5.     由Eden区、From Survior区向To Survior区复制时,对象大小大于ToSurvior区可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小时

how - 如何回收(垃圾回收算法)

标记 - 清除算法

  • 原理:该算法分为「标记」和「清除」两个阶段,首先标记出所有需要回收的对象,在标记完成后统一进行回收
  • 特点
  1.         效率:标记和清除两个阶段的效率都不高
  2.         空间:标记清除之后未进行碎片整理,产生大量不连续的内存碎片,分配大对象时空间不够会经常触发垃圾回收,最终影响的仍然是效率

标记 - 整理算法

  • 原理:该算法分为「标记」和「整理」两个阶段,标记如同标记清除中的标记,整理是指不直接对标记的对象进行清理,而是让所有存活的对象移动到一边,清除掉端边界以外的内存
  • 特点
  1. 效率:标记和整理两个阶段的效率不高
  2. 空间:优点是解决了内存碎片的问题

复制算法(新生代采用的算法)

  • 原理:新生代分为 Eden、Survivor1(from)、Survivor2(to)三部分,每次使用Eden和其中一块Survivor,回收时,将存活对象复制到另一块Survivor,然后清理到刚使用的Eden和Survivor
  • 特点
  1. 效率:每次对整个半区进行回收,内存分配是不需要考虑内存碎片的情况;内存分配时只需要移动堆顶指针,效率高
  2. 空间:未进行垃圾回收时,会有一部分空间未用上

分代收集算法

  • 原理: 将Java 堆分为新生代和老年代,根据各个年代的特点采用适当的收集算法
  • 特点:充分利用不同各个年代的特点采用最适当的收集算法
  • 新生代:次垃圾收集时都发现只有少量存活,选择使用复制算法,只需要付出少量存活对象的复制成本就可以完成收集
  • 老年代:因为对象存活率高、没有额外空间进行分配担保,使用「标记-清理」或者「标记-整理」算法进行回收

垃圾收集器

Serial 收集器

  • 算法:堆内存年轻代采用“复制算法”;堆内存老年代采用“标记-整理算法”
  • 单线程收集器,只会使用一个 CPU 或一条收集线程去完成垃圾收集工作
  • 垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(又称为「Stop The World」)
  • 优点是简单而高效(与其他收集器的单线程相比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然就可以获得最高的单线程收集效率

ParNew 收集器

  • 算法:堆内存年轻代采用“复制算法”
  • ParNew 收集器就是 Serial 收集器的多线程版本
  • 只能用于新生代
  • 多线程收集,并行
  • 在多 CPU 环境下,随着 CPU 的数量增加,它对于 GC 时系统资源的有效利用是有益的。它默认开启的收集线程数与 CPU 的数量相同
  • ParNew 收集器在单 CPU 的环境中绝对不会有比 Serial 收集器有更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个 CPU 的环境中都不能百分之百地保证可以超越

Parallel Scavenge 收集器

  • 算法:堆内存年轻代采用“复制算法”;配合收集器:ParallelOldGC,堆内存老年代采用“标记-整理算法”
  • 多线程收集器
  • 只适用于新生代
  • 自适应调节策略
  • Parallel Scavenge收集器的目标是达到一个可控制的吞吐量
  • Parallel Scavenge 收集器无法与 CMS 收集器配合使用

CMS 收集器

  • 运作过程
  1. 初始标记

            仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要“Stop The World”

  1. 并发标记

            进行 GC Roots 追溯所有对象的过程,在整个过程中耗时最长

  1. 重新标记

            为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”

  1. 并发清除
  • 特点
  1. Concurrent Mark Sweep,基于“标记-清除”算法实现
  2. 各阶段耗时:并发标记/并发清除 > 重新标记 > 初始标记
  3. 对 CPU 资源非常敏感
  4. 标记-清除算法导致的空间碎片
  5. 并发收集、低停顿,因此 CMS 收集器也被称为并发低停顿收集器
  6. 无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次 Full GC 的产生
  7. 由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作;所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

G1 收集器

  • 运作过程
  1. 初始标记

            仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要“Stop TheWorld”

  1. 并发标记

            进行 GC Roots 追溯所有对象的过程,可与用户程序并发执行

  1. 最终标记

            修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录

  1. 筛选回收

            对各个Region的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来指定回收计划

  • 特点
  1. 空间整合:整体上看来是基于“标记-整理”算法实现的,从局部(两个Region)上看来是基于“复制”算法实现的
  2. 面向服务端应用的垃圾收集器
  3. 并行与并发
  4. 分代收集
  5. 可预测的停顿:G1收集器可以非常精确地控制停顿,既能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java的垃圾收集器的特征
  6. G1将整个Java堆(包括新生代、老年代)划分为多个大小相等的内存块(Region),每个Region 是逻辑连续的一段内存,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域

问题总结

局部变量表与操作数栈有什么区别?

  • 局部变量表是存放编译器可知的各种基本数据类型、对象引用类型和方法返回地址;通过索引访问;局部变量表所需内存空间在编译期已确定大小
  • 操作数栈是工作区,在这里进行根据指令对数据进行压栈、出栈进行数据运算;通过出栈入栈方式访问;

StackOverflowError 与 OutOfMemoryError 的区别?

  • StackOverflowError 是指栈请求的深度大于虚拟机规定的栈深度,此时内存空间可能还足够
  • OutOfMemoryError 是指内存空间不足,无法分配内存

内存泄漏和内存溢出的区别?

  • 申请内存后,无法释放已申请的内存空间,内存泄漏堆积的结果是内存溢出
  • 内存溢出是指申请内存时,没有足够的内存空间供申请者使用

符号引用和直接引用的区别?

  • 符号引用是字面量,用符号来描述引用目标,只包含语义信息,与具体实现无关
  • 直接引用是与具体实现息息相关,是直接指向目标的指针

常量池和运行时常量池的区别?

  • 常量池存在于静态的存储文件中,Java 中表现为 .class 文件,主要包含字面量和符号引用
  • 运行时常量池存在于内存中,是常量池被加载到内存之后的版本,并且字面量可以动态添加,比如String.intern()

Minor GC 与 Full GC 的区别

  • Minor GC 称为新生代GC,是发生在新生代的垃圾回收动作,因为 Java 对象绝大多数具备朝生夕灭的特性,Minor GC 触发非常频繁,并且回收速度很快。
  • Full GC 称为老年代GC,发生在老年代,出现了 Full GC,通常会伴随一次 Minor GC。Full GC的速度一般比 Minor GC 慢 10 倍以上。

直接内存

直接内存不属于虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。

这部分也被频繁的使用,而且也可能出现OOM异常。

JDK1.4中加入NIO,可用于Native函数库直接分配堆外内存,然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作;提高新能,避免在java堆和Native堆中来回复制数据

优点:减少垃圾回收,减少复制过程,提高性能

缺点:失去jvm管理内存的可见性,排查问题困难

 

JDK1.2-JDK6:HotSpot使用永久代和GC分代实现方法区

JDK7:HotSpot开始移除永久代:符号表移到Native Heap中,字符串敞亮和类引用被移到JavaHeap中

JDK8:永久代完全被元空间(MearSpace)取代。

 

 基本数据类型是放在栈中还是放在堆中,这取决于基本类型在何处声明,下面对数据类型在内存中的存储问题来解释一下:

   一:在方法中声明的变量,即该变量是局部变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量就放在方法栈中,当方法结束系统会释放方法栈,其对应在该方法中声明的变量随着栈的销毁而结束,这就局部变量只能在方法中有效的原因

      在方法中声明的变量可以是基本类型的变量,也可以是引用类型的变量。

         (1)当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在JAVA虚拟机栈中

         (2)当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在JAVA虚拟机的栈中,该变量所指向的对象是放在堆类存中的。

   二:在类中声明的变量是成员变量,也叫全局变量,放在堆中的(因为全局变量不会随着某个方法执行结束而销毁)。

       同样在类中声明的变量即可是基本类型的变量 也可是引用类型的变量

       (1)当声明的是基本类型的变量其变量名及其值放在堆内存中的

       (2)引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象。引用变量名和对应的对象仍然存储在相应的堆中

参考资料:

《深入理解Java虚拟机》

链接:Java虚拟机的内存组成以及堆内存介绍-HollisChuang's Blog

链接:Java堆和栈看这篇就够 - Johnny-Zhuang's Technology Blog

链接:Java虚拟机的堆、栈、堆栈如何去理解? - 知乎

链接:Java 内存之方法区和运行时常量池 - 漠然的博客 | mritd Blog

链接:从0到1起步-跟我进入堆外内存的奇妙世界 - 简书

部分图,内容借鉴球友

 

 

 

你可能感兴趣的:(JVM,JVM)