JVM

一、类加载

(1)过程

加载 >> 验证 >> 准备 >> 解析 >> 初始化
1、加载

1)通过类的全限定名获取存储该类的class文件
2)解析成运行时数据存放在方法区,即instanceKlass实例,
3)在堆区生成该类的Class对象,即instanceMirrorKlass实例
懒加载:使用的时候才加载
什么时候加载?
1)new、getstatic、putstatic、invokestatic
2)反射
3)初始化一个类的子类会去加载其父类
4)启动类(main函数所在类)

2、验证

1)文件格式验证
2)元数据验证
3)字节码验证
4)符号引用验证

3、准备

给静态变量分配内存、赋初值
注:被final修饰,在编译阶段会给变量添加ConstantValue属性,直接赋值

4、解析

将符号引用替换为直接引用
即:指向运行时常量池替换为指向内存地址

5、初始化

静态变量赋值,执行静态代码块
注:执行的顺序和静态变量定义的顺序一致
如一个静态属性定义在构造方法之后,会覆盖构造方法对静态属性的操作

(2)类加载器分类

类加载器加载类
引导类加载器(bootstrapLoader)c++加载支撑JVM运行的位于JRE的lib目录下的核心类库
扩展类加载器(extClassloader)加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包
应用程序类加载器(appClassLoader)加载ClassPath路径下的类包,主要是自己写的
自定义加载器自定义加载路径

(3)类加载器初始化

public Launcher() {
	// 声明扩展类加载器
    Launcher.ExtClassLoader var1;
    try {
    	// 获取扩展类加载器,并设置parent为null(因为bootstrapLoader是c++的)
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
    	// 获取app类加载器,并设置parent为ExtClassLoader
    	// app类加载器赋值给加载器
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
	// 设置当前线程类加载器为AppClassLoader
    Thread.currentThread().setContextClassLoader(this.loader);
    String var2 = System.getProperty("java.security.manager");
    ...
}

(4)双亲委派机制

在这里插入图片描述
原因

  • 沙箱安全机制
    自己写的java.lang.String.class类不会被加载,防止核心API库被随意篡改
  • 避免类的重复加载
    当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性

注:当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入

(5)自定义类加载器

继承ClassLoader重写findClass,调用defineClass()

public class MyClassLoader extends ClassLoader {

   // 指定路径
   private String classPath;


   public MyClassLoader(String classPath) {
       this.classPath = classPath;
   }

   /**
    * 重新findClass
    */
   @Override
   protected Class<?> findClass(String name) throws ClassNotFoundException {
       try {
           byte[] data = loadByte(name);
           // defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节 数组。 
           return defineClass(name, data, 0, data.length);
      } catch (Exception e) {
           throw new ClassNotFoundException();
       }
   }

   /**
    * 将class文件转化为字节码数组
    */
   private byte[] loadByte(String name) throws Exception {
       name = name.replaceAll("\\.", "/");
       FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
       int len = fis.available();
       byte[] data = new byte[len];
       fis.read(data);
       fis.close();
       return data;
   }

   public static void main(String args[]) throws Exception {
       // 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader 
       MyClassLoader classLoader = new MyClassLoader("D:/test");
       // D盘创建 test/com/zzuhai 几级目录,将File1.class丢入该目录 
       Class clazz = classLoader.loadClass("com.zzuhai.File1");
       Object obj = clazz.newInstance();
       Method method = clazz.getDeclaredMethod("folderMethod2", null);
       method.invoke(obj, null);
       System.out.println(clazz.getClassLoader().getClass().getName());
   }
}

控制台输出

我是方法folderMethod2
com.zzuhai.MyClassLoader

(6)打破双亲委派机制

1、不委派

重写loadClass方法,注释委托的代码,新增不用双亲加载的类的条件判断

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
	synchronized (getClassLoadingLock(name)) {
       Class<?> c = findLoadedClass(name);
//      if (c == null) {
//          long t0 = System.nanoTime();
//          try {
//              if (parent != null) {
//                  c = parent.loadClass(name, false);
//              } else {
//                  c = findBootstrapClassOrNull(name);
//              }
//          } catch (ClassNotFoundException e) {
//              // ClassNotFoundException thrown if class not found
//              // from the non-null parent class loader
//          }

       if (c == null) {
           long t1 = System.nanoTime();
           if (!name.startsWith("com.zzuhai")) {
               c = this.getParent().loadClass(name);
           } else {
               c = findClass(name);
           }

//          sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
           sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
           sun.misc.PerfCounter.getFindClasses().increment();
       }
//      }
       if (resolve) {
           resolveClass(c);
       }
       return c;
   }
}

