jvm详解

jvm概述

结构图

image

虚拟栈

栈的存储单位:栈帧(局部变量表,操作数栈,动态链接,方法返回地址,一些附加信息)
局部变量表:存储的基本单位是slot,4个字节,可重复利用
操作数栈:在方法执行过程中,根据字节码指令,在栈中写入数据或提取数据
代码追踪:javap -v xxx.class或者使用idea的jclasslib插件(view->show Bytecode With jclasslib)
栈顶缓存技术(Top-of-Stack-Caching):将栈顶元素全部缓存在物理cpu的寄存器中,降低对内存的读写次数,提升引擎的执行效率
动态链接:每一个栈帧内部都包含一个指向运行时常量池中该帧所属方法的引用。(常量池的作用:提供一些符号和常量,便于指令的识别)
方法的调用:早期绑定;晚期绑定(如参数为接口);非虚方法(静态,final等);虚方法(晚期绑定的方法)
invokestatic
invokespecial
invokevirtual
invokeinterface
invokedynamic:动态解析出需要调用的方法,java8 lambda之后才有直接生成的方法
虚方法表
方法返回地址:存放调用该方法的程序计数器的值,即调用该方法的指令的下一条指令的地址
一些附加信息:虚拟机实现相关,比如对程序调试支持的信息

本地方法栈

native标识,java代码调用非java代码的接口,方法的实现由非java语言实现

java虚拟机栈用于管理java方法的调用,本地方法栈用于管理本地方法的调用

程序计数器

存储指向下一条指令的地址,由执行引擎读取下一条指令

为什么线程私有?cpu时间片轮转,需要记录当前线程的执行指令

一个JVM实例只存在一个堆内存,只有一个运行时实例(Runtime是饿汉式单例模式),对也是JVM内存管理的核心区域
java堆区在JVM启动的时候即被创建,其空间大小也就确定了,大小可以调节
所有的线程共享java堆,在这里还可以划分线程私有的缓冲区TLAB(Thread Local Allocation Buffer)
几乎所有的对象实例及数组都在堆分配,数组和对象可能永远不会分配在栈上,因为栈帧中保存引用,这个引用指向对象或数组在堆中的位置
方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除,(GC工作时会停止用户线程)
堆是GC(Garbage Collection)执行垃圾回收的重点区域,虚拟机栈不会GC的

方法区(元空间)

类的元数据(元数据并不是类的Class对象!Class对象是加载的最终产品,类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)才是存在方法区

static 、final定于的静态常量

类加载器

1.JVM支持两种类型的加载器,分别为引导类加载器C/C++实现(BootStrap ClassLoader)和自定义类加载器由Java实现

2.从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。

3.注意上图中的加载器划分关系为包含关系,并不是继承关系

4.按照这样的加载器的类型划分,在程序中我们最常见的类加载器是:引导类加载器BootStrapClassLoader、自定义类加载器(Extension Class Loader、System Class Loader、User-Defined ClassLoader)

双亲委派机制

image

从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
好处: 如果有人想替换系统级别的类

弊端:不能向下委派,不能不委派

打破双亲委派可以

1.通过spi机制,使用ServiceLoader.load去加载

2.通过自定义类加载器,继承classloader,重写loadclass方法

执行引擎

image

执行引擎概述:

  1. 执行引擎是 Java 虚拟机核心的组成部分之一。

  2. JVM 的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只 是一些能够被 JVM 所识别的字节码指令、符号表,以及其他辅助信息。

  3. 那么,如果想要让一个 Java 程序运行起来,执行引擎(Execution Engine) 的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM 中的执行引擎充当了将高级语言翻译为机器语言的译者。

注意区分概念:

  1. 前端编译:从 Java 程序员-字节码文件的这个过程叫前端编译.

  2. 执行引擎这里有两种行为:一种是解释执行,一种是编译执行(这里的是后端 编译)

    (
    前端编译(.java --> .class):
    字节码 不等于 机器码
    需要JVM将字节码加载我们的内存中,再需要执行引擎将字节码解释/编译成机器码
    后端编译(.class --> 机器码);
    )

什么是解释器?什么是 JIT 编译器?

解释器:当 Java 虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。

JIT(Just In Time Compiler)编译器:就是虚拟机将源代码一次性直接编译 成和本地机器平台相关的机器语言,但并不是马上执行。

执行引擎机制:

  1. 解释器:将字节码逐行解释执行.
  2. JIT编译器(即时编译器):将字节码整体编译为机器码执行.

