others linux服务器运维 django3 监控 k8s golang 数据库 大数据 前端 devops 理论基础 java oracle 运维日志

java 虚拟机调优

访问量:922 创建时间:2021-05-06

java 虚拟机的生命周期: 一个运行时的java虚拟机实例的职责是负责运行一个java程序,在启动一个java程序的同时会诞生一个虚拟机实例,当该程序退出时,虚拟机实例也随之消亡,如果在同一台计算机上同时运行三个java程序,会得到三个java虚拟机实例,每个java程序都运行于自己的java虚拟机实例中。java虚拟机内部有两个线程:守护线程与非守护线程,守护线程通常由虚拟机内部使用,例如执行垃圾收集任务,而java的初始线程为非守护线程。当非守护线程终止后,虚拟机实例将自动退出当然也可以通过System.exit方法退出。

java虚拟机内存模型

程序计数器

程序计数器是一块很小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。 字节码解释工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理等基础功能都由计数器所完成。

java虚拟机栈

java虚拟机栈也是线程私有的内存空间,它和java线程在同一时间创建,它保存方法的局部变量、部分结果,并参与方法的调用和返回。每一个方法被调用直至完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 java虚拟机规范中,定义了两种异常与栈空间相关:StackOverflowError及OutOfMemoryError。 使用-Xss参数来设置栈的大小,栈的大小直接决定了函数调用可以达的深度。

测试栈深度:

[root@name2 java]# cat Test.java
public class Test{
  private int count=0;
  public void testAdd(){
    count++;
        testAdd();
  }

  public void test(){
    try {
          testAdd();
        } catch(Throwable e) {
          System.out.println(e);
          System.out.println("栈深度:"+count);
        }
  }

  public static void main(String[] args){
    new Test().test();
  }
}
[root@name2 java]# javac Test.java 
[root@name2 java]# java Test
java.lang.StackOverflowError
栈深度:18184
###-Xss设置栈大小
[root@name2 java]# java -Xss1m Test
java.lang.StackOverflowError
栈深度:18039
[root@name2 java]# java -Xss2m Test
java.lang.StackOverflowError
栈深度:70959
[root@name2 java]# java -Xss4m Test
java.lang.StackOverflowError
栈深度:83444

由于栈中包括局部变量表,操作数栈,返回地址等信息,方法在调用时,如果方法的参数和局部变量相对较多,那么栈中的局部变量表就越大,它的栈帧就越大,栈深度越浅。

局部变量不能通过gc回收,可以通过重定义新的局部变量,覆盖前面的作用空间。

[root@name2 java]# cat Test3.java 
public class Test3 {
  public static void test1(){
    {
    byte[] b = new byte[6*1024*1024];
    }
    System.gc();
    System.out.println("hello world");
  }

  public static void main(String[] args){
    test1();
  } 
}
[root@name2 java]# javac Test3.java 
###-verbose:gc 显示gc, 下面可以看出6968K->6407K(987648K),gc后内存没回收
[root@name2 java]# java -verbose:gc Test3
[GC (System.gc())  16465K->6968K(987648K), 0.0053125 secs]
[Full GC (System.gc())  6968K->6407K(987648K), 0.0035284 secs]
hello world
[root@name2 java]# cat Test3.java 
public class Test3 {
  public static void test1(){
    {
    byte[] b = new byte[6*1024*1024];
    }
    // 定义a 将b的空间重用掉
    int a = 0;
    System.gc();
    System.out.println("hello world");
  }

  public static void main(String[] args){
    test1();
  }
}
[root@name2 java]# javac Test3.java 
###下面可以看到904K->263K ,内存被回收
[root@name2 java]# java  -verbose:gc Test3
[GC (System.gc())  16465K->904K(987648K), 0.0010755 secs]
[Full GC (System.gc())  904K->263K(987648K), 0.0035940 secs]
hello world
[root@name2 java]# cat Test3.java 
public class Test3 {
  public static void test1(){
    {
    byte[] b = new byte[6*1024*1024];
    b= null;
    }
    // 定义a 将b的空间重用掉
    //int a = 0;
    System.gc();
    System.out.println("hello world");
  }

  public static void main(String[] args){
    test1();
  } 
}
[root@name2 java]# javac Test3.java 
[root@name2 java]# java  -verbose:gc Test3
[GC (System.gc())  16465K->856K(987648K), 0.0010985 secs]
[Full GC (System.gc())  856K->263K(987648K), 0.0036413 secs]
hello world

