JVM面试题

1.谈谈JVM内存模型

其实这块没有太深入研究我把我自己知道的说下 ,首先我说一下内存模型,然后看下面

JVM面试题_第1张图片

绿色为线程共享区域,有线程安全问题。白色为线程私有

1.8同1.7比,最大的差别就是:元数据区取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存

1. 程序计数器

每个线程一块,指向当前线程正在执行的字节码代码的行号。如果当前线程执行的是native方法,则其值为null。

就是记录当前线程执行到哪儿了

2. Java虚拟机栈

线程私有。每个Java方法在被调用的时候都会创建一个栈帧,并入栈。一旦完成调用,则出栈。所有的的栈帧都出栈后,线程也就完成了使命。

入栈弹栈都知道吧?一句话就是跑方法的 先进后出,FILO(first in last out)

类似于我们学volatile里面的高速缓存

3. 本地方法栈

执行native修饰的方法,其它跟上面一样

4. 堆

线程不安全,放的是new的对象,所有线程共享(类似于我们学volatile里面的计算机内存)所以这里也可以用之前学volatile的原子性,可见性,指令重排序解释

堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:所有的对象实例及数组都在对上进行分配。1.8后,字符串常量池从永久代中剥离出来,存放在堆中

5. 元空间

线程不安全

1.7以前加方法区(或者永久代),1.8以后叫元空间

存放虚拟机加载的类信息(class对象),静态变量,常量(就是final修饰的成员变量)等数据。

6. 直接内存

jdk1.4引入了NIO,它可以使用Native函数库直接分配堆外内存。了解即可

2.为什么java虚拟机可以跨平台

因为java是被编译成class文件

每个平台都有自己的jvm,比如windows的jvm就跟linux的不一样,而这些jvm就可以解析class,并且按照当前的系统运行,因为jvm都跟系统配套了,所以一处编译class后就可以到处运行

3.谈谈Java类加载器

1、一共有四种类加载器

首先JVM三种预定义类型类加载器,还有一种是自定义类加载器

当JVM启动的时候,Java开始使用如下三种类型的类加载器:

启动(Bootstrap)类加载器

启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,

出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类

它用来加载Java的核心库,是用原生代码实现的,并不继承自java.lang.ClassLoader

这个类通过java代码拿不到,获得的是null

启动类加载器是用本地代码实现的类加载器,它负责将JAVA_HOME/lib下面的核心类库或-Xbootclasspath选项指定的jar包等虚拟机识别的类库加载到内存中。由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用。具体可由启动类加载器加载到的路径可通过System.getProperty(“sun.boot.class.path”)查看。

扩展(Extension)类加载器

扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将JAVA_HOME /lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器,具体可由扩展类加载器加载到的路径可通过System.getProperty("java.ext.dirs")查看。

JVM面试题_第2张图片

系统(System)类加载器

系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径,如第四节中的问题6所述)下的类库加载到内存中。开发者可以直接使用系统类加载器,具体可由系统类加载器加载到的路径可通过System.getProperty("java.class.path")查看

JVM面试题_第3张图片

一般来说,Java应用的类都是由它来完成加载的

三个类加载器的子父关系

JVM面试题_第4张图片

自定义类加载器(了解,记下方法名)

继承classloader

主要方法

//加载指定名称(包括包名)的二进制类型,供用户调用的接口  
public Class loadClass(String name) throws ClassNotFoundException{ … }  

//加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是这里的resolve参数不一定真正能达到解析的效果),供继承用  
protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }  

//findClass方法一般被loadClass方法调用去加载指定名称类,供继承用  
protected Class findClass(String name) throws ClassNotFoundException { … }  

//定义类型,一般在findClass方法中读取到对应字节码后调用,final的,不能被继承  
//这也从侧面说明:JVM已经实现了对应的具体功能,解析对应的字节码,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用就可以了)  
protected final Class defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … }  

2.双亲委派机制

为了避免java类的重复加载,java采用双亲委派机制

JVM面试题_第5张图片

自定义加载器----》app-------->ext------->bootstrap