为什么 Java 是半编译半解释型语言?

起初将 Java 语言定位为“解释执行”还是比较准确的。再后来,Java 也发展出可以直接生成本地代码的编译器。现在 JVM 在执行 Java 代码的时候,通常都会将解释执行与编译执行二者结合起来进行。

原因:

JVM 设计者们的初衷仅仅只是单纯地为了满足 Java 程序实现跨平台特性,因此避免采用静态编译的方式由高级语言直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法。

解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行,执行效率低。

JIT 编译器将字节码翻译成本地代码后,就可以做一个缓存操作,存储在方法区 的 JIT 代码缓存中(执行效率更高了)。

是否需要启动 JIT 编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。

JIT 编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升 Java 程序的执行性能。

一个被多次调用的方法,或者是一-个方法体内部循环次数较多的循环体都可以被称之为“热点代码”。

目前 HotSpot VM 所采用的热点探测方式是基于计数器的热点探测。

总结:

逐行解释执行效率低,

JVM会针对使用频率较高的热点代码进行编译并缓存起来,执行效率提高)

JIT 编译器执行效率高为什么还需要解释器?

  1. 当程序启动后,解释器可以马上发挥作用,响应速度快,省去编译的时间,立即执行。

  2. 编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间,但编译 为本地代码后,执行效率高。就需要采用解释器与即时编译器并存的架构来换取 一个平衡点。

逃逸分析

编程语言的编译优化原理中,分析指针动态范围的方法称之为逃逸分析。可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。也就是说通过逃逸分析可以判断对象的引用和使用范围从而决定是否要将这个对象分配到堆上面。

对象逃逸的范围有:全局逃逸、参数逃逸、没有逃逸;

Java的逃逸分析只发在JIT的即时编译中,因为在启动前已经通过各种条件判断出来是否满足逃逸,通过上面的流程图也可以得知对象分配不一定在堆上,所以可知满足逃逸的条件如下,只要满足以下任何一种都会判断为逃逸。

一、对象被赋值给堆中对象的字段和类的静态变量。

二、对象被传进了不确定的代码中去运行。

注意:逃逸分析不是直接的优化手段,而是分码分析手段。

相关配置

开启逃逸分析(JDK8中,逃逸分析默认开启。)
-XX:+DoEscapeAnalysis
关闭逃逸分析
-XX:-DoEscapeAnalysis
查看逃逸分析结果
-XX:+PrintEscapeAnalysis

package com.escape;
 
import com.Student;
 
/**
 * @author: csh
 * @Date: 2021/4/21 11:01
 * @Description:线程逃逸
 */
public class ThreadEscape {
 
    public static Integer sum=0;
 
 
    