本地方法栈: 本地方法栈和java虚拟机栈的功能很相似。java虚拟机栈用于管理java函数的调用,而本地方法栈用于管理本地方法的调用。本地方法并不是java实现的,而是由C来实现的。

java堆

java堆是java运行时内存中最为重要的部分,几乎所有的对象和数组都是在堆中分配空间。 java堆可以分为新生代和老年代两部分,新生代用于存放刚刚产生的对象和年轻对象,如果对象一直没有被回收,生存得足够长,新生代对象就会被移入到老年代。新生代包括:Eden区 s0区 s1区(s0与s1为survivor空间)。 老年代只有一个区,老年代是存放时间较长,经过垃圾回收次数较多的对象。

[root@name2 java]# cat Test4.java 
public class Test4 {
  public static void main(String[] args){
    byte[] b1 = new byte[1024*1024];
    byte[] b2 = new byte[1024*1024];
    System.gc();
  }
}
[root@name2 java]# javac Test4.java 
###-XX:+PrintGCDetails  查看详细的gc细节
[root@name2 java]# java -XX:+PrintGCDetails test4 
Error: Could not find or load main class test4
Heap
 PSYoungGen      total 300544K, used 15482K [0x0000000671500000, 0x0000000686400000, 0x00000007c0000000)
  eden space 258048K, 6% used [0x0000000671500000,0x000000067241ebb0,0x0000000681100000)
  from space 42496K, 0% used [0x0000000683a80000,0x0000000683a80000,0x0000000686400000)
  to   space 42496K, 0% used [0x0000000681100000,0x0000000681100000,0x0000000683a80000)
 ParOldGen       total 687104K, used 0K [0x00000003d3e00000, 0x00000003fdd00000, 0x0000000671500000)
  object space 687104K, 0% used [0x00000003d3e00000,0x00000003d3e00000,0x00000003fdd00000)
 Metaspace       used 2530K, capacity 4480K, committed 4480K, reserved 1056768K
  class space    used 283K, capacity 384K, committed 384K, reserved 1048576K
###案例
[root@name2 java]# cat Test5.java 
import java.util.*;
public class Test5{
  public static void main(String[] args){
    Vector v = new Vector();
    for (int i=0;i<10;i++){
      byte[] b = new byte[1024*1024];
      v.add(b);
      System.out.println(i);
    }
    // 获取系统可用的最大堆内存
    System.out.println(Runtime.getRuntime().maxMemory()/1024/1024+"M");
  }
}
[root@name2 java]# javac Test5.java 
Note: Test5.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
[root@name2 java]# java Test5
0
1
2
3
4
5
6
7
8
9
14281M
###设置-Xmx5m 抛异常
[root@name2 java]# java -Xmx5m Test5
0
1
2
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at Test5.main(Test5.java:6)
[root@name2 java]# java -Xmx15m Test5
0
1
2
3
4
5
6
7
8
9
15M

设置最小堆内存: java应用程序可以使用-Xms设置最小堆内存,也就是jvm启动时,所占据的操作系统内存的大小。java程序在运行时,首先会被分配-Xms指定的内存大小,并尽可能尝试在这个空间内运行程序,确实无法满足时才会向操作系统申请更多内存,直到内存大小达到-Xmx指定的最大内存为止,如果超过-Xmx的值,则抛出OutofMemoryError。将-Xms设置与-Xmx大小相等,有利于减少miniorGC次数(演示过程省略),如果最小堆设置过小会提升minorGC次数甚至引发FullGC.

设置新生代: 新生代越大,GC次数越少,案例如下:

[root@name2 java]# cat Test6.java 
import java.util.*;
public class Test6{
  public static void main(String[] args){
    Vector v = new Vector();
    for (int i=0;i<10;i++){
      byte[] b = new byte[1024*1024];
      v.add(b);
      if(v.size()==3){
        v.clear();
      }
    }
  }
}
[root@name2 java]# javac Test6.java 
Note: Test6.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
###-Xmn设置新生代2m,gc次数如下
[root@name2 java]# java -Xmx11M -Xms11M -Xmn2M -verbose:gc Test6
[GC (Allocation Failure)  9621K->9816K(11776K), 0.0010642 secs]
[GC (Allocation Failure)  9816K->9936K(11776K), 0.0008553 secs]
[Full GC (Allocation Failure)  9936K->263K(11776K), 0.0032093 secs]

