常见底层面试题

目录

一、JVM

 二、java泛型

三、String类的深入理解

四、equals和==的区别

五、int和Integer的区别

六、null和""(空字符串)的区别

七、23种设计模式

八、多线程

九、GC

十、Java编译一个.java文件生成的.class文件有多少?


一、JVM

    大多数 JVM 将内存区域划分为 Method Area(Non-Heap)(方法区),Heap(堆),Program Counter Register(程序计数器) , VM Stack(虚拟机栈,也有翻译成JAVA 方法栈的),Native Method Stack ( 本地方法栈 ),其中Method Area 和 Heap 是线程共享的 ,VM Stack,Native Method Stack 和Program Counter Register 是非线程共享的。为什么分为 线程共享和非线程共享的呢?请继续往下看。

    首先我们熟悉一下一个一般性的 Java 程序的工作过程。一个 Java 源程序文件,会被编译为字节码文件(以 class 为扩展名),每个java程序都需要运行在自己的JVM上,然后告知 JVM 程序的运行入口,再被 JVM 通过字节码解释器加载运行。那么程序开始运行后,都是如何涉及到各内存区域的呢?

    概括地说来,JVM初始运行的时候都会分配好 Method Area(方法区) 和Heap(堆) ,而JVM 每遇到一个线程,就为其分配一个 Program Counter Register(程序计数器) , VM Stack(虚拟机栈)和Native Method Stack (本地方法栈), 当线程终止时,三者(虚拟机栈,本地方法栈和程序计数器)所占用的内存空间也会被释放掉。这也是为什么我把内存区域分为线程共享和非线程共享的原因,非线程共享的那三个区域的生命周期与所属线程相同,而线程共享的区域与JAVA程序运行的生命周期相同,所以这也是系统垃圾回收的场所只发生在线程共享的区域(实际上对大部分虚拟机来说知发生在Heap上)的原因。

    当我们创建一个对象(new Object)时,就会调用它的构造函数来开辟空间,将对象数据存储到堆内存中,与此同时在栈内存中生成对应的引用,当我们在后续代码中调用的时候用的都是栈内存中的引用,还需注意的一点,基本数据类型是存储在栈内存中。

heap和stack有什么区别?heap和stack有什么区别? 
    java的内存分为两类,一类是栈内存,一类是堆内存。

  • 栈内存是指程序进入一个方法时,会为这个方法单独分配一块私属存储空间,用于存储这个方法内部的局部变量,当这个方法结束时,分配给这个方法的栈会释放,这个栈中的变量也将随之释放。
  • 堆是与栈作用不同的内存,一般用于存放不放在当前方法栈中的那些数据,例如,使用new创建的对象都放在堆里,所以,它不会随方法的结束而消失。方法中的局部变量使用final修饰后,放在堆中,而不是栈中。

    更多参考:

    JVM介绍

    Java内存区域与内存溢出

 

 二、java泛型

  • 加入泛型之前:如果我们有如下需求:一个StringArr,你只想放string,但你并不能阻止其他类型数据放入。为了通用性,这样的数组一般都是Object。当我们获取里面的值的时候,就得强制转换,这就是它的缺点。
  • 加入泛型之后:例如:ArrayList<String> stringValues=new ArrayList<String>();这样,就指定了具体的类型,添加一些数据的时候,如果不符合初定的类型,就会报错,安全性提高!再有就是指定了具体的类型,提高了代码的质量,可读性提高

实现原理:

è¿éåå¾çæè¿°

如上,两者的类型分明不同,输出的结果却是true,这是因为,泛型它不会影响java虚拟机生成的汇编代码,在编译阶段,虚拟机就会把泛型的类型擦除,还原成没有泛型的代码,顶多编译速度稍微慢一些,执行速度是完全没有什么区别的。这就是为什么,Java的泛型被称为“伪泛型”的原因

注意:

泛型变量不允许是基本数据类型,只能是他们的包装类

è¿éåå¾çæè¿°

静态方法和静态变量不可以使用泛型类所声明的泛型类型参数

è¿éåå¾çæè¿°

三、String类的深入理解

