基于jdk8的jvm(hotspot)笔记(jdk8特性把永久代废除引入元空间)

jvm笔记


先引入一张结构图
在这里插入图片描述

程序计数器

Program Counter Register 程序计数器(寄存器
作用:是记住下一条jvm指令的执行地址
特点:是线程私有的 不会存在内存溢出

虚拟机栈

Java Virtual Machine Stacks (Java 虚拟机栈)
FILO先进后出,暂存数据的地方。每个线程都包含一个栈区!栈存放在一级缓存中,存取速度较快。
有线程隔离的特点
每个线程运行时所需要的内存,称为虚拟机栈 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

垃圾回收是否涉及栈内存?

不会。当活动栈帧结束后会自动弹出栈。

栈内存分配越大越好吗?

不。因为每个线程都会分配单独的栈。总内存不变。栈内存变大,则允许的多线程数会减少。

方法内的局部变量是否线程安全?

如果方法内局部变量没有逃离方法的作用访问,它是线程安全的 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

/**
 * 局部变量的线程安全问题
 */
public class Demo1_17 {

    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        sb.append(4);
        sb.append(5);
        sb.append(6);
        new Thread(()->{
            m2(sb);
        }).start();
    }
//sb对象外部无法访问。线程安全
    public static void m1() {
        StringBuilder sb = new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }
//sb是通过参数传过来的。可能出现两个线程同时操作sb对象。线程不安全
    public static void m2(StringBuilder sb) {
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }
//sb作为参数传出去。线程也不安全
    public static StringBuilder m3() {
        StringBuilder sb = new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        return sb;
    }
}

本地方法栈

本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native(调用其他语言实现的方法) 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。

Heap 堆 jvm只有一个堆区被所有线程所共享!堆存放在二级缓存中,调用对象的速度相对慢一些,生命周期由虚拟机的垃圾回收机制定。
通过 new 关键字,创建对象都会使用堆内存
堆的优点-可以动态的分配内存大小,生命周期不确定。缺点-速度略慢

内存溢出代码测试

虽然有垃圾回收机制。使用不当还是会有内存溢出
参数:-Xmx8m(堆最大值)

import java.util.ArrayList;
import java.util.List;

/**
 * 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
 * -Xmx8m
 */

public class Demo {
    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "hello";
            while (true) {
                list.add(a); // hello, hellohello, hellohellohellohello ...
                a = a + a;  // hellohellohellohello
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);//24
        }
    }
       //结果
      //报错:java.lang.OutOfMemoryError: Java heap space
    //........................
      //17

}

方法区

jdk1.8开始方法区被移到元空间了使用的是本地内存(操作系统的内存)不被jvm管理了。
存储的信息:主要存放已被虚拟机加载的类信息、运行时常量、静态变量、即时编译器编译后的代码等数据
jdk1.6结构图
在这里插入图片描述
jdk1.8结构图
注意:图中常量池是运行时常量池
在这里插入图片描述

可以看到永久代在jdk1.8永久废除了。引入了元空间。同时字符串常量池放在了堆内存中。
当方法区内存溢出时

  • 内存溢出 java.lang.OutOfMemoryError: Metaspace *
  • 设置方法区的最大内存:-XX:MaxMetaspaceSize=8m

常量池

常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量 等信息。 (存在堆内存)(当超过一个两个字节的数据会列入常量池)
运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量 池,并把里面的符号地址变为真实地址(存元空间)

StringTable

StringTable是字符串常量池。StringTable数据结构是hashtable.存放的位置在堆中所以有垃圾回收机制。
调优参数:-XX:StringTableSize=桶个数 -XX:+PrintStringTableStatistics (打印StringTable信息)

经典的string面试题

//在jdk1.8下
String s1 = "a"; 
String s2 = "b"; 
String s3 = "a" + "b"; //在编译期间会优化。和“ab”等效
String s4 = s1 + s2; 
String s5 = "ab"; 
String s6 = s4.intern();
// 问会下面语句输出什么 
System.out.println(s3 == s4); //false
System.out.println(s3 == s5); //true
System.out.println(s3 == s6);//true
String x2 = new String("c") + new String("d"); 
String x1 = "cd";
x2.intern();
System.out.println(x1 == x2);//false
// 问,如果调换了【到数第二第三行】的位置呢
//System.out.println(x1 == x2);//true
//如果是jdk1.6呢 
//System.out.println(x1 == x2);//false

StringTable
当程序运行时会吧常量池的信息放到运行时常量池。此时的StringTable还是空。
当程序运行到String s1 = “a”; 时会去StringTable查找有没有“a”对象。
没有:就创建一个并放入StringTable中。StringTable=[“a”]。
有:就直接在StringTable中获取。不会创建新的对象。
String s4 = s1 + s2;(s1+s2相当于new StringBuilder().append(“a”).append(“b”).toString() ;toString() 方法就相当于new String(“ab”);) (s1=“a”,s2=“b”);所以s4是new出来的会存才堆内存中。
得出:System.out.println(s3 == s4); //false