    /**
     * 详细日志
     * -XX:+PrintGCDetails
     * 简单日志
     * -XX:+PrintGC
     *
     * 功能描述: -XX:+PrintGC -Xms5M -Xmn5M -XX:+DoEscapeAnalysis
     *
     * @param:
     * @return:
     * @auther: csh
     * @date: 2021/4/21 13:48
     */
    public static void main(String[] args) throws InterruptedException {
        //-XX:-DoEscapeAnalysis 未开启锁消除 234毫秒 //-XX:+DoEscapeAnalysis 开启则128毫秒
// long start = System.currentTimeMillis();
// for(int i =0;i<5_000_000;i++){
// escape("a", "b");
// }
// long end = System.currentTimeMillis();
// System.out.println("耗时"+(end-start)+"毫秒");
 
        //-XX:-DoEscapeAnalysis 313毫秒 -XX:+DoEscapeAnalysis 开启:273毫秒
// long start = System.currentTimeMillis();
// for(int i =0;i<5_000_000;i++){
// new ThreadEscape().noEscape("a", "b");
// }
// long end = System.currentTimeMillis();
// System.out.println("耗时"+(end-start)+"毫秒");
 
        //-XX:-DoEscapeAnalysis 关闭:53毫秒 -XX:+DoEscapeAnalysis 开启:43毫秒
// long start = System.currentTimeMillis();
// for(int i =0;i<5_000_000;i++){
// new ThreadEscape().setSum(i);
// }
// long end = System.currentTimeMillis();
// System.out.println("耗时"+(end-start)+"毫秒");
 
        //-XX:-DoEscapeAnalysis 关闭:145毫秒 -XX:+DoEscapeAnalysis 开启:8毫秒
// long start = System.currentTimeMillis();
// for(int i =0;i<5_000_000;i++){
// new ThreadEscape().newLockNoEscape();
// }
// long end = System.currentTimeMillis();
// System.out.println("耗时"+(end-start)+"毫秒");
 
        long start = System.currentTimeMillis();
        for(int i=0;i<5000000;i++){
            newObject();
        }
// for(int i =0;i<5000000;i++){
// newObject2();
// }
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end-start)+"毫秒");
        Thread.sleep(100000);
    }
 
 
    /**
     *
     * 功能描述: 全局变量赋值发生逃逸
     *
     * @param:
     * @return:
     * @auther: csh
     * @date: 2021/4/21 14:23
     */
    public void setSum(Integer number){
        sum = number;
    }
 
    /**
     *
     * 功能描述: 线程逃逸,因为该StringBuffer 会将结果返回,可能被其他地方所引用
     *
     * @param:
     * @return:
     * @auther: csh
     * @date: 2021/4/21 11:03
     */
    public static StringBuffer escape(String s1, String s2){
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(s1);
        stringBuffer.append(s2);
        return stringBuffer;
    }
 
    /**
     *
     * 功能描述: 未发生逃逸
     *
     * @param:
     * @return:
     * @auther: csh
     * @date: 2021/4/21 11:05
     */
    public  void noEscape(String s1,String s2){
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(s1);
        stringBuffer.append(s2);
    }
 
    /**
     *
     * 功能描述: 创建线程可见,但对象无法逃逸
     *
     * @param:
     * @return:
     * @auther: csh
     * @date: 2021/4/21 14:30
     */
    public void newLockNoEscape(){
        //创建线程可见,但对象无逃逸
        synchronized (new Object()){
 
        }
        //创建线程可见,但对象无逃逸
        Object noEscape = new Object();
    }
 
    /**
     *
     * 功能描述: 方法逃逸
     *
     * @param:
     * @return:
     * @auther: csh
     * @date: 2021/4/22 11:55
     */
    public static Object newObject(){
        return new Object();
    }
 
    /**
     *
     * 功能描述: 没有逃逸
     *
     * @param:
     * @return:
     * @auther: csh
     * @date: 2021/4/22 11:55
     */
    public static void newObject2(){
        new Object();
    }
 
 
}


测试如下:

-XX:-DoEscapeAnalysis -XX:+PrintGC

long start = System.currentTimeMillis();
        for(int i=0;i<5000000;i++){
            newObject();
        }
// for(int i =0;i<5000000;i++){
// newObject2();
// }
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end-start)+"毫秒");
        Thread.sleep(100000);

结果:39毫秒,一次gc 并且有一百多万的垃圾回收。

[GC (Allocation Failure) 65536K->880K(251392K), 0.0013300 secs]
耗时39毫秒

-XX:+DoEscapeAnalysis -XX:+PrintGC

long start = System.currentTimeMillis();
        for(int i=0;i<5000000;i++){
            newObject();
        }
// for(int i =0;i<5000000;i++){
// newObject2();
// }
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end-start)+"毫秒");
        Thread.sleep(100000);

结果:只有4毫秒,没有gc,提高了快10倍效率,并且堆中只有十几万。逃逸了

耗时4毫秒

可以发现一个逃逸和没逃逸的问题,只要是对象有被方法外部或者全局引用到那肯定会存在逃逸。

MinorGC,MajorGC,FullGC

JVM再进行GC时,并非每次都对三个内存(新生代,老年代,方法区)区域一起回收。

针对HotSpot VM的实现,它里面的GC按照回收区域可分为两大类:1. 部分收集 2. 整堆收集

部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为

新生代收集(MinorGC/YoungGC):只是新生代(Eden/S0/S1)的垃圾收集
老年代收集(MajorGC/OldGC):只是老年代的垃圾收集(目前只有CMS GC会有单独收集老年代的行为。很多时候Major GC会和Full GC混淆使用,具体要分表是老年代回收还是整堆回收)
混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集。(目前,只有G1 收集器会有这种行为)
整堆收集:FullGC 收集整个java堆和方法区

MinorGC 触发机制

当年轻代空间不足时,会触发MinorGC,这里的年轻代是只Eden区域,Survivor满不会引发GC。(每次YoungGC会清理年轻代的区域包括Survivor)
MinorGC会触发STW(stop the world),暂停其他用户的线程,等待垃圾回收结束,用户线程才恢复运行

