JAVA面试汇总-4.JVM

可以扫描下面二维码访问我的小程序来打开,随时随地通过微信访问。

1.什么情况下会触发类的初始化?

(1)首先是类未被初始化时,创建类的实例(new 的方式),访问某个类或接口的静态变量,或者对该静态变量赋值,调用类的静态方法。
(2)对类进行反射调用的时候,如果类没有进行,则需要先触发其初始化。
(3)如果在初始化某一个类时候,其父类没有被初始化时候,则会触发父类的初始化。
(4)当咱们打的jar包,在执行完java -jar命令后,用户需要指定一个要执行的主类(包含 main() 方法的那个类,例如SpringBoot的那个启动类的main方法,非SpringBoot的,咱们自己手动打包的一般在MANIFEST.MF文件中指定),虚拟机会先初始化这个主类。
(5)JDK 1.7新增了一种反射方式java.lang.invoke.MethodHandle,通过实MethodHandle同样是访问静态变量,对该静态变量赋值,调用类的静态方法,前提仍然是该类未被初始化。

2.谈谈你对解析与分派的认识。

(1)解析调用是将那些在编译期就完全确定,在类加载的解析阶段就将涉及的符号引用全部转变为可以确定的直接引用,不会延迟到运行期再去完成。
(2)分派又分为静态分派和动态分派
(3)静态分派:同样是将编译期确定的调用,重载(Oveload)就是这种类型,在编译期通过参数的静态类型(注意不是实际类型)作为判断依据,找到具体的调用的方法。

public class TestOverLoad {
    public static void main(String[] args) {
        //静态类型都是Parent,实际类型分别是Sun和Daughter
        Parent sun = new Sun();
        Parent daughter = new Daughter();
        TestOverLoad test = new TestOverLoad();
        //输出结果按照静态类型执行
        test.testMethod(sun);
        test.testMethod(daughter);
    }
    static abstract class Parent { }
    static class Sun extends Parent { }
    static class Daughter extends Parent { }
    public void testMethod(Parent parent) {
        System.out.println("hello, Parent");
    }
    public void testMethod(Sun sun) {
        System.out.println("hello, Sun");
    }
    public void testMethod(Daughter daughter) {
        System.out.println("hello, Daughter");
    }
}

//输出
hello, Parent
hello, Parent

(4)动态分派:运行期根据实际类型确定方法执行版本的分派过程称为动态分派。重写(Override),在运行时期,通过判断实体的真实类型,判断具体执行哪一个方法。

public class TestOverride {
    public static void main(String[] args) {
        //静态类型都是Parent,实际类型分别是Sun和Daughter
        Parent sun = new Sun();
        Parent daughter = new Daughter();
        //这时候输出结果按照实际类型找到方法
        sun.testMethod();
        daughter.testMethod();
    }
    static abstract class Parent {
        public void testMethod() {
            System.out.println("hello, Parent");
        }
    }
    static class Sun extends Parent {
        @Override
        public void testMethod() {
            System.out.println("hello, Sun");
        }
    }
    static class Daughter extends Parent {
        @Override
        public void testMethod() {
            System.out.println("hello, Daughter");
        }
    }
}
//输出
hello, Sun
hello, Daughter

3.Java类加载器包括⼏种?它们之间的⽗⼦关系是怎么样的?双亲委派机制是什么意思?有什么好处?

(1)启动类加载器(Bootstrap ClassLoader),由C语言编写的。负责把\lib目录中的类库加载到虚拟机内存中。
(2)扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
(3)应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
(4)自定义类加载器。下面是自定义类加载器的方式,这个有几点注意,TestDemo编译出来class,把class复制到idea或者eclipse生成target目录之外,因为需要删除掉TestDemo.java,这样target下的class可能也自动没有了,另外如果不删除TestDemo.java会导致一直输出默认的应用程序加载器,因为你运行环境里有,双亲委派的应用程序加载器能找TestDemo,所以默认用父类的了,所以必须删除掉。

