大家好,我是小水珠。
从今天开始,我将和你一起探讨Java虚拟机(JVM)的性能调优。JVM算是面试中的高频问题了,通常情况下总会有人问到:请你讲解下JVM的内存模型,JVM的性能调优做过吗?
一 为什么JVM在Java中如此重要?
JVM不仅承担了Java字节码的分析和执行,同时也内置了自动分配内存管理机制。这个机制可以大大降低手动分配回收机制可能带来的内存泄漏和内存溢出风险,使Java开发人员不需要关注每个对象的内存分配以及回收,从而更专注于业务本身。
二 从了解内存模型开始
JVM自动内存分配管理机制的好处很多,但实则是把双刃剑。这个机制在提升Java开发效率的同时,也容易使Java开发人员过渡依赖于自动化,弱化对内存的管理能力,这样系统就很容易发生JVM的堆内存异常,垃圾回收(GC)的方式不合适以及GC次数过于频繁等问题,这些都将直接影响到应用服务的性能。
三 JVM内存模型的具体设计
1.堆
堆是JVM内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由From Survivor 和 To Survivor组成。
在Java6版本中,永久代在非堆内存区;到了Java7版本,永久代的静态变量和运行时常量池被合并到了堆中;而到了Java8,永久代被元空间取代了。结构如下图所示:
2.程序计数器
程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支,循环,跳转,异常,线程恢复等都依赖于计数器。
3.方法区
很多开发者都习惯将方法区成为“永久代”,其实两者并不是等价的。
HotSpot虚拟机使用永久代来实现方法区,但在其它虚拟机中,例如,Oracle的JRockit,IBM的J9就不存在永久代一说。因此,方法区只是JVM规范中的一部分,可以说,在HotSpot虚拟机中,设计人员使用了永久代来实现了规范的方法区。
方法区主要用来存放已经被虚拟机加载的类相关信息,包括类信息,运行时常量池,字符串常量池。类信息又包括了类的版本,字段,方法,接口和父类等信息。
方法区和堆空间类似,也是一个共享内存区,所以方法区是线程共享的。加入两个线程都试图访问方法区中的同一个类信息,而这个还没有装入JVM,那么此时就只允许一个线程去加载它,另一个线程必须等待。
在HotPot虚拟机,Java7版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在JVM的非堆内存中,而Java8版本已经将方法区中实现的永久代去掉了,并用元空间代替了之前的永久代,并且元空间的存储位置是本地内存。之前永久代的类的元数据存储在了元空间,永久代的静态变量以及运行时常量池则跟Java7一样,转移到了堆中。
那你可能又有疑问了,Java8为什么使用元空间代替永久代,这样做有什么好处?
官方给出的解释是:
移除永久代是为了融合HotSpot JVM 与 JRockit VM 而做出的努力,因为JRockit没有永久代,所以不需要配置永久代。
永久代内存经常不够用或发生内存溢出,爆出异常java.lang.OutOfMemoryError:PermGen。这是因为在JDK1.7版本中,指定的PermGen区大小为8M,由于PermGen中类的元数据信息在每次FullGC的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有,为PermGen分配多大的空间很难确定,PermGen的大小依赖于很多因素,比如。JVM加载的class总数,常量池的大小和方法的大小等。
4.虚拟机栈
Java虚拟机栈是线程私有的内存空间,它和Java线程一起创建。当创建一个线程时,会在虚拟机栈中申请一个线程栈,用来保存方法的局部变量,操作数栈,动态链接方法和返回地址等信息,并参与方法的调用与返回。每一方法的调用都伴随着栈帧的入栈操作,方法的返回则是栈帧的出栈操作。
5.本地方法栈
本地方法栈跟Java虚拟机栈的功能类似,Java虚拟机栈用于管理Java函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是用Java实现的,而是由C语言实现的。
四 JVM的运行原理
看到这里,相信你对JVM内存模型已经有个充分的了解了。接下来,我们通过一个案例来了解下代码和对象是如何分配存储的,Java代码又是如何在JVM中运行的。
当我们通过Java运行以上代码时,JVM的整个处理过程如下:
1.JVM向操作系统申请内存,JVM第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存大小找到具体的内存分配表,然后把内存段的起始地址和终止地址分配给JVM,接下来JVM就进行内部分配。
2.JVM获得内存空间后,会根据配置参数分配堆,栈以及方法区的内存大小。
3.class文件加载,验证,准备以及解析,其中准备阶段会为类的静态变量分配内存,初始化为系统初始值。
4.完成上一个步骤后,将会进行最后一个初始化阶段。在这个阶段中,JVM首先会执行构造器方法,编译器会在.java文件被编译成.class文件时,收集所有类的初始化代码,包括静态变量赋值语句,静态代码块,静态方法。
5.执行方法。启动main线程,执行main方法,开始执行第一行代码。此时堆中会创建一个Student对象,对应引用student就存放在栈中。
6.此时再创建一个JVMCase对象,调用sayHello非静态方法,sayHello方法属于对象JVMCase,此时sayHello方法入栈,并通过栈中的student引用调用堆中的Student对象;之后,调用静态方法print,print静态方法属于JVMCase类,是从静态方法中获取,之后放入到栈中,也是通过student引用调用堆中的student对象。
五 总结
这讲我们主要深入学习了最基础的内存模型设计,了解了各个分区的作用及实现原理。
如今,JVM在很大程度上减轻了Java开发人员投入到对象生命周期的管理精力。在使用对象的时候,JVM会自动分配内存给对象,在不使用对象的时候,垃圾回收器会自动回收对象,释放占用的内存。
但在某些情况下,正常的生命周期不是最优的选择,有些对象按照JVM默认的方式,创建成本会很高。比如,我在之前的讲解中说到String对象,在特定的场景使用String.intern可以很大程度地节约内存成本。我们可以使用不同的引用类型,改变一个对象的正常生命周期,从而提高JVM的回收效率,这也是JVM性能调优的一种方式。