如图,就一句话,当请求加载一个类的时候,类加载器不加载,依此向上找父类,看父类有没有加载,父类加载了直接返回

父类没有加载就会抛出异常,下面的类加载器会捕获异常,然后依此走到下面,到最后都没加载就抛出classNotfoundException

3.能否自定义java.lang.String类

package java.lang;
 
public class String {
 
    public static void main(String[] args) {
        System.out.println("Hello String");
    }
 
}

出现异常,在类 java.lang.String 中找不到主方法, 请将主方法定义为: public static void main(String[] args)

因为双亲委派机制,自定义类加载器不会加载该类,然后去父类找,AppClassloader已经加载过java.lang.string类型的类,所以直接返回,不会去加载这个类,而原生的String类中并没有main方法 所以出现异常

4.其它

双亲委派机制不是一定的,线程上下文类加载器破坏了“双亲委派模型”

4.谈谈类的加载机制

一、类加载机制概述

一个.java文件在编译后会形成相应的一个或多个Class文件(若一个类中含有内部类,则编译后会产生多个Class文件),但这些Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能被运行和使用。事实上,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程就是虚拟机的 类加载机制。

二. 类加载的生命周期(过程)

Java类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸载(Unloading)七个阶段。其中准备、验证、解析3个部分统称为连接(Linking),如图所示

JVM面试题_第6张图片

加载

简单来说,加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。

这里有两个重点:

  • 字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译
  • 类加载器。一般包括启动类加载器扩展类加载器应用类加载器,以及用户的自定义类加载器

注:为什么会有自定义类加载器?

  • 一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。
  • 另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。

验证

主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。

包括对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?

对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?

对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。

对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?

准备(重要)

主要是为类变量(注意是static变量,不是实例变量)分配内存,并且赋予初值

特别需要注意,初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。

比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值,final static tmp = 456, 那么该阶段tmp的初值就是456

解析

将常量池内的符号引用替换为直接引用的过程。

两个重点:

  • 符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
  • 直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量

举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。

在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。

初始化(重要)

这个阶段主要是对**类变量(静态变量)**初始化,是执行类构造器的过程。

换句话说,只对static修饰的变量或语句进行初始化。

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

三.案例

public class StaticTest {
    public static void main(String[] args) {
        staticFunction();
    }

    static StaticTest st = new StaticTest();//A:这里调用构造方法 所以构造方法最先执行,下面的b变量现在是初始值0

    static {   //静态代码块
        System.out.println("1"); //E:这个代码跟静态变量一个级别 上面执行完了 该这里了,这里的b还是0
    }

    {       // 实例代码块   B: 优先执行 因为在A步,构造方法执行之前构造代码块与实例变量会先执行
        System.out.println("2");
    }

    StaticTest() {    // 实例构造器
        System.out.println("3"); //C:实例变量与构造代码块执行完了 就该构造方法了
        System.out.println("a=" + a + ",b=" + b);  //D:因为实例变量已经执行完了所以是110 b变量还是初始值0
    }

    public static void staticFunction() {   // 静态方法
        System.out.println("4"); //F:然后才是这个方法执行
    }

    int a = 110;    // 实例变量
    static int b = 112;     // 静态变量
}
/* 输出结果:
        2
        3
        a=110,b=0
        1
        4
 *///:~

如果有继承

1.执行父类的静态变量与静态代码块

2.执行子类的静态变量与静态代码块

3.执行父类的构造方法(构造方法执行之前需要执行完父类的实例变量与构造代码块)

4.执行子类的构造方法(构造方法执行之前需要执行完父类的实例变量与构造代码块)

注意:

静态变量与静态代码块是一个级别的谁在前谁先执行

实例变量与构造代码块是一个级别的谁在前谁先执行

无论实例变量还是静态变量都会先执行显示初始化(就是=号左边的,这个时候是初值),然后执行赋值初始化(就是=号后面的),这个时候变量才真正的被赋了我们在代码中写的值

5.谈谈Java对象的创建过程

在Java中,一个对象在可以被使用之前必须要被正确地初始化,这一点是Java规范规定的。在实例化一个对象时,JVM首先会检查相关类型是否已经加载并初始化,如果没有,则JVM立即进行加载并调用类构造器完成类的初始化。

