Java大厂面试题—虚拟机(一),Java面试必问

讲一下JVM内存结构?

JVM内存结构分为5大区域,程序计数器虚拟机栈本地方法栈方法区

程序计数器

        线程私有的,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址。程序计数器主要有两个作用:

        当前线程所执行的字节码的行号指示器,通过它实现代码的流程控制,如:顺序执行、选择、循环、异常处理。

        在多线程的情况下,程序计数器用于记录当前线程执行的位置,当线程被切换回来的时候能够知道它上次执行的位置。

        程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

虚拟机栈

        Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表操作数栈动态链接方法出口信息。每一次函数调用都会有一个对应的栈帧被压入虚拟机栈,每一个函数调用结束后,都会有一个栈帧被弹出。

        局部变量表是用于存放方法参数和方法内的局部变量。

        每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,在方法调用过程中,会进行动态链接,将这个符号引用转化为直接引用。

        部分符号引用在类加载阶段的时候就转化为直接引用,这种转化就是静态链接

        部分符号引用在运行期间转化为直接引用,这种转化就是动态链接

        Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。

        可以通过 -Xss 参数来指定每个线程的虚拟机栈内存大小:

java -Xss2M

本地方法栈

        虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。Native方法一般是用其它语言(C、C++等)编写的。

        本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

        堆用于存放对象实例,是垃圾收集器管理的主要区域,因此也被称作GC堆。堆可以细分为:新生代(Eden空间、From Survivor、To Survivor空间)和老年代。

        通过-Xms设定程序启动时占用内存大小,通过-Xmx设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常。

java -Xms1M -Xmx2M

方法区

        方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

        对方法区进行垃圾回收的主要目标是对常量池的回收和对类的卸载

永久代

方法区是 JVM 的规范,而永久代PermGen是方法区的一种实现方式,并且只有HotSpot有永久代。对于其他类型的虚拟机,如JRockit没有永久代。由于方法区主要存储类的相关信息,所以对于动态生成类的场景比较容易出现永久代的内存溢出。

元空间

JDK 1.8 的时候,HotSpot的永久代被彻底移除了,使用元空间替代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。两者最大的区别在于:元空间并不在虚拟机中,而是使用直接内存。

为什么要将永久代替换为元空间呢?

        永久代内存受限于 JVM 可用内存,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是相比永久代内存溢出的概率更小。

运行时常量池

        运行时常量池是方法区的一部分,在类加载之后,会将编译器生成的各种字面量和符号引号放到运行时常量池。在运行期间动态生成的常量,如 String 类的 intern()方法,也会被放入运行时常量池。

直接内存

        直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。

        NIO的Buffer提供了DirectBuffer,可以直接访问系统物理内存,避免堆内内存到堆外内存的数据拷贝操作,提高效率。DirectBuffer直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制,不受最大堆内存的限制。

        直接内存的读写操作比堆内存快,可以提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到直接内存。

Java对象的定位方式

        Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针两种:

        如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。

        直接指针。reference 中存储的直接就是对象的地址。对象包含到对象类型数据的指针,通过这个指针可以访问对象类型数据。使用直接指针访问方式最大的好处就是访问对象速度快,它节省了一次指针定位的时间开销,虚拟机hotspot主要是使用直接指针来访问对象。

说一下堆栈的区别?

        堆的物理地址分配是不连续的,性能较慢;栈的物理地址分配是连续的,性能相对较快。

        堆存放的是对象的实例和数组;栈存放的是局部变量,操作数栈,返回结果等。

        堆是线程共享的;栈是线程私有的。

什么情况下会发生栈溢出?

        当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出StackOverFlowError异常。这种情况通常是因为方法递归没终止条件。

        新建线程的时候没有足够的内存去创建对应的虚拟机栈,虚拟机会抛出OutOfMemoryError异常。比如线程启动过多就会出现这种情况。

类文件结构

Class 文件结构如下:

ClassFile {

    u4            magic; //类文件的标志

    u2            minor_version;//小版本号

    u2            major_version;//大版本号

    u2            constant_pool_count;//常量池的数量

    cp_info        constant_pool[constant_pool_count-1];//常量池

    u2            access_flags;//类的访问标记

    u2            this_class;//当前类的索引

    u2            super_class;//父类

    u2            interfaces_count;//接口

    u2            interfaces[interfaces_count];//一个类可以实现多个接口

    u2            fields_count;//字段属性

    field_info    fields[fields_count];//一个类会可以有个字段

    u2            methods_count;//方法数量

    method_info    methods[methods_count];//一个类可以有个多个方法

    u2            attributes_count;//此类的属性表中的属性数

    attribute_info attributes[attributes_count];//属性表集合

}

