李琪的技术专栏 System Research

Java虚拟机结构及jvm参数

2020-04-29
Clear Li

阅读:


Java虚拟机原理

所谓虚拟机,就是一台虚拟的机器。他是一款软件,用来执行一系列虚拟计算指令,大体上虚拟机可以分为

系统虚拟机和程序虚拟机, 大名鼎鼎的Visual Box、Vmare就属于系统虚拟机,他们完全是对物理计算的仿真,

提供了一个可以运行完整操作系统的软件平台。

虚拟机内存结构

手绘了一个图记录一下

image-20200429164939845

1、 类加载子系统 : 负责从文件系统或者网络加载 Class 信息,加载的信息存放在一块称之方法区的内存空间。

2、 方法区 : 就是存放类的信息、常量信息、常量池信息、包括字符串字面量和数字常量等。

3、 Java 堆:在Java虚拟机启动的时候建立*Java堆,它是Java 程序最主要的内存工作区域,几乎所有的对象实例都存放到

Java 堆中,堆空间是所有线程共享。

4、 直接内存: JavaNio 库允许 Java 程序直接内存,从而提高性能,通常直接内存速度会优于 Java 堆。读写频繁的场合可能会考虑使用。

5、 每个虚拟机线程都有一个私有栈,一个线程的 Java 栈在线程创建的时候被创建, Java 栈保存着局部变量、方法参数、同事 Java 的方法调用、

返回值等。

6、 本地方法栈,最大不同为本地方法栈用于本地方法调用。 Java 虚拟机允许 Java 直接调用本地方法(通过使用 C 语言写)

7、 垃圾收集系统是 Java 的核心,也是不可少的, Java 有一套自己进行垃圾清理的机制,开发人员无需手工清理,下一节课详细讲。

8、 PC ( Program Couneter )寄存器也是每个线程私有的空间, Java 虚拟机会为每个线程创建 PC 寄存器,在任意时刻,

一个 Java 线程总是在执行一个方法,这个方法称为当前方法,如果当前方法不是本地方法, PC 寄存器总会执行当前正在被执行的指令,

如果是本地方法,则 PC 寄存器值为 Underfined ,寄存器存放如果当前执行环境指针、程序技术器、操作栈指针、计算的变量指针等信息。

9、 虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般户先进行编译成机器码后执行。

Java堆

堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。

根据垃圾回收机制的不同,Java堆有可能拥有不同的结构,最为常见的就是将整个Java堆分为

新生代和老年代。其中新声带存放新生的对象或者年龄不大的对象,老年代则存放老年对象。

新生代分为eden区、s0区、s1区,s0和s1也被称为from和to区域,他们是两块大小相等并且可以互相角色的空间。

绝大多数情况下,对象首先分配在eden区,在新生代回收后,如果对象还存活,则进入s0或*s1区,之后每经过一次

新生代回收,如果对象存活则它的年龄就加1,对象达到一定的年龄后,则进入老年代。

img

Java栈

Java栈是一块线程私有的空间,一个栈,一般由三部分组成:局部变量表、操作数据栈和帧数据区**

局部变量表:用于报错函数的参数及局部变量

操作数栈:主要保存计算过程的中间结果,同时作为计算过程中的变量临时的存储空间。

帧数据区:除了局部变量表和操作数据栈以外,栈还需要一些数据来支持常量池的解析,这里帧数据区保存着

访问常量池的指针,方便计程序访问常量池,另外当函数返回或出现异常时卖虚拟机子必须有一个异常处理表,方便发送异常

的时候找到异常的代码,因此异常处理表也是帧数据区的一部分。

img

Java方法区

Java方法区和堆一样,方法区是一块所有线程共享的内存区域,他保存系统的类信息。

比如类的字段、方法、常量池等。方法区的大小决定系统可以保存多少个类。如果系统

定义太多的类,导致方法区溢出。虚拟机同样会抛出内存溢出的错误。方法区可以理解

为永久区。

Jvm基本参数