2、SPI机制,向下委派

全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类

1)编写接口

package com.viewscenes.netsupervisor.spi;
public interface SPIService {
	void execute();
}

2)编写两个实现类

package com.viewscenes.netsupervisor.spi;
public class SpiImpl1 implements SPIService{
   public void execute() {
       System.out.println("SpiImpl1.execute()");
   }
}
package com.viewscenes.netsupervisor.spi;
public class SpiImpl2 implements SPIService{
   public void execute() {
       System.out.println("SpiImpl2.execute()");
   }
}

3)classpath添加文件在
文件名称是接口全限定名,内容是实现的全限定名

com.viewscenes.netsupervisor.spi.SpiImpl1
com.viewscenes.netsupervisor.spi.SpiImpl2

4)main方法调用

ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class);
while(providers.hasNext()) {
	SPIService ser = providers.next();
	ser.execute();
}

输出不同结果

(7)tomcat打破双亲委派机制

每个webapp都有一个加载器,每个jsp都有一个加载器

二、JVM内存模型

在这里插入图片描述

(1)栈

在这里插入图片描述


  • 一个线程分配一个栈,包含多个栈帧
  • 栈帧
    每个方法分配一个栈帧,按照方法调用顺序先后入栈,先进后出
  • 局部变量表
    - 存放方法局部变量的表,相当于一个数组,0存放的是this,1…n存放的局部变量
    - 栈帧中局部变量表的对象变量,指向堆存放对象的内存地址
  • 操作数栈
    - 程序运行过程中临时存放操作数的栈。
    - 执行加减指令时,出栈到cpu寄存器进行计算,结果继续入栈。
    - 执行赋值指令时,出栈给局部变量赋值
    - 执行返回执行时,出栈返回数据
  • 动态链接
    方法对应的jvm对象在元空间中的内存地址
  • 方法出口
    存放调用方法的那行代码的位置,方法执行完后方便返回接着执行代码

(2)堆

存放对象
最小是物理内存的1/64
最大是物理内存的1/4
分为年轻代和老年代,默认大小比例是1:2
年轻代分为Eden区和survivor区的两块区域s0和s1,默认大小比例:8:1:1

可通过参数-XX:-UseAdaptiveSizePolicy关闭自适应大小策略
什么对象会进入老年代?(见下文对象内存分配)
1)15次gc
2)大对象
3)空间担保
4)动态年龄判断

(3)方法区(元空间)

主要存放常量、类元信息、类加载器,不同的类加载器加载的类存放在方法区的不同区域

  • JVM参数
    -XX:MetaspaceSize
    设置元空间的最大值,默认-1,不限制
    -XX:MaxMetaspaceSize(字节)
    设置元空间触发fullgc初始阈值,默认21M,收集器会对这个值进行动态调整
    1)gc释放大量空间,调小改值
    2)反之,调大改值,不超过最大值
  • 调优
    一般两个参数设置成一样,占物理内存的1/32,避免太小启动的时候频繁full gc太慢了
  • 方法区回收条件
    1)该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
    2)加载该类的 ClassLoader 已经被回收。(自定义的类加载器才行(如tomcat加载jsp的),其他一般不会被回收)
    3)该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

(4)程序计数器

每个线程有一个程序计数器,存放执行到的指令的存放地址
为了在多线程执行的时候,线程被挂起又唤醒时能知道执行到哪一行了

(5)本地方法栈

java调用本地方法(本地的c/c++程序)的时候,分配给本地方法的栈内存

oop-Klass模型

