JVM整理文档

大致先记录到这里,以后有时间我会更加详细的总结出自己的一套东西,下面是我对jvm基础的算精简的总结,加油!

jvm官方说明:https://docs.oracle.com/en/java/javase/11/tools/tools-and-command-reference.html

main-tools-create-and-build-applications/java就能看到各种可以调整的参数设置

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

常量池是在方法区的,但是字符串除外,字符串的常量池存储在堆空间【静态变量也是存放在堆空间的】【针对于jdk7及以后,jdk6及以前是存放在永久代,这都快过时了,下面主要针对于jdk8】

类加载子系统

类加载过程

类加载器:系统类加载器-》扩展类加载器-》系统类加载器/自定义类加载器

其实除了系统类加载器外,它是用C或C++编写的,其它的都可以叫做自定义类加载器

整个加载过程只会执行一次,包括加载、验证、准备、解析、初始化!

类的二进制流加载进来

实例化该类的时候,直接进行以下操作:

验证

准备

解析【将符号引用转为直接引用】

初始化【只要有static就会生成(),加载的时候执行这个方法】

加载完毕后的类的数据被放在方法区的元空间(jdk8)

一个子类要被实例化之前,它的父类必须完全加载完,包括加载、验证、准备、解析、初始化(由此下面代码就很好理解了)

static class Father{
    static int a = 1;
    static{
        a = 2;
    }
}
static class Son extends Father{
    static int b = a; // 此时b的值是2
}

由此可以看出,一个类被加载一次之后,就不用再次加载了,它被存储在元空间了,以后实例化它只需要继续执行接下来操作即可。

双亲委派机制

当前类的加载首先往上找父类加载器,如果父类能加载则直接加载,如果不能,再递减,直至子类加载器,举个例子:

我们自己创建java.lang包,在里面写String类,这样是外部是加载不了的,因为String所处的包是java.lang,而这个包可以被BootStrap加载,因此加载的是系统的String类,即使我们在自己创建的java.lang下放其它系统里没有的类,去访问也是有问题的,因为java.lang的加载归启动类加载器管,而我们是没有权限访问这个加载器的。

沙箱安全机制

双亲委派机制就是沙箱安全的,也就是我们无论怎么操作,都不会影响外部正常的使用,这就叫沙箱安全。

运行时数据区

image-20201229204025412

查看

jps
返回:java进程号 进程名

jinfo -flag MetaspaceSize 进程号
返回:-XX:MetaspaceSize=21807104 #使用的元空间内存大小

jinfo -flag MaxMetaspaceSize 进程号
#非管理员禁止访问,返回的是本地可用内存大小
-XX:MaxMetaspaceSize=18446744073709486080

JVM栈

参数调节

-Xss256k:设置栈空间大小设置为256kb 

  -Xss1m
  -Xss1024k
  -Xss1048576

栈内部有局部变量、方法返回地址、操作数栈、动态链接、一些附加信息。*

局部变量表把需要的数据汇总,操作数栈从这里面取,按步骤去执行

操作数栈取数据计算,存储临时数据,计算完成后,将数据放回局部变量表中

局部变量表

局部变量表的大小在编译阶段就确定了,运行阶段不会动态调整!

方法形参,及方法内部使用的局部变量,包括8种值类型,引用类型则存放引用地址

局部变量表的基本单位是slot,double与long占用2个slot,其余占用一个slot【是double与long,如果是Double与Long就是引用类型了,它也是占用1个slot】

非静态方法和构造器,第一个slot放的是this,因此我们可以在构造方法和非静态方法中使用this,而static中并没有,因此无法调用this

slot可以重复利用的,如下示例

int a = 1;
{
    int b = a + 1;
}
int c = 2;

0位放的是a

1位放的是b,b的作用范围没了

1位放c【此时这个slot就被重用了】

