likes
comments
collection
share

JVM技术

作者站长头像
站长
· 阅读数 1

你了解哪些JVM产品?

JVM仅仅是一种规范,基于这种规范,它会有不同实现,例如:

  • Oracle公司的HotSpot。
  • IBM公司的J9。
  • 阿里公司的TaobaoVM。

JVM的构成有哪几部分?

第一:类加载子系统(负责将类读到内存,校验类的合法性,对类进行初始化)

第二:运行时数据区(方法区/堆区/栈区/计数器,负责存储类信息,对象信息,执行逻辑)

第三:执行引擎(负责从指定地址对应的内存中读取数据然后解释执行以及GC操作)

第四:本地库接口(负责实现JAVA语言与其它编程语言之间的协同,例如调用C库)

JVM技术

你知道哪些类加载器?

第一:BootStrapClassLoader (根类加载器,使用c编写,负责加载基础类库中的类,例如Object,String,...)

第二:ExtClassLoader (扩展类加载器,负责加载jdk自带的扩展类,例如javax.xxx包中的类)

第三:AppClassLoader (应用类加载器,负责加载我们自己写的类)

第四:自定义ClassLoader(当系统提供的默认类加载器不满足我们需求时,可以自己创建)

JVM技术

  1. 什么是双亲委派类加载模型?(高频)

所谓双亲委派模型可以简单理解为向上询问、向下委派。当我们的类在被加载时,首先会询问类加载器对象的parent对象(两者之间不是继承关系),是否已经加载过此类,假如当前parent没有加载过此类,则会继续向上询问它的parent,依次递归。如果当前父加载器可以完成类加载则直接加载,假如不可以则委托给下一层类加载器去加载(可以理解为逐层分配任务)。

JVM技术

这里可以思考一下生活或工作中的业务,假如你在一个团队中,负责联系客户,我们联系之前可以问一下team leader,这个客户是否是我们的客户,假如你的team leader,它可以继续向上问.

双亲委派方式加载类有什么优势、劣势?

通过双亲委派类加载机制,保证同一个类只能被加载一次,同时也是对类资源的一种保护。例如我们自己也写了一个java.lang.Object类,为了保证Java官方的java.lang.Object类加载后不再加载我们的Object就可以使用双亲委派机制。但是这里也有一个缺陷,例如我们同一个JVM下有多个项目,但是不同项目中有包名类名都相同的类(类中的内容是不同的),此时只能有一个项目中的类会被加载,其它项目则无法加载。还有这种双亲委派模型可能会因为向上询问和向下委托,多少会影响一些性能。

描述一下类加载时候的基本步骤是怎样的?

第一:查找类(例如通过指定路径+类全名找到指定类)

第二:读取类(通过字节输入流读取类到内存,并将类信息存储到字节数组)

第三: 对字节数组中的信息流进行校验分析以及初始化并将其结构内容存储到方法区。

第四: 创建字节码对象(java.lang.Class),基于此对象封装类信息的引用,基于这些引用获取方法区类信息。

JVM技术

什么情况下会触发类的加载?

我们可以将类的加载分为显式加载和隐式加载,显式加载是通过类加载器的loadClass方法或Class类的forName方法直接对类进行加载。隐式加载一般指构建对象、访问类中属性或方法时触发的类加载。

注意,用类型定义变量时,类不会被加载.


class ClassA{
    static int a=10;
    static{
        System.out.println("ClassA.static{}");
    }
}
class ClassB{
    static int b=10;
    static{
        System.out.println("ClassB.static{}");
    }
}
class ClassC{
    static int c=10;
    static{
        System.out.println("ClassC.static{}");
    }
}
//跟踪类的加载可以使用JVM参数:-XX:+TraceClassLoading
public class ClassLoaderTraceTests {
    public static void main(String[] args) throws ClassNotFoundException, InterruptedException {
       //1.隐式加载?
       // ClassA a1;//这种情况不会加载ClassA
       // int a2=ClassA.a;//此时会加载ClassA
       // new ClassA();//假如ClassA已经被加载,则此时会使用内存中ClassA,不再进行加载
       //2.显示加载?(直接使用指定类加载器去加载)
        // ClassLoader.getSystemClassLoader()//获取类加载器
        //.loadClass("com.java.jvm.loader.ClassB");//类会被加载但不会执行静态代码块

        //Class.forName("com.java.jvm.loader.ClassC");//会加载类并执行静态代码块
         Class.forName("com.java.jvm.loader.ClassC",
                false,//false表示只加载类,但是不会执行初始化
                ClassLoader.getSystemClassLoader());//AppClassLoader
       //思考:类加载时一定会执行静态代码块?不一定
    }
}