String s3 = “a” + “b”;
StringTable=[“a”,“b”,“ab”]
String s3 = “a” + “b”; //在编译期间会优化。和“ab”等效。所以s3会在StringTable中获取“ab”对象。所以 System.out.println(s3 == s5);//true。

intern();
String s6 = s4.intern();//jdk1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池(StringTable), 会把串池中的对象返回.jdk1.6会将对象复制一份在放
(StringTable)System.out.println(s3 == s6);//true(jdk1.8返回true,jdk1.6返回false)

直接内存

Direct Memory 常见于 NIO 操作时,用于数据缓冲区 分配回收成本较高,但读写性能高 不受 JVM 内存回收管理。
使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法 ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调 用 freeMemory 来释放直接内存

垃圾回收

判断对象可以回收

1.引用计数法

对象被引用一次就会计数器就加一。放开引用就减一。当计数器为零是证明没有引用可以当做垃圾回收。
但是会有问题。如图AB相互引用计数器无法清零。也就无法回收。
在这里插入图片描述
2.可达性分析算法jvm采用该算法
Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以 回收
哪些对象可以作为 GC Root ?
1.虚拟机栈中引用的对象(即栈帧中的本地变量表)
2.方法区中的常量引用的对象
3.方法区中的类静态属性引用的对象
4.本地方法栈中的JNI(Native方法)的引用对象
5.活跃线程的引用对象

四种引用

  1. 强引用
    只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
    例子程序员平常new的都是强引用
  2. 软引用(SoftReference) 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用 对象 可以配合引用队列来释放软引用自身
  3. 弱引用(WeakReference) 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 可以配合引用队列来释放弱引用自身
  4. 虚引用(PhantomReference) 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存

垃圾回收算法三种

算法一标记+清除

英文: Mark Sweep
优点:速度较快。缺点:会产生内存碎片(内存空间不连续)如图

在这里插入图片描述

算法二标记+整理

英文:Mark Compact
优点:没有内存碎片。缺点:速度慢(因为要移动数据保证了内存连续)

在这里插入图片描述

算法三标记+复制

优点:不会有内存碎片 缺点:需要占用双倍内存空间
在这里插入图片描述
jvm会结合3种算法对垃圾进行回收。不会单单使用某一种。

分代垃圾回收

对象首先分配在伊甸园区域

新生代空间不足时,触发 minor gc,

伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的 对象年龄加 1并且交换 from to

minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行

当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit) 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时 间更长
在这里插入图片描述

垃圾回收器

串行

老年代标记整理算法
新生代标记复制算法
单线程 堆内存较小,适合个人电脑
参数:-XX:+UseSerialGC = Serial + SerialOld(设置新生代和老年代的回收器是串行)
在这里插入图片描述

吞吐量优先

让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高(简单的说就是一次做多点。做的次数少点)
老年代标记整理算法
新生代标记复制算法
XX:+UseParallelGC ~ -XX:+UseParallelOldGC (UseParallelGC 新生代的并行回收采用标记复制算法,UseParallelOldGC 老年代的并行回收采用标记整理算法
-XX:ParallelGCThreads=n(线程数 )
在这里插入图片描述

响应时间优先 (cms在jdk8采用该回收器)

多线程 堆内存较大,多核 cpu 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
(多次回收。一次的时间尽量少)并发执行STW很短。
对吞吐量会有影响。一般用于对吞吐量要求不高的场景。(如web开发)
老年代标记清除算法
新生代标记复制算法
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
(UseConcMarkSweepGC 并发老年代。标记清除算法。UseParNewGC 并发新生代的,标记复制算法。并发失败会退回串行)
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent(设置执行cms的时机)
-XX:+CMSScavengeBeforeRemark(新生代回收再重新标记。减少重新标记的压力)
在这里插入图片描述

演示 字节码指令 和 操作数栈、常量池的关系

/** * 演示 字节码指令 和 操作数栈、常量池的关系 */ 
public class Demo3_1 {   
 public static void main(String[] args) { 
        int a = 10;       
         int b = Short.MAX_VALUE + 1;      
           int c = a + b;       
            System.out.println(c);   
         } 
     }

1.常量池载入运行时常量池
在这里插入图片描述
2.方法字节码载入方法区
在这里插入图片描述
3.main 线程开始运行,分配栈帧内存
在这里插入图片描述
4.执行引擎开始执行字节码
bipush 10 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),
类似的指令还有 sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
ldc 将一个 int 压入操作数栈 ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节) 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
在这里插入图片描述