private static String sstr;// 这样在初始化阶段就不用显示赋值了,也就是在prepare准备阶段,赋值为默认的null
private static String staticStr = "静态变量在prepare阶段,默认赋值(默认值,引用类型就是null),在初始化阶段显示赋值为当前写的字符串";
private String nstr;// 实例化的时候随着对象创建,默认初始化
private String newStr = "实例变量随着对象的创建会在堆空间分配变量空间,并进行默认赋值";

局部变量表也是GC回收的根节点,只要被局部变量表直接或间接引用的对象,是不会被GC的

操作数栈

执行的整个过程:https://www.bilibili.com/video/BV1PJ411n7xZ?p=53

动态链接

java代码被编译到字节码文件的时候,所有的变量和方法都作为符号引用,保存在class文件的常量池里

动态链接作用就是为了将这些符号引用转换为调用方法的引用

常量池实际上是放在方法区的,在运行的时候将类的变量和方法生成常量池信息,放入方法区,因此动态链接也可以叫做指向运行时常量池的方法引用

【即将符号引用转为直接引用】

【更好理解】

静态链接:当一个字节码文件被装载进JVM内部时,如果在编译期即可确定被调用的方法,且运行期保持不变,这种情况下在编译器就会将符号引用转为直接引用,这过程就叫做静态链接。

动态链接:被调用的方法在编译器无法被确定下来,也就是说,只能在程序运行期将调用方法的符号引用转为直接引用,引用的转换过程具备动态性,因此也就被称为动态链接。

【个人理解】

因此常量(final)信息引用的这些,在编译的时候就确定了,因此这些变量在编译器就将符号引用转换为直接引用了。

早期绑定:在编译器就能确定,且在运行期不会改变的引用

晚期绑定:在编译器无法确定,在编译期会改变的引用(如我们方法传递的参数是一个接口,那么我们编译的时候根本无法确定,运行时到底要跑这个实现接口下哪个类的方法)

虚方法与非虚方法

非虚方法:静态方法、私有方法、构造方法、父类方法【invokestatic与invokespecial】

虚方法:编译期间无法确定下来调用哪个的方法【invokevirtual与invokeinterface,加了final的除外,是非虚方法】

方法返回地址

场景:A方法内部调用B方法

当方法内部调用其他方法的时候,其他方法调用完,会将它的返回值压入A方法的操作数栈,并恢复其PC寄存器,局部变量表,让A开始继续往下执行。

方法正常退出则有返回值,方法异常退出则无返回值压入,异常抛给A方法。

可能会出现Error 是否需要GC
PC寄存器
JVM栈 可能(StackOverFlowError) 不(用完栈帧就出栈了)
本地方发展 可能
可能(OOM) 需要
方法区 可能(类字节码信息加载过多) 需要(Full GC)

本地方法栈

调用本地方法后,本地方法具有和jvm一样的权限,它可以直接使用本地寄存器、本地内存等,因此效率会高。(因为本地方法和操作系统一样,都是使用的C或C++实现的)

本地方法接口(native)

1个进程对应1个JVM实例

JVM启动的时候,堆就被创建完成且大小固定

参数调节

-Xms20m -Xmx20m

-Xmn10m 指明新生代大小是10m

初始容量 最大容量

在D:\Enviroment\jdk1.8\bin\jvisualvm.exe启动能看到我们的进程

在工具->插件里,安装下visual GC

堆是线程共享的,但是内部又划分有线程私有的缓冲区(Thread Local Allocation Buffer,TLAB),每个线程占提分,提升更好的并发性(这样每个线程都可以单独操作堆内的数据)【我的理解就是不同线程要修改数据,都需要先拷贝但线程私有的TLAB操作,完成后在拷贝回堆(就是因为这样,线程之间不安全,因为默认线程之间是不可见的,这就是我的理解,随便写的。)】

元空间实际归属于方法区

我们设置的-Xms20m -Xmx20m针对的也是新生代和老年代

开发中建议将初始堆内存和最大堆内存设置成一样的,这样避免不断调整堆大小,造成服务器不必要的压力。

查看:-XX:+PrintGCDetails