Klass:Java类在JVM中的存在形式
oop:Java中的对象在JVM中的存在形式

  • oopDesc
    • MarkOopDesc
      存放锁信息,分代年龄
    • InstanceOopDesc
      非数组对象
    • arrayOopDesc
      数组对象
      • typeArrayOopDesc
        基本数据类型数组
      • objArrayOopDesc
        引用类数组
  • InstanceKlass
    普通类的元信息在JVM中的存在形式,在方法区
    • InstanceMirrorKlass
      镜像类,在堆中,提供给反射使用,class对象是InstanceMirrorKlass的实例
    • InstanceRefKlass
      软、弱、虚引用类的元信息在JVM中的存在形式
    • InstanceClassLoaderKlass
      用于遍历某个加载器加载的类
  • ArrayKlass
    • TypeArrayKlass
      基本类型的数组在JVM中的存在形式
    • ObjArrayKlass
      对象类型的数组在JVM中的存在形式

三、执行引擎

JVM运行java程序的一套子系统

两种解释器

  • 字节码解释器
    解释执行
    java->c++->硬编码(cpu指令)
  • 模板解释器
    执行即时编译器编译后的代码
    java->硬编码(cpu指令)

三种运行模式

  • -Xint 纯字节码解释器
  • -Xcomp 纯模板解释器
  • -Xmixed 字节码解释器 + 模板解释器

即时编译器

  • C1
    是client模式下的即时编译器
    1)触发的条件相对C2比较宽松:需要收集的数据较少
    2)编译的优化比较浅:基本运算在编译的时候运算掉了
    3)c1编译器编译生成的代码执行效率较C2低
  • C2
    是server模式下的即时编译器(64bit机器上只有server模式)
    1)触发的条件比较严格,一般来说,程序运行了一段时间以后才会触发
    2)优化比较深
    3)编译生成的代码执行效率较C1更高
  • 混合
    初期用C1,运行一段时间用C2

四、字节码文件

在这里插入图片描述
参考:字节码文件结构详解

五、常量池

(1)class常量池

java编译时生成的常量池

(2)运行时常量池

1.6、1.7时在方法区的永久代,1.8在元空间

(3)字符串常量池

1.6时在运行时常量池里,1.7之后在堆里

1)字符串操作

  • 直接赋值

如果常量池没有(用equals方法判断),在常量池中生成hello

String s = "hello";
  • new String

先查找常量池有没有,如果没有,池中生成hello,然后new String在堆中生成hello,共两个对象

String s1 = new String("hello");
  • intern方法
String s2 = s1.intern();

intern方法:native方法,如果常量池中已经包含此String对象的字符串,则返回池中对象,如果没有,返回指向堆中字符串的引用

2)示例

  • 示例1(堆中和常量池中的区别)
String s1 = "hello";
String s2 = new String("hello"); 
System.out.println(s1 == s2);// false

s1会在常量池中新增hello,s2中会在堆中新增hello。
注:s1指向的是常量池中hello,s2指向堆中的hello,所以不相等

  • 示例2(intern)
String s1 = new String("he") + new String("llo");
String s2 = s1.intern(); 
System.out.println(s1 == s2);// true

以上代码会在字符串常量池中新增he,llo两个对象,每个new String会在堆中一个对象,‘+’号底层实现是StringBuilder,StringBuilder的toString方法中会new String,在堆中生成hello对象,共5个String对象。
注:s1和s2都指向堆中的hello,所以相等
如下图
在这里插入图片描述

  • 示例3(关键字)
String s1 = new String("ja") + new String("va");
String s2 = s1.intern(); 
System.out.println(s1 == s2);// false

和上个示例只有字符串不相同,由于java是关键字,所以启动程序时肯定会有用到java的地方,所以在常量池中早就有java,所以s2的intern指向常量池中的java
注:s1指向的是堆中的java,s2指向常量池中的,所以不相等

  • 示例4(编译优化)
String s1="hello"; 
String s2="he" + "llo"; 
System.out.println( s1==s2 ); //true

s2由于是两个字符串常量拼接而成,在编译期会被优化成String s2=“hello”,s1和s2都是常量池中的hello,所以相等

六、对象创建和内存分配

(1)创建对象

1、分配内存

1)划分内存的方法

  • 指针碰撞(Bump the Pointer)(默认)
    如果堆中内存是规则的连续的,使用的在一边,空闲的在另一边,中间放个指针指示分界点,在划分内存的时候,把指针向空闲的一边移动一段与要划分内存的对象大小相等的距离。
  • 空闲列表(Free List)
    如果堆中空闲的内存是分散的,虚拟机会维护一个列表,记录哪些内存是空闲的,在划分内存的时候,选一块足够大的内存给要划分内存的对象,然后更新列表记录。