类加载时静态代码块一定会执行吗?

不一定,静态代码块是否会执行,取决于类加载时,是否会执行类初始化。

 ClassLoader.getSystemClassLoader()//获取类加载器
    .loadClass("com.java.jvm.loader.ClassB");//类会被加载但不会执行静态代码块

如何理解类的主动加载和被动加载?

主动加载:访问本类成员或方法时触发的加载。

被动加载:访问本类(当前类)对应的父类属性时,本类(当前类)属于被动加载。被动加载不会触发当前类的初始化,静态代码块不会执行.

package com.java.jvm.loader;
class ClassAB{
    static int a=10;
    static{
        System.out.println("ClassAB.static{}");
    }
    static void doMethod(){
        System.out.println("ClassAB.doMethod()");
    }
}
class ClassCD extends ClassAB{
    static{
        System.out.println("ClassCD.static{}");
    }
}

/**
 * -XX:+TraceClassLoading
 * 测试类的主动加载和被动加载
 * Passive:被动加载
 * 当通过子类直接访问父类的静态成员时,
 * 父类为主动加载,子类为被动加载(被动加载的类不会执行类初始化)。
 */
public class ClassPassiveLoadingTests {
    public static void main(String[] args) {
        System.out.println(ClassCD.a);
        //ClassCD.doMethod();
    }
}

为什么要自己定义类加载器,如何定义?

当系统提供的类加载器不能完全满足我们需求时,我们可以考虑自定义类加载器,例如:

  1. 指定加载源头(系统提供的类加载器已经确定了从哪些位置加载对应类,假如我们的类不在指定范围内呢?)
  2. 保证类安全(可以在类编译时对字节码进行加密,类加载时对字节码进行解密)
  3. 打破双亲委派模型(同一个JVM下有多个项目时,假如不同项目中有相同名字的类,这些类都需要加载。)

如何自己定义类加载器呢?

直接或间接的继承ClassLoader类型并重写findClass方法,例如:

package cn.tedu.jvm;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.File;
import java.io.FileInputStream;

/**自定义类加载器*/
class SimpleAppClassLoader extends  ClassLoader{

    private String basicPath;
    public SimpleAppClassLoader(String basicPath){
        this.basicPath=basicPath;
    }
    /**
     * 查找类,并将类读取到内存,然后创建字节码对象
     * @param name  这个类名为全类名(包名.类名)
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String classFilePath=basicPath+name.replace(".","\")+".class";
        File file=new File(classFilePath);
        if(!file.exists())
            throw new ClassNotFoundException("没有找到对应的类");
        FileInputStream fis=null;
        try {
            //读取类数据
            fis = new FileInputStream(file);
            byte[] buf = new byte[fis.available()];
            fis.read(buf);
            //创建字节码对象
            return defineClass(name,buf,0,buf.length);//这个方法一般不用重写
        }catch (Exception exception){
             exception.printStackTrace();
             return null;
        }finally{
            if(fis!=null)try{fis.close();}catch (Exception e){}
        }
    }
}
@SpringBootTest
public class SimpleAppClassLoaderTests {
     @Test
     void testClassLoader() throws ClassNotFoundException {
        String basicDir="C:\Users\TEDU\IdeaProjects\";
        SimpleAppClassLoader appClassLoader=new SimpleAppClassLoader(basicDir);
        //appClassLoader.loadClass("pkg.Hello");
         Class.forName("pkg.Hello",true,appClassLoader);
     }
}

假如要打破双亲委派,需要重写loadClass方法,例如:

package cn.tedu.jvm;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.File;
import java.io.FileInputStream;

/**自定义类加载器*/
class SimpleAppClassLoader extends  ClassLoader{