堆的可用内存是Eden区大小 + 一个survivor大小 + 老年代大小

参数

jps
-XX:+PrintGCDetails #执行完毕后,打印GC细节情况

-XX:NewRatio=2 #设置新生代与老年代的占比为1:2
-XX:NewRatio=4 #设置新生代与老年代的占比为1:4

-XX:SurvivorRatio=8 #设置Eden区与survivor区的占比(显示指定,则就是按照显示指定比率的来分配空间)
-XX:-UseAdaptiveSizePolicy #默认是开启自适应的内存分配策略的,我们通过这条命令关掉
-XX:+UseAdaptiveSizePolicy #开启自适应的内存分配策略(+就是用  -就是不用)

-XX:MaxTenuringThreshold=15  #设置对象经过MinorGC几次之后直接放入老年代

总结:

频繁收集年轻代,较少收集老年代,基本不动永久代(jdk7)/元空间(jdk8)

Minor GC、Major GC、Full GC

Minor GC:Eden区满了进行收集

Major GC:只收集老年代

Full GC:收集整个堆及方法区

只有Eden区满的之后才会触发MinorGC

TLAB(及查看)

每个线程私有的,存放在堆里,分配的空间大约占Eden区的1%

以下说明很清晰:【TLAB就是解决对象分配内存的问题】

防止这块内存已经分配给某个对象,另外一个对象又过来占用,导致对象创建失败

因此每个线程创建的时候有自己一块TLAB,这样就不用加锁,创建就行,其他线程可以访问,但是不能在那里创建,如果创建的线程TLAB空间不足了,那么就需要在Eden的非TLAB区加锁创建(防止这块区域被其他对象创建覆盖了!)

可见TLAB针对于的是对象实例化之前,如果能在TLAB分配则在这里分配,如果不能则在Eden的非本线程的TLAB区分配。

堆空间参数设置

-XX:HandlePromotionFailure 目前jd7及以后修改无效,也就是说下面的情况,满足则Minor GC否则直接Full GC

对象一定在堆上吗?

答案是不一定的,如果没有发生逃逸,对象是可以在栈上创建,使用完随着栈帧的弹出而销毁。

答案是:目前我们使用HotSpot是对象只能存放在堆上的,栈上不能存放对象,只能讲开启了逃逸分析和标量替换,它将不逃逸的对象替换成标量存放在了局部变量表中了!

逃逸分析

-XX:+DoEscapeAnalysis #默认也是开启的逃逸分析
-XX:-DoEscapeAnalysis #关闭逃逸分析

什么叫逃逸? 就是看当前new的对象实体有没有在方法外被使用【与本方法中调用方法接收没关系,关心的是本方法内部new的对象有没有在外部被使用】,如果当前对象从当前方法创建,外部无法使用到这个对象是,那么则说这个对象不会发生逃逸,这时就可能将这个对象分配在栈上(为什么呢?因为这个对象进入到堆里在GC的时候一定会被GC,这样做就是为了减少GC)

由此可见,能使用局部变量的就不要使用在方法外定义全局变量【也就是尽量让我们创建的对象不发生逃逸,从而减少往Eden区放入无用对象,从而减少GC的次数】

同步省略(锁消除)

有些对象只会被当前的对象访问到,因此即使加锁了,JIT也会在编译阶段进行优化,因此以下执行代码速度是一样的,因为JIT将上方代码优化为下方的代码了,当然我们开发中不要去这样写【默认是开启的逃逸分析,只要开启】

private static void jitOptimizeTest(){
    HeadIfDistributionObject headIfDistributionObject = new HeadIfDistributionObject();
    synchronized(headIfDistributionObject){
        HeadIfDistributionObject headIfDistributionObject1 = new HeadIfDistributionObject();
    }
}
private static void jitOptimizeTest2(){
    HeadIfDistributionObject headIfDistributionObject = new HeadIfDistributionObject();
    HeadIfDistributionObject headIfDistributionObject1 = new HeadIfDistributionObject();\
}

