• 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:Enviromentjdk1.8injvisualvm.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:
    ingxinjiecodeAnyTestsrcmainjavacomjdclassloader> 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);
            }
        }
    }
    
  • 相关阅读:
    Nginx负载均衡:分布式/热备Web Server的搭建
    CentOS6.6 32位 Minimal版本纯编译安装Nginx Mysql PHP Memcached
    windows下nginx安装、配置与使用
    Redis基本操作——List
    MongoDB aggregate 运用篇 个人总结
    构建一个较为通用的业务技术架构
    2016年31款轻量高效的开源JavaScript插件和库
    正则表达式
    前端学习路线
    可变参数
  • 原文地址:https://www.cnblogs.com/ningxinjie/p/14260624.html
Copyright © 2020-2023  润新知