JVM 基本指南

小白学JVM

Posted by Liao on November 17, 2014

本文概述了 Java 虚拟机 JVM 的基本概念,原文为 Java Virtual Machine: the Essential Guide ,由本人翻译,若有疏漏望海涵。

Java Virtual Machine: the Essential Guide

Introduction

Java 虚拟机是一个 Java 应用程序的执行环境。一般来说,JVM 是按照某些规范和参数定义的一个抽象计算机器,可以解释 Java 源码编译生成的字节码。更通常的理解,JVM 是指对一组严格的指令集和综合内存模型的实现。也可以把JVM看作软件的运行时实现。通常我们会将 HotSpot 看作 JVM 的实现参考。

JVM 的规范确保任何 JVM 的实现都能够按照完全一致的方式解释字节码。它可以被实现为一个进程,一个独立的 JAVA OS 或者一个直接执行字节码的处理器芯片。最知名的 JVM 是运行在常见平台(Windows, OS X, Linux, Solaris, etc.)上作为进程运行的软件方式实现。 JVM 的架构使得它可以在 Java 程序运行时进行精细的控制。它在一个沙箱环境中运行,确保了应用程序在没有合适权限的情况下不能够直接访问本地文件系统,进程和网络。在远程执行的情况下,代码必须有证书签名。

除了解释 Java 字节码外,大多数的 JVM 实现都包含一个即使编译器(JIT Compiler)为频繁使用的方法生成机器码。机器码是 CPU 的本机语言,可以比解释字节码执行的更快。

Architecture

JVM 规范定义了它的子系统和行为。JVM 主要有下面的子系统:

  • Class Loader: 类加载器,负责读取 Java 源代码并加载类至数据区中。

  • Execution Engine: 负责从数据区中执行指令。

数据区占据 JVM 从底层操作系统中分配的内存。

jvm-arch

Class Loader

JVM 使用不同的类加载器,它们组织成下面的层级:

  • bootstrap class loader 是其他类加载器的父加载器,用于加载 Java 的核心库,并且是唯一一个用本机代码编写的

  • extension class loader 是 bootstrap class loader 的子加载器,用于加载扩展库

  • system class loader 是 extension class loader 的子加载器,用于加载在类库路径下的类文件

  • user-defined class loader 是 system class loader 或其他 user-defined class loader 的子加载器

当一个类加载器收到请求加载一个类,它会检查缓存查看类是否已经被加载,然后将请求委托至父加载器。如果父加载器加载失败,然后子加载器再尝试加载这个类。一个子类加载器可以检查父加载器的缓存,但是父加载器无法制动类是否被子类加载。这种设计是因为子加载器不应该加载一个已经被父加载器加载过的类。

Execution Engine

执行引擎执行被加载到数据区中的字节码。为了使指令可以被计算机读取,执行引擎使用两种方法

  • Interpretation 执行引擎将每发现一个指令,就将其转为机器码

  • Just-in-time(JIT) compilation 如果一个方法被频繁使用,执行引擎就将其编译为本机语言并存放至缓存中。之后,所有和这个方法关联的指令就可以直接执行而不需要解释。

尽管 JIT 编译器比一次性解释器消耗更多的时间,但它可以将那些需要被调用千万次的方法仅仅编译一次即可。将这些方法作为本机语言执行,相比每次都一个一个的解释每一条指令,可以节省大量的执行时间。

JIT 编译器并不是 JVM 规范所需要的,它也不是用来提升 JVM 性能的唯一技术。JVM 规范仅定义了字节码如何与本机语言相关联。而定义执行引擎如何将它们之间进行转换取决于具体实现。

Memory Model

Java 内存模型是基于自动内存管理的理念构建的。当一个对象不再被任何一个应用程序所引用,垃圾回收器就将其丢弃并释放其内存空间。这一点和那些需要手动从内存中释放对象的编程语言是不同的。

JVM 从底层操作系统中分配内存并将其划分为下面的区域。

  • Heap Space 用于存储对象的共享内存区,它会被垃圾回收器所扫描

  • Method Area 存放加载的类,之前被称作永久代(Permanent Generation),最近被JVM移除了,现在类被作为元数据加载到操作系统的内存中

  • Native Area 这个区存放基本类型的引用和变量

将内存分块可以保证高效的内存管理,因为这样垃圾回收器就不用扫描整个堆。大多数的对象生存周期都很短,那些生存期很长的对象不需要被丢弃,知道程序结束。

当 Java 程序创建一个对象,它被存放在堆内存的 eden pool 中。一旦它被存满, eden pool 将触发一次垃圾回收。首先,垃圾回收器标记那些过期的对象(不再被程序所引用),并增加存活对象的年龄(年龄指的是对象存活过垃圾回收器的数量)。然后垃圾回收器丢弃掉过期对象并将存活对象移至 survivor pool 中,使 eden pool 清空。

当一个存活的对象到达了一个确定的年龄,它就被移动到堆的老年代(Old Generation)中: tenured pool。最后 tenured pool 被填满,此区域将触发一个主垃圾回收进行清理。

当进行垃圾回收时,所有的程序线程都停止,造成暂停。次垃圾回收的次数是很频繁的,但被优化至可以快速的移除过期对象,这些对象属于新生代(Young Generation) 的主要部分。主垃圾回收的速度要慢得多因为它涉及的大部分是存活对象。有很多不同类型的垃圾回收器,有些回收器在特定的清醒下执行主垃圾回收的速度会很快。

堆大小是动态的。如果需要,就会分配内存至堆中。当堆被填满,JVM就申请更多的内存,直到达到最大值。堆内存的再次分配也会导致程序的短暂停止。