只有开启了逃逸分析,这两段代码速度才一样,进位jvm根据逃逸分析发现我们加锁的对象只在方法内使用了,因此优化为不加锁,如果不开启逃逸分析,那么就会加锁执行,性能严重下降!

无论是否开启逃逸分析,在生成字节码的时候,只要加synchronized锁都会看到monitorenter与monitorexit,只是在运行的时候,开启逃逸分析的,可能会去掉。

标量替换

参数

-XX:+EliminateAllocations

默认是开启的

其实就是对象未发生逃逸,那么我将对象中的属性,如int,double等类型其内部的数据替换这个对象,并将这些值放在栈(局部变量表)中

小结:

测试了一下即使开启了逃逸分析,但是关闭标量替换,仍然是和没开启逃逸分析GC效果是一样的

目前Oracle公司也就是我们用的Hospot虚拟机,还不支持栈上分配,因此它基于的只有标量替换!

方法区

方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、静态变量、及时编译器编译后的代码等数据。

元空间使用的是本地内存,也就是非jvm的内存

参数设置:

查询

jps
返回:java进程号 进程名

jinfo -flag MetaspaceSize 进程号
返回:-XX:MetaspaceSize=21807104 #使用的元空间内存大小

jinfo -flag MaxMetaspaceSize 进程号
#非管理员禁止访问,返回的是本地可用内存大小
-XX:MaxMetaspaceSize=18446744073709486080

jconsole #也是一个jdk自带监控的,cmd直接输入即可
-XX:MetaspaceSize=100m #设置元数据区初始化大小,默认21M左右

元数据

使用的是本地内存,默认是21M左右,最大默认无限的

如何解决OOM思路

方法区的内部结构

类型信息、运行时常量池、静态变量(对于静态变量如何是new的对象,我的理解仍然是存放在堆中,这里保存的是引用地址)、JIT编译后的代码缓存【随着jdk版本的变化,不断改变,目前String类型的常量池就在堆中】【针对的是经典的版本来讲的】

javap -v -p HeadIfDistributionObject.class > text.txt:将反编译的字节码文件放入text.txt中

目前反编译的是准备进入到类加载器的编译文件,经过类加载器之后,除了会将类中一些信息记录,还会带上这个类是被哪个类加载器加载进来的,同时,类加载器在方法区也记录了它加载过哪些类

常量池:

方法区内有运行时常量池

字节码文件,内部包含了常量池

常量池就是将我们的字面量用符号引用代替

为什么要用常量池?

最简单举例子,我们写的类都是继承自Object,如果每次加载的时候我们都去加载一次Object显然是没必要的,我们用符号代替Object这个类,在解析阶段,将符号引用替换为直接引用即可。

常量池,可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名[类、接口、注解、枚举等]、方法名、参数类型、字面量等类型

运行时常量池:

jdk各版本方法区的区别

对象都是在堆中,以下指的都是变量名或方法名【名字】;

小结:

static final一起修饰的,在编译的时候就赋值了【要赋值的是对象除外】

只有static是在prepare设置默认值,在initialization设置我们想要赋予的值

之后带着类加载器写入方法区(记住,此时并没有产生这个类的对象)

示例:

源代码:

public static int count = 1;
public static final int count1 = 2;
public static StringBuilder sb = new StringBuilder("ouhou");
public static final StringBuilder sb2 = new StringBuilder("aiya");

编译后classfile部分如下(此时只是经过编译,并没有进入类加载器):

 public static int count;
    descriptor: I
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC

  public static final int count1;
    descriptor: I
    flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 2  //可以看到在编译阶段2就被赋予进去了

  public static java.lang.StringBuilder sb;
    descriptor: Ljava/lang/StringBuilder;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC

  public static final java.lang.StringBuilder sb2;
    descriptor: Ljava/lang/StringBuilder;
    flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL  //因为是对象,因此在编译阶段并没有赋予进去

总结:

基本类型变量有final与static在编译阶段就赋值完成;

