java面试之JVM和GC

参考:

  • www.atguigu.com
  • https://www.bilibili.com/video/av70166821?p=13
  • https://www.bilibili.com/video/av48961087
  • 转载自我的个人博客:https://machine4869.gitee.io/

JVM体系结构

JVM类型

HotSpot

> java -version
java version "1.8.0_231"
Java(TM) SE Runtime Environment (build 1.8.0_231-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.231-b11, mixed mode)

JVM架构图

image-20200215124708658

灰色:线程私有,内存很小(kb),不存在垃圾回收(因为生命周期随线程生死)。

橘色:线程共享,存在垃圾回收,大部分垃圾回收都是收的堆。

类装载器ClassLoader

负责加载class文件,class文件在文件开头有特定的文件标示(cafe babe),将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定

image-20200215112732617

ClassLoader类型

  • 虚拟机自带的加载器
    • 启动类加载器(Bootstrap)C++
    • 扩展类加载器(Extension)Java
    • 应用程序类加载器(AppClassLoader)Java也叫系统类加载器,加载当前应用的classpath的所有类
  • 用户自定义加载器
    • Java.lang.ClassLoader的子类,用户可以定制类的加载方式
image-20200215115409132

这些加载器的使用场合?

package com.mxx.jvm;

public class MyObject {
    public static void main(String[] args) {
        // 1. java自带的用Bootstrap
        // 因为根加载器(Bootstrap)一开始就加载了这些类
        // jre/lib/rt.jar包一启动就加载进了JVM里
        Object object = new Object();
        System.out.println(object.getClass().getClassLoader()); // null,因为Bootstrap是c++写的

        // 2. 用户自己写的用AppClassLoader
        // sun.misc.Launcher就是JVM相关调用的入口程序
        MyObject myObject = new MyObject();
        System.out.println(myObject.getClass().getClassLoader().getParent().getParent());// null
        System.out.println(myObject.getClass().getClassLoader().getParent());// sun.misc.Launcher$ExtClassLoader@1b6d3586
        System.out.println(myObject.getClass().getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2

        // 3. javax开头的包就是使用扩展类加载器(Extension)加载的
        // lib/ext/*里面的jar包会被装载
        
        
        // 4. 用户自定义,继承java.lang.ClassLoader

    }
}

双亲委派机制 沙箱安全机制

双亲委派机制:

一个类是如何找加载器的?

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。

证明:

public class String {
    public static void main(String[] args) {
        System.out.println("hello");
        // 会报错:错误: 在类 java.lang.String 中找不到 main 方法
        // 因为先找的Bootstrap,结果直接找到了。所以用户自定义的这个类是无法被加载的。
    }
}

即,你写的代码不能污染java源代码,


沙箱安全机制:
沙箱安全机制是由基于双亲委派机制上 采取的一种JVM的自我保护机制,假设你要写一个java.lang.String 的类,由于双亲委派机制的原理,此请求会先交给Bootstrap试图进行加载,但是Bootstrap在加载类时首先通过包和类名查找rt.jar中有没有该类,有则优先加载rt.jar包中的类,因此就保证了java的运行机制不会被破坏.

加载顺序

静态代码块> 构造快> 构造方法

Execution Engine执行引擎

负责解释命令,提交操作系统执行。

Native Method Stack & Native Interface & native libraies

本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。

目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达(因此不用关心内部),比如可以使用 Socket通信,也可以使用Web Service等等

native方法举例

new Thread().start();
// 核心源码:
private native void start0();
// 线程是系统级的,需要底层操作系统支持。而native申明的方法就是调用的本地方法库。
// 这个方法只有申明,实现是交给底层的。
// 如果是native方法,就放native栈里。

PC寄存器

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。

如果执行的是一个Native方法,那这个计数器是空的。

用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出(OutOfMemory=OOM)错误。(占用很小,只是记个行号)

Method Area 方法区

供各线程共享的运行时内存区域。它存储了每一个类的结构信息(Xxx.class–>classLoader -->Xxx Class,这个Xxx Class就存在方法区里),例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。上面讲的是规范,在不同虚拟机里头实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace)。

But

实例变量存在堆内存中,和方法区无关。

Stack栈

栈管运行,堆管存储。

栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配

函数栈内存:
基本类型:int,long,float
引用变量:User = new User(); 等号左边叫引用变量
实例方法:user.eat()

