JVM概述 & GC概述

Posted billxxx

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM概述 & GC概述相关的知识,希望对你有一定的参考价值。

参考:

  1. https://blog.csdn.net/seu_calvin/article/details/51404589
  2. Oracle:Java Garbage Collection Basics

0. Java Garbage Collection Basics 学习记录

Java 概述

任何运行java程序的PC需要获取一个Java runtime environment,JRE由Java虚拟机(JVM),Java平台核心类和支持的Java平台库组成。

在Java 7开始, Java程序可以使用Java Web start 通过网络安装;或作为浏览器中的Web Embedded应用程序使用JavaFX。

JDK,用于开发Java应用程序的工具的集合;提供用于打包和分发应用程序的工具。

JVM,Java Virtual Machine,JVM是一个程序,看起来就像是机器,在其中执行编写的程序。

Java虚拟机对Java编程语言一无所知,仅对特定的二进制格式进行处理(class文件)。
为了安全起见,Java虚拟机对类文件中的代码施加了严格的语法和结构约束。

Java虚拟机可以托管任何具有可用有效类文件表示的functionality的语言。受到通用的,与机器无关的平台的吸引,其他语言的实现者可以将Java虚拟机用作其语言的交付工具。

探索 JVM 架构

  1. Hotspot Architecture

    HotSpot JVM 实现了一种体系结构,其具有强大的特性和功能基础,并支持实现高性能和大规模可伸缩性。例如,HotSpot JVM JIT编译器会生成动态优化。换句话说,它们在Java应用程序运行时做出优化决策,并生成针对底层系统体系结构的高性能本机指令。

    此外,通过对其 运行时环境和多线程垃圾收集器 的日趋成熟的演变 和 持续的工程设计,即使在最大的可用计算机系统上,HotSpot JVM仍可实现高可伸缩性。

    技术图片

    JVM其中的三个组件(JIT Compiler, Heap, Garbage Collector)在调整性能时被重视。
    堆是存储对象数据的位置。然后,该区域由启动时选择的垃圾收集器管理。大多数调整选项都与调整堆大小 和 根据情况选择最合适的垃圾收集器种类 有关。

    JIT编译器对性能也有很大影响,但在较新版本JVM中很少需要进行调整。

性能基础

通常,对Java程序的调优,关注于两点: responsiveness(反应速度),throughput(吞吐量).

  • responsiveness: 指应用程序或系统响应请求的数据有多快。较大的暂停时间是不可接受的。重点是在短时间内做出响应。
  • Throughput: 在特定时间段内最大限度地提高应用程序的工作量。high pause times 对于注重吞吐量的应用程序是可以接受的。由于高吞吐量应用程序会在更长的时间内专注于基准测试(benchmarks),因此无需考虑快速响应时间。

Garbage Collection 概述

自动垃圾回收

一个进程,检查 heap memory, 识别哪些object还在用,哪些不再用了,并删除掉不在用的object(回收这个object使用的memory);

在用的(in use or referenced) object是指程序当前还有维持pointer到这个object; 不在用的(unused or unreferenced )object 是指这个object不再被程序的任何一个部分referenced了;

处理步骤:

  1. marking

    collector进程 确定哪些memory在用,哪些不在用;
    在该阶段对所有对象进行扫描,所以该步骤是 十分耗时的操作。
    技术图片

  2. Normal Deletion

    删除 未引用的对象; memory allocator 保存 可分配新对象的可用空间块的引用。
    技术图片

    为了进一步提高性能了,可以在删除后对空间进行紧凑操作;
    技术图片

为什么要分代收集垃圾?

标记和紧凑JVM中的所有对象效率很低。随着分配的对象越来越多,对象列表越来越多,导致垃圾收集时间越来越长。但是,对应用程序的经验分析表明,大多数对象生命周期都很短。随着时间的推移,分配的对象越来越少。实际上,大多数对象的寿命很短,如图表左侧较高的值所示。

技术图片

JVM 分代算法

为了提高JVM性能, 将堆(heap)分成较小的 段或几代。堆的组成部分是:年轻一代,老一代(终身一代),以及永久一代。

