• JVM面试题总结


    JVM面试题

    常量池的类型有哪些?字面量进入字符串常量池的时机.

    常量池的类型有3种

    1. .class文件中的常量池
      主要存放两种常量:
      1. 字面量:文本字符串等
      2. 符号引用,包含三大类常量:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符.
    2. 运行时常量池
      方法区的一部分,.class文件中的常量池中存放的内容在类加载后会进入到运行时常量池中
    3. 全局字符串常量池
      本质是一个HashSet,是一种纯运行时的结构,且是一种惰式维护.它只存储Java.lang.String对象的引用,而不存储内容.

    字面量进入字符串常量池的时机:在类加载时字面量会进入运行时常量池中,但不会进入到全局字符串常量池中,也不会在堆中产生相应的对象.

    intern()方法的作用以及"=="的混淆概念解析

    intern方法的作用是当一个字符串的字面量引用存在于字符串常量池时,就返回字符串常量池中的该引用;当不存在时会会在字符串常量池创建一个引用指向该字符串并返回该引用.

    "==“比较的是字符串的地址而非字符串的值.常见的”=="混淆场景
    3. 字面量和字符串引用.

    ```java
    	    String s1 = "Java";
            String s2 = new String("java");
            System.out.println(s1 == s2); // false
    ```
    s1指向字面量"Java",而字面量引用是直接存储在字符串常量池中,所以s1指向字符串常量池中的引用;而s2首先先在堆中创建一个对象指向字符串的引用,然后s2指向堆上的对象引用.所以s1和s2指向的不是同一个地址.![在这里插入图片描述](https://img-blog.csdnimg.cn/bbf8dc56467b47bda29eb60c92546828.png)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. 连接得到的字符串和字面量

      		String s1 = "Java";
              String s2 = "Ja" + "va";
              String s3 = new String("Java");
              String s4 = new String("Ja") + "va";
              String s5 = new String("Ja") + new String("va");
              String s6 = new String("J") + new String("ava");
      		
              System.out.println(s1 == s2); //true
      
              System.out.println(s1 == s4);//false
              System.out.println(s3 == s4);//false
      
              System.out.println(s1 == s5);//false
              System.out.println(s3 == s5);//false
      
              System.out.println(s1 == s6);//false
              System.out.println(s3 == s6);//false
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17

      由字面量连接得到的字符串仍是一个字面量,所以也会直接指向字符串常量池中的引用;而一旦涉及到在堆上创建新的对象,则新的对象的引用一定是不同的.

    2. 字符串和字符串的intern

      	    String s1 = "Java";
              String s2 = s1.intern();
      
              String s3 = new String("Java");
              String s4 = s3.intern();
              System.out.println(s1 == s2); // true
              System.out.println(s3 == s4); // false
              System.out.println(s1 == s4); // true
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

      首先s1指向字符串常量池中的字面量引用.
      s2指向s1.intern,由于字符串常量池中存在s1字面量的引用,所以s2也直接指向字符串常量池中的引用.
      s3在指向的时候会先查看"Java"在字符串常量池中是否存在,如果没有存在则会创建一个字符串引用.然后再在堆上创建对象.
      s4指向s3.intern,由于s3的字面量和s1的字面量相同,都是Java,所以s3的字面量引用也存在于字符串常量池中,所以s4指向字符串常量池中的引用,所以s1,s2,s4都是指向字符串常量池中的字面量引用,而s3指向堆上新创建的对象

      	String s1 = new String("C") + new String("++");
      	
          System.out.println(s1 == s1.intern()); //true
      
      • 1
      • 2
      • 3

      字符串拼接并不会产生"C++"这样的字面量,所以在创建s1时不会在字符串常量池中产生相关字面量的应用.所以s1,intern后字符串常量池中的引用就是s1指向的堆上的引用.

      注意:在字符串拼接过程中,并不是所有字符串都是如此,诸如"Java"以及一些基本数据类型的字符串会在编译器就将字面量intern进字符串常量池中.

    		String s1 = new String("Ja") + new String("va");
    
           System.out.println(s1 == s1.intern()); //false
    
    • 1
    • 2
    • 3

    实例化对象的过程有什么?

    首先在Java中,实例化对象是非常常见的.诸如Demo demo = new Demo()就是一个对象实例化.在这个语句中,可以被分成四部分,首先new Demo是创建一个Demo对象,创建对象时需要在堆上为对象分配内存空间,同时需要加载Demo这个类.类加载的过程分为加载,验证,准备,解析,初始化;之后new Demo()中的()则是执行Demo类中的构造方法初始化Demo类中的字段并执行实例代码块.对于一个子类而言,构造方法的执行顺序:父类构造器优先于子类构造器执行;然后Demo demo则是在栈上创建一个名为demo的对象引用;最后的=则是将对象引用指向堆上的对象实例.(类加载的详细过程参考[类加载模型])

    GC回收机制

    GC回收机制是在程序中当对象调用结束后不再使用后会变成垃圾占用内存.为了释放内存给其他资源使用,所以Java中有GC回收机制.
    GC回收算法一共有4中算法,分别是标记-清除,复制算法,标记-整理,分代算法,

    1. 标记-清除算法分为两个过程:标记和清除.其中标记是为了找到内存中的垃圾对象.标记就是可达性分析的过程,在内存中规定一些变量作为GC Root,然后从GC Root开始以类似dfs的方式遍历内存并对能够遍历到的对象进行标记.标记完后,内存中给所有没有标记的对象就是垃圾对象.清除就是将内存中的垃圾对象占用的内存直接回收. 标记-清除算法有2个缺点:效率低,直接清除会形成大量的离散的内存碎片,这样在之后在为占用内存较大的对象分配内存时就无法正常分配.
    2. 复制算法:在为对象开辟内存时,会同时开辟两块内存A,B,对象放入其中一个内存A中,在触发GC时,会将内存A中仍存活的对象放入另一块内存B中,并将内存A回收掉.复制算法解决了标记-清除算法的内存碎片的问题,但缺点是空间利用率低
    3. 标记-整理算法:与标记-清除算法类似,也是分为两个过程:标记和整理.标记是一样的.整理相当于是对清除的一种优化.假如我们将内存比作成一个矩形,整理就是将内存中仍存活的对象移动到矩形的四个角中的一个.这样就可以保证回收的内存是连续的.缺点是整理时耗费的资源更多.
    4. 分代算法:首先将内存区域分成了两部分:新生代,老年代,分别占内存的1/3.2/3.在新生代中存放的是一些新创建的对象,在老年代中存放的是熬过多轮GC扫描后仍存活的对象.在新生代中,分为Eden区,Survivor From区,Survivor To区,占用比例为8:1:1.采用复制算法回收内存:新创建的对象放入Eden区,GC回收时会将Eden区存活的对象放到Survivor From区,然后回收Eden区的内存;之后再将新创建的对象放到Eden区,GC扫描时会将Eden区和Survivor From区存活的对象存储到Survivor To区,然后回收Eden区和Survivor From区内存.之后Survivor From区和Survivor To区交换角色,重复上述这个过程.在新生代中熬过15轮GC扫描后仍存活对象会移动到老年代中,老年代中采用标记-整理算法进行GC回收. 分代算法也是现在JVM中使用的GC回收算法.

    类加载模型

    类加载的整个过程分为加载,连接(验证,准备,解析),初始化,

    1. 加载
      加载阶段JVM主要进行三个方面的工作
      1. 找到类的,class文件
      2. 打开并读取,class文件
      3. 生成.class文件对应的类对象.
    2. 连接
      1. 验证
        确保.class文件中的各项数据符合规范要求,如果不符合则抛出异常,表示类加载失败.
      2. 准备
        为类中的静态字段分配内存并分配默认值(默认值不是初始值)
      3. 解析
        将常量池中的符号引用转换为直接引用,并为常量分配初始值.
        符号引用指的是常量池中的常量都有自己的一个编号,在默认情况下结构体中存储的是常量的编号
        直接引用:将编号替换为真正的对象中的内容
    3. 初始化
      JVM执行类构造器,对类对象初始化.

    双亲委派模型

    从Java虚拟机方面看,类加载器可以被分为两种:一种是启动类加载器(BoostrapClassLoader),是虚拟机自身的一部分;另一种就是其他的类加载器,独立存在于虚拟机外部.
    而从开发者角度,为了更细致的进行类加载,保留了三层类加载器,双亲委派的类加载架构器.
    双亲委派模型由3部分组成:启动类加载器(BootstrapClassLoader),标准扩展类加载器(ExtensionClassLoader)和应用类加载器(ApplicationClassLoader)以及自定义类加载器(UserClassLoader).其中启动类加载器负责加载JDK中lib目录下的核心类库,即加载标准库中的类;标准扩展类负责加载jdk目录中扩展的类;应用类加载器负责加载当前项目中的类,自定义类加载器负责加载指定路径下的类.加载的等级依次降低.而双亲委派模型则是按照加载等级从高到低加载,当父加载器加载完后仍无法加载到需要的类,才会在本加载器中查找加载.
    双亲委派模型类加载过程:

    1. UserClassLoader:先判断其父类是否加载过,如果加载过就加载UserClassLoader中管控的类
    2. ApplicationClassLoader先判断其父类是否加载过,如果加载过就加载ApplicationClassLoader中管控的类
    3. ExtensionClassLoader先判断其父类是否加载过,如果加载过就加载ExtensionClassLoader中管控的类
    4. BootSrapClassLoader由于没有父类,则直接在自己管控中的类中查找带查找的.class文件

    双亲委派模型的优点

    1. 避免重复加载类,即使自定义的类与标准库中的某个类重名也会优先加载标准库中的类不加载自定义的类.
    2. 安全:由于不会重复加载类,也就保证了Java核心API不被篡改.

    破坏双亲委派模型

    尽管双亲委派模型有很多优点,但在一些场景下也存在一定的问题,如Java中SPI(Server Provider Interface)机制下的JDBC实现
    JDBC中的DriveManger类中的实现类loadInitialDrivers()是由线程上下文加载器加载的(属于ApplicationClassLoader),并没有向上去委派其父类类加载器,造成这种现象的原因是其父类类加载器只能加载指定路径下的类,而该实现类是由开发者实现的,其父类类加载器加载不到.
    在这里插入图片描述
    双亲委派模型的细节参考我竟然被“双亲委派”给虐了

    谈一谈内存溢出和内存泄露,内存泄漏的场景

    内存溢出是指程序中使用的内存超过了系统可分配的最大内存而发生了out of memory.
    内存泄露是当某个对象使用完成后,本应该被回收的内存因为某些原因而没有被回收.这种现象就是内存泄漏.
    当应用系统中存在大量内存泄露或占用内存过多就会导致内存溢出.

    内存泄漏的场景:

    1. static字段引起的内存泄漏
      static字段拥有和程序相匹配的生命周期.当程序中大量使用static字段时,容易引发内存泄漏

      public class Test{
      	static List<Integer> list = new ArrayList<>();
      	public void func(){
      		for(int i = 0; i < 100; i++){
      			list.add(i);
      		}
      	}
      	public static void main(String[] args){
      		new Test().func();
      	}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

      解决方法是少使用static字段以及使用懒汉模式初始化静态字段

    2. 资源未关闭而导致内存泄漏,如数据库连接后没有调用close()函数,调用ReetrantLock后没有调用lock()函数关闭锁
      解决方法是程序中使用finally块关闭资源.

    3. hashcode数据结构产生的内存泄漏:

      1. HashMap中key属性是没有重写equals和hashcode的自定义类对象,因此会存入很多重复的对象.
      2. HashSet中存储的是重写了equals和hahscode的自定义类对象,在存储后对对象的属性值修改造成了对象的hash值改变而无法被HashSet清除.
        解决方法:自定义类时重写hashcode和equals方法并且要采用合适的方式重写.
    4. ThreadLocal使用结束后没有使用remove函数造成的内存泄漏.
      ThreadLocal中使用一个弱引用map,当ThreadLocal中的强引用被回收后,map里的value没有被回收掉但却不会再使用,因此造成了内存泄露.尤其是Threadlocal实例被置位null,更容易引发内存泄露.
      解决方法:使用完ThreadLocal后使用remove函数

    5. 监视器未释放
      web开发中经常会用到监视器(Listener),但在释放对象的时候却没有去删除这些监听器,增加了内存泄漏的机会。

    6. 内部类和外部模块的引用
      在非静态内部类和匿名内部类中,通常会隐含引用其包装类.当在程序中使用内部类时,即使包装类被回收,内部类也不会被回收.
      解决方法是尽可能多的使用静态内部类而非非静态内部类.

    JVM中哪些可以作为GC Root?

    1. 栈中引用的对象(类型为引用类型的局部变量)
    2. 类变量(静态变量)
    3. 常量
    4. 本地方法(Native)中引用的对象

    新生代和老生代的比例是怎样的?

    1:2

    JVM的性能分析工具有用过吗?

    JVM哪些区域可能会出现OOM?

    1. 堆内存
    2. 虚拟机栈/本地方法栈
    3. 方法区

    JMM内存模型

    JMM是JVM定义的一套内存模型,是为了解决不同操作系统的内存访问的差异性.

    主内存与工作内存

    JMM的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存中和从内存中读取变量.此处的变量包括实例字段,静态字段和构成数组对象的元素,不包括局部变量和形参.
    JMM规定了所有变量都存储到主内存中,每条线程拥有自己的工作内存,工作内存中保存了该线程使用的变量的主内存的拷贝,线程对变量的操作在工作内存中完成.不同的线程不能直接访问其他线程工作内存中的变量,而需要主内存去传递对应的变量值.

    内存间交互操作

    为了实现主内存和工作内存之间的交互,JMM定义了8种操作.且保证这8种操作都是原子的,不可分的.

    1. lock:作用于主内存的变量,表示该变量被一个线程独占.
    2. unlock:作用于主内存的变量,将一个lock态的变量释放出来.处于unlock状态的变量才可以被其他线程锁定.
    3. read:作用于主内存的变量,它将一个主内存中的变量传输到工作内存中.
    4. load:作用于工作内存的变量,把read操作之后的变量放入到工作内存的变量副本中.
    5. use:作用于工作内存的变量,把一个工作内存中变量的值传递给执行引擎.
    6. assign:作用域工作内存的变量,把执行引擎中的值传递给工作内存中对应的变量
    7. store:作用于工作内存的变量,将工作内存中变量的值传递给主内存.
    8. write:作用于主内存的变量,将store操作后的值放入到主内存的变量中.

    JMM的三大特性

    1. 原子性
    2. 可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改.
    3. 有序性:如果在本线程内观察,所有操作是有序的;如果在线程中观察另外一个线程,所有的操作是无序的.

    因此,并发地执行程序,只有同时保证原子性,可见性和有序性才能保证程序正常运行,三者缺一不可.

  • 相关阅读:
    【生日快乐】Node.js 实战 第1章 欢迎进入Node.js 的世界 1.1 一个典型的Node Web 应用程序
    go-fastdfs分布式文件存储集群搭建和测试
    PTA题目 用天平找小球
    C#计算不规则多边形关系
    Python中 utf-8和gbk以及unicode编码
    工业RFID读写器的读写性能受哪些因素影响?
    KEIL仿真 logic analyzer
    [C语言、C++]数据结构作业:用双向链表实现折返约瑟夫问题
    微信小程序 js中写一个px单位转rpx单位的函数
    语义分割概述
  • 原文地址:https://blog.csdn.net/weixin_52477733/article/details/126167224