package test;

public class TestDemo {
    private String name;
    public TestDemo()
    {
    }
    public TestDemo(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return name;
    }
    public void setName(String name)
    {
        this.name = name;
    }
    public String toString()
    {
        return "Demo name is " + name;
    }
}
package test;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class TestMyClassLoader extends ClassLoader
{
    public TestMyClassLoader()
    {
    }
    public TestMyClassLoader(ClassLoader parent)
    {
        super(parent);
    }
    protected Class findClass(String name) throws ClassNotFoundException
    {
        File file = getClassFile(name);
        try
        {
            byte[] bytes = getClassBytes(file);
            Class c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return super.findClass(name);
    }
    private File getClassFile(String name)
    {
        //重点是这个路径,在本地编译好TestDemo后,把class放在一个其他路径下
        //不要默认用idea或者eclipse的target路径
        //注意运行这个类之前把代码的TestDemo.java删除掉或者注释掉
        //否则怎么运行都是默认的加载器AppClassLoader
        File file = new File("/Users/buxuesong/TestDemo.class");
        return file;
    }
    private byte[] getClassBytes(File file) throws Exception
    {
        // 这里要读入.class的字节,因此要使用字节流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        fis.close();
        return baos.toByteArray();
    }
    public static void main(String[] args) throws Exception
    {
        TestMyClassLoader mcl = new TestMyClassLoader();
        Class c1 = Class.forName("test.TestDemo", true, mcl);
        Object obj = c1.newInstance();
        System.out.println(obj);
        System.out.println(obj.getClass().getClassLoader());
    }
}
//输出
Demo name is null
test.TestMyClassLoader@5cad8086
//如果没删除TestDemo.java输出
Demo name is null
sun.misc.Launcher$AppClassLoader@18b4aac2

(5)扩展类加载器的父类是启动类加载器,应用程序类加载器的父类是扩展类加载器,自定义类加载器的父类是应用程序类加载器。
(6)双亲委派机制:除了启动类加载器,其余加载器都应该有自己的父类加载器,当一个类加载器需要加载某个类时,默认把这个累交给自己的父类去加载,只有当父类无法加载这个类时候(它的搜索范围中没有找到所需的类),自己才去加载,按照这个规则,所有的累加载最终都会到启动类加载器过一遍。
(7)双亲委派实际上保障了Java程序的稳定运作,因为随着这种父类关系自带了一种层级关系,按照层级关系来分别加载,如果不按照顺序各个加载器自行加载,用户如果自己写了一个java. lang.Object的类,系统会出现多个Object类,导致整个java体系无法运转。

4.如何⾃定义⼀个类加载器?你使⽤过哪些或者你在什么场景下需要⼀个⾃定义的类加载器吗?

(1)这问题问了我上面的,我在写一遍,自定义类加载器,有几点注意,TestDemo编译出来class,把class复制到idea或者eclipse生成target目录之外,因为需要删除掉TestDemo.java,这样target下的class可能也自动没有了,另外如果不删除TestDemo.java会导致一直输出默认的应用程序加载器,因为你运行环境里有,双亲委派的应用程序加载器能找TestDemo,所以默认用父类的了,所以必须删除掉。

package test;

public class TestDemo {
    private String name;
    public TestDemo()
    {
    }
    public TestDemo(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return name;
    }
    public void setName(String name)
    {
        this.name = name;
    }
    public String toString()
    {
        return "Demo name is " + name;
    }
}
package test;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class TestMyClassLoader extends ClassLoader
{
    public TestMyClassLoader()
    {
    }
    public TestMyClassLoader(ClassLoader parent)
    {
        super(parent);
    }
    protected Class findClass(String name) throws ClassNotFoundException
    {
        File file = getClassFile(name);
        try
        {
            byte[] bytes = getClassBytes(file);
            Class c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return super.findClass(name);
    }
    private File getClassFile(String name)
    {
        //重点是这个路径,在本地编译好TestDemo后,把class放在一个其他路径下
        //不要默认用idea或者eclipse的target路径
        //注意运行这个类之前把代码的TestDemo.java删除掉或者注释掉
        //否则怎么运行都是默认的加载器AppClassLoader
        File file = new File("/Users/buxuesong/TestDemo.class");
        return file;
    }
    private byte[] getClassBytes(File file) throws Exception
    {
        // 这里要读入.class的字节,因此要使用字节流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        fis.close();
        return baos.toByteArray();
    }
    public static void main(String[] args) throws Exception
    {
        TestMyClassLoader mcl = new TestMyClassLoader();
        Class c1 = Class.forName("test.TestDemo", true, mcl);
        Object obj = c1.newInstance();
        System.out.println(obj);
        System.out.println(obj.getClass().getClassLoader());
    }
}
//输出
Demo name is null
test.TestMyClassLoader@5cad8086
//如果没删除TestDemo.java输出
Demo name is null
sun.misc.Launcher$AppClassLoader@18b4aac2

(2)我们之前写的获取数据库连接,通过class.forname去加载数据库驱动。以及热加载这种方式,咱们修改了java文件,但是tomcat没有手动重启,这个时候有一个能够监控到java有变化重新编译了的情况,通过线程出发tomcat重启,就达到了热加载的机制。还有就是apk加密的方式,打包时候,源码-》class-》加密-》打成jar包-》安装-》运行-》classLoader解密-》classLoader加载-》用户使用app,这样只有实现解密方法的classloader才能正常加载,其他的classLoader无法运行。

5.堆内存设置的参数是什么?

(1)-Xms初始堆内存大小
(2)-Xmx最大堆内存大小,生产环境中,JVM的Xms和Xmx建议设置成一样的,能够避免GC时还要调整堆大小。
(3)-XX:NewSize=n,设置年轻代大小-XX:NewRatio=n设置年轻代和年老代的比值。如:-XX:NewRatio=3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4,默认新生代和老年代的比例=1:2
(4)-XX:SurvivorRatio=n,设置年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个,默认是8,表示Eden:S0:S1=8:1:1如:-XX:SurvivorRatio=3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5。
(5)-XX:+HeapDumpOnOutOfMemoryError,-XX:HeapDumpPath,这两个是设置但程序OOM后,输出Dump文件以供分析原因的,但是对于目前的K8s的情况,这俩没什么用,OOM之后,K8s发现服务没响应,直接kill了,然后重启一个新的,OOM根本就来不及生成,因为生成文件耗时较多,K8s杀的很快。
(6)-Xss128k 设置每个线程的堆栈大小。
(7)-XX:+PrintGCDetails,输出GC日志。

6.在JVM中,如何判断一个对象是否死亡?

(1)通过引用计数算法,引用计数器,每当一个地方引用这个对象的时候,计数器就加1,当引用失效,计数器就减1;任何时刻计数器为0的对象就是不可能再被使用了。实现简单,效率高,但是它很难解决对象的循环引用问题。
(2)可达性分析算法,这个算法的基本思路是通过一系列称为“GC Roots”(一组必须活跃的引用)作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链时候,那么证明此对象是不可用的。
(3)但是上面两种算法都不能直接判定对象已经死亡,只有gc时,针对两中算法都已经失效的情况下,对两个对象回收后才真正算是死亡。

7.Perm Space中保存什么数据?会引起OutOfMemory吗?

(1)全称是Permanent Generation space,是指内存的永久保存区域。该块内存主要是被JVM用来存储类的元信息、类变量以及内部字符串(interned string)等内容。Perm Space使用的JVM内存。在JDK1.8被元空(Metaspace)替代。永久代是hotspot VM的实现特有。
(2)Metaspace没有存储字符串常量池,而在jdk7的时候已经被移动到了堆中,MetaSpace其他存储的东西,包括类文件,在JAVA虚拟机运行时的数据结构,以及class相关的内容,如Method,Field道理上都与永久代一样,只是划分上更趋于合理。metaspace使用的本地内存而不是JVM内存,因此理论上可以扩展到和本地剩余内存一样大。
(3)当Perm Space中加载的类过多时候,或者存储的内部字符串过多,空间不足时候都可能会导致java.lang.OutOfMemoryError: PermGen space。

8.分派:静态分派与动态分派。

(1)上面第二个问题,讲过了
(2)静态分派:同样是将编译期确定的调用,重载(Oveload)就是这种类型,在编译期通过参数的静态类型(注意不是实际类型)作为判断依据,找到具体的调用的方法。

public class TestOverLoad {
    public static void main(String[] args) {
        //静态类型都是Parent,实际类型分别是Sun和Daughter
        Parent sun = new Sun();
        Parent daughter = new Daughter();
        TestOverLoad test = new TestOverLoad();
        //输出结果按照静态类型执行
        test.testMethod(sun);
        test.testMethod(daughter);
    }
    static abstract class Parent { }
    static class Sun extends Parent { }
    static class Daughter extends Parent { }
    public void testMethod(Parent parent) {
        System.out.println("hello, Parent");
    }
    public void testMethod(Sun sun) {
        System.out.println("hello, Sun");
    }
    public void testMethod(Daughter daughter) {
        System.out.println("hello, Daughter");
    }
}

//输出
hello, Parent
hello, Parent

(3)动态分派:运行期根据实际类型确定方法执行版本的分派过程称为动态分派。重写(Override),在运行时期,通过判断实体的真实类型,判断具体执行哪一个方法。

public class TestOverride {
    public static void main(String[] args) {
        //静态类型都是Parent,实际类型分别是Sun和Daughter
        Parent sun = new Sun();
        Parent daughter = new Daughter();
        //这时候输出结果按照实际类型找到方法
        sun.testMethod();
        daughter.testMethod();
    }
    static abstract class Parent {
        public void testMethod() {
            System.out.println("hello, Parent");
        }
    }
    static class Sun extends Parent {
        @Override
        public void testMethod() {
            System.out.println("hello, Sun");
        }
    }
    static class Daughter extends Parent {
        @Override
        public void testMethod() {
            System.out.println("hello, Daughter");
        }
    }
}
//输出
hello, Sun
hello, Daughter

9.请解释StackOverflowError和OutOfMemeryError的区别?

(1)当一个线程启动的时候,jvm就会给这个线程分配一个栈,随着程序的执行,会不断执行方法,因此栈帧会不断入栈和出栈。然后,一个栈所能容纳的栈帧是有限的,当栈帧的数量超过了栈所允许的范围的时候(比如递归调用),就会抛出StackOverflowError异常。
(2)程序在执行的过程中,需要不断的在堆内存new对象,每new一个对象,就会占用一段内存,当对没有足够的内容分配给对象示例时,就会抛出OutOfMemeryError异常。

10.你有没有遇到过OutOfMemory问题?你是怎么来处理这个问题的?处理过程中有哪些收获?

(1)读取文件时,每条数据对应一个实体,创建大量的实体,实体放入集合中,达到一定程度后,又无法通过GC回收被占用的内存,最终超过配置的内存大小,则会抛出OutOfMemeryError。
(2)从数据库中取出大量数据,每条数据一个实体,实体放入集合中,达到一定量级,超过配置的内存大小,会抛出OutOfMemeryError。
(3)实际上就一个根本原因,集合使用的内存超过了分配的最大内存了。这个时候有两种方案,分别是扩展分配的最大内存,使程序的集合能够分配到足够的内存,这种情况不推荐,治标不治本,下次集合占用的更多,还是会抛出OutOfMemeryError;另一种方案,使针对集合拆分,例如读取文件,每次读取200条,处理完成后,原来的集合设置null,重新创建新的集合,重新加入200条,这样GC的时候就可以释放掉已经处理完的实体集合。数据库读取大量数据建议分页处理,每次处理几十条,几百条即可。

11.StackOverflow异常有没有遇到过?⼀般你猜测会在什么情况下被触发?如何指定⼀个线程的堆栈⼤⼩?⼀般你们写多少?

(1)每当java程序启动一个新的线程时,java虚拟机会为他分配一个栈,java栈以帧为单位保持线程运行状态;当线程调用一个方法时,jvm压入一个新的栈帧到这个线程的栈中,只要这个方法还没返回,这个栈帧就存在。
(2)StackOverflow的意思是栈内存溢出,往栈里存放的过多,导致内存溢出。出现在递归方法,参数个数过多,递归过深。方法的嵌套调用层次太多(如递归调用),随着java栈中的帧的增多,最终导致这个线程的栈中的所有栈帧的大小的总和大于-Xss设置的值,而产生StackOverflowError溢出异常。
(3)-Xss256k 可以设置每个线程的堆栈大小,jdk1.5以后默认是1M。个人感觉不用单独配置,除非这个服务会有大量递归调用循环操作,否则不需要单独配置。

12.内存模型以及分区,需要详细到每个区放什么。

(1)分为方法区,虚拟机栈,本地方法栈,堆,程序计数器
(2)方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。对于HotSpot虚拟机来说实际上这部分在以前叫做永久代。运行时常量池,也在方法区中存储,存放编译期生成的常量,运行期间的常量也可以存储器中,例如String的intern()方法产生的。
(3)虚拟机栈,线程私有的。每个方法在执行时,会创建一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。虚拟机栈中有局部变量表部分,局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
(4)本地方法栈,为虚拟机使用到的Native方法服务。
(5)Java堆,Java虚拟机管理的内存中最大的一块,虚拟机启动创建的,大部分的对象实例存储在其中,也是GC操作的重点区域,许多分配的区域,新生代、老年代、Eden,Survivor之类都在其中。
(6)程序计数器,线程私有的内存,占用空间较小,如果现成正在执行的一个Java方法,程序计数器存储的是正在执行的虚拟机字节码指令的地址;如果是native方法,这个计数器值则为空(Undefined)。唯一不会抛出OutOfMemoryError的空间。
(7)直接内存,这个不是Java虚拟机中的内存区域。NIO可以使用Native函数库直接分配对外内存,Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,可以提高性能,因为避免了在Java堆和Native堆中来回复制数据。如果分配了jvm大量内存,如果直接内存无法分配更多的内存时,也会抛出OutOfMemoryError。

13.做GC时,⼀个对象在内存各个Space中被移动的顺序是什么?

(1)大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
(2)大对象会直接分配到老年代中。大对象是需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。

byte[] a = new byte[4*1024*1024];

(3)虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1,当它的年龄增加到一定程度(默认为15),就将会被晋升到老年代中。这个阈值可以通过-XX:MaxTenuringThreshold设置。

14.虚拟机在运行时有哪些优化策略

(1)针对一些热点运行的代码,虚拟机在运行过程中发现热点代码后,将其编译成本地机器码。再次运行的时候运行效率更高的本地机器码。
(2)公共子表达式消除:如果一个表达式E已经计算过了,并且从先前的计算到现在E中的所有变量的值都没有发生变化,那个E的这次出现就成为了公共子表达式。对于这样的表示式,没有必要对它再次进行计算了,直接沿用之前的结果就可以了。
(3)数组范围检查消除:例如在某些情况下,如果重复的取某个数组内固定位置的值,例如arr[2],只要在编译优化时判断2在arr的范围内即可,编译优化出来的机器码就不在判断范围了;另外咱们写的代码都是在循环内,重复获取数组内容,那么编译优化时,只要判断循环的范围不超过数据范围,那么编译优化的代码就可以不判断范围了,直接运行即可。
(4)方法内联:像下面的两个方法,内联在一起看的话,可以发现f方法没什么意义,因此编译优化时,直接将下面调用f(obj);代码编译过滤掉即可。

public static void f(Object obj){
    if)(obj != null){
        System.out.println("do something");
    }
}
public static void test(String[] args){
    Object obj = null;
    f(obj);
}

(5)逃逸分析:基本行为就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。栈上分配:如果确定不会出现方法逃逸的话,编译优化时候可以将其直接分配到栈上,这样当方法执行完毕,就自动在栈上消除了,避免了垃圾收集。同步消除:如果确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以消除掉。标量替换:如果一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替,这样成员变量会分配到栈上。

15.你知道哪些或者你们线上使⽤什么GC策略?它有什么优势,适⽤于什么场景?

(1)标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。标记和清除两个过程的效率都不高;标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(2)复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。直接分成两块太占用空间,IBM提出了另一种方式,分成1个Eden和2个Survivor空间,Eden和Survivor的大小比例是8∶1,当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。分配的空间小了,可能会导致某些对象由于没有空间直接分配到老年代中。复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。
(3)标记-整理算法:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。这样空间连续,易于管理。另外标记-整理算法主要用于老年代的回收机制,主要是由于清理次数较少,可能被清除的对象也不多。

==========2022.8.16更新=======

16. JVM是什么?

(1)JVM解释,JVM全称为Java Virtual MAchine-Java虚拟机,我们可以直观的从名字上得出一个定义:JVM是java上的一个虚构出来的计算机,是一个位于Java与操作系统之间的中间态。有自己完善的硬件结构,如处理器、堆栈、寄存器等,还具有相应的指令系统。
(2)JVM作用,Java程序的跨平台特性主要就是因为JVM实现的。在编译java程序时会将写好的源程序通过编译器编译生成.class文件(又称为字节码文件),之后就是通过JVM内部的解释器将字节码文件解释成为具体平台上的机器指令执行,所以就可以实现java程序的跨平台特性。
JVM内部体系结构大致分为三部分:类装载器(ClassLoader)子系统,运行时数据区和执行引擎。
(3)Java程序运行与JVM的关系:Java源文件编译生成.class文件(字节码);字节码由JVM解释运行。因为Java程序既要编译同时也要经过JVM的解释运行,所以java被称为半解释语言。

17. 什么是类加载器?

(1)类加载器是一个用来加载类文件的类。Java源代码通过javac编译器编译成类文件。之后JVM来执行类文件中的字节码来执行程序。
(2)类加载器负责加载文件系统、网络或其他来源的类文件。
(3)有三种默认使用的类加载器:Bootstrap类加载器、Extension类加载器和System类加载器(或者叫作Application类加载器)。每种类加载器都有设定好从哪里加载类。

18. 什么是HotSpot?

(1)Hotspot ,为java1.3开发的jvm 虚拟机,现在仍在使用中。
(2)直接翻译成中文是热点的意思。JVM首先识别程序中那一部分被调用的最频繁,这一部分也叫“热点方法”,然后跳过JVM解释器,直接把这一部分编译成机器码,相当于为这部分方法加速。
(3)在一般的java程序生命周期中,从java源码开始,源码经过javac命令处理后,得到java的字节码,新的类文件,也就是.class文件,新的类文件通过类的加载机制载入到虚拟机,从而把新的类提供给解释器执行。
(4)可以通过命令看到我们常用的jdk1.8实际也是HotSpot虚拟机。

192:~ buxuesong$ Java -version
java version "1.8.0_271"
Java(TM) SE Runtime Environment (build 1.8.0_271-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.271-b09, mixed mode)

19. Java 中堆和栈有什么区别?

(1)JVM 中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局部变量,而对象总是在堆上分配。栈通常都比堆小,也不会在多个线程之间共享,而堆被整个 JVM 所有线程共享。
(2)栈:在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配,当在一段代码块定义一个变量时,Java 就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java 会自动释放掉为该变量分配的内存空间,该内存空间可以立即被另作它用。
(3)堆:堆内存用来存放由 new 创建的对象和数组,在堆中分配的内存,由 Java 虚拟机的自动垃圾回收器来管理。在堆中产生了一个数组或者对象之后,还可以在栈中定义一个特殊的变量,让栈中的这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或者对象,引用变量就相当于是为数组或者对象起的一个名称。

20. 说一下类装载的执行过程?

(1)加载:根据查找路径找到相应的 class 文件然后导入;
(2)检查:检查加载的 class 文件的正确性;
(3)准备:给类中的静态变量分配内存空间;
(4)解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
(5)初始化:对静态变量和静态代码块执行初始化工作。

21. 有哪几种垃圾回收器,有哪些优缺点?

(1)垃圾回收器主要分为以下几种:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1
(2)Serial,单线程的收集器,收集垃圾时,必须stop the world,使用复制算法。
(3)ParNew,Serial收集器的多线程版本,也需要stop the world,复制算法。
(4)Parallel Scavenge,新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量,和ParNew的最大区别是GC自动调节策略;虚拟机会根据系统的运行状态收集性能监控信息,动态设置这些参数,以提供最优停顿时间和最高的吞吐量。
(5)Serial Old,Serial收集器的老年代版本,单线程收集器,使用标记整理算法。
(6)Parallel Old,是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。
(7)CMS,是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片;
(8)G1,标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选回收。不会产生空间碎片,可以精确地控制停顿。G1将整个堆分为大小相等的多个Region(区域),G1跟踪每个区域的垃圾大小,在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的区域,已达到在有限时间内获取尽可能高的回收效率。

22.详细介绍一下 CMS 垃圾回收器?

(1)CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。
(2)CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。

23. 说一下 JVM 运行时数据区?

(1)不同虚拟机的运行时数据区可能略微有所不同,但都会遵从 Java 虚拟机规范, Java 虚拟机规范规定的区域分为以下 5 个部分。
(2)程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成。
(3)Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
(4)本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的。
(5)Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存。
(6)方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

24. 队列和栈是什么?有什么区别?

(1)队列和栈都是被用来预存储数据的。
(2)队列允许先进先出检索元素,但也有例外的情况,Deque 接口允许从两端检索元素。
(3)栈和队列很相似,但它运行对元素进行后进先出进行检索。

25. 什么是双亲委派模型?

双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。

26. 说一下 JVM 有哪些垃圾回收算法?

(1)标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
(2)标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
(3)复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
(4)分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

27. 新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?

(1)新生代回收器:Serial、ParNew、Parallel Scavenge
(2)老年代回收器:Serial Old、Parallel Old、CMS
(3)整堆回收器:G1
(4)新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。

28. 简述分代垃圾回收器是怎么工作的?

(1)分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
(2)新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
(3)把 Eden + From Survivor 存活的对象放入 To Survivor 区;
(4)清空 Eden 和 From Survivor 分区;
(5)From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
(6)每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
(7)老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

29. 说一下 JVM 调优的工具?

(1)JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
(2)jconsole:用于对 JVM 中的内存、线程和类等进行监控;
(3)JVisualVM:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

30. 常用的 JVM 调优的参数都有哪些?

-Xms
s为strating,表示堆内存起始大小
-Xmx
x为max,表示最大的堆内存
(一般来说-Xms和-Xmx的设置为相同大小,因为当heap自动扩容时,会发生内存抖动,影响程序的稳定性)
-Xmn
n为new,表示新生代大小
(-Xss:规定了每个线程虚拟机栈(堆栈)的大小)
-XX:SurvivorRator=8
表示堆内存中新生代、老年代和永久代的比为8:1:1
-XX:PretenureSizeThreshold=3145728
表示当创建(new)的对象大于3M的时候直接进入老年代
-XX:MaxTenuringThreshold=15
表示当对象的存活的年龄(minor gc一次加1)大于多少时,进入老年代
-XX:-DisableExplicirGC
表示是否(+表示是,-表示否)打开GC日志

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