一个Java对象的创建过程往往包括 类初始化类实例化 两个阶段

一、Java对象创建时机(什么时候触发对象创建)

1). 使用new关键字创建对象

这是我们最常见的也是最简单的创建对象的方式,通过这种方式我们可以调用任意的构造函数(无参的和有参的)去创建对象

2). 使用Class类的newInstance方法(反射机制)

我们也可以通过Java的反射机制使用Class类的newInstance方法来创建对象,事实上,这个newInstance方法调用无参的构造器创建对象

Student student2 = (Student)Class.forName(“Student类全路径”).newInstance(); 
或者:
  Student stu = Student.class.newInstance();

3). 使用Constructor类的newInstance方法(反射机制)

4). 使用Clone方法创建对象(深克隆,浅克隆 了解即可 你说以前看过项目中没用过)

5). 使用(反)序列化机制创建对象

当我们反序列化一个对象时,JVM会给我们创建一个单独的对象,在此过程中,JVM并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口

       Constructor constructor = Student.class
                .getConstructor(Integer.class);
        Student stu3 = constructor.newInstance(123);
       // 写对象
        ObjectOutputStream output = new ObjectOutputStream(
                new FileOutputStream("student.bin"));
        output.writeObject(stu3);
        output.close();

        // 读对象
        ObjectInputStream input = new ObjectInputStream(new FileInputStream(
                "student.bin"));
        Student stu5 = (Student) input.readObject();

二. Java 对象的创建过程

当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的**实例变量(非static修饰的)**及其从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)

在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值,或者null)。

在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序猿的意志进行初始化。在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化(非static修饰的)、实例代码块初始化(非static) 以及 构造函数初始化

1、实例变量初始化与实例代码块初始化

我们在定义(声明)实例变量的同时,还可以直接对实例变量进行赋值或者使用实例(static)代码块对其进行赋值。如果我们以这两种方式为实例变量**(非static修饰的)**进行初始化,那么它们将在构造函数执行之前完成这些初始化操作。