###不设置新生代默认情况下:GC五次(4次minorGC一次FullGC)
[root@name2 java]# java -Xmx11M -Xms11M  -verbose:gc Test6
[GC (Allocation Failure)  2530K->2632K(11776K), 0.0014930 secs]
[GC (Allocation Failure)  4740K->3736K(11776K), 0.0010714 secs]
[GC (Allocation Failure)  5833K->3800K(11776K), 0.0006653 secs]
[GC (Allocation Failure)  5901K->5904K(11776K), 0.0012846 secs]
[Full GC (Ergonomics)  5904K->2310K(11776K), 0.0033474 secs]

方法区

方法区也是JVM内存中非常重要的一块内存区域,与堆空间类似,它是被JVM中所有的线程共享,方法区主要保存的信息是类的元数据。方法区中最重要的是类的类型信息,常量池,域信息,方法信息等。方法区也被称为永久区,是一块独立于java堆的内存空间,但GC也能回收,通常通过两方面回收:1、对常量池回收,2、对元数据回收。

设置持久代: 持久代(方法区)不属于堆的一部分,使用-XX:MaxPermSize可以设置持久代的最大值,使用-XX:PermSize可以设置持久代的初始大小。 持久代的大小决定了系统可以支持多少个类定义和多长常量。 一般来说MaxPermSize设置为64MB已经可以满足绝大部分的程序正常工作,如果依然出现溢出,可以设置为128MB。主要用于大量的动态类生成,当需要生成大量的动态类时需要设置。

[root@name2 java]# cat Test7.java 
public class Test7{
  public static void main(String[] args){
    for (int i=0;i<Integer.MAX_VALUE;i++){
      String s = String.valueOf(i).intern();
    }
  }
}
[root@name2 java]# javac Test7.java 
[root@name2 java]# java -XX:PermSize=2m -XX:MaxPermSize=4m -XX:+PrintGCDetails Test7
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=2m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=4m; support was removed in 8.0
[GC (Allocation Failure) [PSYoungGen: 258048K->768K(300544K)] 258048K->776K(987648K), 0.1921285 secs] [Times: user=0.20 sys=0.00, real=0.19 secs] 

线程栈

线程栈是线程的一块私有空间,在JVM中可以使用-Xss参数设置线程栈的大小。-Xss设置越大 线程数越小,且与堆大小有关。

[root@name2 java]# cat Test8.java 
public class Test8{
  public static class MyThread extends Thread {
    public void run(){
      try{
        Thread.sleep(10000);
      } catch(Exception e){
        e.printStackTrace();
      }
    }
  }

  public static void main(String[] args){
    int i =0;
    try{
      for (i=0;i<10000;i++){
        new MyThread().start();
      }
    }catch (OutOfMemoryError e){
      System.out.println(i);
    }
  }
}
[root@name2 java]# javac Test8.java 
[root@name2 java]# java Test8

在需要多线程高并发时,通常设置较小的堆和较小的栈,有助于提高线程数。-Xss越大,线程数越小,Xmx越大线程数越小。

垃圾收集GC

垃圾收集器的类型

垃圾收集器的选择

[root@centos1 java]# cat Test9.java 
import java.util.*;

public class Test9 {
  static HashMap map = new HashMap();

  public static void main(String[] args){
    long begintime = System.currentTimeMillis();
    for(int i=0;i<10000;i++){
      if(map.size()*512/1024/1024>=400){
        map.clear();
        System.out.println("清除");
      }
      byte[] b1;
      for (int j=0;j<100;j++){
        b1=new byte[512];
        map.put(System.nanoTime(),b1);//用于消耗内存
      }
    }
    long endtime = System.currentTimeMillis();
    System.out.println(endtime-begintime);
  }
}
[root@centos1 java]# javac Test9.java 
Note: Test9.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
[root@centos1 java]# java -Xmx512M -Xms512M -XX:+UseParNewGC Test9
Java HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release
清除
1164
[root@centos1 java]# java -Xmx512M -Xms512M -XX:+UseParallelOldGC Test9
清除
1376
[root@centos1 java]# java -Xmx512M -Xms512M -XX:+UseSerialGC Test9
清除
1119
[root@centos1 java]# java -Xmx512M -Xms512M -XX:+UseConcMarkSweepGC Test9
清除
1932

调优方法

新对象预留在新生代

由于FullGC的成本要远高于MinorGC,因此尽可能将对象分配在新生代是明智的。大部分情况下JVM会尝试在eden区分配对象,但由于空间紧张,很可能不得不将部分年轻对象向老年代压缩,所以调优时,可以为应用程序分配一个合理的新生代空间,以最大限度避免新对象直接进入老年代。