2)解决并发问题的方法:

  • CAS(compare and swap)
    CAS+失败重试,保证操作的原子性,保证对分配内存空间的动作进行同步处理。
  • 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
    每个线程在Java堆中都预先分配一小块内存。各个线程的对象都在该线程分配的内存里进行分配内存,互不干扰。
    注: 如果预先分配的大小放不下,就还是放在不是预先分配的内存的地方,采用CAS
    注: 默认开启,可通过­XX:­-UseTLAB参数来关闭使用TLAB,通过­XX:TLABSize指定TLAB大小。

3)分配内存过程

  • 见下文对象内存分配

2、初始化

初始化为0值,为了保证对象的实例字段在代码中不赋值也能使用

3、设置对象头

对象头见下文

4、init

为属性赋值(上面初始化是赋0值,这里是按代码逻辑赋值),执行构造方法

(2)对象内存布局

在这里插入图片描述

  • 对象占用大小计算

(1)对象头

1)Mark Word
32bit机器占4B
64bit机器8B
2)类型指针 Klass pointer
instanceKlass在方法区的地址
开启指针压缩占4B
关闭指针压缩占8B
3)数组长度
如果这个对象不是数组,占0B
如果这个对象是数组,占4B

(2)实例数据
类的非静态属性

类型占用大小
boolean1B
byte1B
char2B
short2B
int4B
float4B
double8B
long8B
引用类型开启指针压缩 4B
关闭指针压缩 8B

(3)对齐填充

不是8的倍数字节,填充至8的倍数
注:如果对象有数组对象这种特殊的属性,在关闭指针压缩时,会先在对象头进行填充,在最后再进行填充,进行两次对齐填充

示例
引入包

<dependency>
	<groupId>org.openjdk.jol</groupId>
	<artifactId>jol-core</artifactId>
	<version>0.9</version>
</dependency>
public class obj {
   int a = 1;
   int b = 1;

   public static void main(String[] args) {
       obj obj = new obj();
       System.out.println( ClassLayout.parseInstance(obj).toPrintable() );
   }
}

打印出对象大小:
开启指针压缩(默认开启)是24 bytes=8+4+0+4*2+4
关闭指针压缩也是24 bytes=8+8+0+4*2+0

  • 指针压缩
    节省内存,寻址更高效

对象都是8字节倍数对齐的,8二进制表示为:1000,所以对象大小最后3位都是0
存储的时候后三位0抹去,使用的时候后三位补0

  • 超过32G堆内存指针压缩失效
    类型指针 Klass pointer在指针压缩开启的时候占4字节,就是32bit,最大大小就是2的32次方,在使用的时候,后三位补3个0,就是35bit,最大就是32G
    解决方法可以二次开发jdk,修改成16字节对齐,最大就是2的36次方,64G

(3)对象内存分配

在这里插入图片描述

1、栈上分配
一般对象进入堆,在没有被引用的时候,依靠GC进行回收,为了减少临时的对象进入堆,给GC带来较大压力,我们先考虑在栈上分配,先进行逃逸分析,判断对象不会被外部访问,接着标量替换,用对象的成员变量来替代整个对象,分散的存在栈帧上面存储,这样对象就能随着方法结束栈帧的出栈,进行销毁
注:逃逸分析和标量替换需要同时开启,才可能进行栈上分配

  • 逃逸分析