先看源码:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
{
    /** The value is used for character storage. */
    private final char value[];

    /** The offset is the first index of the storage that is used. */
    private final int offset;

    /** The count is the number of characters in the String. */
    private final int count;

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > count) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    if (beginIndex > endIndex) {
        throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
    }
    return ((beginIndex == 0) && (endIndex == count)) ? this :
        new String(offset + beginIndex, endIndex - beginIndex, value);
}

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    char buf[] = new char[count + otherLen];
    getChars(0, count, buf, 0);
    str.getChars(0, otherLen, buf, count);
    return new String(0, count + otherLen, buf);
}

public String replace(char oldChar, char newChar) {
    if (oldChar != newChar) {
        int len = count;
        int i = -1;
        char[] val = value; /* avoid getfield opcode */
        int off = offset;   /* avoid getfield opcode */

        while (++i < len) {
        if (val[off + i] == oldChar) {
            break;
        }
        }
        if (i < len) {
        char buf[] = new char[len];
        for (int j = 0 ; j < i ; j++) {
            buf[j] = val[off+j];
        }
        while (i < len) {
            char c = val[off + i];
            buf[i] = (c == oldChar) ? newChar : c;
            i++;
        }
        return new String(0, len, buf);
        }
    }
    return this;
  }
}

可以看出:

  • String是final类,这意味着,这个类不能被继承,也不可有子类,其中的方法默认都是final方法
  • String类是通过char数组来保存字符串的
  • String类对字符串的操作都是对新字符串操作。

 

也就是说,String对象一定被创建就不会改变,任何改变操作都不会改变原字符串,而是生成了新的对象

字符串常量池:

  • 每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串不可变,所以常量池中一定不存在两个相同的字符串
  • 静态常量池和运行时常量池:
  1. 静态常量池,即.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
  2. 运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

è¿éåå¾çæè¿°

字符串的比较,我们使用equals方法,但用==号就可以看出,a和b指向的同一个对象。而new以后就产生新的对象。

如果使用equals比较三者,得出的结果,肯定都是true 。

è¿éåå¾çæè¿°

             

由于c是new出来的,所以产生了两个对象,一个是栈区中的c,另一个就是堆中的123,他们的引用关系是c->123->123(常量池中的)

也就是说,尽管c是创建在堆中,但其value还是常量池中的123

当我们对字符串,进行拼接,替换等操作时,会创建一个新的对象来操作,之后旧的对象,就会被当作垃圾回收。
 

四、equals和==的区别

他们最大的区别就是一个是方法,一个是关系操作符。

Object类中的equals方法源码:

public boolean equals(Object obj)
   {
       return this == obj;
   }

可见它直接比较的对象本身。

String类中的equals方法源码(重写Object中的):

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String) anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                        return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

上面的equals比较当中,都运用到了“==”,这就说明,equals(不管哪个类中的)是==的扩展。

引用《java编程思想》中的原话:

       关系操作符生成的是一个boolean结果,它们计算的是操作数的值之间的关系

总结:

  • 当==比较基本数据类型的时候,就是比较他们本身的值
  • 而==比较引用数据类型的时候, 就是比较他们在内存的地址
  • equals用来比较引用数据类型一般都要重写该方法。例如String类就重写了这个方法
  • 如果没有重写equals就直接比较,就是比较他们的内存地址
  • equals不能用于基本数据类型 

è¿éåå¾çæè¿°

上面的str中存储的并不是“a”,而是它所指向的对象的地址。所以将他赋值给str2,比较他俩时返回的就是true 

è¿éåå¾çæè¿°

上面的Student类并没有重写equals方法,结果位false

还有hashCode()方法 

è¿éåå¾çæè¿°

五、int和Integer的区别

基本区别:

(1)Integer是int的包装类;int是基本数据类型;
(2)Integer变量必须实例化后才能使用;int变量不需要;
(3)Integer实际是对象的引用,指向此new的Integer对象;int是直接存储数据值 ;
(4)Integer的默认值是null;int的默认值是0。
深入比较:

package com.nuc;

public class Demo {
    public static void main(String[] args) {
        Integer i1 = new Integer(1);
        Integer i2 = new Integer(1);
        Integer i3 = 1;
        Integer i4 = 127;
        Integer i5 = 127;
        Integer i6 = 128;
        Integer i7 = 128;
        int i8 = 1;
        //1、直接比较时,比较两者的地址,每次new生成的都是新的对象
        System.out.println(i1==i2);//false
        //2、integer和int比较时会自动拆箱转换为int类型,然后比较两者的值
        System.out.println(i1==i8);//ture
        //3、非new生成i3指向的是java常量池的对象,new是新生成的
        System.out.println(i1==i3);//false
        //4、127和128问题,JavaAPI中对Integer定义:在-128到127(含)之间的数会缓存,只存在一个对象中,即在此创建只是从缓存中取
            //超过这个每次创建就会new,即产生新的对象
        System.out.println(i4==i5);//true
        System.out.println(i6==i7);//false
    }
}