5.istore_1
将操作数栈顶数据弹出,存入局部变量表的 slot 1
在这里插入图片描述
6.ldc #3
从常量池加载 #3 数据到操作数栈 注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算 好的
在这里插入图片描述
7.istore_2
在这里插入图片描述

8.iload_1
在这里插入图片描述
9.iload_2
在这里插入图片描述
10.iadd
在这里插入图片描述
11.istore_3
在这里插入图片描述
12.getstatic #4
在这里插入图片描述
在这里插入图片描述
13.iload_3
在这里插入图片描述
14.invokevirtual #5
找到常量池 #5 项 定位到方法区 java/io/PrintStream.println:(I)V 方法 生成新的栈帧(分配 locals、stack等) 传递参数,执行新栈帧中的字节码
在这里插入图片描述
15.执行完毕,弹出栈帧 清除 main 操作数栈内容
在这里插入图片描述
16.return
完成 main 方法调用,弹出 main 栈帧 程序结束

面试题i++

看代码

public class Demo {
    public static void main(String[] args) {
        int i = 0;
        int x = 0;
        while (i < 10) {
            x = x++;
            i++;
        }
        System.out.println(x); // 结果是 ?
    }
}

x++是先iload(x)吧x(0)放进操作数栈中。然后在inc x 1 (inc不会放入栈中执行)这是x=1了。开始x=x++。会在栈中取所以x又重新赋值回零了。循环再多次也是初始值。

finally 面试题 finally 出现了 return

public class Demo {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result);//20
    }
    public static int test() {
        try {
            return 10;  
        
        } finally { 
        
            return 20; 
        } 
    }
}

由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准
如果在 finally 中出现了 return,会 吞掉异常

public class Demo {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result);//10
    }
    public static int test() {
       int i = 10; 
       try { 
           return i;
       } finally {
           i = 20;
       }
    }
}

return是将数值保存起来然后在返回。(保存在无名的变量)。所以finally 修改i是没用的

类加载阶段

加载

将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
_java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴 露给 java 使用
_super 即父类
_fields 即成员变量
_methods 即方法
_constants 即常量池
_class_loader 即类加载器
_vtable 虚方法表
_itable 接口方法表
如果这个类还有父类没有加载,先加载父类 加载和链接可能是交替运行的
instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror 是存储在堆中
在这里插入图片描述

链接

1.验证
验证类是否符合 JVM规范,安全性检查
如字节码开头必须是 CA FB BA BE(cafe babe java图标是咖啡。babe代表第一个吧。个人理解)
2.准备
static 修饰的变量在jdk1.7后会保存在类对象后面,保存在堆里。(可看上一张图)

当完成字节码文件的校验之后,JVM 便会开始为**类变量(被 static 修饰的变量)**分配内存。这里需要注意两个关键点,即内存分配的对象以及初始化的类型。
(内存分配的对象。Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。)

static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶 段完成
如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

3.解析
当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。
其实这个阶段对于我们来说也是几乎透明的,了解一下就好。

初始化

:在jvm第一次加载class文件时调用,包括静态变量初始化语句和静态块的执行(只会被执行一次)

:在实例创建出来的时候调用,包括调用new操作符;调用Class或java.lang.reflect.Constructor对象的newInstance()方法;调用任何现有对象的clone()方法;通过java.io.ObjectInputStream类的getObject()方法反序列化。

发生的时机
概括得说,类初始化是【懒惰的】
main 方法所在的类,总会被首先初始化
首次访问这个类的静态变量或静态方法时
子类初始化,如果父类还没初始化,会引发
子类访问父类的静态变量,只会触发父类的初始化
Class.forName new 会导致初始化
不会导致类初始化的情况
访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
类对象.class 不会触发初始化
创建该类的数组不会触发初始化

典型应用 - 完成懒惰初始化单例模式(线程安全)

public class Load9 {
    public static void main(String[] args) {
//        Singleton.test();
        Singleton.getInstance();
    }

}

class Singleton {

    public static void test() {
        System.out.println("test");
    }

    private Singleton() {}

    private static class LazyHolder{
        private static final Singleton SINGLETON = new Singleton();
        static {
            System.out.println("lazy holder init");
        }
    }

    public static Singleton getInstance() {
        return LazyHolder.SINGLETON;
    }
}

类加载器

名称加载哪的类说明
Bootstrap ClassLoaderJAVA_HOME/jre/lib无法直接访问
Extension ClassLoaderJAVA_HOME/jre/lib/ext上级为 Bootstrap
Application ClassLoaderclasspath上级为 Extension
自定义类加载器自定义上级为 Application

自定义类加载器