分析对象的动态作用域,能被外部访问的叫作能逃逸,不能访问的叫不能逃逸
默认开启,关闭使用参数(-XX:-DoEscapeAnalysis

// File1对象能被外部访问,能逃逸
public File1 canEscape() {
	File1 File1 = new File1();
	File1.setId(1);
	return File1;
}
// File1对象不能被外部访问,不能逃逸
public void cannotEscape() {
	File1 File1 = new File1();
	File1.setId(1);
}
  • 标量替换

在确定对象不能逃逸以后,jvm不直接创建对象,而是把对象的成员变量替代对象,在栈帧或寄存器上分散的分配空间,这样避免了因为没有一块连续的栈帧空间而无法分配对象内存的情况
默认开启,关闭使用参数(-XX:-EliminateAllocations

  • 锁消除

锁对象不能逃逸的时候,会优化,消除锁

2、大对象直接进老年代

大对象就是需要大量连续内存空间来存储的对象(如:字符串),避免大对象在内存操作时进行复制操作降低效率

注:可通过参数-XX:PretenureSizeThreshold设置大对象的大小(只在Serial和ParNew两个收集器下有效,G1有自己的大对象定义

3、TLAB

见上文"本地线程分配缓冲"

4、对象在Eden区分配

大多数情况,对象会在年轻代Eden区分配内存,在Eden区内存不够时,会触发minor gc,回收Eden大部分内存,并将未回收内存放入Survivor区中(如果未回收的在Survivor区中放不下,直接放入老年代),下一次Eden区满了,又触发minor gc,将Eden和Survivor区大部分内存回收,未回收的又放入另一个Survivor区中,如此循环。

5、老年代空间担保机制

在这里插入图片描述
1)每次minor gc之前都会计算老年代剩余可用空间
2)如果老年代可用空间<年轻代所有对象大小之和
3)判断是否配置了-XX:-HandlePromotionFailure(默认配置),如果配置了
4)会判断老年代可用空间<历史每一次minor gc进入老年代对象大小平均值
5)如果满足4,进行Full GC,如果不满足4进行minor gc
6)minor gc后内存还不够,进行full gc
7)full gc后还不够,oom

6、长期存活对象进入老年代

每个对象都有一个年龄计数器,一个对象在Eden区出生,经过一次minor gc后仍然存活,并且进入Survivor区,那么这个对象的年龄就会增加一岁。对象在Survivor区中每熬过一次minor gc年龄都会加1,在年龄增加到一定程度(默认15岁,CMS收集器默认6岁),会进入老年代。
注:可通过参数-XX:MaxTenuringThreshold修改年龄的阈值

7、对象动态年龄判断机制

如果在Survivor区中存放对象的那个区域有一批对象总内存大于这块Survivor区域内存大小的50%(可用参数指定-XX:TargetSurvivorRatio),那么大于等于这批对象最大年龄的对象,进入老年代,使长期存活的的对象尽早进入老年代。一般发生在minor gc之后。

例如:现在Survivor区域中有年龄1+年龄2+…+年龄m的对象,其中年龄1+年龄2+…+年龄n的对象总大小大于当前Survivor区域内存大小的50%,那么年龄为n~m的对象,进入老年代

(4)对象内存回收

  • Minor GC/Young GC
    指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
  • Major GC/Full GC
    一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上。
  • 对象引用类型
  • 强引用
    普通的变量引用,有引用不会被回收
public static File1 file1 = new File1();
  • 软引用
    将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但GC做完后发现释放不出空间存放新的对象,会被回收
public static SoftReference<File1> file1 = new SoftReference<File1>(new File1());
  • 弱引用
    用WeakReference引用类型的对象包裹,GC会直接回收掉,很少用
public static WeakReference<File1> file1 = new WeakReference<File1>(new File1());
  • 虚引用
    不使用
  • 引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
注:效率高,无法解决对象间相互引用问题

  • 可达性分析算法

将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象

  • GC Roots
  1. 虚拟机栈(栈帧中的局部变量)中引用的对象。
  2. 本地方法栈(native)中引用的对象。
  3. 方法区中常量引用的对象。
  4. 方法区中类静态属性引用的对象。
  • finalize()方法

在可达性分析后发现是可回收对象后,不会马上回收,会进行一次标记
接着会判断一下这个类是否重写了finalize()方法
如果没有重写,直接回收
如果重写了finalize()方法,会先执行finalize()方法
由此我们可以在回收之前在拯救一下这个对象,在finalize()方法中重新给对象关联上引用链的对象
注意:一个对象的finalize()方法只会被执行一次,就是只能拯救一次

  • 记忆集与卡表
  • 记忆集
    在垃圾回收进行可达性分析的时候,可能会出现新生代和老年代的跨代引用,为了避免将老年代进行扫描,引入记忆集来维护一个某块非收集区域是否存在指向收集区域的指针的集合,就无需跨代扫描
  • 卡表
    1)hotspot对记忆集的具体实现形式,字节数组CARD_TABLLE[]实现,每个卡表项对应一个卡页。
    2)每个卡页512字节,包含多个对象,如果一个对象存在跨代指针,卡表元素标识(1字节)变成1(写屏障维护)。
    3)gc时筛选元素标识为1的元素加入gcroot