Thread

JVM 是作为单个进程运行的,但它可以同时执行多个线程,每一个都运行各自自身的方法。这是 Java 的基本部分。一个程序例如一个即使消息客户端,至少运行两个线程; 一个等待用户的输入,一个检查服务器传来的消息。另一个例子是一个服务器程序通过不同的线程处理请求,有时单个请求也可能涉及同时运行多个线程。

所有的线程对 JVM 进程共享内存和其他资源。每个 JVM 进程在程序入口启动一个主线程。其他的线程通过它启动,并代表一个独立的执行路径。线程可以在多个处理器上同时运行,或者可以共享一个处理器。线程调度器控制线程如何在单个处理器上轮流执行。

Performance Optimization

JVM 的性能取决于它的配置是否与程序的功能特性相匹配。尽管内存是通过垃圾回收器自动管理的,且可以再次分配内存,你也可以控制它们的频率。通常,你的程序可获得的内存越多,会导致程序暂停的内存管理的需求就越少。

如果垃圾回收的次数比想要的更频繁,可以以更多的堆内存启动 JVM。堆内存的一代被填满的时间越长,垃圾回收就越少发生。配置最大堆大小,在启动 JVM 的时使用 -Xmx 选项。默认情况下,最大堆大小被设置为操作系统物理内存的 1/4 大小,或者 1GB 大小(最小值)。

如果问题出现在内存的再分配,可以设置初始堆大小和最大值一样大。这意味着 JVM 不会再需要分配更多的内存至堆内存。然而,这样会失去堆内存动态分配的自动优化的特性。在启动程序的时候开始堆内存就是固定大小。配置初始堆内存大小,在启动 JVM 时使用 -Xms 选项。默认情况下,初始堆内存大小被设置为操作系统物理内存大小的 1/64, 或者一些不同平台而不同的最小值。

垃圾回收(主或次)会导致性能下降,可以设置新生代和老年代的比率,且不需要调整整个堆大小。对于那些许多短生存周期对象的程序,增大新生代的大小(这会减小老年代的内存大小)。对于频繁操作大量长生存期对象的程序,增大老年代的大小(通过设置更小的新生代内存大小)。下面的方法可以用来控制新生代和老年代的大小

  • 使用 -XX:NewRatio 选项启动 JVM 来设置新生代和老年代的比率。例如,让老年代为新生代的 5 倍大小,指定 -XX:NewRatio=5。默认情况下,这个比率是 2(老年代占据 2/3的堆内存,新生代占据1/3)

  • 使用 -Xmn 选项启动 JVM 来设置新生代的初始和最大内存大小。老年代的值就是堆内存中除去新生代的剩余值。

  • 使用 -XX:NewSize 和 -XX:MaxNewSize 选项启动 JVM 来分别设置新生代的初始大小和最大大小。老年代的大小就是堆内存中除去新生代的剩余值。

大多数的程序(特别是服务)需要并行执行,处理大量的任务。这些任务中,一部分在一个特定的时刻是更重要的,其他的则是可以在 CPU 闲时执行的后台任务。任务是在不同的线程中执行的。例如,一个服务可能有一个低优先级的线程用于基于数据计算统计信息,一个高优先级的线程用于处理接收的数据,另一个更高优先级的线程用于响应对计算过数据的请求。数据的来源可能有很多,并且有很多客户端向服务器请求数据。每个会短暂的停止后台计算线程的执行来响应请求。因此必须要监控正在运行的线程数,并保证计算线程有足够的 CPU 时间用于进行必要的计算。

每个线程有一个栈用于保存方法调用,返回的地址等等。一部分的内存被分配至栈,栈是线程独有的,如果有太多的线程,就可能导致内存溢出错误。即使有足够的堆内存存放对象,程序也可能无法启动一个新的线程。这种情况下,考虑限制线程栈的最大大小。在启动 JVM 时使用 -Xss 选项来配置线程栈大小。默认情况下,线程的栈大小被设置为 320KB 或 1024KB,这取决于操作系统。

Performance Monitoring

在开发或运行一个 Java 程序时,监控 JVM 的性能是很重要的。配置 JVM 并不是一劳永逸的,特别是需要处理运行 Java 的服务时。必须要不断的检查堆和非堆内存的分配和使用,程序创建的线程数,载入内存的类数量。这些是核心参数。

使用 JVM 监视器测量下面的值

  • 总内存使用 即 JVM 使用的内存大小。如果 JVM 使用了所有的可用内存,将会影响底层操作系统的总体性能。

  • 堆内存使用 即运行的 Java 程序使用的JVM分配给对象的内存大小。未使用的对象会周期性的被垃圾回收器从堆中移除。如果这个值增大,表明应用程序没有移除未使用对象的引用,或者需要对垃圾回收器进行配置

  • 非堆内存使用 即分配给方法区和代码缓存的内存使用。方法去是用来存储对已加载类的引用。如果这些引用没有被正确的移除,永久代池(permanent generation pool)会在每次程序重新部署时增长,导致非堆内存泄漏。它也可能表明线程创建泄漏。

  • 总内存池使用 即被 JVM 分配的不同内存池的总使用量(也即总内存使用减去代码缓存区)。这个值可以反应出除去 JVM 的开销后的应用程序内存消耗。

  • 线程数 即 JVM 中的活动线程数。例如,每个对 Tomcat 的请求是在一个独立线程中处理,因此这个值反应当前处理的请求数,是否影响后台的设置为低优先级的线程任务。

  • 类数 即加载类的数量。如果程序动态创建大量的类,可能会导致严重的内存泄漏。

</br></br>