主要参数如下:

        魔数:class文件标志。

        文件版本:高版本的 Java 虚拟机可以执行低版本编译器生成的类文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的类文件。

        常量池:存放字面量和符号引用。字面量类似于 Java 的常量,如字符串,声明为final的常量值等。符号引用包含三类:类和接口的全限定名,方法的名称和描述符,字段的名称和描述符。

        访问标志:识别类或者接口的访问信息,比如这个Class是类还是接口,是否为public或者abstract类型等等。

        当前类的索引:类索引用于确定这个类的全限定名。

什么是类加载?类加载的过程?

        类的加载指的是将类的class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个此类的对象,通过这个对象可以访问到方法区对应的类信息。

加载

        通过类的全限定名获取定义此类的二进制字节流

        将字节流所代表的静态存储结构转换为方法区的运行时数据结构

        在内存中生成一个代表该类的Class对象,作为方法区类信息的访问入口

验证

        确保Class文件的字节流中包含的信息符合虚拟机规范,保证在运行后不会危害虚拟机自身的安全。主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证

准备

        为类变量分配内存并设置类变量初始值的阶段。

解析

        虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用用于描述目标,直接引用直接指向目标的地址。

初始化

        开始执行类中定义的Java代码,初始化阶段是调用类构造器的过程。

什么是双亲委派模型?

        一个类加载器收到一个类的加载请求时,它首先不会自己尝试去加载它,而是把这个请求委派给父类加载器去完成,这样层层委派,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

        双亲委派模型的具体实现代码在java.lang.ClassLoader中,此类的loadClass()方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出ClassNotFoundException,此时尝试自己去加载。源码如下:

public abstract class ClassLoader {

    // The parent class loader for delegation

    private final ClassLoader parent;

    public Class loadClass(String name) throws ClassNotFoundException {

        return loadClass(name, false);

    }

    protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {

        synchronized (getClassLoadingLock(name)) {

            // First, check if the class has already been loaded

            Class c = findLoadedClass(name);

            if (c == null) {

                try {

                    if (parent != null) {

                        c = parent.loadClass(name, false);

                    } else {

                        c = findBootstrapClassOrNull(name);

                    }

                } catch (ClassNotFoundException e) {

                    // ClassNotFoundException thrown if class not found

                    // from the non-null parent class loader

                }

                if (c == null) {

                    // If still not found, then invoke findClass in order

                    // to find the class.

                    c = findClass(name);

                }

            }

            if (resolve) {

                resolveClass(c);

            }

            return c;

        }

    }

    protected Class findClass(String name) throws ClassNotFoundException {

        throw new ClassNotFoundException(name);

    }

}

为什么需要双亲委派模型?

        双亲委派模型的好处:可以防止内存中出现多份同样的字节码。如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证。

什么是类加载器,类加载器有哪些?

        实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器。

主要有一下四种类加载器:

启动类加载器:用来加载 Java 核心类库,无法被 Java 程序直接引用。

扩展类加载器:它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

系统类加载器:它根据应用的类路径来加载 Java 类。可通过ClassLoader.getSystemClassLoader()获取它。

自定义类加载器:通过继承java.lang.ClassLoader类的方式实现。

类的实例化顺序?

父类中的static代码块,当前类的static代码块

父类的普通代码块

父类的构造函数

当前类普通代码块

当前类的构造函数

如何判断一个对象是否存活?

        对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不再被任何途径引用的对象)。判断对象是否存活有两种方法:引用计数法和可达性分析。

引用计数法

        给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。

这种方法很难解决对象之间相互循环引用的问题。比如下面的代码,obj1和obj2互相引用,这种情况下,引用计数器的值都是1,不会被垃圾回收。

public class ReferenceCount {

    Object instance = null;

    public static void main(String[] args) {

        ReferenceCount obj1 = new ReferenceCount();

        ReferenceCount obj2 = new ReferenceCount();

        obj1.instance = obj2;

        obj2.instance = obj1;

        obj1 = null;

        obj2 = null;

    }

}

可达性分析

通过GC Root对象为起点,从这些节点向下搜索,搜索所走过的路径叫引用链,当一个对象到GC Root没有任何的引用链相连时,说明这个对象是不可用的。

可作为GC Roots的对象有哪些?

1.虚拟机栈中引用的对象

2.本地方法栈中Native方法引用的对象

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

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

什么情况下类会被卸载?

需要同时满足以下 3 个条件类才可能会被卸载 :

1.该类所有的实例都已经被回收。

2.加载该类的类加载器已经被回收。

3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述 3 个条件的类进行回收,但不一定会进行回收。

Java学习视频

Java基础:Java300集,Java必备优质视频_手把手图解学习Java,让学习成为一种享受

你可能感兴趣的:(Java大厂面试题—虚拟机(一),Java面试必问)