-XX:+PrintGCDetails -Xmx20M -Xms20M 与-XX:+PrintGCDetails -Xmx20M -Xms20M -Xmn6M 对比

[root@centos1 java]# cat Test11.java 
public class Test11 {
  public static void main(String[] args){
    byte[] b1,b2,b3,b4;
    b1 = new byte[1024*1024];
    b2 = new byte[1024*1024];
    b3 = new byte[1024*1024];
    b4 = new byte[1024*1024];
  }
}
[root@centos1 java]# javac Test11.java 
[root@centos1 java]# java -XX:+PrintGCDetails -Xmx20M -Xms20M Test11
Heap
 ###new generation 新生代 总大小6144k  total 6144K
 def new generation   total 6144K, used 4655K [0x00000000fec00000, 0x00000000ff2a0000, 0x00000000ff2a0000)
  eden space 5504K,  84% used [0x00000000fec00000, 0x00000000ff08bc00, 0x00000000ff160000)
  from space 640K,   0% used [0x00000000ff160000, 0x00000000ff160000, 0x00000000ff200000)
  to   space 640K,   0% used [0x00000000ff200000, 0x00000000ff200000, 0x00000000ff2a0000)
 tenured generation   total 13696K, used 0K [0x00000000ff2a0000, 0x0000000100000000, 0x0000000100000000)
   the space 13696K,   0% used [0x00000000ff2a0000, 0x00000000ff2a0000, 0x00000000ff2a0200, 0x0000000100000000)
 Metaspace       used 2467K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 264K, capacity 386K, committed 512K, reserved 1048576K

[root@centos1 java]# java -XX:+PrintGCDetails -Xmx20M -Xms20M  -Xmn1M Test11
Heap
 ### -Xmn1M 参数指定新生代1M, new generation 新生代总大小  total 960K,used使用388k, 可见这4个对象没有在新生代里面
 def new generation   total 960K, used 388K [0x00000000fec00000, 0x00000000fed00000, 0x00000000fed00000)
  eden space 896K,  43% used [0x00000000fec00000, 0x00000000fec61098, 0x00000000fece0000)
  from space 64K,   0% used [0x00000000fece0000, 0x00000000fece0000, 0x00000000fecf0000)
  to   space 64K,   0% used [0x00000000fecf0000, 0x00000000fecf0000, 0x00000000fed00000)
  ###tenured generation 老年代total 19456K, used  4096K,可见4个对象在老年代
 tenured generation    total 19456K, used 4096K [0x00000000fed00000, 0x0000000100000000, 0x0000000100000000)
   the space 19456K,  21% used [0x00000000fed00000, 0x00000000ff100040, 0x00000000ff100200, 0x0000000100000000)
 Metaspace       used 2467K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 264K, capacity 386K, committed 512K, reserved 1048576K