技术图片

  1. 年轻代区域:是所有新对象进行分配和变老的地方。当年轻代区域填满时,将发生次要垃圾回收事件(minor garbage collection)。根据高对象死亡率推断,可以优化这个次要垃圾回收。充满死亡对象的年轻代区域可以很快被回收完成。一些尚生存的对象视为老化,并最终移至老一代区域。

    stop the world event: 所有次要垃圾回收事件 都是 ‘stop the world’事件,即所有的程序线程都要停止,直到‘stop the world’事件完成。次要垃圾回收事件always是stop the world 事件。

  2. 老一代区域: 用于存储尚生存的对象。通常,为年轻一代对象设置一个阈值,并且当达到该年龄时,该对象将移至老一代区域。最终需要回收旧的一代。此事件称为主要垃圾回收(major garbage collection)。

    主要垃圾回收也是“停止世界”事件。通常,主要回收要慢得多,因为它涉及所有生存对象。因此,对于响应式应用程序,应尽量减少主要垃圾回收。另请注意,主要垃圾回收的Stop the World事件的长度 受 旧一代区域的垃圾收集器的类型的影响。

  3. 永久代区域: 包含JVM所需的元数据,用于描述应用程序中需要使用的类和方法。永久代区域 由JVM在运行时根据应用程序使用的类填充。另外,Java SE库类和方法可以存储在这里。如果JVM发现不再需要这些类,并且空间可能被其他类所需要,则可以回收(卸载)这些类。永久生成包含在完整的垃圾收集中。

分代垃圾回收处理过程

下面描述 分代算法中 对象分配和生存时间增长 的处理过程。

  1. 任何新对象 分配到eden区域, 两个survivor区域初始为空。

    技术图片

  2. 当eden区域填满时,触发一次 次要垃圾回收事件。

    技术图片

  3. 尚存活的对象 移动到第一个survivor区域。其他没有引用的对象被删除。

    技术图片

  4. 在下一次回收时,步骤依然如上。不同的是,此次存活的对象将会移动到第二个survivor区域(S1)。此外,上一次在S0区域中 仍然存活的对象,将会增加一次生存年龄,并移动到S1。当所有存活对象都移动到S1后,对eden和S0区域执行回收操作。注意此时survivor区域的对象拥有不同的岁数。

    技术图片

  5. 在下一次回收过程中,以上类似步骤重复。不同的是,此次将调换两个survivor区域。存活的对象将移至S0。这些的对象会增加岁数。Eden和S1的垃圾被回收。

  6. 当存活对象的岁数到达一定的阈值时(图中例子为8),对象会从年轻代区域 移动到 老一代区域。

    技术图片

  7. 随着次要GC的继续发生,对象将继续被提升到老一代区域。

    技术图片

  8. 以上描述了年轻一代的整个基本处理过程。最终,将对旧一代执行主要GC,以清理并压缩该空间。

具体内存状态变化演示

官网教程介绍了一个可视化查看程序内存状况的工具visualVM。在这里简略介绍一下,官网有具体的指导步骤:这里

该工具在jdk里,如果你的电脑已经将java路径加入了系统的环境变量,那么直接在cmd里执行 jvisualvm 即可;

打开后,点击 Tools --> Plugins, 选择 ‘可用插件‘,安装列表里的‘Visual GC’插件;

完成后,执行一个java程序,java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar c:javademosdemojfcJava2DJava2demo.jar
官网教程使用了官方demo里面的一个程序,就可以在visualVM里面看到他了,双击打开,点击到visual GC切换页,就可以看到内存垃圾回收的可视化展示了。