-XX:+PrintGC 每次触发GC的时候打印相关日志

-XX:+UseSerialGC 串行回收

-XX:+PrintGCDetails 更详细的GC日志

-Xms 堆初始值

-Xmx 堆最大可用值

-Xmn 新生代堆最大可用值

-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例.

在idea的运行配置中加入参数

测试堆初始值和堆最大可用值

 -Xms5m -Xmx20m -XX:+PrintGCDetails -XX:+UseSerialGC -XX:+PrintCommandLineFlags

设置堆的初始值为5m,最大值为20m,如下图所示

image-20200429201056668

定于两个方法用于显示内存信息

static private String toM(long maxMemory) {
    float num = (float) maxMemory / (1024 * 1024);
    DecimalFormat df = new DecimalFormat("0.00");// 格式化小数
    return df.format(num);
}

static private void jvmInfo() {
    // 最大内存
    long maxMemory = Runtime.getRuntime().maxMemory();
    System.out.println("maxMemory:" + maxMemory + ",转换为M:" + toM(maxMemory));
    // 当前空闲内存
    long freeMemory = Runtime.getRuntime().freeMemory();
    System.out.println("freeMemory:" +freeMemory+",转换为M:"+toM(freeMemory));
    // 已经使用内存
    long totalMemory = Runtime.getRuntime().totalMemory();
    System.out.println("totalMemory:" +totalMemory+",转换为M"+toM(totalMemory));
}

测试方法

public static void main(String[] args) throws InterruptedException {
    //获取到最大的堆内存大小
  /*  System.out.println("获取到最大的堆内存大小"+Runtime.getRuntime().maxMemory()/(1024*1024));

    System.out.println("当前空闲内存"+Runtime.getRuntime().freeMemory()/(1024*1024));
    System.out.println("当前使用了多少内存"+Runtime.getRuntime().totalMemory()/(1024*1024));
 */
    //启动时默认4m
   // Thread.sleep(3000);
    byte[] bytes = new byte[1024*1024];
    System.out.println("分配1m内存");
    jvmInfo();
    byte[] bytes2 = new byte[1024*1024*4];
    System.out.println("分配4m内存");
    jvmInfo();
  byte[] bytes3 = new byte[1024*1024*20];
    System.out.println("分配20m内存");
    jvmInfo();
}

当使用1MB堆内存时。如下图

image-20200429201500322

当使用4MB内存时明显可以看到堆内存的使用多了大概4MB

image-20200429201638792

假如分配的内存大于堆内存的最大值呢

这里使用20MB内存试一下,

image-20200429201811536

结果是会爆出堆溢出错误。

测试新生代大小

-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC

上述为设置新生代大小为1MB,新生代中的eden区比上 s0区和s1区的比值为2:1如下

image-20200429215932247

当新生代设置的大小比较小时,会经常gc,假如设置比较大时则不会经常gc,但是gc一次所耗的时间会很长

测试新生代与老年代的比例

-Xms20m -Xmx20m  -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:NewRatio=2

除了可以设置新生代的绝对大小(-Xmn),可以使用(-XX:NewRatio)设置新生代和老年

代的比例:-XX:NewRatio=老年代/新生代

总结:不同的堆分布情况,对系统执行会产生一定的影响,在实际工作中,

应该根据系统的特点做出合理的配置,基本策略:尽可能将对象预留在新生代,

减少老年代的GC次数。

目的:

  • GC的时间足够小
  • GC的次数足够少
  • 发生Full GC 的周期足够长

对策:

  • 如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。
  • 本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理
  • 通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间

堆溢出

错误原因: java.lang.OutOfMemoryError: Java heap space

解决办法:设置堆内存大小 -Xms1m -Xmx70m -XX:+HeapDumpOnOutOfMemoryError

栈溢出

错误原因: java.lang.StackOverflowError

栈溢出 产生于递归调用,循环遍历是不会的,但是循环方法里面产生递归调用, 也会发生栈溢出。

解决办法:设置线程最大调用深度

-Xss5m 设置最大调用深度