JVM调优(一) - 基本概念

Contents
  1. 前言
  2. JVM架构
    1. HotSpot架构
    2. HotSpot关键组件
  3. 堆与栈
  4. Java内存分配
  5. 应用程序性能衡量要素
    1. 响应时间
    2. 吞吐量

前言

接下来一段时间会对JVM的GC机制做充分的调研和整理,关联之前整理过的Python内存管理,从中可以看到各个语言底层的内存管理以及GC机制,对于各语言深入理解有一定帮助。

在阅读这篇文章之前,我建议你阅读《HotSpot垃圾回收算法概述》的概念介绍部分。

本系列主要介绍:

  • JVM调优相关的基本概念
  • 基本的垃圾回收算法和垃圾收集器
  • GC面临的问题
  • 分代GC详述

本文主要介绍JVM调优的相关基本概念。

JVM架构

HotSpot架构

HotSpot JVM架构支持较强的基本特征和功能,此外还支持高性能高吞吐率的特性。例如,JVM JIT编译器产生动态优化的代码,也就是,编译器是在Java运行的时候的时候进行优化,并为当前的系统架构生成高性能的目标机器指令。此外,经过对运行时环境和多线程垃圾回收器不断地设计和优化,现在的HotSpot JVM甚至在大型的系统上都具有较高的伸缩性。JVM 的主要组件包括:ClassLoader、运行时数据区和执行引擎。

jvm_architecture

从上图可知,JVM主要包括四个部分:

  • 类加载器(ClassLoader):在JVM启动时或者在类运行时将需要的class加载到JVM中。
  • 内存区(也叫运行数据区):是在JVM运行的时候操作所分配的内存区。运行时内存区主要可以划分为5个区域,如图:

runtime_data_areas

方法区:用于存储类结构信息的地方,包括常量池、静态变量、构造函数等。虽然JVM规范把方法区描述为堆的一个逻辑部分, 但它却有个别名non-heap(非堆),所以大家不要搞混淆了。方法区还包含一个运行时常量池。

堆(Heap):存储java实例或者对象的地方。这块是GC的主要区域(后面解释)。从存储的内容我们可以很容易知道,方法区和堆是被所有java线程共享的。

栈(Stack):java栈总是和线程关联在一起,每当创建一个线程时,JVM就会为这个线程创建一个对应的java栈。在这个java栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。所以java栈是现成私有的。

程序计数器(PC Register):用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。

本地方法栈(Native Method Stack):和java栈的作用差不多,只不过是为JVM使用到的native方法服务的。

  • 执行引擎:负责执行class文件中包含的字节码指令。
  • 本地方法接口:主要是调用C或C++实现的本地方法及返回结果。

HotSpot关键组件

与性能密切相关的JVM的关键组件,有堆、JIT编译器,垃圾收集器,在下图中这些组件用深色标注。

key_jvm_architecture_components

性能优化主要关注这三个组件即可。堆是存储对象的地方,该区域由用户指定(可以在启动应用程序的时候指定)的垃圾回收器来管理。大多数优化选项都是通过配置堆的大小和选择最合适的垃圾回收器来实现。JIT编译器对性能也能产生比较大的影响,但是对于更新版本的JVM很少需要对其进行优化。

堆与栈

堆和栈是程序运行的关键,很有必要把他们的关系说清楚。

heap_and_stack

栈是运行时的单位,而堆是存储的单位

栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。

在Java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而堆只负责存储对象信息。

一个问题:为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?

  1. 从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。
  2. 堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间
  3. 栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。
  4. 面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。不得不承认,面向对象的设计,确实很美。

在Java中,Main函数就是栈的起始点,也是程序的起始点

Java内存分配

Java的内存分配原理与C/C++不同,C/C++每次申请内存时都要malloc进行系统调用,而系统调用发生在内核空间,每次都要中断进行切换,这需要一定的开销,而Java虚拟机是先一次性分配一块较大的空间,然后每次new时都在该空间上进行分配和释放,减少了系统调用的次数,节省了一定的开销,这有点类似于内存池的概念;二是有了这块空间过后,如何进行分配和回收就跟GC机制有关了。

java一般内存申请有两种:静态内存和动态内存。很容易理解,编译时就能够确定的内存就是静态内存,即内存是固定的,系统一次性分配,比如int类型变量;动态内存分配就是在程序执行时才知道要分配的存储空间大小,比如java对象的内存空间。根据上面我们知道,java栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭,栈中的栈帧随着方法的结束也会撤销,内存自然就跟着回收了。所以这几个区域的内存分配与回收是确定的,我们不需要管的。但是java堆和方法区则不一样,我们只有在程序运行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态的。一般我们所说的垃圾回收也是针对的这一部分。

总之Stack的内存管理是顺序分配的,而且定长,不存在内存回收问题;而Heap 则是为java对象的实例随机分配内存,不定长度,所以存在内存分配和回收的问题。

应用程序性能衡量要素

通常说来,在优化一个Java应用时,我们通常关心的是响应时间吞吐量两者中的一个,在此对这两个概念做一下介绍。

响应时间

响应时间指的是应用或者系统对一个请求数据的回应。例如:

  • 桌面UI对鼠标事件的响应速度
  • 网站返回页面的速度
  • 数据库查询返回的速度

所以,对于重点关心响应时间的应用,较长时间的应用暂停是不可接受的。我们要做到尽可能的提升响应速度,减少响应时间。

吞吐量

吞吐量重点关心特定时间内应用程序处理工作的最大值。例如,吞吐量可以通过以下形式来衡量:

  • 给定时间内的完成的事物数量
  • 一个小时你完成的批处理程序的个数
  • 一个小时内完成的数据库查询的次数

这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的。