引类型有final与static,不会在编译阶段赋值,因为需要赋值的对象还没new出来啊,我认为如下:在prepare阶段设置初始值null,在initialization阶段创建对象并赋值【待验证】

不过有一点可以肯定,static修饰的类成员变量在进入方法区之前一定有值,因为测试如下代码就知道了:

class Order{
    public static int count = 1;
}

//以下是main方法里面代码:
Order order = null;
order.count;//是不会报错的
  1. 当我们要加载第三方jar包很多的时候,因为元数据区存放类的方法,属性,构造器,及类本身的字节码等信息,因此我们可以稍微调整初始大小大一点,否则一旦设定的初始化大小满了,则会触发Full GC,如果类还没完全加载完就满了 ,显然白白进行Full GC,因为空间不足,类才刚刚加载进来,就需要继续扩大元空间大小,这期间不断执行没必要的Full GC.

以下三个条件就是方法区的类信息被回收的必要条件,都满足了才有可能被回收

实例都被回收了

Class对象不被使用了,没有反射生成的对象

类加载器被回收了

面试题

对象实例化方式及步骤

  1. 加载类元信息
  2. 为对象分配内存
  3. 如果开启TLAB,首先在本线程TLAB分配,如果不能则在Eden其与区域创建,并需要加锁(CAS)
  4. 对象属性设置默认值
  5. 设置对象头信息(包括指向方法区的类元信息等)
  6. 对象中的属性显示初始化、代码块中的初始化、构造器中的初始化。

对象内的属性显示赋值、代码块、构造函数赋值等都在执行init方法内执行。

内存布局

对象的内存布局:对象头(运行时数据区,类型指针),实例数据,对齐填充

对象访问定位

1.使用句柄访问,也就是栈中的栈帧中局部变量表中变量指向的是堆中的对象的句柄,这个句柄指向这个对象,对于该对象的类元数据也是这样

2.使用直接指针(hotspot采用这种方式),变量直接指向堆中的对象,对象内部有类型指针执行方法区中该类元信息。

直接内存

-XX:MaxDirectMemorySize1G 来设置直接内存(防止占用本地过多内存,这些一般是在NIO的时候可以操作本地内存)

package com.nxj.other;

import java.nio.ByteBuffer;
import java.util.Scanner;

/**
 * @author ningxinjie
 * @date 2021/1/3
 * NIO使得用户程序可以直接使用直接内存,用于数据缓冲区,这样一来,对于文件频繁读写
 * 效率显然会提升,因为不需要内核态与用户态来回切换,来回拷贝了
 */
public class NIOBufferTest {
    private static final int BUFFER = Integer.MAX_VALUE ;
    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
        System.out.println("直接内存分配完毕");
        Scanner scanner = new Scanner(System.in);
        scanner.next();
        System.out.println("直接内存开始释放");
        byteBuffer = null;
        System.gc();
    }
}

元数据和使用NIO操作的直接内存都是本地内存。

因此我们java占用系统的内存实际上是jvm占用内存与程序占用本地内存的和

执行引擎

解释器与即使编译器(JIT编译器)

解释器:逐行解释字节码执行

JIT编译器:将字节码编译为机器指令并缓存。

JIT编译器

解释器的好处是响应速度快,它可以直接将字节码进行逐行执行,但是JIT需要先编译,编译完后才能执行,当然编译完了之后执行效率就非常高了

因此jvm让二者互补,将二者都留了下来

解释器与编译器模式设置,默认是混合模式

String常量池【jdk8及以后】

底层就是数组+链表【它不扩容】

可以通过-XX:StringTableSize=10000来设置数组长度

字符串常量池是放在堆里的

使用字面量或者.intern()方法将字符创放入字符创常量池

intern()方法!

将字符串放入字符串常量池

如果字符串常量池中有则将字符串放入

如果没有,则会把对象的引用地址复制一份,放入字符串常量池中,并返回串池中的引用地址(实际就是它自己)

很重要的String理解!