Java方法 = 栈帧 (就是把方法压栈的意思)

image-20200215150006565

栈存储什么?
栈帧中主要保存3 类数据:
本地变量(Local Variables):输入参数和输出参数以及方法内的变量;
栈操作(Operand Stack):记录出栈、入栈的操作;
栈帧数据(Frame Data):包括类文件、方法等等。

栈运行原理:

栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧 F1,并被压入到栈中

A方法又调用了 B方法,于是产生栈帧 F2 也被压入栈,
B方法又调用了 C方法,于是产生栈帧 F3 也被压入栈,
……
执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……
遵循“先进后出”/“后进先出”原则。

每个方法执行的同时都会创建一个栈帧,用于存储局部变量表(方法内的参数)、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体JVM的实现有关,通常在256K~756K之间,与等于1Mb左右。

SOF(StackOverflowError)

package com.mxx.jvm;

public class Stack {

    public static void m1(){
        m1();
    }
    public static void main(String[] args) {
        m1();
        // Exception in thread "main" java.lang.StackOverflowError
        // 使用递归把java栈压爆
        // 原理:方法的加载,深度调用使栈被压爆。
    }
}

这不是异常,属于错误

java.lang.Object 
	java.lang.Throwable 
		java.lang.Error 
			java.lang.VirtualMachineError 
				java.lang.StackOverflowError 

栈+堆+方法区的交互关系

image-20200215152824741

User user = new User()

reference: 就是等号左边的引用user
referrence指向右边的实例对象,这个对象存在堆里。
实例对象里有指针,指向对象类型数据,这类型数据存在方法区里。

HotSpot是使用指针的方式来访问对象:
Java堆中会存放访问类元数据的地址(类结构信息),
reference存储的就直接是对象的地址

堆体系结构

堆结构简介

一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为三部分

image-20200215161821093

一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。

堆内存逻辑上分为三部分:新生+养老+永久
物理上:新生+养老