垃圾回收器配置

  1. 常用的 heap相关的配置

    选项 描述
    -Xms Sets the initial heap size for when the JVM starts.
    -Xmx Sets the maximum heap size.
    -Xmn Sets the size of the Young Generation.
    -XX:PermSize Sets the starting size of the Permanent Generation.
    -XX:MaxPermSize Sets the maximum size of the Permanent Generation
  2. 串行GC

    串行收集器是Java SE 5和6中client style machines的默认设置。使用串行收集器,次要垃圾回收和主要垃圾回收都是通过串行方式完成的(使用单个虚拟CPU)。此外,它使用标记紧凑的收集方法。此方法将较旧的内存移动到堆的开头,以便在堆的末尾将新的内存分配放入单个连续的内存块中。内存的压缩使新的内存块分配到堆变得更快。

    对于大多数没有低时延要求 并在client-style machines上运行的应用程序,串行GC是首选的垃圾收集器。它仅利用单个虚拟处理器进行垃圾收集工作。而且,在当今的硬件上,串行GC可以有效地管理具有数百MB Java堆的许多非平凡(non-trivial)程序,而最坏的情况下暂停时间相对较短(对于完整的垃圾回收,大约需要几秒钟)。

    串行GC的另一种流行用法是在同一台机器上运行大量JVM的环境中(在某些情况下,more JVMs than available processors!)。在此类环境中,当JVM进行垃圾回收时,最好使用一个处理器以最大程度地减少对其余JVM的干扰,即使垃圾回收可能持续更长时间。串行GC非常适合这种折衷方案。最后,随着嵌入式硬件的增加(具有小内存和很少的内核),串行GC可能会卷土重来。

    要启用串行收集器,请使用:-XX:+UseSerialGC

    举例:java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar c:javademosdemojfcJava2DJava2demo.jar

  3. 并行GC

    并行垃圾收集器使用多个线程来执行年轻一代垃圾收集。默认情况下,在具有N个CPU的主机上,并行垃圾收集器在收集中使用N个垃圾收集器线程。垃圾收集器线程的数量可以通过命令行选项控制:-XX:ParallelGCThreads=<desired number>

    在具有单个CPU的主机上,即使已请求并行垃圾收集器,也会使用默认垃圾收集器。在具有两个CPU的主机上,并行垃圾收集器的性能通常与默认垃圾收集器相同。在具有两个以上CPU的主机上,可以预期减少年轻代垃圾收集器的暂停时间。

    并行收集器也称为吞吐量收集器。由于它可以使用多个CPU来加快应用程序吞吐量。当需要完成大量工作并且可以接受长时间暂停时,应使用此收集器。例如,批处理,例如打印报告、账单、执行大量的数据库查询。

    -XX:+UseParallelGC
    使用此命令行选项,您将获得一个多线程年轻代收集器和一个单线程旧代收集器。该选项还可以完成旧代的单线程压缩。

    -XX:+UseParallelOldGC
    使用该选项,GC既是多线程的年轻代收集器,又是多线程的旧代收集器。它也是多线程压缩收集器。HotSpot仅在老一代中进行压缩。HotSpot中的年轻一代被视为副本收集者;因此,不需要压实。压缩描述了移动对象的行为,即对象之间没有空洞。清除垃圾之后,活动对象之间可能会留有孔。压实会移动对象,以便没有残留的孔。垃圾收集器可能是非压缩收集器。因此,并行收集器和并行压缩收集器之间的差异可能是后者在垃圾收集清除之后压缩了空间。前者不会。

  4. 并发标记扫描收集器:Concurrent Mark Sweep (CMS) Collector

    并发标记扫描收集器(也称为并发低暂停收集器)收集长期保有的代。它尝试通过与应用程序线程同时执行大多数垃圾回收工作来最大程度地减少由于垃圾回收导致的??暂停。通常,并发低暂停收集器不会复制或压缩活动对象。无需移动活动对象即可完成垃圾回收。如果碎片成为问题,分配更大的堆。

    注意:CMS收集器对于年轻代使用与并行收集器相同的算法。

    CMS收集器应用于需要短暂停时间并可以与垃圾收集器共享资源的应用程序。例子包括响应事件的桌面UI应用程序,响应请求的Web服务器或响应查询的数据库。

    启动CMS Collector: -XX:+UseConcMarkSweepGC

    设置线程数量: -XX:ParallelCMSThreads=<n>

  5. G1 Garbage Collector

    Java 7中提供了Garbage First(G1)垃圾收集器,旨在长期替代CMS收集器。G1收集器是并行,并发,且渐进压缩的低暂停垃圾收集器,其设计与前面所述的其他垃圾收集器截然不同。但是,详细讨论超出了本OBE的范围。

    要启用G1收集器,请使用:-XX:+UseG1GC


1. Java运行构成

JVM = 类加载器 classloader + 执行引擎 executionengine + 运行时数据区域 runtime data area

  • java原文件 通过 java编译器 形成class字节码文件;
  • class文件 交给 类加载器classloader 加载完毕后, 交给执行引擎execution engine执行;
  • 执行过程中使用 运行时数据区(runtime data area) 存储程序执行需要的数据和相关信息, 即通常所指的内存; (通常所说的内存管理、空间回收 即是指的该部分)

1.1 classloader 类加载器

classloader负责将 class文件 加载到 jvm的运行时数据区域; 不负责检查文件是否能执行;

1.2 runtime dataArea

官方文档:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.5

技术图片