实际上,如果我们对实例变量直接赋值或者使用实例代码块赋值,那么编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后(还记得吗?Java要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前。

JVM面试题_第7张图片

例子2:

public class InstanceInitializer {  
    private int j = getI();  
    private int i = 1;  

    public InstanceInitializer() {  
        i = 2;  
    }  

    private int getI() {  
        return i;  
    }  

    public static void main(String[] args) {  
        InstanceInitializer ii = new InstanceInitializer();  
        System.out.println(ii.j);  
    }  
}  

这个代码输出是0 因为在执行private int j = getI(); 代码的时候 i只有默认值0 所以return的是0

2、构造函数初始化

实例变量初始化与实例代码块初始化总是发生在构造函数初始化之前,那么我们下面着重看看构造函数初始化过程。众所周知,每一个Java中的对象都至少会有一个构造函数,如果我们没有显式定义构造函数,那么它将会有一个默认无参的构造函数。在编译生成的字节码中,这些构造函数会被命名成()方法,参数列表与Java语言书写的构造函数的参数列表相同
Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性。事实上,这一点是在构造函数中保证的:Java强制要求Object对象(Object是Java的顶层对象,没有超类)之外的所有对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其他的构造函数,如果我们既没有调用其他的构造函数,也没有显式调用超类的构造函数,那么编译器会为我们自动生成一个对超类构造函数的调用

3.总结

总而言之,实例化一个类的对象的过程是一个典型的递归过程,如下图所示。进一步地说,在实例化一个类的对象时,具体过程是这样的:

在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类。此时,首先实例化Object类,再依次对以下各类进行实例化,直到完成对目标类的实例化。具体而言,在实例化每个类时,都遵循如下顺序:先依次执行实例变量初始化和实例代码块初始化,再执行构造函数初始化。也就是说,编译器会将实例变量初始化和实例代码块初始化相关代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前。

JVM面试题_第8张图片

4、实例变量初始化、实例代码块初始化以及构造函数初始化综合实例

//父类
class Foo {
    int i = 1;

    Foo() {
        System.out.println(i);
        int x = getValue();
        System.out.println(x);
    }

    {
        i = 2;
    }

    protected int getValue() {
        return i;
    }
}
//子类
class Bar extends Foo {
    int j = 1;
    Bar() {
        System.out.println(j);
        j = 2;
    }
    {
        j = 3;
    }

    @Override
    protected int getValue() {
        return j;
    }
}
public class ConstructorExample {
    public static void main(String... args) {
        Bar bar = new Bar();
        System.out.println(bar.getValue());
    }
}/* Output:
            2 第一个i 因为要在构造代码块之后执行
            0 第二个根据多态调用的是子类的方法,因为现在是子类对象,子类的j还没有初始化,因为前面说了代码是在父类构造之后执行
            3 第三个j 已经执行了构造代码块 j=3
            2 第四个打印的是j 因为构造代码块j=3 然后再构造中j=2  前面说了构造代码块跟实例变量都在构造方法的代码之前执行
 *///:~

问题

1、一个实例变量在对象初始化的过程中会被赋值几次?

我们知道,JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。如果我们在声明实例变量x的同时对其进行了赋值操作,那么这个时候,这个实例变量就被第二次赋值了。如果我们在实例代码块中,又对变量x做了初始化操作,那么这个时候,这个实例变量就被第三次赋值了

6.谈谈java的垃圾回收

垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露(比如有个无用对象占着内存,就是说内存浪费了)。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。

1.垃圾判断算法

引用计数法

在堆中存储对象时,在对象头处维护一个counter计数器,如果一个对象增加了一个引用与之相连,则将counter++。如果一个引用关系失效则counter–。如果一个对象的counter变为0,则说明该对象已经被废弃,不处于存活状态。

优点:简单

缺点:无法解决循环引用的问题

例如:

先创建一个字符串,String m = new String("jack");,这时候 “jack” 有一个引用,就是m。然后将m设置为null,这时候 “jack” 的引用次数就等于 0 了,在引用计数算法中,意味着这块内容就需要被回收了。

循环引用:

public class MyObject {
    public Object ref = null;
    public static void main(String[] args) {
        MyObject myObject1 = new MyObject();
        MyObject myObject2 = new MyObject();
        myObject1.ref = myObject2;//这一步 myObject1 myObject2会记1
        myObject2.ref = myObject1;//这一步 myObject1 myObject2又会记1,所以两个都是2
        myObject1 = null;//然后只减了一个
        myObject2 = null;//只减了一个
    }
}

可达性分析算法(现在都是这种,因为计数法有循环引用)

通过GC ROOT的对象作为搜索起始点,通过引用向下搜索,所走过的路径称为引用链。通过对象是否有到达引用链的路径来判断对象是否可被回收。

如果所有的引用链都没连到这个对象那么该对象会被回收

JVM面试题_第9张图片

上图中DEF都没有根节点能找到,都不在引用链中所以应该被回收

java中可作为GC Root的对象有

1.虚拟机栈中引用的对象(本地变量表)

2.方法区中静态属性引用的对象

  1. 方法区中常量引用的对象

4.本地方法栈中引用的对象(Native对象)

优点:更加精确和严谨,可以分析出循环数据结构相互引用的情况;

缺点:实现比较复杂、需要分析大量数据,消耗大量时间、分析过程需要GC停顿(引用关系不能发生变化),即停顿所有Java执行线程(称为"Stop The World",是垃圾回收重点关注的问题)。

2.分代垃圾回收内存图

JVM面试题_第10张图片

JDK1.8以后 元空间替换永久代

然后堆中被分为新生代与老年代

默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小

然后新生代中有

eden:伊甸园区 占新生代8

s1、s2:Survivor 分别占1

默认的,Edem : from(s1) : to(s2) = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。

新生代GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。

老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上

当一个对象创建时,优先是分配在新生代的eden(伊甸园)区

进入老年代情况

1.大对象会直接分配到老年代

-XX:PretenureSizeThreshold即对象的大小大于此值, 就会绕过新生代, 直接在老年代分配, 此参数只对 Serial及ParNew两款收集器有效。

2.空间分配担保

​ 当MinorGC时,如果存活对象过多,无法完全放入Survivor区,就会向老年代借用内存存放对象

3.长期存活的对象将进入老年代

​ 虚拟机对每个对象定义了一个对象年龄(Age)计数器。当年龄增加到一定的临界值时,就会晋升到老年代 中,该临界值由参数:-XX:MaxTenuringThreshold来设置,。

​ 当对象在 Eden出生后,在经过一次 Minor GC 后,如

果对象还存活,并且能够被一块 Survivor 区域所容纳(s1),则使用复制算法将这些仍然还存活的对

象复制到另外一块 Survivor 区域 中,然后清理所使用过的 Eden ,并且将这些对象的年龄设置为+1

当第二次Minor GC:如果不需要进入老年代,会将伊甸园区及s1中所有对象,采用复制算法,全部移入s2区 域,然后年龄+1,以后就是一次Minor GC ,就从s1到s2,或者s2到s1,然后每次对象年

龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定

),这些对象就会成为老年代。