MajorGC

指老年代空间不足时发生在老年代的GC,对象从老年代消失时,会说Major GC 或 Full GC发生了

出现MajorGC,经常会伴随至少一次MinorGC(但非绝对的,在Parallel Scavenge收集器的策略里就有直接进行Major的策略选择过程,也就是在老年代空间不足时,指定先触发minorGC,如果之后空间还不足触发Major GC)

MajorGC的速度一般会比MinorGC满10倍以上,STW的时间更长

FullGC

调用System.gc() 时,系统建议执行FullGC,但不保证执行
老年代空间不足
方法去空间不足
通过MinorGC后进入老年代的平均大小大于老年代的可用内存
由Eden区,Survivor 0 (From Space) 向 Survivor1 (To Space)区复制时,对象大小小于To Space可用内存,则把对象放入老年代,且老年代空间由放不下

volatile 作用

1.线程可见性
2.禁指重排 (防止指令重排序)
3.不保证原子性

public class VolatileTest {
    /**
     * 线程停止
     * 有 volatile 修饰时,就会通知所有访问此变量的线程,重新load值到本地内存
     */
    private static volatile boolean volatileFlag=true;
    /**
     * 线程不停止
     * 没有 volatile 修饰时,就一直访问的 线程本地内存,一直为true
     */
    private static boolean flag=true;


    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (volatileFlag){

            }
            System.out.println("end");
        },"sever").start();
        Thread.sleep(1000);
        volatileFlag=false;
    }

}

public class VolatileTest2 {
    private static int x=0,y=0;
    private static int a=0,b=0;