堆的new对象流程

  1. new Book()是new在新生代伊甸园区

  2. 当伊甸园区用完,程序又需要创建对象,就会触发GC(或者叫YGC,Young GC)

  3. YGC将伊甸园区中的不再被其他对象所引用的对象进行销毁

  4. 然后将伊甸园中的剩余对象移动到幸存 0区(S0区)

  5. 如果一直往伊甸园new对象,就造成幸存 0区也满了,再对该区进行垃圾回收,然后移动到 1 区。

  6. 那如果1 区也满了呢?再移动到养老区。

  7. 若养老区也满了,那么这个时候将产生MajorGC(FullGC,FGC),进行养老区的内存清理。

  8. 若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。

    如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:

    (1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。

    (2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

MinorGC的过程

结构划分:物理上就分为新生区和老年区

image-20200301162712077

MinorGC的过程(复制->清空->互换)

  1. eden、SurvivorFrom 复制到 SurvivorTo,年龄+1
    首先,eden区满,触发第一次GC,然后将存活对象拷贝到from;当eden区再次触发GC,会扫描eden区和from区,将存活对象复制到to区;同时将对象年龄+1,若到达老年标准,则复制到老年区

  2. 清空 eden、SurvivorFrom
    清空Eden和SurvivorFrom中的对象

  3. SurvivorTo和 SurvivorFrom 互换

    to区和from区交换,部分对象在From和To区域中复制来复制去,如此交换15次(jvm默认参数15),最终如果还是存活,就存入到老年代

永久代

98%的对象都是临时对象

方法区和永久代

  • 方法区:是JVM的一种规范,存放类信息、常量、静态变量、即时编译器编译后的代码等;
  • 永久代:是HotSpot的一种具体实现,实际指的就是方法区

关于永久代:

  1. 方法区(Method Area)和堆一样,是各个线程共享的内存区域
  2. 虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
  3. 对于HotSpot虚拟机,很多开发者习惯将方法区称之为“永久代(Parmanent Gen)” 。但严格来说,永久代是方法区的一个实现
  4. 永久存储区(jdk7有)是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。
  5. Java8将永久代取消了,变成元空间。
image-20200301173125519

字符串常量池

字符串虽然是引用类型,但传进方法是无法改变方法外部的值的。为什么?
因为存在字符串常量池

public class StringTest {

    public static void changeStr(String str){
        str = "bbb";
    }
    public static void main(String[] args) {
        String str = "aaa";
        changeStr(str);
        System.out.println(str);    // aaa
    }
}

原理:

image-20200215171959791

常量池和永久代

  • jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。放入java堆中

001

String对象与常量池

/*String s1 = new String(“abc”)做了如下事情:
	1、在常量池中创建了“abc”
	2、在java堆中创建了new String(“abc”)对象,该对象指向常量池的“abc”
	3、在java栈中创建了s1引用,指向java堆中的对象
	如下图
*/

002

String#intern()方法

intern方法会先去查询常量池中是否有字符串已经存在,如果存在,则返回常量池中的引用,这一点jdk1.6、jdk1.7一样

区别在于,如果在常量池找不到对应的字符串:

jdk1.6 会再将字符串拷贝到常量池,返回指向常量池的引用。

jdk1.7 则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串(java堆中)的引用。

简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。

demo测试


public class StrTest {

    public static void main(String[] args) {
       /*
        String s1 = "abc";
        String s2 = "abc";
        System.out.println(s1 == s2);   //true,均指向常量池中对象
        */

       /*
        String s1 = new String("abc");

        String s2 = new String("abc");
        System.out.println(s1 == s2);   //false,两个引用指向堆中的不同对象
        */

       /*
        String s1 = "abc";
        String s2 = "a";
        String s3 = "bc";
        String s4 = s2 + s3;
        String s5 = s2 + s3;
        System.out.println(s1 == s4);   //false,因为s2+s3实际上是使用StringBuilder.append来完成,会生成不同的对象
        System.out.println(s4 == s5);   //false,StringBuilder.append在堆中生成不同的对象
        */

       /*
        String s1 = "abc";
        final String s2 = "a";
        final String s3 = "bc";
        String s4 = s2 + s3;
        System.out.println(s1 == s4);   //true,因为final变量在编译后会直接替换成对应的值,所以实际上等于s4=”a”+”bc”,而这种情况下,编译器会直接合并为s4=”abc”,所以最终s1==s4
        */

       /*
        String s = new String("abc");
        String s1 = "abc";
        String s2 = new String("abc");
        System.out.println(s == s1.intern());   //false,"abc"在常量池中已存在,s1.intern()指向常量池的字符串。而s指向堆中的字符串对象
        System.out.println(s == s2.intern());   //false,同理
        System.out.println(s1 == s2.intern());  //true,
        */    
		
        /*
        String str1 = new StringBuilder("a").append("bc").toString();   //会在常量池生成"a"、"bc",在堆区生成"abc"对象,返回给str1
        System.out.println(str1.intern() == str1);//true,str1.intern()在常量池中没找到"abc",jdk1.7之后会在常量池生成一个引用指向堆区的"abc"对象

        String str2 = new StringBuilder("ja").append("va").toString();//在堆区生成"java"对象
        String str3 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2 == str3);//false
        System.out.println(str2.intern() == str2);//false。str2.intern()在常量池中找到了"java",则str2.intern()指向的是常量池的"java"
        //常量池中为什么会存在"java"这样的字符串?
        //类加载等操作使"java"字符串加入到了常量池中
        */
    }
}

堆参数调优

以JDK1.8+HotSpot为例

永久代和元空间

java7:

image-20200301174034310

java8: 将最初的永久代取消了,由元空间取代

image-20200301174050606
  1. 元空间的本质和永久代类似
  2. 最大的区别在于:永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存(native memory)
  3. 因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。
  4. -Xms: 堆空间初始化大小;-Xmx: 堆空间最大化大小;-Xmn: 新生区大小;

堆内存调优

  • Xms: 设置初始分配大小,默认为物理内存的1/64
  • Xmx: 最大分配内存,默认为物理内存的1/4
  • 生产环境下Xms和Xmx的设置需要相同,理由:避免内存忽高忽低,不稳定。

IDEA下的配置

-Xms1024m -Xmx1024m -XX:+PrintGCDetails

demo

public class HeapDemo {

    /*
    打印:
    Xmx: MAX_MEMORY = 1029177344(字节)、981.5MB
    Xms: TOTAL_MEMORY = 1029177344(字节)、981.5MB
    Heap
     PSYoungGen      total 305664K, used 15729K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
      eden space 262144K, 6% used [0x00000000eab00000,0x00000000eba5c420,0x00000000fab00000)
      from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
      to   space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
     ParOldGen       total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
      object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
     Metaspace       used 3237K, capacity 4496K, committed 4864K, reserved 1056768K
      class space    used 349K, capacity 388K, committed 512K, reserved 1048576K
     */
    public static void main(String[] args) {

        // 查看cpu核数
        System.out.println(Runtime.getRuntime().availableProcessors());

        // JVM试图使用的最大内存量
        long maxMemory = Runtime.getRuntime().maxMemory();

        // JVM中的内存总量
        long totalMemory = Runtime.getRuntime().totalMemory();

        System.out.println("Xmx: MAX_MEMORY = " + maxMemory + "(字节)、" + (maxMemory / (double)1024 / 1024) + "MB");
        System.out.println("Xms: TOTAL_MEMORY = " + totalMemory + "(字节)、" + (totalMemory / (double)1024 / 1024) + "MB");

    }
}

OOM案例演示

public class OOMDemo {

    public static void main(String[] args) {
        String str = "www.123456.com" ;
        while(true)
        {
            str += str + new Random().nextInt(88888888) + new Random().nextInt(999999999) ;
        }

    }
}

报错:

image-20200302182017402

GC垃圾回收

GC收集日志信息

young GC日志:

[GC (Allocation Failure) [PSYoungGen: 2029K->506K(2560K)] 2029K->821K(9728K), 0.0007042 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

解析:

image-20200302182911831

Full GC 日志:

[Full GC (Ergonomics) [PSYoungGen: 384K->0K(2560K)] [ParOldGen: 6455K->3574K(7168K)] 6839K->3574K(9728K), [Metaspace: 3224K->3224K(1056768K)], 0.0045390 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

解析:

image-20200302183316199

GC是什么?

  • 分代收集算法(不同代使用不同GC算法)
  • 次数上频繁搜集Young区,较少搜集Old区,基本不动元空间。

FullGC触发条件

https://blog.csdn.net/qq_21267357/article/details/100690863

垃圾收集器

Java垃圾回收(GC)机制详解

可达性分析

Java垃圾回收(GC)机制详解

Minor GC 和 Full GC的区别

Minor GC:只针对新生代GC,因为大多数java对象存活率不高,所以Minor GC非常频繁,一般回收速度也比较快。

Full GC:发生在老年代的GC,经常伴随至少一次Minor GC(不绝对)。Minor GC速度一般比Minor GC慢10倍以上。(老年区比较大)

GC四大算法

  1. 引用计数法
  2. 复制算法(Copying)
  3. 标记清除(Mark-Sweep)
  4. 标记压缩(Mark-Compact)

引用计数法

缺点:

  • 每次对对象赋值都要维护引用计数器,且计数器本身也有一定消耗。
  • 较难处理循环引用

JVM的实现一般不采用这种方式

循环引用demo

image-20200302205755263

调用System.gc();是手动开GC的意思,但不是立即执行。

复制算法

  1. Minor GC中使用复制算法
  2. 将from区和eden区的存活对象复制到to区,且将对象年龄设置为1。以后每交换一次to区和from区,就将年龄+1,当年龄到达某值(默认15,由JVM参数MaxTenuringThreshold决定),这些对象就成为老年代。
  3. 复制算法基本思想就是将内存分两块, from和to,每次只用一块,然后不停发生交换。复制算法不会产生内存碎片
  4. eden、from、to 内存大小为 8:1:1
  5. 从eden存活下来的对象很少,10%左右,所以复制算法才靠谱。
image-20200302211234038

缺点:

  • 浪费一半内存,致命缺点。
  • 如果对象存活率很高,那么复制代价将变得不可忽视。(不仅要将活下来的对象都复制一遍,且要将所有引用地址都重置一遍)

优点:

  • 没碎片

标记清除

  1. 解决了复制算法浪费空间的缺点
  2. 老年代一般用标记清除 或 标记清除与标记整理(标记压缩)的混合实现
  3. 分为标记和清除两个阶段。先标记要回收的对象,然后统一回收掉。
  4. 当内存被耗尽时,触发GC,将程序暂停,随后标记清除,然后再让程序恢复运行。
image-20200302212835246

缺点:

  • 效率低,耗时。(两次扫描,一次标记、一次清除,耗时;遍历区域大;GC时会暂停应用,导致用户体验差)
  • 会产生内存碎片(复制算法不会)。(JVM不得不维持空闲列表,这又是开销;且分配数组的时候,连续空间不好找)

优点:

  • 不需要额外空间(复制算法需要)

标记压缩

  1. 老年代一般用标记清除 或 标记清除与标记整理(标记压缩)的混合实现
  2. 两个阶段。先扫描一次,标记;再扫描一次,并往一端移动存活对象,然后直接清除边界以外的内存。
  3. 要给新对象分配内存时,JVM只需要拿到一个内存边界,这比维护空闲链表少很多开销。
image-20200302214818405

优点:

  • 无碎片

缺点:

  • 效率也不高,移动对象需要成本,还要整理新的引用地址。
  • 从效率上说,标记/整理算法低于复制算法

结合:标记-清除-压缩

  1. 前两种算法的结合
  2. 先和Mark-Sweep保持一致,多次GC后才进行一次Compact
  3. 这种结合算法属于生产实际用到的算法,不算在四大算法里

优点:

  • 减少对象移动成本

四大算法总结

优势对比:

image-20200302215306699

还有又快又节约内存的更优秀的算法吗?

…还真有…Java9默认垃圾回收器G1

新生区和养老区GC算法选择

  • 新生区对象存活率低。用复制算法,速度快,又不至于浪费空间
  • 养老区区域大,对象存活率高。复制算法不合适,用标记清除或标记清除和标记整理结合

可以开多线程,并发标记,增加效率

JMM Java内存模型

本地内存和主内存那个图,参考

15326929363424/#2-3-JAVA内存模型(JMM)
20200221192831555/#JMM抽象结构图描述

结合volatile理解可见性

GC ROOTs

什么是垃圾?如何判断对象是否需要被回收?

什么是垃圾?

  • 不再被引用的对象

如何判断对象是否需要被回收?

  • 引用计数法:缺点是很难解决循环互相引用
  • 可达性分析

可达性分析,哪些可作为GC ROOTs对象?

可达性分析

  • 如果一个对象到GC ROOTs没有引用链,说明次对象不可用
  • 给定一个集合的引用作为根,遍历对象图,能被遍历到的判为存活,否则死亡。

哪些可作为GC ROOTs对象?

  • 虚拟机栈中的引用的对象(栈帧中的局部变量区)
  • 方法区中类的静态属性引用的对象
  • 方法区中常量的引用对象
  • 本地方法栈中的引用对象

JVM调优和参数配置

JVM有哪些参数类型?XX参数?

JVM参数类型

  • 标配参数
    • -version
    • -help
    • java -showversion
  • x参数
    • -Xint
    • -Xcomp
    • -Xmixed
  • xx参数(重要)

xx参数

  • Boolean类型:
    • 公式:XX:+某个属性 或者XX:-某个属性 ,+是开启,-是关闭
    • 是否打印GC收集细节:-XX:-PrintGCDetails
  • kv设置类型:
    • 公式:XX:属性=属性值
    • XX:MetaspaceSize=128m
  • jinfo:查看当前运行程序的配置
    • 公式:jinfo -flag 配置项 进程编号
    • jinfo -flag PrintGCDetails 5988
    • 查看当前程序所有的配置参数:jinfo -flags 5988
  • -Xms等价于-XX:InitialHeapSize,-Xmx等价于-XX:MaxHeapSize

查看JVM默认值

查看初试默认值

  • -XX:PrintFlagsInitial
image-20200326170643147

查看修改更新值

  • -XX:PrintFlagsFinal
  • 冒号代表被修改过
image-20200326171040000

查看常用参数

  • -XX:PrintCommandLineFlags
  • 能看到当前使用的垃圾回收器

项目中常用的JVM配置参数

  • -Xms

    • 堆初始大小,默认物理内存1/64
  • -Xmx

    • 堆最大分配,默认物理内存1/4
  • -Xss

    • 设置单个线程栈的大小,默认512k~1024,等价XX:ThreadStackSize,默认=0表示使用初始默认值,不是空间为0
  • -Xmn

    • 设置新生区大小,一般不调,默认堆空间1/3
  • -XX:Metaspace

    • 元空间使用本地内存,不在虚拟机中,但默认大小只有20多M
  • -XX:+PrintGCDetails

    • 日志分析
  • -XX:SurvivorRatio

    • 幸存区占比,eden:s0:s1默认8:1:1
  • -XX:NewRatio

    • 新生区占比,默认值为-XX:NewRatio=2,表示新生代:老年代为1:2,默认占整个堆 1/3
  • -XX:MaxTenuringTreshold

    • 设置垃圾最大年龄,默认15。在新生区幸存15次就被转移到养老区。这个值调大,就让对象留在新生区更久,则让对象最大可能在新生代就被回收。

强/软/弱/虚 四大引用

整体架构?四大引用的区别?

java.lang.ref.*

image-20200326213809103
  • 强引用(默认支持模式):就算OOM也不会回收。只要对象有一个引用,都不收。
  • 软引用:内存足够时不收,内存不足时回收。
  • 弱引用:不管内存够不够用,只要GC了都要收。
  • 虚引用:就跟没有引用一样,GC来了就被回收,多了一个finalize()通知。
    • 不能单独使用,也无法使用get()访问对象,必须和ReferenceQueue联合使用
    • 其意义在说明对象进入了finalization阶段,可以被GC。GC时改对象会收到一个通知,覆写finalize()方法在GC前进行一些清理工作。

软引用/弱引用适用场景

一个场景需要读取大量本地图片:

  • 每次从硬盘读,性能下降
  • 全部加载到内存,OOM

设计思路:HashMap<图片路径,图片对象软引用>,内存不足时,会自动回收图片对象,避免OOM。

WeakHashMap

  • 如果key的引用被置空,GC时entry会被回收
  • 减少OOM的概率

引用队列ReferenceQueue

  • 回收前,弱/软/虚引用 先放ReferenceQueue保存一下

    WeakReference<Object> wrf = new WeakReference<Object>(o1,referenceQueue)
    

应用场景?

  • 回收前对象被放入引用队列里,可做一些销毁前的操作。

引用和GC ROOTs总结

image-20200326222726588

OOM

StackOverflowError

举例:深度递归,没有出口。

是错误不是异常

image-20200401125931837

OutOfMemoryError:Java heap space

举例:while死循环里面new对象

OutOfMemoryError:GC overhead limit exceeded

发生场合:大量资源用于GC,却得不到好的回收效果

image-20200401130956779

OutOfMemoryError:Direct buffer memory

导致原因:直接把本地内存撑爆了。NIO编程,直接分配本地内存不会触发GC。

image-20200401132405152

OutOfMemoryError:unable to create new native thread

高并发场景下出现,因为一个系统创建的的最大线程数是有上限的。

导致原因

image-20200401133307072

服务器级别调参:

上限调整

image-20200401133935841

修改

image-20200401133953437

OutOfMemoryError:Metaspace

封装类信息,常量池。

cglib,反射,动态生成字节码,撑爆元空间

垃圾回收器

垃圾回收算法和垃圾回收器的关系

算法是方法论,回收器是落地实现

没有万能的收集器,只有最适合的收集器,进行分代搜集。

4种主要垃圾收集器

Serial、Parallel、CMS、G1

  • 串行垃圾回收器Serial:为单线程设计,只使用一个线程进行垃圾回收,会暂停所有用户线程。所以不适合服务器环境
  • 并行垃圾回收器Parallel:多个垃圾收集器线程并行工作,搜集时用户线程也是暂停的(因为是并行搜集,所以暂停时间会比串行短),适用于科学计算/大数据 等弱交互场景
  • 并发垃圾回收器CMS:并发标记清除,用户线程和垃圾收集线程同时执行(也可以交替),不需要停顿用户线程,互联网公司多用,适合对响应时间有要求的场景

对比

image-20200401140505346

Java8主要用这三种

  • G1:Java9的默认垃圾回收器,将堆内存分割为不同区域然后并发进行回收。

查看默认垃圾收集器

-XX:PrintCommandLineFlags

Java8默认ParallelGC

image-20200401141851858

jdk有哪些默认垃圾收集器?

image-20200401142053353

SerialOldGC

一共7种

如何配置?

-XX:+UseSerialGC

7大垃圾收集器概述

image-20200401142948380

对应源码层

image-20200401143023258

约定参数说明

  • DefNew : Default New Generation,默认新生代使用的垃圾回收器
  • Tenured : Old
  • ParNew : Parallel New Generation
  • PSYoungGen : Parallel Scavenge
  • ParOldGen : Parallel Old Generation

JVM的Server/Client模式是什么?

  • 基本都是用Server模式的
image-20200401144142355

下图表示新生代和老年代垃圾回收器的配合使用。

image-20200401144341418

Serial (Serial Copying)

image-20200401144657747
  • 最古老,简单高效,单核下垃圾搜集效率高。
  • client模式下默认的
  • -XX:+UseSerialGC,开启后Serial+Serial Old组合使用。
  • 新生代使用复制算法,老年代使用标记整理算法
  • 1:1

ParNew

  • 也会暂停、是Serial的并行版
  • server模式下新生代的默认垃圾回收
  • -XX:+UseParNewGC,启用ParNew,老年代不变,还是Serial Old,这种组合法已经不推荐。常见场景是配合老年代 CMS GC使用
  • 新生代使用复制算法,老年代使用标记整理算法
  • N:1

Parallel (Parallel Scavenge)

image-20200401150413875
  • Java8默认配置
  • 新生代和老年代都用并行的
  • N:N

与ParNew相比,为什么引入Parallel?

  • 吞吐量、自适应调节
image-20200401150828151

Parallel Old

  • 目的是为了在老年代同样提供吞吐量优先

配置

image-20200401152720327

CMS

image-20200401153154519
  • 并发标记清除
  • 目的是获得最短回收时间,适合b/s的网站,重视响应速度。是G1出现之前大型应用首选。
  • 并发搜集停顿低,并发指与用户线程一起执行

开启组合

image-20200401153313437

四步过程:

  • 初始标记:标记GC ROOTs的关联对象,速度很快,会暂停工作线程
  • 并发标记:进行GC ROOTs的跟踪过程,和用户线程一起工作,不需要暂停工作线程
  • 重新标记:修正标记记录(修正期间因用户继续运行导致标记变动的小部分对象的标记记录),会暂停。
  • 并发清除:和用户线程一起。清除GC不可达对象

将工作细分,将耗时最长的并发标记并发清除两个过程与用户线程一起工作,减少停顿。

优缺点:

  • 优:并发搜集停顿低
  • 缺:
    • 并发对CPU压力大;(必须在老年代堆耗尽前GC,否则失败会启动串行GC,造成大停顿)
    • 导致碎片(因为没有整理)

Serial Old

  • 串行的老年代,单线程,使用标记-整理算法
  • client模式默认,CMS后备方案
  • java8实际中已经弃用了

如何选择垃圾收集器?

image-20200401155550251

表格对比

image-20200401155612176

G1

  • -XX:UseG1GC
  • 面向服务器的,用于多处理器和大内存环境
  • 暂停更小,吞吐更高
  • 与CMS一样,与用户线程并发。目的是取待CMS
    • 有整理,无碎片
    • Stop The World可控,在停顿上加了预测机制,用户可指定期望停顿时间
  • 主要改变是
    • 新生区(eden、survivor)、老年区不再连续,而是变成一个个resgion。每个region 1-32M不等

之前收集器的特点

  • 年轻代和老年代各自独立,且连续内存
  • 设计原则:尽可能少而快速的执行GC

G1特点

image-20200401163213844

底层原理

  • Region区域化垃圾搜集器,最大好处是化整为0,避免全内存扫描,只需按区域扫描
  • 不要求对象存储在物理上连续,只需要逻辑上连续
  • 每个区域不会固定为某个代服务,可以切换
  • 可参数指定区域大小,默认将整个堆划分为2048个分区
  • 最大支持32M*2048=64G

区域化垃圾收集器

image-20200402153647115

解释:

image-20200402153716684

回收步骤:

YoungGC: 小区域搜集+形成连续内存块

image-20200402154904841

image-20200402155015375

搜集后

image-20200402155029448

4步过程

image-20200402155257750

常用参数配置

G1和CMS相比的优势

1、无内存碎片

2、精确控制停顿(用于设期望值、作为软目标,JVM尽可能停顿时间小于这个)

JVMGC结合SpringBoot微服务优化

公式

java -server JVM各种参数 -jar xxx.jar/war
jps
jinfo -flags 进程号
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值