PC计数器(The PC?Register)

  1. 每一个Java线程都有一个PC寄存器,用以记录比如在线程切换回来后恢复到正确的执行位置。

  2. 如该线程正在执行一个Java方法,则计数器记录的是正在执行的虚拟机字节码地址,如执行native方法,则计数器值为空。

  3. 此内存区域是唯一一个在JVM中没有规定任何OutOfMemoryError情况的区域。

?

JVM栈(Java Virtual MachineStacks)

  1. JVM栈是线程私有的,并且生命周期与线程相同。并且当线程运行完毕后,相应内存也就被自动回收。

  2. 栈里面存放的元素叫栈帧,每个方法从调用到执行结束,其实是对应一个栈帧的入栈和出栈。栈帧用于存储执行方法时的一些数据,如局部变量表、操作数栈(执行引擎计算时需要),方法出口等等。

  3. 这个区域可能有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常(如:将一个函数反复递归自己,最终会出现这种异常)。如果JVM栈可以动态扩展(大部分JVM是可以的),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。

?

本地方法栈(Native Method Stacks)

  1. 本地方法栈与JVM栈的作用很相似,他们的区别在于虚拟机栈是为执行Java代码方法服务,而本地方法栈是为Native方法服务。

  2. 和JVM栈一样,这个区域也会抛出StackOverflowError和OutOfMemoryError异常。

?

方法区(Method Area)

  1. 方法区域是全局共享的,比如每个线程都可以访问同一个类的静态变量。在方法区中,存储了已被JVM加载的类的信息、静态变量、编译器编译后的代码等。如,当程序中通过getName、isInterface等方法来获取信息时,这些数据来源于方法区。

  2. 由于使用反射机制的原因,虚拟机很难推测哪个类信息不再使用,因此这块区域的回收很难!另外,对这块区域主要是针对常量池回收,值得注意的是JDK1.7已经把常量池转移到堆里面了。

  3. 同样,当方法区无法满足内存分配需求时,会抛出OutOfMemoryError。

?

运行时常量池(Runtime Constant Pool)

  1. 存放类中固定的常量信息、方法引用信息等,其空间从方法区域中分配。(java8中 String Pool 放在堆区中)

  2. Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有就是常量表,用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。但是Java语言并不要求常量一定只有编译期预置入Class的常量表的内容才能进入方法区常量池,运行期间也可将新内容放入常量池(最典型的String.intern()方法)。

  3. 当常量池无法在申请到内存时会抛出OutOfMemoryError异常,上面也分析过了。

?

Java堆

  1. Java堆是JVM所管理的最大的一块内存。它是被所有线程共享的一块内存区域,在虚拟机启动时创建。

  2. 几乎所有的实例对象都是在这块区域中存放。(JIT编译器貌似不是这样的)。

  3. Java堆是垃圾收集管理的主要战场。所有Java堆可以细分为:新生代和老年代。再细致分就是把新生代分为:Eden空间、FromSurvivor空间、To Survivor空间。JVM具体的垃圾回收机制总结请查看我的另外一篇JVM——内存管理和垃圾回收。

  4. 根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

?

堆和栈的区别

这是一个非常常见的面试题,主要从以下几个方面来回答。

  1. 各司其职

    最主要的区别就是栈内存用来存储局部变量和方法调用信息。
    而堆内存用来存储Java中的对象。无论是成员变量、局部变量还是类变量,它们指向的对象都存储在堆内存中。
    ?
  2. 空间大小

    栈的内存要远远小于堆内存,如果你使用递归的话,那么你的栈很快就会充满并产生StackOverFlowError。
    关于如何设置堆栈内存的大小,可以查看JVM——内存管理和垃圾回收中的相关介绍。

  3. 独有还是共享

    栈内存归属于线程的私有内存,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见。
    而堆内存中的对象对所有线程可见,可以被所有线程访问。

  4. 异常错误

    如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。

    如果JVM栈可以动态扩展(大部分JVM是可以的),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。

    而堆内存没有可用的空间存储生成的对象,JVM会抛出java.lang.OutOfMemoryError。

以上是关于JVM概述 & GC概述的主要内容,如果未能解决你的问题,请参考以下文章

JVM中GC Roots及引用类型概述

JVM的GC概述

Day341.垃圾回收器 -JVM

Day338.垃圾回收概述 -JVM

JVM19_G1垃圾收集器概述特点常用参数Region详解记忆集与写屏障年轻代GC并发标记过程Mixed GCFull GC

JVM内存管理概述