private static void testStringNiuBi(){
 String str1 = new String("a");//生成2个对象,一个new的时候在堆里创建,另一个在常量池创建a(如果没有的话)
 str1.intern();//将字符串字面量放入常量池(此时a在创建的时候已经放入了,这里只是返回刚刚创建的结果
 String str2 = "a";//字面量,去常量池找,没有则创建,有则返回地址
 System.out.println(str1 == str2);//false

 /**
         * str3:创建对象:
         * 对象1:StringBuilder 变量+的时候,编译会帮我们优化为StringBuilder的拼接
         * 对象2:new String("a") 在堆里
         * 对象3:a 在字符串常量池
         * 对象4 new String("b")
         * 对象5 StringBuilder在toString的时候 new String(char[]) 创建一个在堆里
         */
    String str3 = new String("a") + new String("b");//生成5个对象 3个在堆里,2个在常量池(如果常量池不存在的话)
    str3.intern();//将字符串字面量放入常量池(此时因为str3的字面量就是ab,为了节省空间,常量池会以一个指针形式指向str3)
    String str4 = "ab";//字面量,去常量池找,没有则创建,有则返回地址
    System.out.println(str3 == str4);//true
}

对于程序中如果存在很多重复的字符串,使用intern会节省很大空间,如下所示:

String[] strs = new String[100000];
int[] arr = new int[]{1, 2, 3, 4, 5};
for(int i = 0; i < 100000 ;i++){
    // 以下两种截然不同
    // 给每一个String.valueOf(arr[i % 5])都生成一个对象
    // String.valueOf(arr[i % 5]).intern();返回的是字符串常量池中的常量字符串地址
    strs[i] = String.valueOf(arr[i % 5]);
    strs[i] = String.valueOf(arr[i % 5]).intern();
}

-XX:PrintStringTableStatistics

垃圾回收

hotspot回收的是堆和方法区

频繁回收年轻代

较少回收老年代

基本不动元空间

标记阶段

引用计数法

无法处理循环引用的问题

可达性分析算法GC Roots

总之最常见GC Roots的就是:

  1. 栈内的变量(局部变量表)【因为它指向堆内的对象】
  2. 本地方法栈同样
  3. 堆内静态变量区的变量
  4. 方法区的运行时常量池、堆内的字符串常量池。
  5. 具有synchronized锁的对象
  6. 内部基本类型对应的类及基本异常或者错误类等对象,也包括类加载器
  7. jvm本地代码缓存等,这些都是GC Roots不能被回收

面试题:

对象的finalization机制

对象在GC回收之前会调用其finalize方法

获取dump文件

方式一:命令行使用jmap

方式二:使用JVisualVM导出

参数

-XX:HeapDumpOnOutOfMemoryError
这个参数就是当我们出现OOM的时候回生成一个dump文件

dump文件可以通过MAT或者Jprofileer来查看,寻找问题

清除阶段

标记-清除

遍历两次,第一次标记存活,第二次清除垃圾(维护一个空闲列表,记录这些地址,下次有对象来直接来这个列表找,如果有足够空间直接覆盖【它是这样做的】)

标记:从根节点出发,标记可达的对象(非垃圾),记录在对象的对象头中

清除:遍历堆内所有的对象,如果该对象的对象头没有被标记则清除。

缺点:效率低,内存碎片严重

复制算法

优点:高效,内部连续(因为直接将存活的对象复制过去)

缺点:内存缩小一半,如果对象存活多的话,性能就很差了,因为全部要复制过去,而且原来栈中指向也要重新指向我复制的内存区域。

因此复制算法适用于朝生夕死较多的时候,如年轻代就很适合

标记-整理

执行过程:

第一阶段和标记-清除算法一样,从根节点开始标所有被引用的对象。

第二阶段:将所有存活的对象压缩到内存的一端,按顺序排放(解决了内存不连续),

然后将边界外所有空间清除

优点:内存连续

增量回收算法:

用户线程和GC线程切换执行,这样用户线程体验稍微好一些,因为不用完全STW,但是这样增加了CPU调度成本,系统吞吐量下降