4.动态对象年龄判定

虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor区中相同 年龄(设年龄为age)的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄(age)的对象就 可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

3.垃圾回收算法

1.标记-清除算法

步骤:

​ 1.标记垃圾(可达性分析法)

​ 2.清除垃圾
JVM面试题_第11张图片
标记清除算法(Mark-Sweep)是最基础的一种垃圾回收算法,它分为2部分,先把内存区域中的这些对象进行标记,哪些属于可回收标记出来,然后把这些垃圾拎出来清理掉。就像上图一样,清理掉的垃圾就变成未使用的内存区域,等待被再次使用。但它存在一个很大的问题,那就是内存碎片

内存碎片:就是内存区域不连续,比如第二个图第一个未使用的方块是3M,第二个是1M,第三个1M,第四第五个是4M,那么现在要放一个9M的就放不小,虽然有这么多内存,但是不连续

特点:(1)效率问题,标记和清除的效率都不高;(2)空间的问题,标记清除以后会产生大量不连续的空间碎片,空间碎片太多可能会导致程序运行过程需要分配较大的对象时候,无法找到足够连续内存而不得不提前触发一次垃圾收集。
地方 :适合在老年代进行垃圾回收,比如CMS收集器就是采用该算法进行回收的。

2.复制算法

复制算法(Copying)是在标记清除算法基础上演化而来,解决标记清除算法的内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。保证了内存的连续可用,内存分配时也就不用考虑内存碎片等复杂情况。复制算法暴露了另一个问题,例如硬盘本来有500G,但却只能用250G,代价实在太高

特点:没有内存碎片,只要移动堆顶指针,按顺序分配内存即可。代价是将内存缩小位原来的一半。
地方:适合新生代区进行垃圾回收

3 .标记-整理算法

标记-整理算法标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。

标记整理算法解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。标记整理算法对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。一般是把Java堆分为新生代老年代,这样就可以根据各个年代的特点采用最适当的收集算法

特点:不会产生空间碎片,但是整理会花一定的时间。
地方
适合老年代进行垃圾收集

4. 分代收集算法(综合)

当前商业虚拟机都是采用分代收集算法,它根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法,在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,就选用复制算法,而老年代因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记清理”或者“标记整理”算法来进行回收

4.触发GC的条件(避免fullgc)

minor gc:当Eden区满时,触发Minor GC

Full GC触发条件:

(1)调用System.gc时,系统建议执行Full GC,但是不必然执行,所以不要手动调用

(2)老年代空间不足

(3)元空间空间不足