问问自己,什么时候需要自定义类加载器
1)想加载非 classpath 随意路径中的类文件 2)都是通过接口来使用实现,希望解耦时,常用在框架设计 3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

  1. 继承 ClassLoader 父类
  2. 要遵从双亲委派机制,重写 findClass 方法 注意不是重写 loadClass 方法,否则不会走双亲委派机制
  3. 读取类文件的字节码
  4. 调用父类的 defineClass 方法来加载类
  5. 使用者调用该类加载器的 loadClass 方法
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Load7 {
    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader();
        Class<?> c1 = classLoader.loadClass("MapImpl1");
        Class<?> c2 = classLoader.loadClass("MapImpl1");
        System.out.println(c1 == c2);

        MyClassLoader classLoader2 = new MyClassLoader();
        Class<?> c3 = classLoader2.loadClass("MapImpl1");
        System.out.println(c1 == c3);

        c1.newInstance();
    }
}

class MyClassLoader extends ClassLoader {

    @Override // name 就是类名称
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "e:\\myclasspath\\" + name + ".class";

        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Files.copy(Paths.get(path), os);

            // 得到字节数组
            byte[] bytes = os.toByteArray();

            // byte[] -> *.class
            return defineClass(name, bytes, 0, bytes.length);

        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到", e);
        }
    }
}

双亲委派模式

例如用Application ClassLoader加载器时要看他的上级加载了没,在他的上级都没加载时才会被允许加载(判断一个类是否相同要看类加载器+类名)

jvm对代码的优化

JVM 将执行状态分成了 5 个层次:
0 层,解释执行(Interpreter)
1 层,使用 C1 即时编译器编译执行(不带 profiling)
2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
4 层,使用 C2 即时编译器编译执行
即时编译器(JIT)与解释器的区别
解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释 JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需 再编译 解释器是将字节码解释为针对所有平台都通用的机器码 JIT 会根据平台类型,生成平台特定的机器码
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运 行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速 度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由 来)

public static void main(String[] args) {
        for (int i = 0; i < 200; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                new Object();
            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\n", i, (end - start));
        }
    }

运行这段代码你会发现越来越快(发送了热点代码。还发送了逃逸)

逃逸

逃逸是在方法内部定义外部没用到的对象。

方法内联

(Inlining)

private static int square(final int i) {    return i * i; }
System.out.println(square(9));

如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、 粘贴到调用者的位置:

System.out.println(9 * 9);

还能够进行常量折叠(constant folding)的优化

System.out.println(81);

字段优化

1字段读取优化
即时编译器会优化 实例字段 和 静态字段 的访问以 减少总的内存访问次数

即时编译器将 沿着控制流 ,缓存各个字段 存储节点 将要存储的值,或者字段 读取节点 所得到的值
当即时编译器 遇到对同一字段的读取节点 时,如果缓存值还没有失效,那么将读取节点 替换 为该缓存值
当即时编译器 遇到对同一字段的存储节点 时,会 更新 所缓存的值
当即时编译器遇到 可能更新 字段的节点时,它会采取 保守 的策略, 舍弃所有的缓存值
方法调用节点 :在即时编译器看来,方法调用会执行 未知代码
内存屏障节点 :其他线程可能异步更新了字段
在这里插入图片描述
字段存储优化
如果一个字段先后被存储了两次,而且这 两次存储之间没有对第一次存储内容读取 ,那么即时编译器将 消除 第一个字段存储
在这里插入图片描述

反射优化

关键还是 invoke 方法的底层实现。

当这个方法调用次数少于15次时,使用调用native方法。大于15次之后则使用ASM生成新的类类处理反射调用。

这就是inflation机制。

由于ASM生成新的类要花比较长的时间,比直接native调用要长3倍左右的时间。但是生成类以后,native调用就会比ASM调用花的时间长20倍。

所以刚开始不知道你反射要调用多少次,所以优先采用native调用。然后后面发现你反射居然要调用那么多次,那干脆给你ASM生成新类调用还快点。

jvm常见的参数

项目Value
堆初始大小-Xms(参数如8m)
堆最大大小-Xmx 或 -XX:MaxHeapSize=size
新生代大小-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态)-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例-XX:SurvivorRatio=ratio
晋升阈值-XX:MaxTenuringThreshold=threshold
晋升详情-XX:+PrintTenuringDistribution
GC详情-XX:+PrintGCDetails -verbose:gc
晋升详情-XX:+PrintTenuringDistribution
FullGC 前 MinorGC-XX:+ScavengeBeforeFullGC
串行
-XX:+UseSerialGC = Serial + SerialOld(设置新生代和老年代的回收器是串行)
吞吐量优先
XX:+UseParallelGC ~ -XX:+UseParallelOldGC (UseParallelGC 新生代的并行回收采用标记复制算法,UseParallelOldGC 老年代的并行回收采用标记整理算法
-XX:ParallelGCThreads=n(线程数 )
cms(响应时间优先)
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
(UseConcMarkSweepGC 并发老年代。标记清除算法。UseParNewGC 并发新生代的,标记复制算法。并发失败会退回串行)
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent(设置执行cms的时机)
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值