分区算法:

将堆区划分一小块一小块独立的内存,进行GC的时候独立回收(这样我们限定回收时间,就会按照时间之内选择region进行回收)

内存溢出

堆空间不足以放下对象了,导致内存溢出

内存泄漏

对象我们不用了,但是通过可达性分析仍然能找到这些对象,导致虽然我们不用、用不到,但是无法回收,造成内存泄漏,内存泄漏累积过多就会产生内存溢出,从而报OOM

安全点

安全区域

四种引用

强引用

软引用

内存足够不回收,内存不够即回收

弱引用

gc发现就回收

虚引用

内部维护队列,回收的时候会找到这个队列,然后就能知道这个对象被回收了,也就是说追踪垃圾回收的过程

垃圾回收器

吞吐量:GC时间占比尽可能少

低延迟:用户线程被暂停的时间越短越好

二者是矛盾的,G1就是二者这种的方案,也就是规定最大暂停时间,在这个时间之内,吞吐量越大越好

垃圾回收期可组合的方式

查看垃圾回收器

参数

-XX:+PrintCommandLineFlags

jdk1.8结果:-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC

jdk1.9默认就是G1了

-XX:+PrintCommandLineFlags  #查看目前使用的垃圾回收器

-XX:+UseSerialGC

-XX:+UseParNewGC


以下二者互相激活,即激活一个另一个自动激活

-XX:+UseParallelGC 新生代使用

-XX:+UseParallelOldGC 老年代使用


-XX:+UserAdaptiveSizePolicy 自适应调节策略(堆空间自适应调整,尽量使用程序高吞吐,性能优一点)

-XX:ParallelGCThreads 设置年轻代并行收集器的线程数

CMS【老年代使用】

初始标记:寻找GC Roots,是STW的

并发标记:顺着这些GC Roots寻找对象,是并发的与用户线程一起执行

重新标记:对已经标记对象进行修正,移除掉不使用的对象,在并发标记阶段分不清到底是不是垃圾在这里进行了修正(因为这些是与用户线程一起并发标记的嘛),是STW的

并发清除:删除掉标记阶段判断已经死亡的对象,释放内存,因为是标记清理,存活的对象不用移动,因此与用户线程并发的执行

缺点:

  1. 产生内存碎片

  2. 对CPU资源很敏感

  3. 无法处理浮动垃圾

    无法清除浮动垃圾:在并发阶段如果用户线程让一些对象变成垃圾了,本次CMS操作无法感知了,只能下一次CMS才能知道,这些垃圾就叫做浮动垃圾。

因为CMS与用户并发执行,因此不能等待内存快不足的时候再回收,否则OOM风险就很高,为了解决CMS过程中OOM,从而无法使用,CMS有一个备选方案,此时使用SerialOld垃圾回收期,STW单线程的回收。

正是由于CMS这些缺点,CMS在jdk14后被jvm彻底移除

参数设置

-XX:+UserConcMarkSweepGC 开启该参数后就会自动将-XX:UserParNewGC打开。

这个就不写那么多了,因为G1完全是目前的主流垃圾回收期,此CMS后期也被移除了,因此了解一下即可。。。

G1【年轻代与老年代通用】

Garbage first(垃圾优先)

分代回收 jdk9之后默认的垃圾回收器

参数设置

-XX:UseG1GC #显示开启G1

特点

G1是为了简化jvm的调优的,只要设置如下三个参数,jvm就可以自动调优:

  1. 开启G1: -XX:UseG1GC
  2. 设置堆的大小:-Xms -Xmx
  3. 设置期望达到GC最大停顿时间:-XX:MaxGCPauseMillis

Region是一块一块的,大小相同的,物理空间可以不连续的(一块region可以在某一时刻是Eden块,也可以是Survivor块的,也可以是Old块的,就是一个region被清空放入空闲列表中后,它是可以切换角色的)

G1回收过程

每一个region是复制算法,但整体上看又是标记-整理算法