(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存

(5)堆中产生大对象超过阈值

5.垃圾回收器(记住名字,记住作用地方,及主要特点即可)

并行和并发

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。

垃圾回收吞吐量(Throughput)

吞吐量就是CPU用于运行用户代码的时间CPU总消耗时间的比值,即

吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。

假设虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

新生代垃圾回收器:如下三个

Serial收集器

Serial,是单线程执行垃圾回收的。当需要执行垃圾回收时,程序会暂停一切手上的工作,然后单线程执行垃圾回收,单线程地好处就是减少上下文切换,减少系统资源的开销。但这种方式的缺点也很明显,在GC的过程中,会暂停程序的执行。若GC不是频繁发生,这或许是一个不错的选择,否则将会影响程序的执行性能,对于新生代来说,区域比较小,停顿时间短,所以比较使用。

ParNew收集器

ParNew同样用于新生代,是Serial的多线程版本,并且在参数、算法(同样是复制算法)上也完全和Serial相同

因为是多线程执行,所以在多CPU下,ParNew效果通常会比Serial好。但如果是单CPU则会因为线程的切换,性能反而更差。

Parallel Scavenge 收集器

Parallel Scavenge收集器也是一个并行多线程新生代收集器,它也使用复制算法。关注的是吞吐量

老年代收集器:如下三个

Serial Old收集器

Serial Old 是 Serial收集器的老年代版本,它同样是一个单线程收集器,使用**“标记-整理”(Mark-Compact)**算法。

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和**“标记-整理”**算法

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于**“标记-清除”**算法实现的。

CMS收集器工作的整个流程分为以下4个步骤:

  • 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
  • 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。
  • 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
  • 并发清除(CMS concurrent sweep)

G1收集器(一般记住这个要记住然后说清除好)

G1,Garbage First,在JDK 1.7版本正式启用,是当时最前沿的垃圾收集器

高效益优先。G1会预测垃圾回收的停顿时间,原理是计算老年代对象的效益率,优先回收最大效益的对象。

以前的收集器分代是划分新生代、老年代、元空间等G1把内存区域重新划分

G1则是把内存分为多个大小相同的区域Region,每个Region拥有各自的分代属性,但这些分代不需要连续。
JVM面试题_第12张图片

堆内存会被切分成为很多个固定大小区域(Region),每个是连续范围的虚拟内存。

堆内存中一个区域(Region)的大小可以通过-XX:G1HeapRegionSize参数指定,大小区间最小1M、最大32M,总之是2的幂次方。

默认把堆内存按照2048份均分。

每个Region被标记了E、S、O和H(就是上图的不同颜色),这些区域在逻辑上被映射为Eden,Survivor和老年代

优势:

  • 并行与并发 G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

  • 分代收集 与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。

  • 空间整合 G1从整体来看是基于**“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”**算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

  • 可预测的停顿 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

流程:

  • 初始标记(Initial Marking) 仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。

  • 并发标记(Concurrent Marking) 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行

  • 最终标记(Final Marking) 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行

    一句话就是:标记那些在并发标记阶段发生变化的对象,将被回收。

  • 筛选回收(Live Data Counting and Evacuation) 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

    G1中提供了两种模式垃圾回收模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。

G1改了名字

YoungGC年轻代收集

在分配一般对象(非巨型对象)时,当所有eden region使用达到最大阀值并且无法申请足够内存时,会触发一次YoungGC。每次younggc会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。

mixed gc

当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。

G1没有fullGC概念,需要fullGC时,调用serialOldGC进行全堆扫描(包括eden、survivor、o、perm)。

ZGC

在JDK 11当中,加入了实验性质的ZGC。它的回收耗时平均不到2毫秒。它是一款低停顿高并发的收集器。你说用的jdk8,没有研究

7.jvm常见错误

1.OutOfMemoryError(说出情况)

1.堆内存溢出,内存不足

现在是代码有问题,如果代码没问题可以通过下面几个参数设置堆大小,例如:

  • -Xmx4g:堆内存最大值为4GB。

  • -Xms4g:初始化堆内存大小为4GB。
    JVM面试题_第13张图片

    2.java.lang.OutOfMemoryError:Java heap space

​ 1)原因:Heap内存溢出,意味着Young和Old generation的内存不够。

​ 2)解决:调整java启动参数 -Xms(初始化堆内存大小) -Xmx(堆内存最大值) 来增加Heap内存。

​ 3.java.lang.OutOfMemoryError:unable to create new native thread

​ 1)原因:Stack空间不足以创建额外的线程,要么是创建的线程过多,要么是Stack空间确实小了。