七、垃圾收集器

(1)垃圾收集算法

  • 标记-复制算法

1)将内存分为大小相同的两块,每次使用其中的一块
2)垃圾收集的时候,标记不可回收的对象,复制到另一半未使用的区域
3)然后再把使用的空间一次清理掉
这样就使每次的内存回收都是对内存区间的一半进行回收。
优点:效率快
缺点:只能使用一半空间

  • 标记-清除算法

1)标记存活的对象
2)回收所有未被标记的对象
存在问题:

  1. 需要标记的对象太多时,效率不高
  2. 标记清除后会产生大量不连续的碎片
  • 标记-整理算法

1)标记存活的对象
2)所有存活的对象向一端移动
3)清理掉端边界以外的内存。

(2)一般垃圾收集器

Serial收集器

在这里插入图片描述
-XX:+UseSerialGC -XX:+UseSerialOldGC

  1. 单线程
  2. 新生代采用标记-复制算法,老年代采用标记-整理算法

优点:简单而高效
缺点:单线程STW时间长

Parallel Scavenge收集器(jdk1.8默认)

在这里插入图片描述
-XX:+UseParallelGC -XX:+UseParallelOldGC
Serial收集器的多线程版本

  1. 多线程
  2. 新生代采用标记-复制算法,老年代采用标记-整理算法

在注重吞吐量以及CPU资源的场合,可以优先考虑此收集器

ParNew收集器

在这里插入图片描述
-XX:+UseParNewGC
和Parallel类似

  1. 新生代采用标记-复制算法
  2. 可以和CMS收集器配合使用

(3)CMS收集器(-XX:+UseConcMarkSweepGC)

在这里插入图片描述
CMS(Concurrent Mark Sweep),垃圾收集时间比Parallel长,但STW时间短,用户体验好,第一款实现垃圾收集线程和用户线程并发执行,采用标记-清理算法
参数

参数作用
-XX:+UseConcMarkSweepGC启用cms
-XX:ConcGCThreads并发的GC线程数
-XX:+UseCMSCompactAtFullCollectionFullGC之后做压缩整理(减少碎片)
-XX:CMSFullGCsBeforeCompaction多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
-XX:CMSInitiatingOccupancyFraction当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
-XX:+UseCMSInitiatingOccupancyOnly只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
-XX:+CMSScavengeBeforeRemark在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
-XX:+CMSParallellnitialMarkEnabled表示在初始标记的时候多线程执行,缩短STW
-XX:+CMSParallelRemarkEnabled在重新标记的时候多线程执行,缩短STW;
  • 初始标记
    STW,标记gc roots直接能引用的对象为黑色,速度很快。
  • 并发标记
    从GC Roots的直接关联对象开始遍历整个对象图, 这个过程耗时较长但 是不需要停顿用户线程, 可以并发运行。
    注:因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
  • 重新标记
    为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录
    注:停顿时间一般比初始标记时间稍长,远远比并发标记时间短。主要对三色标记里的增量更新(见下面详解)的列表做重新标记。
  • 并发清理
    开启用户线程,同时GC线程开始对未标记的区域做清扫。
    注:这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。
  • 并发重置
    重置本次GC过程中的标记。

优点

  1. 并发收集,低停顿

缺点

  1. 对CPU资源敏感(会和服务抢资源)
  2. 在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了;
  3. 标记-清除产生大量碎片(可通过-XX:+UseCMSCompactAtFullCollection让jvm进行整理)
  4. 并发标记和并发清理阶段系统执行可能还有对象进入堆,此时没回收完又触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收

三色标记算法和读写屏障

三色标记

把Gcroots可达性分析遍历对象过程中遇到的对象,按照“是否访问过”这个条件标记成以下三种颜色

  • 黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。
  • 灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
  • 白色: 表示对象尚未被垃圾收集器访问过。在分析结束的阶段,仍然是白色的对象,即代表不可达,可以进行回收。