R SET让本region记录一下里面的对象哪些被其他区域引用了,如果有则不回收,如果没有则回收,这样就不用遍历整个堆来确保我这个对象是否可以被回收了,用空间换取了时间

年轻代GC

并发标记

垃圾回收器总结

GC日志分析(-XX:+PrintGCDetails)

参数

-XX:+PrintCommandLineFlags -XX:+UseG1GC   -XX:+UseParallelGC(默认)     -XX:+PrintGCDetails
查看当前垃圾回收期及基本情况    使用G1垃圾回收器   使用Parallel回收器(jkd8默认)   打印GC详细日志

-Xms10m -Xmx10m -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./logs/gc.log

将GC日志记录下来:-Xloggc:./logs/gc.log,其中./指的是在当前工程下,必须文件夹存在才可以,否则不会自动创建,会报警告:

Java HotSpot(TM) 64-Bit Server VM warning: Cannot open file ./logs/gc.log due to No such file or directory

日志分析

GCViewer 【git hub上下载】

GC Easy https://gceasy.io/

​ 通过 -Xloggc:./logs/gc.log将gc日志输出,然后将文件导入进去,进行分析

自定义类加载器

loadClass #上方是实现类的双亲委派机制
findClass #实现自定义的加载

建议我们重新findClass,保留双亲委派机制

自定义类加载器

package com.jd.classloader;

import java.io.*;

/**
 * @author ningxinjie
 * @date 2021/1/9
 */
public class CustomClassLoader extends ClassLoader {
    private String byteCodePath;

    public CustomClassLoader(String byteCodePath) {
        this.byteCodePath = byteCodePath;
    }

    public CustomClassLoader(ClassLoader parentClassLoader, String byteCodePath) {
        super(parentClassLoader);
        this.byteCodePath = byteCodePath;
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        // 获取字节码文件的完整路径
        String fileName = byteCodePath + className +".class";

        BufferedInputStream bin = null;
        ByteArrayOutputStream barrout = null;

        try {
            // 获取输入流
             bin = new BufferedInputStream(new FileInputStream(fileName));
            // 获取输出流
             barrout = new ByteArrayOutputStream();
            // 具体读入数据并写出的过程
            int len;
            byte[] data = new byte[1024];
            while ((len = bin.read(data)) != -1){
                barrout.write(data,0,len);
            }
            // 获取内存中完整的字节数组的数据
            byte[] byteCodes =barrout.toByteArray();
            // 调用defineClass将自己数组的数据转换为Class的实例
            Class<?> clazz = defineClass(null, byteCodes, 0, byteCodes.length);
            return clazz;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if(barrout != null)
                    barrout.close();
                if(bin != null)
                    bin.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

测试类

package com.jd.classloader;

/**
 * @author ningxinjie
 * @date 2021/1/9
 *
 * javac HotClassTest.java 编译
 */
public class HotClassTest {
    public void print(){
        System.out.println("HotClassTest -- new");
    }
}

编译成字节码

打开终端
D:\ningxinjie\code\AnyTest\src\main\java\com\jd\classloader> javac HotClassTest.java #在当前目录下

测试

package com.jd.classloader;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author ningxinjie
 * @date 2021/1/9
 */
public class CustomClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, InterruptedException {
        //此时的while (true) 仅仅是为了不停地读取字节码,这样我在运行期间修改了测试类,重新编译下,就能热加载了
        while (true){
            CustomClassLoader loader = new CustomClassLoader("D:/ningxinjie/code/AnyTest/src/main/java/com/jd/classloader/");
            Class<?> clazz = loader.loadClass("HotClassTest");
            System.out.println("加载此类的类的加载器为" + clazz.getClassLoader().getClass().getName());
            Object o = clazz.newInstance();
            Method print = clazz.getMethod("print");

            print.invoke(o);
            Thread.sleep(3000);
        }
    }
}
posted @ 2021-01-11 09:14  程序杰杰  阅读(333)  评论(0编辑  收藏  举报