​ 2)解决:由于JVM没有提供参数设置总的stack空间大小,但可以设置单个线程栈的大小;而系统的用户空间一共是3G,除了Text/Data/BSS/MemoryMapping几个段之外,Heap和Stack空间的总量有限,是此消彼长的。因此遇到这个错误,可以通过两个途径解决:1.通过-Xss启动参数减少单个线程栈大小,这样便能开更多线程(当然不能太小,太小会出现StackOverflowError);2.通过-Xms -Xmx 两参数减少Heap大小,将内存让给Stack(前提是保证Heap空间够用)。

​ 4.java.lang.OutOfMemoryError:PermGen space

​ 1)原因:Permanent Generation空间不足,不能加载额外的类。

​ 2)解决:调整-XX:PermSize= -XX:MaxPermSize= 两个参数来增大PermGen内存。一般情况下,这两个参数不要手动设置,只要设置-Xmx足够大即可,JVM会自行选择合适的PermGen大小。

​ 5.java.lang.OutOfMemoryError:Requested array size exceeds VM limit

​ 1)原因:这个错误比较少见(试着new一个长度1亿的数组看看),同样是由于Heap空间不足。如果需要new一个如此之大的数组,程序逻辑多半是不合理的。

​ 2)解决:修改程序逻辑吧。或者也可以通过-Xmx来增大堆内存。

​ 6.java.lang.OutOfMemoryError: GC overhead limit exceeded

​ 1)原因:在GC花费了大量时间,却仅回收了少量内存时,也会报出OutOfMemoryError。当使用-XX:+UseParallelGC或-XX:+UseConcMarkSweepGC收集器时,在上述情况下会报错,在HotSpot GC Turning文档上有说明:

The parallel(concurrent) collector will throwan OutOfMemoryError if too much time is being spent in garbage collection: ifmore than 98% of the total time is spent in garbage collection and less than 2%of the heap is recovered, an OutOfMemoryError will be thrown.

对这个问题,一是需要进行GC turning,二是需要优化程序逻辑

2.java.lang.StackOverflowError

1)原因:这也内存溢出错误的一种,即线程栈的溢出,要么是方法调用层次过多(比如存在无限递归调用),要么是线程栈太小。

2)解决:优化程序设计,减少方法调用层次;调整-Xss参数增加线程栈大小。

xss:为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M

8.JVM调优(你就回答实战没有做过,自己学过)

主要使GC调优,这个没有实战很老火,列举点简单的,大家以后要做这方面,需要去买一本书,深入理解java虚拟机

  1. 何时需要做jvm调优?
      1. heap 内存(老年代)持续上涨达到设置的最大内存值;
      2. Full GC 次数频繁;
      3. GC 停顿时间过长(超过1秒);
      4. 应用出现OutOfMemory 等内存异常;
      5. 应用中有使用本地缓存且占用大量内存空间;
      6. 系统吞吐量与响应性能不高或下降。

Full GC 次数频繁:

1.禁止代码中使用System.gc()

  • system.gc其实是做一次full gc
  • system.gc会暂停整个进程
  • system.gc一般情况下我们要禁掉,使用-XX:+DisableExplicitGC
  • system.gc在cms gc下我们通过-XX:+ExplicitGCInvokesConcurrent来做一次稍微高效点的GC(效果比Full GC要好些)
  • system.gc最常见的场景是RMI/NIO下的堆外内存分配等(应该配置减少频繁调用)

2、老年代代空间不足

老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space
为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组

其它的去这里面总结点标题:

https://tech.meituan.com/2017/12/29/jvm-optimize.html

9.JVM常见参数

参数设置也是在jvm调优的基础上建立的,记几个常见参数即可

  • -Xmx :堆的最大值
  • -Xms :堆的最小值
  • -Xmn :堆年轻代大小
  • -Xss:栈内存的大小

10.代码执行题

执行顺序题:

https://www.cnblogs.com/Qian123/p/5713440.html

String类问题:

https://blog.csdn.net/u011635492/article/details/81048150

你可能感兴趣的:(面试,jvm,java,面试)