读写屏障

  • 写屏障
    给某个对象的成员变量赋值时,底层代码实现像aop一样,会在赋值的前后执行一些处理
  • 读屏障
    读取成员变量时,记录下读取到的对象

CMS并发标记过程的问题

多标

在并发标记过程中,可能会出现多标记的情况(浮动垃圾)

  • gcroot引用的对象被扫描过标记为非垃圾对象,由于方法结束,导致局部变量被销毁,成为垃圾对象
  • 在并发标记开始后产生的新对象,一般设置成黑色,并发标记期间也可能变为垃圾

解决:浮动垃圾不会影响回收正确性,可以等到下一轮垃圾回收再清除

漏标

在灰色对象删除指向白色对象的引用关系后,白色对象重新被黑色对象引用,这时候因为黑色不会再扫描,还是白色,会导致被引用的对象被当成垃圾误删除

  • 解决办法
    (1)增量更新(写屏障实现)
    黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录到列表, 在重新标记中,以列表中的黑色对象为根重新扫描一次
    (2)原始快照(SATB)(写屏障实现)
    灰色对象要删除指向白色对象的引用关系时,将这个引用记录到列表,在重新标记的时候,将列表中的对象标记成黑色(可能是浮动垃圾),避免这一轮被回收

解决漏标使用方法

垃圾收集器处理方法
CMS写屏障 + 增量更新
G1,Shenandoah写屏障 + SATB
ZGC读屏障

(4)G1收集器(-XX:+UseG1GC)

G1(Garbage-First)具有高吞吐量,可控制停顿时间

  • 结构

在这里插入图片描述

  1. Region
    G1和前面的垃圾收集器不同,G1将Java堆划分为2048个大小相等的独立区域(Region)。 Region可以不连续,一般Region大小等于堆大小除以2048。
    注:修改2048可通过参数- XX:G1HeapRegionSize设置。
  2. 新生代占比
    1)默认年轻代对堆内存的占比是5%
    注:可以通过-XX:G1NewSizePercent设置新生代初始占比
    2)最多新生代的占比不会超过60%
    注:可以通过-XX:G1MaxNewSizePercent调整。
    3)年轻代中的Eden和 Survivor对应的region也跟之前一样,默认8:1:1
  3. Region的区域动态变化
    一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代。
  4. Humongous区
    1)G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。
    2)G1垃圾收集器对于对象什么时候会转移到老年代跟上文(5.3对象内存分配)的原则一样,唯一不同的是对大对象的处理。
    3)大对象的判定规则:一个对象超过了一个Region大小的50%就算大对象,如果太大,可能会横跨多个Region来存放。
    注:可以节约老年代的空间,避免因为老年代空间不够的GC开销。Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。
  • 回收步骤在这里插入图片描述
  1. 初始标记(initial mark,STW)
    同CMS初始标记
  2. 并发标记(Concurrent Marking)
    同CMS的并发标记
  3. 最终标记(Remark,STW)
    同CMS的重新标记
  4. 筛选回收(Cleanup,STW)
    1)首先计算各个Region的回收价值和成本,并进行排序
    2)根据配置的GC停顿时间-XX:MaxGCPauseMillis(默认200ms)来制定回收计划,回收部分Region
    注:老年代需要回收1000个Region,回收成本计算得知,回收800个Region需要200ms,那么就只会回收800个Region。剩余的下一次gc再回收。
    注:一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,G1会优先选择后面这个Region回收
    3)用的复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样产生很多内存碎片还需要整理一次,G1回收几乎不会有太多内存碎片。
  • G1垃圾收集分类
  1. Young GC
    1)Eden区满了不会马上触发Young GC,G1会先计算现在Eden区回收大概要多久时间
    2)如果回收时间远远小于设置的停顿时间( -XX:MaxGCPauseMills),那么继续增加年轻代的region给新对象存放,不会Young GC
    3)直到G1计算回收时间接近设定的停顿时间,那么就会触发Young GC
    4)或者Eden区占比达到60%,也会触发Young GC
  2. Mixed GC
    1)老年代的堆占有率达到参数-XX:InitiatingHeapOccupancyPercent(默认45%)设定的值则触发,回收所有的 Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,
    2)使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC
    3)如果回收过程中由于复制算法不断空闲出来的Region数量达到堆内存的5%(-XX:G1HeapWastePercent),回收结束。
  3. Full GC
    停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了)
  • 特点
  1. 并行和并发
    充分利用多核、多线程CPU,尽量缩短STW。
  2. 分代收集
    虽然还保留着新、老两代的概念,但物理上不再隔离,而是融合在Region中。
  3. 空间整合
    G1整体上看是标整算法,在局部看又是复制算法,不会产生内存碎片。
  4. 可预测停顿
    用户可以指定一个GC停顿时间,G1收集器会尽量满足。
  • 为什么使用SATB
    避免增量更新的深度扫描,G1内存分散,不好跨代扫描,使用SATB只需要标记,不用扫描