#########################################-Xmn10M,发现所有对象在新生代里面,老年代空的
[root@centos1 java]# java -XX:+PrintGCDetails -Xmx20M -Xms20M  -Xmn10M Test11
Heap
 def new generation   total 9216K, used 4768K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  58% used [0x00000000fec00000, 0x00000000ff0a8058, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 Metaspace       used 2467K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 264K, capacity 386K, committed 512K, reserved 1048576K

大对象进入老年代

将大对象直接分配到老年代,保持新生代对象的结构和完整性,提高GC效率。-XX:+PrintGCDetails -Xmx20M -Xms20M 与-XX:+PrintGCDetails -Xmx20M -Xms20M -XX:PretenureSizeThreshold=1000000 进行比较观察出大对象分配给老年代;-XX:PretenureSizeThreshold=1000000设置大对象直接进入老年代的阀值,当对象的大小超过这个值,将直接在老年代分配。

[root@centos1 java]# cat Test12.java 
public class Test12 {
  public static void main(String[] args){
    byte[] b1;
    b1 = new byte[1024*1024*2];
  }
}
[root@centos1 java]# javac Test12.java 
###默认大对象在新生代
[root@centos1 java]# java -XX:+PrintGCDetails -Xmx20M -Xms20M -Xmn5M Test12
Heap
 def new generation   total 4608K, used 2556K [0x00000000fec00000, 0x00000000ff100000, 0x00000000ff100000)
  eden space 4096K,  62% used [0x00000000fec00000, 0x00000000fee7f158, 0x00000000ff000000)
  from space 512K,   0% used [0x00000000ff000000, 0x00000000ff000000, 0x00000000ff080000)
  to   space 512K,   0% used [0x00000000ff080000, 0x00000000ff080000, 0x00000000ff100000)
 tenured generation   total 15360K, used 0K [0x00000000ff100000, 0x0000000100000000, 0x0000000100000000)
   the space 15360K,   0% used [0x00000000ff100000, 0x00000000ff100000, 0x00000000ff100200, 0x0000000100000000)
 Metaspace       used 2467K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 264K, capacity 386K, committed 512K, reserved 1048576K
###-XX:PretenureSizeThreshold=1000000 直接让大对象分配在老年代
[root@centos1 java]# java -XX:+PrintGCDetails -Xmx20M -Xms20M -Xmn5M -XX:PretenureSizeThreshold=1000000 Test12 
Heap
 def new generation   total 4608K, used 508K [0x00000000fec00000, 0x00000000ff100000, 0x00000000ff100000)
  eden space 4096K,  12% used [0x00000000fec00000, 0x00000000fec7f148, 0x00000000ff000000)
  from space 512K,   0% used [0x00000000ff000000, 0x00000000ff000000, 0x00000000ff080000)
  to   space 512K,   0% used [0x00000000ff080000, 0x00000000ff080000, 0x00000000ff100000)
 tenured generation   total 15360K, used 2048K [0x00000000ff100000, 0x0000000100000000, 0x0000000100000000)
   the space 15360K,  13% used [0x00000000ff100000, 0x00000000ff300010, 0x00000000ff300200, 0x0000000100000000)
 Metaspace       used 2467K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 264K, capacity 386K, committed 512K, reserved 1048576K

稳定震荡堆大小: 稳定的堆大小是对垃圾回收有利的,设置-Xmx与-Xms大小一致。

吞吐量优先调优

参数
-Xmx3800M -Xms3800
-Xss128k 减小线程栈大小(一般设置Xmx的1/3)
-Xmn2G 设置新生代大小
-XX:+UseParallelGC 新生代并行回收器,这是一个关注吞吐量的收集器,可以尽量减少GC的时间
-XX:+parallelGCThreads=20 设置用于垃圾回收的线程数,通常情况下可以喝CPU数量相等。但CPU较多的情况下,设置相对较小的数值也是合理的
-XX:+UseParallelOldGC 老年代也是用并行回收收集器

降低停顿调优案例

为了降低应用软件在垃圾回收的停顿,首先考虑的是使用关注停顿的CMS回收器(G1回收器也可以),其次,为了减少FullGC次数,应尽量将对象预留在新生代,因为新生代的MinorGC成本远低于老年代的FullGC. 可以如下设置降低停顿:

参数
-XX:ParalleGCThreads 设置20个线程进行垃圾回收
-XX:UseParNewGC 新生代使用并行回收器
-XX:UseConcMarkSweepGC 使用CMS收集器降低停顿
-XX:SurvivorRatio=8 设置eden区和survivor区的比例为8:1
-XX:TargetSurvivorRatio=90 设置survivor使用率为90%
-XX:MaxTenuringThreshold=30 年轻对象到老年对象的年龄默认是15次,也就是15次minorGC依然存活,设置为30,既尽量让对象保存在新生代

快照堆

在性能问题排查中,分析快照dump是不可缺少的环节。

[root@centos1 java]# cat Test13.java 
public class Test13 {
  public static void main(String[] args){
    byte[] b1 = new byte[1024*1024*8];
    byte[] b2 = new byte[1024*1024*5];
  }
}
[root@centos1 java]# javac Test13.java 
[root@centos1 java]# java -Xmx10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./test13.dump Test13
java.lang.OutOfMemoryError: Java heap space
Dumping heap to ./test13.dump ...
Heap dump file created [794283 bytes in 0.004 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at Test13.main(Test13.java:3)
[root@centos1 java]# ll test13.dump 
-rw------- 1 root root 794283 May  6 22:24 test13.dump

dump的快照可以使用一些分析工具查看堆的详细信息

获取GC信息

参数
-verbose:gc 或-XX:+PrintGC都能获取GC信息
-XX:+PrintGCDetails 获取更加详细的信息
-XX:PrintGCTimeStamps 获取GC的频率和间隔
-XX:+PrintHeapAtGC 获取堆的使用情况
-Xloggc:D:\gc.log 指定日志情况

tomcat调优(待续)

登陆评论: 使用GITHUB登陆