    /**
     * 正常情况执行顺序,
     * a=1
     * x=b
     * b=1
     * y=a
     * 不会出现 x=0,y=0;的情况
     * 如果指令重排:↓
     * x=b; 默认 b=0;a=0;
     * y = a;
     * b = 1;
     * a =1;
     * 就出现 x=0,y=0;的情况
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        int i=0;
        for(;;){
            i++;
            x=0;y=0;
            a=0;b=0;
            Thread one = new Thread(()-> {
                a=1;
                x=b;
            });
            Thread other = new Thread(()-> {
                b = 1;
                y = a;
            });
            one.start();other.start();
            one.join();other.join();
            String res="第"+i+"次(" + x + "," + y + ")";
            if (x==0 && y==0){
                System.out.println(res);
                break;
            }
        }
    }

}

结果:第723984次(0,0)

常见命令

nohup java -Xmx256m -Xms256m -Xmn128M -Xss256k -jar epay-check-account-0.0.4.jar
-Xms set initial Java heap size 默认物理内存 64/1
-Xmx set maximum Java heap size 默认物理内存 4/1
-Xss 规定了每个线程堆栈的大小
top 、jps
jvisualvm.exe

垃圾回收模型

新生代

Serial GC

使用:-XX:+UseSerialGC

  Serial GC 是新生代的垃圾回收器,Serial 体现在其收集工作是单线程的,并且在垃圾收集过程中,其他线程阻塞,进入 Stop the World 状态。新生代使用 Serial 垃圾回收器,是基于复制算法的。

Parallel Scavenge

  使用:-XX:+UseParallelGC

  Parallel Scavenge 收集器是一个新生代的垃圾回收器,采用的是复制算法。关注的是程序到达一个可控制的吞吐量(CPU 用于运行用户代码的时间 / CPU总消耗时间)。吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。高吞吐量可以最高效率的利用 CPU 时间,尽快完成程序的运算任务。

  其中:

  Parallel Scavenge 收集器有个自适应调节参数。这个参数是:-XX:UseAdaptiveSizePolic。这是一个开关参数,当这个开关打开后,就不需要手动指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PertenureSizeThreshold)等参数细节了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

  可以直接设置暂停时间或者吞吐量等目标,JVM 会自动进行适应性调整。

  -XX:MaxGCPauseMillis=value

  -XX:GCTimeRatio=N // GC时间和用户时间比例 = 1 / (N+1)

ParNew

  使用:-XX:+UseParNewGC

  ParNew 垃圾回收器是 Serial 收集器的多线程版本。采用的也是复制算法。包括 Serial 收集器可用的所有控制参数。也就是可并行的进行垃圾回收。

  可通过 -XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。

老年代

SerialOld

  Serial Old 是 Serial 垃圾收集器的老年代版本,同样是个单线程的收集器,是基于 标记-整理算法。

Parallel Old

  Parallel Old 是 Parallel Scavenge 的老年代版本,使用的是 多线程-标记整理算法。JDK1.6 才开始使用。Parallel Scavenge 是可以保证新生代的吞吐量优先,但是不能保证整体吞吐量。Parallel Old 是为了在老年代同样提供吞吐量优先的垃圾回收器。

CMS

  使用:-XX:+UseConcMarkSweepGC

  CMS 是基于标记清除算法,设计的目的是减少停顿时间。基于标记清除算法,会存在内存碎片化的问题。其处理流程:

  1)初始标记(CMS-initial-mark),会导致 stw;

  2)并发标记(CMS-concurrent-mark),与用户线程同时运行;

  3)重新标记(CMS-remark),会导致 stw;

  4)并发清除(CMS-concurrent-sweep),与用户线程同时运行;

  5)并发重置状态等待下次 CMS 的触发(CMS-concurrent-reset),与用户线程同时运行;

G1

G1 本质上是一个分代垃圾回收器。G1 垃圾回收器相对于 CMS 垃圾回收器,有两个改进:

  1)基于标记-整理算法,不产生内存碎片

  2)可以准确的控制停顿时间,在不牺牲吞吐量的情况下实现低停顿的垃圾回收

  G1 为了避免全域的垃圾收集,把堆内存划分成大小固定的几个独立区域,并跟踪这些区域的回收进度。同时在后台维护一个优先列表,每次根据收集时间,优先回收垃圾最多的区域。

  G1 引入了额外的概念,Region。G1 垃圾回收器把堆划分一个个大小相同的 Region。在 HotSpot 的实现中,整个堆被划分成 2048 左右个的 Region。每个 Region 的大小在 1-32M 之间,具体的大小取决于堆得大小。

  G1 垃圾回收器的分代也是建立在这些 Region 的基础上的。对于 Region 来说,他会有一个分代的类型,并且是唯一一个。即:每一个 Region,它要么是 yong 的,要么是 old 的。还有一类十分特殊的 Humongous。所谓的 Humongous,就是一个对象的大小超了某一阈值 —— HotSpot中是 Region 的 1/2 ,那么它会被标记为 Humongous。如果我们审视 HotSpot 的其余的垃圾回收器,可以发现,这种对象以前被称为大对象,会被直接分配老年代。而在 G1 回收器中,则是做了特殊的处理。

  G1 并不要求相同类型的 region 要相邻。换言之,就是 G1 回收器不要求他们连续。当然在逻辑上,分代依旧是连续的。

 每一个分配的 Region,都可以分成两个部分,已分配的和未分配的。他们之间的界限被称为 top。把一个对象分配到 Region 内,只需要简单的增加 top 的值。这个做法实际上就是 bump-the-pointer。过程如下:(移动 top 线)

 Region 是 G1 回收器一次回收的最小单元,即每一次回收都是回收 N个 Region。这个 N 是多少,主要受到 G1 回收效率和用户设置的软实时目标有关。每一次的回收,G1 会选择可能回收最多垃圾的 Region 进行回收。与此同时,G1 回收器会维护一个空间 Region 的链表。每次回收后的 Region 都会被加入这个链表。每一次都只有一个 Region 处于被分配的状态中,被称为 current region 。在多线程的情况下会带来并发问题。G1 回收器采用和 CMS 一样的 TLABs 的手段,即为每一个线程分配一个 buffer,线程分配内存就在这个 buffer 内分配,但是当线程耗尽了自己的 buffer 以后,需要重新申请新的 buffer。这个时候依然会带来并发的问题。G1 回收器采用的是 CAS 操作。

jdk8 默认组合 Parallel Scavenge + Parallel Old

jvm参数

一、堆内存相关配置

 设置堆初始值
 指令1:-Xms2g
 指令2:-XX:InitialHeapSize=2048m
 
 
 设置堆区最大值
 指令1:`-Xmx2g` 
 指令2: -XX:MaxHeapSize=2048m
 
 
 缩小堆内存的时机
 -XX:MaxHeapFreeRatio=70//堆内存使用率大于70时扩张堆内存,xms=xmx时该参数无效,默认值70
 
 
 扩张堆内存的时机
 -XX:MinHeapFreeRatio=40//堆内存使用率小于40时缩减堆内存,xms=xmx时该参数无效,默认值40
 
 
 新生代内存配置
 指令1:-Xmn512m
 指令2:-XX:MaxNewSize=512m
 
 
 2个survivor区和Eden区大小比率
 指令:-XX:SurvivorRatio=6  //S区和Eden区占新生代比率为1:6,两个S区2:6
 
 
 新生代和老年代的占比
 -XX:NewRatio=4  //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2
 

二、方法区内存配置常用参数

 
 初始化的Metaspace大小,
 -XX:MetaspaceSize
 
 Metaspace最大值
 -XX:MaxMetaspaceSize
   

三、线程栈内存配置常用参数

每个线程栈最大值

 指令1:-Xss256k
 
 指令2:-XX:ThreadStackSize=256k

注意:

栈设置太大,会导致线程创建减少。

栈设置小,会导致深入不够,深度的递归会导致栈溢出。

建议栈深度设置在3000-5000


四、配置垃圾收集器

 Serial垃圾收集器(新生代)
 开启:-XX:+UseSerialGC
 关闭:-XX:-UseSerialGC
 //新生代使用Serial  老年代则使用SerialOld
 
 ParNew垃圾收集器(新生代)
 开启 -XX:+UseParNewGC
 关闭 -XX:-UseParNewGC
 //新生代使用功能ParNew 老年代则使用功能CMS
 
 Parallel Scavenge收集器(新生代)
 开启 -XX:+UseParallelOldGC
 关闭 -XX:-UseParallelOldGC
 //新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
 
 ParallelOl垃圾收集器(老年代)
 开启 -XX:+UseParallelGC
 关闭 -XX:-UseParallelGC
 //新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
 
 CMS垃圾收集器(老年代)
 开启 -XX:+UseConcMarkSweepGC
 关闭 -XX:-UseConcMarkSweepGC
 
 G1垃圾收集器
 开启 -XX:+UseG1GC
 关闭 -XX:-UseG1GC

五、GC策略配置

GC并行执行线程数

 -XX:ParallelGCThreads=16

新生代可容纳的最大对象

 -XX:PretenureSizeThreshold=1000000 //大于此值的对象直接会分配到老年代,设置为0则没有限制。
   //避免在Eden区和Survivor区发生大量的内存复制,该参数只对Serial和ParNew收集器有效,Parallel Scavenge并不认识该参数

进入老年代的GC年龄

 进入老年代最小的GC年龄
 -XX:InitialTenuringThreshol=7 //年轻代对象转换为老年代对象最小年龄值,默认值7,对象在坚持过一次Minor GC之后,年龄就加1,每个对象在坚持过一次Minor GC之后,年龄就增加1
 
 
 进入老年代最大的GC年龄
 -XX:MaxTenuringThreshold=15 //年轻代对象转换为老年代对象最大年龄值,默认值15

六、GC日志信息配置

配置GC文件路径

 -Xloggc:/data/gclog/gc.log//固定路径名称生成
 -Xloggc:/home/GCEASY/gc-%t.log //根据时间生成

滚动生成日志

日志文件达到一定大小后,生成另一个文件。须配置Xloggc

 开启 -XX:+UseGCLogFileRotation
 关闭 -XX:-UseGCLogFileRotation
 
 -XX:NumberOfGCLogFiles=4   //滚动GC日志文件数,默认0,不滚动
 -XX:GCLogFileSize=100k  //GC文件滚动大小,需配置UseGCLogFileRotation,设置为0表示仅通过jcmd命令触发

打印详细的GC日志

打印GC的详细日志,并且在程序运行结束是会打印出JVM的内存占用情况

 开启 -XX:+PrintGCDetails
 关闭 -XX:-PrintGCDetails

打印应用暂停时间

 开启 -XX:+PrintGCApplicationStoppedTime
 关闭 -XX:-PrintGCApplicationStoppedTime

每次GC完成后,打印出JVM堆内存每个区域的使用情况

 开启 -XX:+PrintHeapAtGC
 关闭 -XX:-PrintHeapAtGC

打印存活实例年龄信息

 开启 -XX:+PrintTenuringDistribution
 关闭 -XX:-PrintTenuringDistribution


七、条件触发配置

OutOfMemory异常时输出文件

抛出内存溢出错误时导出堆信息到指定文件

 开启 -XX:+HeapDumpOnOutOfMemoryError
 关闭 -XX:-HeapDumpOnOutOfMemoryError
 //可以通过jinfo -flag [+|-]HeapDumpOnOutOfMemoryError <pid> 或 jinfo -flag HeapDumpOnOutOfMemoryError=<value> <pid> 来动态开启或设置值
 
 
 -XX:HeapDumpPath=/data/dump/jvm.dump//设置文件路径
 //当HeapDumpOnOutOfMemoryError开启的时候,dump文件的保存路径,默认为工作目录下的

在Full GC时生成dump文件

 -XX:+HeapDumpBeforeFullGC       //实现在Full GC前dump
 -XX:+HeapDumpAfterFullGC        //实现在Full GC后dump。
 -XX:HeapDumpPath=e:\dump        //设置Dump保存的路径

 JAVA_OPTS="-XX:+HeapDumpOnOutOfMemoryError  -XX:HeapDumpPath=/data/dump/jvm.dump"
   


八、辅助指令

查看或修改正在运行的JVM 某项配置

指令格式:jinfo -flag 参数 进程号

1、查看HeapDumpOnOutOfMemoryError 配置是否开启

 jinfo -flag HeapDumpOnOutOfMemoryError 870778

2、修改HeapDumpOnOutOfMemoryError 配置(-关闭 +开启)

 jinfo -flag +HeapDumpOnOutOfMemoryError 870778

模糊查找指令

如果忘记了某个指令的全名,可根据部分单词匹配查找

 java -XX:+PrintFlagsInitial |grep GC

查看JVM所有参数

 java -XX:+PrintFlagsInitial


九、常用启动参数

1、设置堆内存大小

设置堆内存大小,Xms 最小内存,Xmx最大内存,不设置默认为物理机内存的四分之一。

 -Xms2g   -Xmx2g`     

2、GC日志参数

生成GC滚动日志记录 ,当需要对GC排查问题时候需要对此日志分析。

 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation  -XX:+PrintHeapAtGC  -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M    -Xloggc:/opt/ard-user-gc-%t.log 
 -Xloggc:/opt/app/ard-user/ard-user-gc-%t.log   设置日志目录和日志名称
 -XX:+UseGCLogFileRotation           开启滚动生成日志
 -XX:NumberOfGCLogFiles=5            滚动GC日志文件数,默认0,不滚动
 -XX:GCLogFileSize=20M               GC文件滚动大小,需开启UseGCLogFileRotation
 -XX:+PrintGCDetails                 开启记录GC日志详细信息(包括GC类型、各个操作使用的时间),并且在程序运行结束打印出JVM的内存占用情况
 -XX:+ PrintGCDateStamps             记录系统的GC时间           
 -XX:+PrintGCCause                   产生GC的原因(默认开启)

3、异常时记录内存日志

抛出内存溢出错误时导出堆信息到指定文件,内存溢出时需要对此日志进行分析

 -XX:+HeapDumpOnOutOfMemoryError  -XX:HeapDumpPath=/data/dump/jvm.dump
 HeapDumpOnOutOfMemoryError  异常后打印堆内存信息

查看参数是否生效

jinfo -flag 参数 进程号

jinfo -flag HeapDumpOnOutOfMemoryError 1162

jvm所设计算法

标记清除算法

image

复制算法

image
image

标记整理算法

image

cms 三色标记算法

image

引用计数算法

引用计数算法就说为每一个对象保存一个计数属性,用来记录对象的引用次数,只要有任何对象引用了此对象引用次数加一,当引用失效时计数器减一。这种方法虽然实现简单,对象辨识度高,回收效率告,但它有着致命的缺点,即无法解决循环引用的问题。

可达性算法

可达性分析算法不仅同样具备实现简单和执行效率高等特点,更重要的是该算法可以有效的解决循环引用的问题,防止内存泄漏。Java垃圾的标记就使用可达性分析算法 。

  • 可达性分析是一以跟对象集合为起点,按照从上至下的方式搜索被根对象集合所连接的目标对象。
  • 使用可达性分析后,内存中存活的对象都会被根对象直接或间接的引用着,搜索走过的路径称为“引用链”,如果对象没有被引用链连接,那么对象就是不可达的,就是垃圾。

可以作为GCRoots的引用

  1. 栈中对对象的引用

  2. 方法区中静态属性对对象的引用

  3. 方法区中常量引用的对象(字符串常量池)

  4. 被synchronized同步锁定的对象

posted @ 2022-06-15 17:18  让人生留下足迹  阅读(420)  评论(1编辑  收藏  举报