虚拟机是一款软件,用来执行一系列虚拟计算机指令。虚拟机可以分为系统虚拟机(如VirtualBox、VMware)和程序虚拟机(如java虚拟机)。系统虚拟机是对物理计算机的仿真,提供了一个可以运行完整操作系统的软件平台。程序虚拟机专门为执行单个计算机程序而设计,如在java虚拟机中执行的指令为java字节码指令。java发展至今,出现过很多虚拟机,最初使用的是Classic的虚拟机,到现在应用最广泛的是HotSpot虚拟机。
java虚拟机的基本结构
类加载子系统、方法区、java堆、直接内存、java栈、本地方法栈、垃圾回收系统、PC寄存器、执行引擎。
类加载子系统:负责从文件系统或者网络中加载Class信息,加载的信息存放在一块称之为方法区的内存空间。
方法区:存放类信息、常量信息、常量池信息,包括字符串字面量和数字常量等。
java堆:在java虚拟机启动的时候建立java堆,它是java程序最主要的内存工作区域,几乎所有的对象实例都存放在java堆中,堆空间是所有线程共享的。
直接内存:java的NIO库允许java程序使用直接内存,从而提高性能,通常直接内存速度优于java堆,在读写频繁的场合可能会考虑使用。
java栈:每个虚拟机线程都有一个私有的栈,一个线程的java栈在线程创建的时候被创建,java栈中保存着局部变量、方法参数,java的方法调用、返回值等。
本地方法栈:与java栈非常类似,最大不同为本地方法栈用于本地方法调用。java虚拟机允许java直接调用本地方法(通常使用C编写)。
垃圾收集系统:它是java的核心,也是必不可少的,java有一套自己进行垃圾清理的机制,开发人员无需手工清理。
PC寄存器:它是每个线程私有的空间,java虚拟机会为每个线程创建PC寄存器,在任意时刻,一个java线程总是在执行一个方法,这个方法被称为当前方法,如果当前方法不是本地方法,PC寄存器就会执行当前正在被执行的指令,如果是本地方法,则PC寄存器值为undefined,寄存器存放如当前执行环境指针、程序计数器、操作栈指针、计算的变量指针等信息。
执行引擎:它是虚拟机最核心组件,负责执行虚拟机的字节码。一般先进行编译成机器码,执行引擎对机器码进行执行。
java堆
堆解决的是数据存储的问题,即数据怎么放,放到哪里。栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。方法区则是辅助堆栈的快永久区,解决堆栈信息的产生,是先决条件。若我们创建一个新的User类的对象user,则User类的信息及静态信息存放在方法区中,新创建的user对象存放在java堆中,当我们去使用的时候,都是使用的对象的引用user,这个user存放在java栈中。
判断新生代和老年代是通过对象经历的垃圾回收的次数
java堆是和java应用程序关系最密切的内存空间,几乎所有的对象都是存放在其中,并且java堆完全是自动化管理的,通过垃圾回收机制,垃圾对象会自动清理,不需要显示地释放。
根据垃圾回收机制的不同,java堆有可能拥有不同的结构。最为常见的就是将整个java堆分成新生代和老年代,其中新生代存放新生的对象或者年龄不大的对象,老年代则存放老年对象。新生代分为eden区、s0区、s1区,其中s0和s1也被称为from区和to区,它们是两块大小相等并且可以互换角色的空间。绝大多数情况下,对象首先分配在eden区,在一次新生代回收后,如果对象还存活,则会进入s0或者s1区,之后每经过一次新生代回收,如果对象存活则它的年龄增加1,当对象达到一定的年龄后,则进入老年代。
新生代的from和to空间使用的垃圾收集算法是复制算法。复制算法的核心思想是将内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存留对象复制到未被使用的内存块中去,之后去清除之前正在使用的内存块中所有对象,反复去交换这两个内存块的角色,完成垃圾回收。
老年代使用的垃圾收集算法是标记压缩法,标记压缩法在标记清除法的基础上做了优化,把存活的对象压缩到内存的一端,而后进行垃圾回收。
新生代和老年代都会经历垃圾回收GC,区别只是GC的频率不同。新生代GC是比较频繁的,而老年代是经历了多次GC之后仍然留存下来的,已经比较稳定,老年代GC没有那么频繁。
java栈
java栈是一块线程私有的内存空间,一般由三部分组成:局部变量表、操作数栈、帧数据区。
局部变量表:用于保存函数的参数及局部变量。
操作数栈:保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
帧数据区:除了局部变量表和操作数栈之外,栈还需要一些数据来支持常量池的解析,这里帧数据区保存着访问常量池的指针,方便程序访问常量池。另外,当函数返回或者出现异常时,虚拟机必须有一个异常处理表,方便发现异常的时候找到异常的代码,异常处理表也是帧数据区的一部分。
虚拟机参数设置
在虚拟机运行过程中,如果可以跟踪系统的运行状态,那么对于问题的故障排查会有一定的帮助。为此,虚拟机提供了一些跟踪系统状态的参数,使用给定的参数执行java虚拟机,就可以在系统运行时打印相关日志,用于分析实际问题。进行虚拟机参数配置,主要是围绕着堆、栈、方法区进行配置。
堆分配参数
-XX:+PrintGC 使用这个参数,虚拟机启动后,只要遇到GC就会打印日志
-XX:+UseSerialGC 配置串行垃圾回收器
-XX:+PrintGCDetails 可以查看详细信息,包括各个区的情况
-Xms: 设置java程序启动时初始堆大小
-Xmx: 设置java程序能获得的最大堆大小
-Xmx20m -Xms5m -XX:+PrintCommandLineFlags 初始堆大小5m,最大堆大小20m,将隐式或显式传给虚拟机的参数输出
在实际工作中,可以直接将初始堆大小与最大堆大小设置相等,这样可以减少程序运行时垃圾回收次数,从而提高性能。
Run as -- Run Configurations ,在Main选项卡中设置project、main class;在Arguments中,Program arguments部分可以给main主函数传递一些参数,VM arguments可以设置JVM参数。
Test01.java
public class Test01 {
public static void main(String[] args) {
//-Xms5m -Xmx20m -XX:+PrintGCDetails -XX:+UseSerialGC -XX:+PrintCommandLineFlags
//查看GC信息
System.out.println("max memory:" + Runtime.getRuntime().maxMemory());
System.out.println("free memory:" + Runtime.getRuntime().freeMemory());
System.out.println("total memory:" + Runtime.getRuntime().totalMemory());
byte[] b1 = new byte[1*1024*1024];
System.out.println("分配了1M");
System.out.println("max memory:" + Runtime.getRuntime().maxMemory());
System.out.println("free memory:" + Runtime.getRuntime().freeMemory());
System.out.println("total memory:" + Runtime.getRuntime().totalMemory());
byte[] b2 = new byte[4*1024*1024];
System.out.println("分配了4M");
System.out.println("max memory:" + Runtime.getRuntime().maxMemory());
System.out.println("free memory:" + Runtime.getRuntime().freeMemory());
System.out.println("total memory:" + Runtime.getRuntime().totalMemory());
}
}
配置的JVM参数
-XX:+PrintGC -Xms5m -Xmx20m -XX:+UseSerialGC -XX:+PrintGCDetails -XX:+PrintCommandLineFlags
Eclipse的console输出如下
-XX:InitialHeapSize=5242880 -XX:MaxHeapSize=20971520 -XX:+PrintCommandLineFlags -XX:+PrintGC -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC
max memory:20316160
free memory:5583856
total memory:6094848
分配了1M
max memory:20316160
free memory:4535264
total memory:6094848
[GC (Allocation Failure) [DefNew: 1523K->192K(1856K), 0.0027518 secs][Tenured: 1110K->1301K(4096K), 0.0026489 secs] 1523K->1301K(5952K), [Metaspace: 2639K->2639K(1056768K)], 0.0055620 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
分配了4M
max memory:20316160
free memory:4797096
total memory:10358784
Heap
def new generation total 1920K, used 51K [0x00000007bec00000, 0x00000007bee10000, 0x00000007bf2a0000)
eden space 1728K, 2% used [0x00000007bec00000, 0x00000007bec0cdd0, 0x00000007bedb0000)
from space 192K, 0% used [0x00000007bedb0000, 0x00000007bedb0000, 0x00000007bede0000)
to space 192K, 0% used [0x00000007bede0000, 0x00000007bede0000, 0x00000007bee10000)
tenured generation total 8196K, used 5397K [0x00000007bf2a0000, 0x00000007bfaa1000, 0x00000007c0000000)
the space 8196K, 65% used [0x00000007bf2a0000, 0x00000007bf7e5618, 0x00000007bf7e5800, 0x00000007bfaa1000)
Metaspace used 2646K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 286K, capacity 386K, committed 512K, reserved 1048576K
其中-XX:InitialHeapSize=5242880 -XX:MaxHeapSize=20971520 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC 为配置-XX:+PrintCommandLineFlags而输出的隐式或显式传给虚拟机的参数。
max memory:20316160为配置-Xmx20m,最大堆大小为20m
free memory:5583856为配置-Xms5m,初始堆大小为5m
total memory:6094848 为已分配的堆大小
分配了1M之后,原free memory减少1M。分配4M时,free memory已不够分配,需要从max memory中申请空间,此时total memory增加到10358784
Tenured: 1110K->1301K(4096K), 0.0026489 secs 老年代内存变化及花费时间
Perm 永久区
Times: user=0.00 sys=0.00, real=0.01 secs 此次垃圾回收各方面所花费的时间
-Xmn: 设置新生代的大小,若设置的新生代比较大会减少老年代的大小,这个参数对系统性能以及GC行为有很大影响,新生代大小一般会设置为整个堆空间的1/3到1/4左右
-XX:SurvivorRatio:用来设置新生代中eden空间和from/to空间的比例,即eden/from=eden/to
不同的堆分布情况,对系统执行会产生一定的影响,在实际工作中,应该根据系统的特点做出合理的配置。尽可能将对象预留在新生代,减少老年代的GC次数。
Test02.java
public class Test02 {
public static void main(String[] args) {
byte[] b = null;
//连续向系统申请10MB空间
for(int i = 0 ; i <10; i ++){
b = new byte[1*1024*1024];
}
}
}
JVM参数
-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -XX:+UseSerialGC -XX:+PrintGCDetails
Eclipse的console输出如下:
eden/from=eden/to=2
-XX:NewRatio 设置老年代和新生代的比例,即老年代/新生代
继续使用上述的Test02.java
JVM参数
-Xms20m -Xmx20m -XX:NewRatio=2 -XX:+UseSerialGC -XX:+PrintGCDetails
Eclipse的console输出如下:
NewRatio=tenured/(eden+from+to)=13696/(5504+640+640)=2
在Tomcat中配置JVM参数,修改catalina.sh。也可以在Eclipse中配置tomcat对应的Open launch configuration
堆溢出处理
在java程序的运行过程中,如果堆空间不足,则会抛出内存溢出的错误(Out Of Memory)OOM。一旦这类问题发生在生产环境,可能引起严重的业务中断,可以考虑使用下面的2个参数
-XX:+HeapDumpOnOutOfMemoryError 可以在内存溢出时导出整个堆信息
-XX:HeapDumpPath 设置导出堆的存放路径
Test03.java
import java.util.Vector;
public class Test03 {
public static void main(String[] args) {
//-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/Test03.dump
//堆内存溢出
Vector v = new Vector();
for(int i=0; i < 5; i ++){
v.add(new Byte[1*1024*1024]);
}
}
}
JVM参数配置
-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/Test03.dump
Eclipse的console输出如下:
内存分析工具Memory Analyzer ,在Eclipse中安装该工具Eclipse Marketplace,搜索Memory Analyzer并安装。
栈配置
Java虚拟机提供了参数-Xss来指定线程的最大栈空间,该参数直接决定了函数可调用的最大深度。
Test04.java
public class Test04 {
//-Xss1m
//-Xss5m
//栈调用深度
private static int count;
public static void recursion(){
count++;
recursion();
}
public static void main(String[] args){
try {
recursion();
} catch (Throwable t) {
System.out.println("调用最大深入:" + count);
t.printStackTrace();
}
}
}
JVM参数
-Xss1m
方法区配置
和java堆一样,方法区是一块所有线程共享的内存区域,它用于保存系统的类信息,方法区(永久区)可以保存多少信息可以对其进行配置。在默认情况下,-XX:MaxPermSize为64MB,如果系统运行时生产大量的类,就需要设置一个相对合适的方法区,以免出现永久区内存溢出的问题。
-XX:PermSize=64M -XX:MaxPermSize=64M
直接内存配置
直接内存也是java程序中非常重要的组成部分,特别广泛应用在NIO中,直接内存跳过了java堆,使java程序可以直接访问原生堆空间,因此在一定程度上加快了内存空间的访问速度。但是说直接内存一定就可以提高内存访问速度也不见得,具体情况具体分析。
相关配置参数:-XX:MaxDirectMemorySize ,如果不设置,则默认值为最大堆空间即-Xmx。直接内存使用达到上限时,就会触发垃圾回收,如果不能有效释放空间,也会引起系统OOM
Client和Server虚拟机工作模式
目前java虚拟机支持Client、Server两种运行模式,使用参数-client可以指定使用Client模式,使用-server可以指定使用Server模式。可以直接在命令行中查看当前计算机系统自动选择的运行模式,即java -version
二者区别:Client模式相对Server模式启动较快,如果不追求系统长时间使用性能,而仅仅是测试,可以使用Client模式。而Server模式则启动比较慢,原因是会对其进行复杂的系统性能信息收集和使用更复杂的算法对程序进行优化,一般在生产环境都会使用Server模式,长期运行其性能要远远快于Client模式。
推荐文章:《JVM系列三:JVM参数设置、分析》