    private String basicPath;
    public SimpleAppClassLoader(String basicPath){
        this.basicPath=basicPath;
    }


    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            return findClass(name);
        }catch (Exception e){
            //出现异常时,使用默认的类加载器和它加载机制对类进行加载即可
            return super.loadClass(name);
        }
    }

    /**
     * 查找类,并将类读取到内存,然后创建字节码对象
     * @param name  这个类名为全类名(包名.类名)
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String classFilePath=basicPath+name.replace(".","\")+".class";
        File file=new File(classFilePath);
        if(!file.exists())
            throw new ClassNotFoundException("没有找到对应的类");
        FileInputStream fis=null;
        try {
            //读取类数据
            fis = new FileInputStream(file);
            byte[] buf = new byte[fis.available()];
            fis.read(buf);
            //创建字节码对象
            return defineClass(name,buf,0,buf.length);//这个方法一般不用重写
        }catch (Exception exception){
             exception.printStackTrace();
             return null;
        }finally{
            if(fis!=null)try{fis.close();}catch (Exception e){}
        }
    }
}
@SpringBootTest
public class SimpleAppClassLoaderTests {
     @Test
     void testClassLoader() throws ClassNotFoundException {
        String basicDir="C:\Users\TEDU\IdeaProjects\";
        SimpleAppClassLoader appClassLoader=new SimpleAppClassLoader(basicDir);
        //appClassLoader.loadClass("pkg.Hello");
         Class.forName("pkg.Hello",true,appClassLoader);
     }
}

内存中一个类的字节码对象可以有多个吗?

可以,即使是同一个类,但是它的类加载器不同,生成的字节码对象也不会相同。

@Test
void testClassLoader() throws ClassNotFoundException {
   String basicDir="C:\Users\TEDU\IdeaProjects\";
   SimpleAppClassLoader appClassLoader1=new SimpleAppClassLoader(basicDir);
   //appClassLoader1.loadClass("pkg.Hello");
   Class<?> c1=Class.forName("pkg.Hello",true,appClassLoader1);
   SimpleAppClassLoader appClassLoader2=new SimpleAppClassLoader(basicDir);
   Class<?> c2=Class.forName("pkg.Hello",true,appClassLoader2);
   System.out.println(c1==c2);
}

JVM运行内存部分(重点)

JVM运行内存是如何划分的?

JVM运行时内存从规范上讲有方法区(Method Area)、堆区(Heap)、Java方法栈(Stack)、本地方法栈、程序计数器(寄存器)。

JVM技术

其中,方法区和堆区为线程共享区,Java方法栈、本地方法栈、程序计数器属于线程私有区。

JVM中的程序计数器用于做什么?

Java中每个线程都有一个程序计数器,为线程私有,用于记录程序执行时的字节码下一条指令地址(偏移量地址)。

JVM虚拟机栈的结构是怎样的?

Java中每个线程有一个虚拟机栈(Java方法栈),每个方法的执行和退出会对应着一次入栈(Push)和出栈(Pop)操作。这个栈中的元素为一个一个的栈帧(Stack Frame)对象,这个栈帧对象封装的是你调用的方法信息,它有如下几部分构成:

  1. 操作数栈(Operation Stack,用于执行运算,例如两个变量值的加减)
  2. 局部变量表(Local Variable,用于存储方法内的局部变量,对于实例方法,局部变量表的第0个位置为this)
  3. 方法返回值(Return Address,记录调用方法的返回值)
  4. 动态链接(Dynamic Link,方法中可以访问常量池数据,可以调用其它方法,如何找到要调用的方法?)
  5. 其它信息 通过代码进行分析,例如:
class IntTests{
 public static void main(String[] args){
   int a=10;
   //上面的第三行编译成字节码后,是如下两行指令
   //0 bipush 10
   //2 istore_1
  
   int b=20;
   int c=30;
 }
}

JVM技术

JVM虚拟机栈中局部变量表的作用是什么?

局部变量表底层实现是一个数组,用于存储方法内的局部变量。对于main方法而言,方法中的args这个变量会存储在局部量表下标为0的位置。对于实例方法,局部变量表下标为0位置存储的是this。

JVM虚拟机栈中操作数栈的做用是什么?

最核心的作用是进行计算。JVM的执行引擎可以基于程序计数器中指令的地址找到具体指令,然后执行。在执行这些指令时,可以将指令对应的数据放到操作数栈、也可以从操作栈将数据取出存储到局部变量表,还可以将局部变量表中的数据取出,进行计算,将计算的结果再存储到局部变量中。

JVM堆的构成是怎样的?

JVM堆主要用于存储我们创建Java对象,从由年轻代(Young 区)和老年代(Old 区)构成,年轻代又分伊甸园区(Eden)和两个幸存区(s0,s1)。

JVM技术

Java对象分配内存的过程是怎样的?

1)编译器通过逃逸分析(JDK8已默认开启),确定对象是在栈上分配还是在堆上分配。

2)如果是在堆上分配,则首先检测是否可在TLAB(Thread Local Allocation Buffer)上直接分配。

3)如果TLAB上无法直接分配则在Eden加锁区(CAS算法进行加锁)进行分配(线程共享区)。

4)如果Eden区无法存储对象,则执行Yong GC(Minor Collection)。*

5)如果Yong GC之后Eden区仍然不足以存储对象,则直接分配在老年代。

其中,逃逸分析为一种判定方法内创建的对象是否发生了逃逸的一种算法。

JVM技术

JVM技术

JVM年轻代的幸存区设置的比较小会有什么问题?

伊甸园区被回收(GC)时,对象要拷贝到幸存区(s0,s1),假如幸存区比较小,拷贝的对象比较大,对象就会直接存储到老年代,这样老年代对象多了,就会增加老年代GC的频率(老年代GC一般会触发FullGC-大GC,而FullGC需要的时间的更长)。而分代回收的思想就会被弱化。

说明,GC时,业务线程(用户线程)会暂停(STW-Stop The World),从而影响用户体验.

JVM年轻代的伊甸园区设置的比例比较小会有什么问题?

我们程序中新创建的对象,大部分要存储到伊甸园区(Eden),假如伊甸园设置的比较小,会增加GC的频率(GC次数越多,GC消耗的时长就会越长),可能会导致STW(Stop The World)的时间变长,进而影响系统性能。

JVM堆内存为什么要分成年轻代和老年代?

为了更好的实现垃圾回收,减少GC时长、提高其执行效率。(思考GC系统是扫描小块内存比较块还是扫描大块内存速度快)

项目中最大堆和初始堆的大小为什么推荐设置为一样的?

我们在设置JVM初始化堆(-Xms)和最大堆(-Xmx)的大小为一样的目的是,避免程序运行过程中,因对象多少或GC后内存发生了变化而调整堆大小,带来的更大系统开销。在很多大厂的开发规范中都推荐初始堆和最大堆的大小是一样的。(例如阿里的开发手册)

什么情况下对象会存储到老年代?

第一:创建的对象比较大,年轻代没有空间存储这个对象。

第二:经过多次GC,没有被回收的对象年龄在增加,默认15岁后会移动老年代(老年代的对象一般都是一些生命力比较顽强的对象)。

Java中所有的对象创建都是在堆上分配内存的?

随着技术的升级,这个说法现在不准确了。对象还可以分配到栈上了(未逃逸的小对象可以分配在栈上)。

如何理解JVM方法区以及它的构成是怎样的?

方法区(Method Area)是JVM中的一种逻辑上规范,不同JDK对规范的落地会有不同,例如在JDK8的HotSport虚拟机中称之为Metaspace(元空间)。方法区主要用于存储已被虚拟机加载的类信息、常 量、静态变量、即时编译后的代码等数据。

JDK8中Hotsport虚拟机的方法区内存在哪里?

JVM堆外内存,严格来讲属于操作系统的一部分内存,也可以通过参数设置具体大小,假如没有设置,可以无限增大,直到操作系统内存不足。

例如: JDK8中的Hotsport虚拟机可以通过‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M方式设置元空间的内存大小.

什么是逃逸分析以及可以解决什么问题?

逃逸分析一种数据分析算法,基于此算法可以检测对象是否发生了逃逸,未逃逸的小对象可以分配栈上,也可以进行标量替换,还可以实现锁消除。总之,可以有效减少Java对象在堆内存中的分配,可以减少线程阻塞,提高其执行效率。

//-Xms16m -Xmx16m -XX:+DoEscapeAnalysis -XX:+PrintGC
class EscapeAnalysisTests{
    public static void main(String[] args){
        for(int i=0;i<1000000;i++){
            alloc1();
        }
    }
    
    static void alloc1(){
       byte[]b1=new byte[1];//b1变量指向对象发生逃逸了吗?没有
    }
    
    static byte[]b2;
    static void alloc2(){
       b2=new byte[1];//在方法内部定义的对象,在方法外部有引用,这样对象可以称之为逃逸对象.
    }
    
    static byte[] alloc3(){
       byte[]b1=new byte[1];//b1变量指向对象发生逃逸了吗? 逃逸了
       return b1;
    }
}

什么是内存溢出以及导致内存溢出的原因?

内存中剩余的内存不足以分配给新的内存请求,此时就会出现内存溢出(OutOfMemoryError)。内存溢出可能直接导致系统崩溃。导致内存溢出的原因可能会有如下几种:

  • 创建的对象太大导致堆内存溢出(内存中没有连续的内存空间可以存储你这个大对象)
  • 创建的对象太多导致堆内存溢出(对象创建的太多,又不能及时回收这些对象)
  • 方法出现了无限递归调用导致栈内存溢出(每次方法的调用都会对应这个一个栈帧对象的创建,同时将栈帧入栈)
  • 方法区内存空间不足导致内存溢出。(将如内存中不断的加载新的类,类越来越多,此时可能出现内存溢出)
  • 出现大量的内存泄漏

JVM运行时内存中唯一一块不会出现内存溢出的区域是程序计数器.

  1. 什么是内存泄漏以及导致内存泄漏的原因?

程序运行时,动态分配的内存空间,在使用完毕后未得到释放,结果导致一直占用着内存单元,直到程序运行结束。这个现象称之为内存泄漏。导致内存泄漏的原因可能有如下几点:

  • 大量使用静态变量(静态变量与程序生命周期一样)

  • IO/连接资源用完没关闭(记得执行close操作)

  • 内部类的使用方式存在问题(实例内部类会默认引用外部类对象)

  • ThreadLocal应用不当(用完记得执行remove操作)

  • 缓存(Cache)应用不当(尽量不要使用强引用)

内部类导致内存溢出的案例分析:

package cn.tedu.jvm;
class Outer{
     //实例内部类对象,默认会保存外部类引用
     class Inner extends Thread{
         @Override
         public void run() {//this
             System.out.println(this);//内部类对象引用
             System.out.println(Outer.this);//外部类对象应用
             while(true){}
         }
     }

}
class StaticOuter{//优化
    //静态内部类,不会保存对外部类的引用,即便是此对象一直在运行,外部类也可以被销毁.
    static class StaticInner extends Thread{
        @Override
        public void run() {
            while(true){}
        }
    }
}
public class OuterInnterTests {
    static void doInstanceInner(){
        Outer outer=new Outer();
        Outer.Inner inner=outer.new Inner();
        inner.start();
        outer=null;
        //outer置为null后,后面就访问不到Outer对象,但是这个对象不会销毁,因为Inner对象一直在运行
    }
    static void doStaticInner(){
        StaticOuter outer=new StaticOuter();
        StaticOuter.StaticInner inner=new StaticOuter.StaticInner();
        inner.start();
        outer=null;
        //这里的outer置为null后,StaticOuter对象是可以销毁的
    }
    public static void main(String[] args) {
        doStaticInner();
    }
}

JAVA中的四大引用类型有什么特点? Java中为了更好控制对象的生命周期,提高对象对内存的敏感度,设计了四种类型的引用。按其在内存中的生命力强弱,可分为强引用(通过引用变量直接引用对象)、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)。其中,"强引用"引用的对象生命力最强,其它引用引用的对象生命力依次递减。JVM的GC系统被触发时,会因对象引用的不同,执行不同的对象回收逻辑。

强引用:此引用引用的对象即便是内存溢出,对象也不会销毁。

Object o1=new Object();//这里的o1就是强引用

Soft引用(SoftReference):此引用引用的对象可以在内存不足(例如触发了FullGC)时会被销毁。

SoftReference<Object> sr=new SoftReference<Object>(new Object());//sr为软引用
sr.get();//获取引用的对象

弱引用(WeakReference): 此引用引用的对象可以在GC触发时被销毁.

WeakReference<Object> sr=new WeakReference<Object>(new Object());//sr为弱引用

虚引用(PhantomReference): 此引用对对象进行引用时,对象就相当于没有引用,使用它主要用于记录被销毁的对象,当它引用的对象被销毁时,这个引用就存储到引用队列(ReferenceQueue)

ReferenceQueue<Object> referenceQueue=new ReferenceQueue<Object>();
PhantomReference<Object> sr=
new PhantomReference<Object>(new Object(),referenceQueue);//sr为虚引用

案例演示:

package com.java.jvm;

import java.lang.ref.*;
import java.util.ArrayList;
import java.util.List;
//https://mp.weixin.qq.com/s?__biz=Mzg4MzIxMDE2Mg==&mid=2247484171&idx=1&sn=b8f1e06437e0c8538aa921096823353f&chksm=cf4ba1d6f83c28c0c3399628273d487a128616854fe0c9d310591d33662866b56b68ef411e71&token=1742423490&lang=zh_CN#rd
//Java中常见引用
//-Xms128m -Xmx128m -XX:+PrintGC
public class ReferenceTests {
    //强引用引用的对象即使是内存溢出都不会销毁对象.
    static void doStrongRef() throws InterruptedException {
        List<byte[]> cache=new ArrayList<>();
        while(true){
            //b1这里为强引用
            byte[] b1=new byte[1024*1024];
            //list集合对b1指向的对象的引用也是强引用
            cache.add(b1);
            //Thread.sleep(100);
        }
    }

    //软引用:SoftReference引用对象时为软引用
    //软引用引用的对象会在内存不足时,将引用的对象销毁(一般是在fullgc时).
    static void doSoftRef() throws InterruptedException {
            List<SoftReference> cache=new ArrayList<>();
            while (true) {
                //b1这里为强引用
                byte[] b1 = new byte[1024 * 1024];
                //list集合对b1指向的对象的引用是软引用
                cache.add(new SoftReference<byte[]>(b1));
               // Thread.sleep(100);
            }
    }
    //弱引用:WeakReference引用对象时为弱引用
    //弱引用引用的对象可能会在GC时(小GC),就会将引用的对象销毁.
    static void doWeakRef() throws InterruptedException {
        List<WeakReference> cache=new ArrayList<>();
        while (true) {
            //b1这里为强引用
            byte[] b1 = new byte[1024 * 1024];
            //list集合对b1指向的对象的引用是弱引用
            cache.add(new WeakReference<byte[]>(b1));
           // Thread.sleep(300);
        }
    }
    //虚引用:PhantomReference引用对象时为虚引用,相当于没有引用,生命力最弱,只是为了记录对象的销毁状态
    static void doPhantomRef() throws InterruptedException {
        List<PhantomReference> list=new ArrayList<>();
        //引用队列
        ReferenceQueue queue=new ReferenceQueue ();//记录已经被销毁的对象的引用
        while (true) {
            try {
                //b1这里为强引用
                byte[] b1 = new byte[1024 * 1024];
                //list集合对b1指向的对象的引用也是强引用
                list.add(new PhantomReference<byte[]>(b1, queue));
            }catch (Throwable e){
                System.out.println(queue.remove());
            }
        }
    }
    public static void main(String[] args) throws Exception{
            //强引用
            //doStrongRef();
            //软引用
            //doSoftRef();
            //弱引用
           // doWeakRef();
            //虚引用(一般不用)
           doPhantomRef();
    }
}

项目中的哪些地方用到了缓存?

项目中使用缓存的目的,主要是用于提高查询的效率(以空间换时间),常见的缓存有:

  1. 数据库内置的缓存?(例如mysql的查询缓存)
  2. 数据层缓存(一般由持久层框架提供,例如MyBatis中LruCache,SoftCache,WeakCache,....)
  3. 业务层缓存(基于map等实现的本地缓存(CurrentHashMap,Caffeine,...),分布式缓存-例如redis)
  4. Nginx缓存(负载均衡/反向代理)
  5. 浏览器内置缓存?(客户端)
  6. CPU缓存(高速缓冲区)

JVM垃圾回收部分(重点)

何为GC?

GC(Garbage Collection)称之为垃圾回收,是对内存中的垃圾对象采用一定的算法进行内存回收的一个动作。

为什么要学习GC?

深入理解GC的工作机制,可以帮你写出更好的Java应用(例如避免内存泄漏,提高运行效率),提高开发效率。

学习GC(垃圾回收)时,要学习哪些内容?

  1. 如何判断对象是否是垃圾? (引用计数法,对象可达性分析)
  2. 常用的垃圾回收的算法有哪些?(标记清除法,标记复制,标记整理)
  3. 执行垃圾回收的线程策略是怎样的?(是单线程还是多线程,是并行还是并发)

如何判定对象是否为垃圾?

1、引用计数法

引用计数法,会给每个对象分配一个引用计数器,这个计数器用来记录这个对象的引用数量,当引用数量的值为0时表示,这个对象已经没有任何引用了,此对象就可以被回收了。但是这种方案会出现因循环引用导致的对象不可回收的问题。

2、可达性分析法

可达性分析是从根对象(GC root对象)开始,查找它引用的对象,假如某个对象通过GC Root对象不可以直接或间接的访问到,说明这个对象是不可达的。对于不可达对象,JVM认为是垃圾对象,是可以直接被回收的。这种可达性分析方法是目前大多数JVM所采用的一种判定对象是否为垃圾对象的方法。

何为GC Root对象呢?

通过实例变量,类变量,局部变量可以直接访问到的对象,都是GC Root对象。

你知道哪些GC算法?

常用GC算法有:标记清除、标记复制、标记整理算法.

“标记清除法”会首先扫描内存,对内存中活着的对象进行标记,然后再次扫描内存对未标记的对象进行清除,这个算法会扫描两次内存,效率可能会比较低--消耗时间比较长,同时还可能会产生大量碎片。这种算法可以应用于JVM中的老年代,因为老年代GC次数比较少,老年代垃圾对象比较少。

JVM技术

“标记复制”这个算法首先会扫描内存,标记活着的对象,并将活着的对象拷贝一块空闲的内存中,最后将原先的内存进行释放,同时也不会产生大量内存碎片。但这种算法会牺牲一定的空间,适合活着的对象比较少的内存区,例如JVM中年轻代-活着的对象少复制的效率就会比较高。

JVM技术 “标记整理算法”首先会扫描内存,找到活着对象,然后将这些对象向一侧移动,最后将边界外的内存进行清空即可。此算法可以考虑应用在老年代。

JVM技术

JVM中有哪些垃圾回收器?

JVM中常用的垃圾回收器(将判定对象是否为垃圾的方法、回收垃圾对象的算法、线程应用策略进行了整合)包括:

  1. 串行(Serial):只有一个线程(GC线程)执行垃圾回收.
  2. 并行(Parallel):允许多个线程(CG线程)并行执行垃圾回收.
  3. 并发(CMS):并发指的是GC线程执行垃圾回收的同时,可以有业务线程执行业务逻辑.
  4. G1(收集器):将原有JVM物理内存连续的分代逻辑打散为逻辑上内存连续的分代区域.

如何查看JVM默认的垃圾收集器?

-XX:+PrintCommandLineFlags -version

说出几个常用的JVM配置参数?

1)堆栈配置相关

-Xmx3550m: 最大堆大小为3550m。

-Xms3550m: 设置初始堆大小为3550m。

-Xmn2g: 设置年轻代大小为2g。

-Xss128k: 每个线程的堆栈大小为128k。

-XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值。

-XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个 Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6

-XX:MaxTenuringThreshold=0: 设置垃圾对象最大年龄。如果设置为0的话,则年轻代对象不 经过Survivor区,直接进入年老代。

垃圾收集器相关

-XX:+UseG1GC:选择G1垃圾收集器

-XX:+UseParallelGC: 选择垃圾收集器为并行收集器。

-XX:ParallelGCThreads=20: 配置并行收集器的线程数

-XX:+UseConcMarkSweepGC: 设置年老代为并发收集。

-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所 以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存 空间进行压缩、整理。

-XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是 可以消除碎片

-XX:+PrintGC 输出基本的GC信息(详细GC可以这样写-XX:+PrintGCDetails)

JAVA中的堆区为什么要分代?

因为GC过程都触发STW(stop the world),也就说可能要暂停正常业务的执行,影响执行效率。如果能够想办法缩短一次GC的时长,那我们是否可以只收集其中的一部分内存区域。基于这样的一种原因就产生分代设计思想。

服务频繁fullgc,younggc次数较少,可能原因?

1.经常有超过大对象阈值的对象进入老年代,可以通过-XX:PretenureSizeThreshold设置,大于这个值的参数直接在老年代分配。

2.老年代参数设置不当,-XX:CMSInitiatingOccupancyFaction=92设置不合理(阈值达到多少才进行一次CMS垃圾回),导致频繁FULLGC

3.FULLGC之后没有整理老年代内存碎片,导致没有连续可用的内存地址,进入恶性循环,导致频繁老年代GC,-XX:CMSFullGCsBeforeCompaction可以设置

4.新生代过小,或者e区和s区比例不当,对象通过动态年龄判断机制频繁进入老年代。

5.不合理使用System.gc(),造成频繁的FullGC,-XX:+DisableExplicitGC这个参数可以禁用System.gc().

6.存在内存泄露,老年代中驻扎着大量不可回收的对象,一定程度上缩小了老年代的大小,造成对象一进入老年代就触发FULLGC

7.Meatspace不够用引发fullgc,甚至无限fullgc,这类问题常见于tomcat热部署,以及使用反射不当。

你知道哪些JVM小工具?

  • Jps

jps主要用来输出JVM中运行的进程状态信息。语法格式如下:

jps [options] [hostid]

-q 不输出类名、Jar名和传入main方法的参数

-m 输出传入main方法的参数

-l 输出main类或Jar的全限名

-v 输出传入JVM的参数

  • Jstack

jstack主要用来查看某个Java进程内的线程堆栈信息。语法格式如下:

jstack [option] pid

jstack [option] executable core

jstack [option] [server-id@]remote-hostname-or-ip

  • jmap

jmap导出堆内存,然后使用jhat来进行分析,jmap语法格式如下:

jmap [option] pid

jmap [option] executable core

jmap [option] [server-id@]remote-hostname-or-ip

  • jstat

jstat是JVM统计监测工具,看看各个区内存和GC的情况。

jstat [ generalOption | outputOptions vmid [interval[s|ms] [count]] ]

例如:

jstat -gc 21711 250 4

vmid是Java虚拟机ID,在Linux/Unix系统上一般就是进程ID。interval是采样时间间隔。count是采样数目。比如下面输出的是GC信息,采样时间间隔为250ms,采样数为4