(5)ZGC收集器(-XX:+UseZGC)

  • 特点
    1)不分代
    2)并发标记整理使用读屏障、颜色指针来实现
  • 目标
    1)支持TB级别的堆大小
    2)最大gc停顿不超过10ms
    3)奠定未来gc特性基础
    4)最多吞吐量会降低15%
  • 问题
    由于停顿时间太短,并且没有分代概念,浮动垃圾过多
    解决
    1)增大堆容量
    2)引入分代概念,集中处理

八、JVM调优

(1)Jmap

查看内存信息,实例个数以及占用内存大小

命令作用
jmap -heap pid查看当前堆相关信息
jmap -histo:live pid查看当前活动类的对象的数量、大小和名称等相关信息
jmap -clstats pid打印类加载器信息
jmap -finalizerinfo pid查看等待回收对象信息
jmap -dump:format=b,file=heapdump.hprof pid生成堆转储快照dump文件
注:启动参数加上
1. -XX:+HeapDumpOnOutOfMemoryError
2. -XX:HeapDumpPath=./ (路径)
内存溢出自动导出dump文件

(2)Jstack

查看线程状态和线程调用方法

命令作用
jstack -l pid查看各个线程状态
使用jstack查询高cpu线程

1)使用top -p pid查看cpu使用
2)使用H(shift+h)查看每个线程的cpu使用
3)将最高的那个线程PID转成16进制
4)使用jstack pid|grep -A 10 PID得到PID这个线程堆栈信息后的10行数据
5)根据10行数据,找到方法,进行解决

(3)Jinfo

查看正在运行的Java应用程序的扩展参数

命令作用
jinfo pid查看当前 jvm 进程的全部参数和系统属性
jinfo -flag pid查看全部参数
jinfo -sysprops pid查看全部系统熟悉
jinfo -flag name pid查看参数状态(name是参数名称)
jinfo -flag [+|-]name pid可以在不重启的情况下开启或者关闭对应名称的参数
jinfo -flag name=value pid同上,直接修改参数值

(4)Jstat

查看堆内存各部分的使用量,以及加载类的数量

命令作用
jstat –gc pid堆信息统计
jstat -gccapacity pid堆内存统计
jstat -gcmetacapacity pid元数据空间统计
jstat -gcnew pid新生代垃圾回收统计
jstat -gcnewcapacity pid新生代内存统计
jstat -gcold pid老年代垃圾回收统计
jstat -gcoldcapacity pid老年代内存统计
jstat -gcutil pid堆内存百分比统计

(5)Arthas

参考文档:https://arthas.aliyun.com/doc/

命令作用
monitor监控方法调用次数,成功失败情况
trace监控方法内部调用路径,并输出方法路径上的每个节点上耗时
watch监控函数调用前后的入参和返回值
stack输出当前方法被调用的调用路径
ognl执行ognl表达式,可以不停机修改参数配置等数据
tt记录每次调用入参和返回信息
thread查看线程运行堆栈
tt -t 类全限定名 方法名
# 显示当前最忙前x个线程,并打印堆栈
thread -n x
thread id
# 显示阻塞线程
thread -b 
ognl ''
# 查看第一个参数属性
watch 类 方法 'params[0].class.属性名'
# 调用第一个参数方法
watch 类 方法 'params[0].方法名()'
# 查看集合类型参数属性
watch 类 方法 'params[0].{#this.属性名}'
# 投影中调用方法
watch 类 方法 'params[0].{#this.方法名()}'
# 观察表达式中过滤出id大于5的
watch 类 方法 'params[0].{? #this.id>5}'
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值