è¿éåå¾çæè¿°

以上是测试结果

六、null和""(空字符串)的区别

本质区别:

null是没有地址的
“”是有地址的,里边的内容是空的


具体区别:

1、做成员变量(字段/属性/类变量)时,如果只写String str;那么是默认赋值为null的。null的话,你屏幕输出(toString方法)的结果为字符串null,而且其它调用str的操作,编译可通过,运行时会空指针异常,此时是异常。
2、做局部变量(方法里的变量)时,如果只写String str;是不会默认赋值null的,这里仅声明了一个str变量,在栈内存中有定义,但没有任何值,null其实也是一种值。此时任何调用str的操作,编译时就均会报错,是error,不是异常。
3、“”是空字符串,但也是字符串,没有什么东西。 而null是空的意思,什么都没有,没有地址 

如果你直接输出str1,本质上其实是调用了toString方法。所以就会报错 
也就是说,你对没有地址的东西做做操作(除了=),就会报错也就是说,你对没有地址的东西做做操作(除了=),就会报错
String str = null;和String str1; 这两者也是不同的,前者是分配了内存,也就是说,你对他可以进行相关操作,而不会报错,后者是不可以的。
 

七、23种设计模式

Java之美[从菜鸟到高手演变]之设计模式 
Java之美[从菜鸟到高手演变]之设计模式二 
Java之美[从菜鸟到高手演变]之设计模式三 
Java之美[从菜鸟到高手演变]之设计模式四 

八、多线程

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
区别并发和并行:并发是指:多个事件在同一时间间隔内发生,并行是指:多个事件在同一时刻发生

更多概念参考操作系统基础知识总结整理的第四大点:处理机管理

更多多线程的知识请参考Java中的多线程你只要看这一篇就够了

九、GC

什么是GC? 
GC是垃圾收集的意思(Gabage Collection)
为什么要有GC? 
内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法
垃圾回收器的基本原理是什么? 
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。当GC确定一些对象为”不可达”时,GC就有责任回收这些内存空间。
有什么办法主动通知虚拟机进行垃圾回收?

程序员可以手动执行System.gc()或者Runtime.getRuntime().gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。 我们只是“提醒”一下
关于Java堆内存结构,分代回收算法,垃圾收集器,GC日志,JVM参数使用,请参考JavaGC介绍

十、Java编译一个.java文件生成的.class文件有多少?

一个.java文件中定义多个类,注意一下几点: 
(1) public权限类只能有一个(可以一个都没有,但最多只有一个);
(2)这个.java文件名只能是public 权限的类的类名;
(3)倘若这个文件中没有public 类,则它的.java文件的名字是随便的一个类名;
(4)当用javac命令生成编译这个.java 文件的时候,则会针对每一个类生成一个.class文件;
(5)内部类也产生.class文件
(6)接口也产生.class文件
 

测试类: 

package com.nuc;

public class Demo {
    class a{ }
    public static void main(String[] args) {
        System.out.println("f");
    }
}
class c{ }
interface a{ }

测试结果:

è¿éåå¾çæè¿°

可以看出,内部类的.class文件的命名格式为,外部类名$内部类名.class,外部类和内部类名使用符号$隔开

如果有两层内部类,如下图:

è¿éåå¾çæè¿°

那么继承父类,实现接口的情况呢?

图一: 

è¿éåå¾çæè¿°

图二: 

è¿éåå¾çæè¿°

图一是,接口和父类,Demo类在同一包下测试的结果,编译Demo产生的class文件 
图二是,不在同一包下的测试结果,Demo类编译时的class文件

那么匿名内部类的情况呢?

匿名内部类的创建格式:

new 父类构造器(参数列表)|实现接口()
    {  
     //匿名内部类的类体部分  
    }

结果:

è¿éåå¾çæè¿°

可见,匿名内部类,也生成class文件,由于匿名内部类没有名字,所以就按数字来命名,其规则是按照匿名内部类的顺序递增的。命名格式和内部类相似,名字换成数字即可


原文:https://blog.csdn.net/Song_JiangTao/article/details/82023526

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值