JAVA基础常见面试题

封装

继承

Java 继承(inheritance)是 Java 面向对象的三大重要特性之一(封装-encapsulation, 继承-inheritance, 多态-polymorphsim) Java 继承很好的管理了具有相似特征的类之间的关系(主要集中在成员变量、方法), 使程序可扩展、易修改,并且成为java多态的基础.
在java中,一个父类被多个子类继承,但一个子类只能继承一个父类。与接口不同的是,一个类可以实现(implement)多个接口。

普通类和抽象类有哪些区别?

抽象类不能被实例化
抽象类可以有抽象方法,抽象方法只需申明,无需实现
含有抽象方法的类必须申明为抽象类
抽象的子类必须实现抽象类中所有抽象方法,否则这个子类也是抽象类
抽象方法不能被声明为静态
抽象方法不能用private修饰
抽象方法不能用final修饰

static可以修饰什么?

可以修饰成员变量,修饰成员方法,修饰内部类,
不可以修饰局部变量,原因:静态成员属于类的,不属于方法的。

static关键字的特点?

  1. 静态成员被所在类的对象共享,
  2. 静态成员可以通过类名进行调用,
  3. 静态成员随着类的加载而加载。
  4. 静态成员优先于对象存在,

注意事项︰先进内存的不能调用后进内存的。

static方法访问特点?

  1. 静态方法只能调用静态成员(静态成员变量,静态成员方法)。
  2. 非静态方法可以调用任意成员(静态和非静态都可以)。

原因∶

  • 静态方法(static修饰的方法),属于类的方法,是随着类的加载而加载,所以静态方法可以直接通过类名来调用.静态方法也可以访问其他静态方法,
  • 非静态方法属于对象方法,因为类优先于对象存在,静态方法随着类加载而加载,所 以当静态方法加载到内存时,非静态方法还没有在内存中存在,先进内存的调用不了后进内存的,所以静态方法调用不了非静态方法,非静态方法需要通过创建对象,用对象进
    行调用

Java中是否可以重写private或者static修饰的方法?,

不可以

  1. private修饰的方法不能被重写, private修饰的成员只能在本类中访问。
  2. 静态的方法可以被继承,但是不能重写。如果父类中有一个静态的方法,子类也有一个与其方法名,参数列表相同静态方法,那么该子类的方法会把原来继承过来的父类的方法隐藏,而不是重写。通俗的讲就是父类的方法和子类的方法是两个没有关系

注意事项
静态方法中不能使用this关键字原因: this代表对象,类优先于对象存在所以,静态方法随着类的加载而加载,也就是说静态成员优先于对象存在,所以静态方法不能使用this关键字。

抽象方法是否可以通过static修饰?.

不可以

原因:
抽象方法,是让子类去重写,完善功能的,而static修饰的方法不能被重写
可以试想,如果可以调用static修饰抽象方法的话,那么通过类名调用一个静态抽象方法,抽象方法没有方法体,有何意义?.

非静态内部类与静态内部类的区别?.

成员变量区别·
静态内部类:可以定义静态和非静态的成员变量﹔
非静态内部类:只能定义韭静态的成员
成员方法区别·
静态内部类:可以定义静态方法和非静态方法
非静态内部类︰只能定义韭静态方法
非静态方法访问区别·
静态内部类―∶可以访问内部的静态和非静态成员,但只能访问外部类的静态成员
非静态内部类∶可以访问内部的非静态成员(静态成员无法定义),外部的静态与非静态,

静态代码块特点及作用?.

格式:

static{ 
	静态代码块中执行的代码
}

特点:
随着类的加载而加载,优先于构造方法执行,并且只执行一次·
作用:
用于给类初始化,加载驱动等。

statc修饰符使用时利弊?.

利:

  1. 静态方法被在类所有的对象共享,没必要每个对象都存储一份,目的节省空间,
  2. 可以通过类名调用,方便使用。

弊:
3. 静态方法随着类的加载而加载,随着类的消失而消失,生命周期较长,
4. 访问出现了局限性,只能访问静态

final和abstract是什么关系?

互斥关系

  • abstract定义的抽象类作为模板让子类继承, final定义的类不能被继承
  • 凑向方法定义通用功能让子类重写, final定义的方法子类不能重写

字符串有长度限制吗?是多少?

首先字符串的内容是由一个字符数组 char[] 来存储的,由于数组的长度及索引是整数,且String类中返回字符串长度的方法length() 的返回值也是int ,所以通过查看java源码中的类Integer我们可以看到Integer的最大范围是2^31 -1,由于数组是从0开始的,所以数组的最大长度可以使【0~2^31】通过计算是大概4GB。

但是通过翻阅java虚拟机手册对class文件格式的定义以及常量池中对String类型的结构体定义我们可以知道对于索引定义了u2,就是无符号占2个字节,2个字节可以表示的最大范围是2^16 -1 = 65535

其实是65535,但是由于JVM需要1个字节表示结束指令,所以这个范围就为65534了。超出这个范围在编译时期是会报错的,但是运行时拼接或者赋值的话范围是在整形的最大范围。

Java 按值调用还是引用调用?

按值调用指方法接收调用者提供的值,按引用调用指方法接收调用者提供的变量地址。
Java 总是按值调用,方法得到的是所有参数值的副本,传递对象时实际上方法接收的是对象引用的副本。方法不能修改基本数据类型的参数,如果传递了一个 int 值 ,改变值不会影响实参,因为改变的是值的一个副本。
可以改变对象参数的状态,但不能让对象参数引用一个新的对象。如果传递了一个 int 数组,改变数组的内容会影响实参,而改变这个参数的引用并不会让实参引用新的数组对象。

浅拷贝和深拷贝的区别?

浅拷贝
只复制当前对象的基本数据类型及引用变量,没有复制引用变量指向的实际对象。修改克隆对象可能影响原对象,不安全。
深拷贝
完全拷贝基本数据类型和引用数据类型,安全。

什么是反射?

在运行状态中,对于任意一个类都能知道它的所有属性和方法,对于任意一个对象都能调用它的任意方法和属性,这种动态获取信息及调用对象方法的功能称为反射。缺点是破坏了封装性以及泛型约束。反射是框架的核心,Spring 大量使用反射。

Class 类的作用?如何获取一个 Class 对象?

在程序运行期间,Java 运行时系统为所有对象维护一个运行时类型标识,这个信息会跟踪每个对象所属的类,虚拟机利用运行时类型信息选择要执行的正确方法,保存这些信息的类就是 Class,这是一个泛型类。
获取 Class 对象:

  1. 类名.class
  2. 对象的getClass方法。
  3. Class.forName(类的全限定名)。

什么是注解?什么是元注解?

注解是一种标记,使类或接口附加额外信息,帮助编译器和 JVM 完成一些特定功能,

例如

  • @Override 标识一个方法是重写方法。 元注解是自定义注解的注解
  • @Target:约束作用位置,值是 ElementType 枚举常量,包括 METHOD 方法、VARIABLE 变量、TYPE 类/接口、PARAMETER 方法参数、CONSTRUCTORS 构造方法和 LOACL_VARIABLE 局部变量等。
  • @Rentention:约束生命周期,值是 RetentionPolicy 枚举常量,包括 SOURCE 源码、CLASS 字节码和 RUNTIME 运行时。
  • @Documented:表明这个注解应该被 javadoc 记录。

什么是泛型,有什么作用?

泛型本质是参数化类型,解决不确定对象具体类型的问题。泛型在定义处只具备执行 Object 方法的能力。

泛型的好处:

  1. 类型安全,放置什么出来就是什么,不存在 ClassCastException。
  2. 提升可读性,编码阶段就显式知道泛型集合、泛型方法等处理的对象类型。
  3. 代码重用,合并了同类型的处理代码。

泛型擦除是什么?

泛型用于编译阶段,编译后的字节码文件不包含泛型类型信息,因为虚拟机没有泛型类型对象,所有对象都属于普通类。

例如

  • 定义List<Object> 或List<String>,在编译后都会变成 List 。 定义一个泛型类型,会自动提供一个对应原始类型,类型变量会被擦除。如果没有限定类型就会替换为 Object,如果有限定类型就会替换为第一个限定类型
  • <T extends A & B> 会使用 A 类型替换 T。

JDK8 新特性有哪些?

lambda 表达式: 允许把函数作为参数传递到方法,简化匿名内部类代码。
函数式接口: 使用 @FunctionalInterface 标识,有且仅有一个抽象方法,可被隐式转换为 lambda 表达式。
方法引用: 可以引用已有类或对象的方法和构造方法,进一步简化 lambda 表达式。
接口: 接口可以定义 default 修饰的默认方法,降低了接口升级的复杂性,还可以定义静态方法。
注解: 引入重复注解机制,相同注解在同地方可以声明多次。注解作用范围也进行了扩展,可作用于局部变量、泛型、方法异常等。
类型推测: 加强了类型推测机制,使代码更加简洁。
Optional 类: 处理空指针异常,提高代码可读性。
Stream 类: 引入函数式编程风格,提供了很多功能,使代码更加简洁。方法包括 forEach 遍历、count 统计个数、filter 按条件过滤、limit 取前 n 个元素、skip 跳过前 n 个元素、map 映射加工、concat 合并 stream 流等。
日期: 增强了日期和时间 API,新的 java.time 包主要包含了处理日期、时间、日期/时间、时区、时刻和时钟等操作。
JavaScript: 提供了一个新的 JavaScript 引擎,允许在 JVM上运行特定 JavaScript 应用。

Java 有哪些基本数据类型?

数据类型内存大小默认值取值范围

数据类型取值范围内存大小(字节)
byte (Byte)-128 ~ 1271
short (Short)-32768~327672
int(Integer)-31^2 ~ 31^2-1(-2147483648~2147483648)4
long (Long)-63^2 ~ 63^2-18
float (Float)1.4013E-45~3.4028E+384
double(Double)4.9E-324~1.7977E+3088
char(Character)0~2(的16次方)-1 ‘\u0000’2
boolean (Boolean)false,true1

JVM 没有 boolean 赋值的专用字节码指令,boolean f = false 就是使用 ICONST_0 即常数 0 赋值。单个 boolean 变量用 int 代替,boolean 数组会编码成 byte 数组。

自动装箱/拆箱是什么?

每个基本数据类型都对应一个包装类,除了 int 和 char 对应 Integer 和 Character 外,其余基本数据类型的包装类都是首字母大写即可。
自动装箱: 将基本数据类型包装为一个包装类对象,例如向一个泛型为 Integer 的集合添加 int 元素。
自动拆箱: 将一个包装类对象转换为一个基本数据类型,例如将一个包装类对象赋值给一个基本数据类型的变量。
比较两个包装类数值要用 equals ,而不能用 == 。

String 是不可变类为什么值可以修改?

String 类和其存储数据的成员变量 value 字节数组都是 final 修饰的。对一个 String 对象的任何修改实际上都是创建一个新 String 对象,再引用该对象。只是修改 String 变量引用的对象,没有修改原 String 对象的内容。

字符串拼接的方式有哪些?

  • 直接用 + ,底层用 StringBuilder 实现。只适用小数量,如果在循环中使用 + 拼接,相当于不断创建新的 StringBuilder 对象再转换成 String 对象,效率极差。 -
  • 使用 String 的 concat 方法,该方法中使用 Arrays.copyOf 创建一个新的字符数组 buf 并将当前字符串 value 数组的值拷贝到 buf 中,buf 长度 = 当前字符串长度 + 拼接字符串长度。之后调用 getChars 方法使用 System.arraycopy 将拼接字符串的值也拷贝到 buf 数组,最后用 buf 作为构造参数 new 一个新的 String 对象返回。效率稍高于直接使用 +。 -
  • 使用 StringBuilder 或 StringBuffer,两者的 append 方法都继承自 AbstractStringBuilder,该方法首先使用 Arrays.copyOf 确定新的字符数组容量,再调用 getChars 方法使用 System.arraycopy 将新的值追加到数组中。StringBuilder 是 JDK5 引入的,效率高但线程不安全。StringBuffer 使用 synchronized 保证线程安全。

String a = “a” + new String(“b”) 创建了几个对象?

常量和常量拼接仍是常量,结果在常量池,只要有变量参与拼接结果就是变量,存在堆。
使用字面量时只创建一个常量池中的常量,使用 new 时如果常量池中没有该值就会在常量池中新创建,再在堆中创建一个对象引用常量池中常量。因此 String a = “a” + new String(“b”) 会创建四个对象,常量池中的 a 和 b,堆中的 b 和堆中的 ab。

谈一谈你对面向对象的理解

面向过程让计算机有步骤地顺序做一件事,是过程化思维,使用面向过程语言开发大型项目,软件复用和维护存在很大问题,模块之间耦合严重。面向对象相对面向过程更适合解决规模较大的问题,可以拆解问题复杂度,对现实事物进行抽象并映射为开发对象,更接近人的思维。
例如开门这个动作,面向过程是 open(Door door),动宾结构,door 作为操作对象的参数传入方法,方法内定义开门的具体步骤。面向对象的方式首先会定义一个类 Door,抽象出门的属性(如尺寸、颜色)和行为(如 open 和 close),主谓结构。
面向过程代码松散,强调流程化解决问题。面向对象代码强调高内聚、低耦合,先抽象模型定义共性行为,再解决实际问题。

面向对象的三大特性?

封装是对象功能内聚的表现形式,在抽象基础上决定信息是否公开及公开等级,核心问题是以什么方式暴漏哪些信息。主要任务是对属性、数据、敏感行为实现隐藏,对属性的访问和修改必须通过公共接口实现。封装使对象关系变得简单,降低了代码耦合度,方便维护。
迪米特原则就是对封装的要求,即 A 模块使用 B 模块的某接口行为,对 B 模块中除此行为外的其他信息知道得应尽可能少。不直接对 public 属性进行读取和修改而使用 getter/setter 方法是因为假设想在修改属性时进行权限控制、日志记录等操作,在直接访问属性的情况下无法实现。如果将 public 的属性和行为修改为 private 一般依赖模块都会报错,因此不知道使用哪种权限时应优先使用 private。
继承用来扩展一个类,子类可继承父类的部分属性和行为使模块具有复用性。继承是"is-a"关系,可使用里氏替换原则判断是否满足"is-a"关系,即任何父类出现的地方子类都可以出现。如果父类引用直接使用子类引用来代替且可以正确编译并执行,输出结果符合子类场景预期,那么说明两个类符合里氏替换原则。
多态以封装和继承为基础,根据运行时对象实际类型使同一行为具有不同表现形式。多态指在编译层面无法确定最终调用的方法体,在运行期由 JVM 动态绑定,调用合适的重写方法。由于重载属于静态绑定,本质上重载结果是完全不同的方法,因此多态一般专指重写。

重载和重写的区别?

  • 重载
    方法名称相同,但参数类型个数不同,是行为水平方向不同实现。对编译器来说,方法名称和参数列表组成了一个唯一键,称为方法签名,JVM 通过方法签名决定调用哪种重载方法。不管继承关系如何复杂,重载在编译时可以根据规则知道调用哪种目标方法,因此属于静态绑定。
  • 重写
    子类实现接口或继承父类时,保持方法签名完全相同,实现不同方法体,是行为垂直方向不同实现。
    元空间有一个方法表保存方法信息,如果子类重写了父类的方法,则方法表中的方法引用会指向子类实现。父类引用执行子类方法时无法调用子类存在而父类不存在的方法。
    重写方法访问权限不能变小,返回类型和抛出的异常类型不能变大,必须加 @Override 。

JVM 在重载方法中选择合适方法的顺序

  1. 精确匹配。
  2. 基本数据类型自动转换成更大表示范围。
  3. 自动拆箱与装箱。
  4. 子类向上转型。
  5. 可变参数。

类之间有哪些关系?

类关系描述权力强侧

举例

  • 继承父子类之间的关系:is-a父类小狗继承于动物实现
  • 接口和实现类之间的关系:can-do接口小狗实现了狗叫接口组合
  • 比聚合更强的关系:contains-a整体头是身体的一部分
  • 聚合暂时组装的关系:has-a组装方小狗和绳子是暂时的聚合关系
  • 依赖一个类用到另一个:depends-a被依赖方人养小狗,人依赖于小狗
  • 关联平等的使用关系:links-a平等人使用卡消费,卡可以提取人的信息

Object 类有哪些方法?

equals:
检测对象是否相等,默认使用 == 比较对象引用,可以重写 equals 方法自定义比较规则。equals 方法规范:自反性、对称性、传递性、一致性、对于任何非空引用 x,x.equals(null) 返回 false。
hashCode:
散列码是由对象导出的一个整型值,没有规律,每个对象都有默认散列码,值由对象存储地址得出。字符串散列码由内容导出,值可能相同。为了在集合中正确使用,一般需要同时重写 equals 和 hashCode,要求 equals 相同 hashCode 必须相同,hashCode 相同 equals 未必相同,因此 hashCode 是对象相等的必要不充分条件。
toString:
打印对象时默认的方法,如果没有重写打印的是表示对象值的一个字符串。
clone:
clone 方法声明为 protected,类只能通过该方法克隆它自己的对象,如果希望其他类也能调用该方法必须定义该方法为 public。如果一个对象的类没有实现 Cloneable 接口,该对象调用 clone 方法会抛出一个 CloneNotSupport 异常。默认的 clone 方法是浅拷贝,一般重写 clone 方法需要实现 Cloneable 接口并指定访问修饰符为 public。
finalize:
确定一个对象死亡至少要经过两次标记,如果对象在可达性分析后发现没有与 GC Roots 连接的引用链会被第一次标记,随后进行一次筛选,条件是对象是否有必要执行 finalize 方法。假如对象没有重写该方法或方法已被虚拟机调用,都视为没有必要执行。如果有必要执行,对象会被放置在 F-Queue 队列,由一条低调度优先级的 Finalizer 线程去执行。虚拟机会触发该方法但不保证会结束,这是为了防止某个对象的 finalize 方法执行缓慢或发生死循环。只要对象在 finalize 方法中重新与引用链上的对象建立关联就会在第二次标记时被移出回收集合。由于运行代价高昂且无法保证调用顺序,在 JDK 9 被标记为过时方法,并不适合释放资源。
getClass:
返回包含对象信息的类对象。
wait / notify / notifyAll:
阻塞或唤醒持有该对象锁的线程。

内部类的作用是什么,有哪些分类?

内部类可对同一包中其他类隐藏,内部类方法可以访问定义这个内部类的作用域中的数据,包括 private 数据。
内部类是一个编译器现象,与虚拟机无关。编译器会把内部类转换成常规的类文件,用 $ 分隔外部类名与内部类名,其中匿名内部类使用数字编号,虚拟机对此一无所知。

静态内部类: 属于外部类,只加载一次。作用域仅在包内,可通过 外部类名.内部类名 直接访问,类内只能访问外部类所有静态属性和方法。HashMap 的 Node 节点,ReentrantLock 中的 Sync 类,ArrayList 的 SubList 都是静态内部类。内部类中还可以定义内部类,如 ThreadLoacl 静态内部类 ThreadLoaclMap 中定义了内部类 Entry。
成员内部类: 属于外部类的每个对象,随对象一起加载。不可以定义静态成员和方法,可访问外部类的所有内容。
局部内部类: 定义在方法内,不能声明访问修饰符,只能定义实例成员变量和实例方法,作用范围仅在声明类的代码块中。
匿名内部类: 只用一次的没有名字的类,可以简化代码,创建的对象类型相当于 new 的类的子类类型。用于实现事件监听和其他回调。

访问权限控制符有哪些?

当前类同一包子类其它任意地方
public(公共的)
protected(受保护的)
friendly(默认)
private(私有)

访问权限控制符本类包内包外子类任何地方public√√√√protected√√√×无√√××private√×××

接口和抽象类的异同?

接口和抽象类对实体类进行更高层次的抽象,仅定义公共行为和特征。

语法维度抽象类接口成员变量无特殊要求默认 public static final 常量构造方法有构造方法,不能实例化没有构造方法,不能实例化方法抽象类可以没有抽象方法,但有抽象方法一定是抽象类。默认 public abstract,JDK8 支持默认/静态方法,JDK9 支持私有方法。继承单继承多继承

接口和抽象类应该怎么选择?

抽象类体现 is-a 关系,接口体现 can-do 关系。与接口相比,抽象类通常是对同类事物相对具体的抽象。

  • 抽象类:
    模板式设计,包含一组具体特征,例如某汽车,底盘、控制电路等是抽象出来的共同特征,但内饰、显示屏、座椅材质可以根据不同级别配置存在不同实现。
  • 接口:
    契约式设计,是开放的,定义了方法名、参数、返回值、抛出的异常类型,谁都可以实现它,但必须遵守接口的约定。例如所有车辆都必须实现刹车这种强制规范。

接口是顶级类,抽象类在接口下面的第二层,对接口进行了组合,然后实现部分接口。当纠结定义接口和抽象类时,推荐定义为接口,遵循接口隔离原则,按维度划分成多个接口,再利用抽象类去实现这些,方便后续的扩展和重构。
例如 Plane 和 Bird 都有 fly 方法,应把 fly 定义为接口,而不是抽象类的抽象方法再继承,因为除了 fly 行为外 Plane 和 Bird 间很难再找到其他共同特征。

子类初始化的顺序

  1. 父类静态代码块和静态变量。
  2. 子类静态代码块和静态变量。
  3. 父类普通代码块和普通变量。
  4. 父类构造方法。
  5. 子类普通代码块和普通变量。
  6. 子类构造方法。

异常有哪些分类?

所有异常都是 Throwable 的子类,分为 ErrorException
Error 是 Java 运行时系统的内部错误和资源耗尽错误,例如 StackOverFlowError 和 OutOfMemoryError,这种异常程序无法处理。
Exception 分为受检异常和非受检异常,受检异常需要在代码中显式处理,否则会编译出错,非受检异常是运行时异常,继承自 RuntimeException。

受检异常:

  1. 无能为力型,如字段超长导致的 SQLException。
  2. 力所能及型,如未授权异常 UnAuthorizedException,程序可跳转权限申请页面。常见受检异常还有 FileNotFoundException、ClassNotFoundException、IOException等。

非受检异常:

  1. 可预测异常,例如 IndexOutOfBoundsException、NullPointerException、ClassCastException 等,这类异常应该提前处理。
  2. 需捕捉异常,例如进行 RPC 调用时的远程服务超时,这类异常客户端必须显式处理。
  3. 可透出异常,指框架或系统产生的且会自行处理的异常
    例如: Spring 的 NoSuchRequestHandingMethodException,Spring 会自动完成异常处理,将异常自动映射到合适的状态码。

创建对象的过程是什么?

字节码角度

  • NEW
    如果找不到 Class 对象则进行类加载。加载成功后在堆中分配内存,从 Object 到本类路径上的所有属性都要分配。分配完毕后进行零值设置。最后将指向实例对象的引用变量压入虚拟机栈顶。
  • DUP:
    在栈顶复制引用变量,这时栈顶有两个指向堆内实例的引用变量。两个引用变量的目的不同,栈底的引用用于赋值或保存局部变量表,栈顶的引用作为句柄调用相关方法。
  • INVOKESPECIAL:
    通过栈顶的引用变量调用 init 方法。

执行角度

  1. 当 JVM 遇到字节码 new 指令时,首先将检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载。
  2. 在类加载检查通过后虚拟机将为新生对象分配内存。
  3. 内存分配完成后虚拟机将成员变量设为零值,保证对象的实例字段可以不赋初值就使用。
  4. 设置对象头,包括哈希码、GC 信息、锁信息、对象所属类的类元信息等。
  5. 执行 init 方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

对象分配内存的方式有哪些?

对象所需内存大小在类加载完成后便可完全确定,分配空间的任务实际上等于把一块确定大小的内存块从 Java 堆中划分出来。

  • 指针碰撞:
    假设 Java 堆内存规整,被使用过的内存放在一边,空闲的放在另一边,中间放着一个指针作为分界指示器,分配内存就是把指针向空闲方向挪动一段与对象大小相等的距离。
  • 空闲列表:
    如果 Java 堆内存不规整,虚拟机必须维护一个列表记录哪些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。

选择哪种分配方式由堆是否规整决定,堆是否规整由垃圾收集器是否有空间压缩能力决定。使用 Serial、ParNew 等收集器时,系统采用指针碰撞;使用 CMS 这种基于清除算法的垃圾收集器时,采用空间列表。

对象分配内存是否线程安全?

对象创建十分频繁,即使修改一个指针的位置在并发下也不是线程安全的,可能正给对象 A 分配内存,指针还没来得及修改,对象 B 又使用了指针来分配内存。

解决方法:

  1. CAS 加失败重试保证更新原子性。
  2. 把内存分配按线程划分在不同空间,即每个线程在 Java 堆中预先分配一小块内存,叫做本地线程分配缓冲 TLAB,哪个线程要分配内存就在对应的 TLAB 分配,TLAB 用完了再进行同步。

对象的内存布局了解吗?

对象在堆内存的存储布局可分为对象头、实例数据和对齐填充。
对象头占 12B,包括对象标记和类型指针。对象标记存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁标志、偏向线程 ID 等,这部分占 8B,称为 Mark Word。Mark Word 被设计为动态数据结构,以便在极小的空间存储更多数据,根据对象状态复用存储空间。
类型指针是对象指向它的类型元数据的指针,占 4B。JVM 通过该指针来确定对象是哪个类的实例。
实例数据是对象真正存储的有效信息,即本类对象的实例成员变量和所有可见的父类成员变量。存储顺序会受到虚拟机分配策略参数和字段在源码中定义顺序的影响。相同宽度的字段总是被分配到一起存放,在满足该前提条件的情况下父类中定义的变量会出现在子类之前。
对齐填充不是必然存在的,仅起占位符作用。虚拟机的自动内存管理系统要求任何对象的大小必须是 8B 的倍数,对象头已被设为 8B 的 1 或 2 倍,如果对象实例数据部分没有对齐,需要对齐填充补全。

对象的访问方式有哪些?

Java 程序会通过栈上的 reference 引用操作堆对象,访问方式由虚拟机决定,主流访问方式主要有句柄和直接指针。

句柄: 堆会划分出一块内存作为句柄池,reference 中存储对象的句柄地址,句柄包含对象实例数据与类型数据的地址信息。优点是 reference 中存储的是稳定句柄地址,在 GC 过程中对象被移动时只会改变句柄的实例数据指针,而 reference 本身不需要修改。
直接指针: 堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference 存储对象地址,如果只是访问对象本身就不需要多一次间接访问的开销。优点是速度更快,节省了一次指针定位的时间开销,HotSpot 主要使用直接指针进行对象访问。

Java 程序是怎样运行的?

首先通过 Javac 编译器将 .java 转为 JVM 可加载的 .class 字节码文件。

Javac 是由 Java 编写的程序,编译过程可以分为:

  1. 词法解析,通过空格分割出单词、操作符、控制符等信息,形成 token 信息流,传递给语法解析器。
  2. 语法解析,把 token 信息流按照 Java 语法规则组装成语法树。
  3. 语义分析,检查关键字使用是否合理、类型是否匹配、作用域是否正确等。
  4. 字节码生成,将前面各个步骤的信息转换为字节码。 字节码必须通过类加载过程加载到 JVM 后才可以执行,执行有三种模式,解释执行、JIT 编译执行、JIT 编译与解释器混合执行(主流 JVM 默认执行的方式)。

混合模式的优势在于解释器在启动时先解释执行,省去编译时间。 之后通过即时编译器 JIT 把字节码文件编译成本地机器码。
Java 程序最初都是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会认定其为"热点代码",热点代码的检测主要有基于采样和基于计数器两种方式,为了提高热点代码的执行效率,虚拟机会把它们编译成本地机器码,尽可能对代码优化,在运行时完成这个任务的后端编译器被称为即时编译器。 还可以通过静态的提前编译器 AOT 直接把程序编译成与目标机器指令集相关的二进制代码。

类加载是什么?

Class 文件中描述的各类信息都需要加载到虚拟机后才能使用。JVM 把描述类的数据从 Class 文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程称为虚拟机的类加载机制。
与编译时需要连接的语言不同,Java 中类型的加载、连接和初始化都是在运行期间完成的,这增加了性能开销,但却提供了极高的扩展性,Java 动态扩展的语言特性就是依赖运行期动态加载和连接实现的。
一个类型从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期经历加载验证准备解析初始化使用卸载七个阶段,其中验证、解析和初始化三个部分称为连接。加载、验证、准备、初始化阶段的顺序是确定的,解析则不一定:可能在初始化之后再开始,这是为了支持 Java 的动态绑定。

类初始化的情况有哪些?

  1. 遇到 new、getstatic、putstatic 或 invokestatic 字节码指令时,还未初始化。典型场景包括 new 实例化对象、读取或设置静态字段、调用静态方法。
  2. 对类反射调用时,还未初始化。
  3. 初始化类时,父类还未初始化。
  4. 虚拟机启动时,会先初始化包含 main 方法的主类。
  5. 使用 JDK7 的动态语言支持时,如果 MethodHandle 实例的解析结果为指定类型的方法句柄且句柄对应的类还未初始化。
  6. 接口定义了默认方法,如果接口的实现类初始化,接口要在其之前初始化。

其余所有引用类型的方式都不会触发初始化,称为被动引用。

被动引用实例:

  1. 子类使用父类的静态字段时,只有父类被初始化。
  2. 通过数组定义使用类。
  3. 常量在编译期会存入调用类的常量池,不会初始化定义常量的类。

接口和类加载过程的区别: 初始化类时如果父类没有初始化需要初始化父类,但接口初始化时不要求父接口初始化,只有在真正使用父接口时(如引用接口中定义的常量)才会初始化。

类加载的过程是什么?

  1. 加载

该阶段虚拟机需要完成三件事:① 通过一个类的全限定类名获取定义类的二进制字节流。② 将字节流所代表的静态存储结构转化为方法区的运行时数据区。③ 在内存中生成对应该类的 Class 实例,作为方法区这个类的数据访问入口。

  1. 验证

确保 Class 文件的字节流符合约束。如果虚拟机不检查输入的字节流,可能因为载入有错误或恶意企图的字节流而导致系统受攻击。验证主要包含四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。
验证重要但非必需,因为只有通过与否的区别,通过后对程序运行期没有任何影响。如果代码已被反复使用和验证过,在生产环境就可以考虑关闭大部分验证缩短类加载时间。

  1. 准备

为类静态变量分配内存并设置零值,该阶段进行的内存分配仅包括类变量,不包括实例变量。如果变量被 final 修饰,编译时 Javac 会为变量生成 ConstantValue 属性,准备阶段虚拟机会将变量值设为代码值。

  1. 解析

将常量池内的符号引用替换为直接引用。
符号引用以一组符号描述引用目标,可以是任何形式的字面量,只要使用时能无歧义地定位目标即可。与虚拟机内存布局无关,引用目标不一定已经加载到虚拟机内存。
直接引用是可以直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。和虚拟机的内存布局相关,引用目标必须已在虚拟机的内存中存在。

  1. 初始化

直到该阶段 JVM 才开始执行类中编写的代码。准备阶段时变量赋过零值,初始化阶段会根据程序员的编码去初始化类变量和其他资源。初始化阶段就是执行类构造方法中的 方法,该方法是 Javac 自动生成的。

有哪些类加载器?

自 JDK1.2 起 Java 一直保持三层类加载器:
启动类加载器 在 JVM 启动时创建,负责加载最核心的类
例如 Object、System 等。无法被程序直接引用,如果需要把加载委派给启动类加载器,直接使用 null 代替即可,因为启动类加载器通常由操作系统实现,并不存在于 JVM 体系。

  1. 平台类加载器 从 JDK9 开始从扩展类加载器更换为平台类加载器,负载加载一些扩展的系统类,比如 XML、加密、压缩相关的功能类等。
  2. 应用类加载器 也称系统类加载器,负责加载用户类路径上的类库,可以直接在代码中使用。如果没有自定义类加载器,一般情况下应用类加载器就是默认的类加载器。
  3. 自定义类加载器通过继承 ClassLoader 并重写 findClass 方法实现。

双亲委派模型是什么?

类加载器具有等级制度非继承关系,以组合的方式复用父加载器的功能。
双亲委派模型要求除了顶层的启动类加载器外,其余类加载器都应该有自己的父加载器。
一个类加载器收到了类加载请求,它不会自己去尝试加载,而将该请求委派给父加载器,每层的类加载器都是如此,因此所有加载请求最终都应该传送到启动类加载器,只有当父加载器反馈无法完成请求时,子加载器才会尝试。
类跟随它的加载器一起具备了有优先级的层次关系,确保某个类在各个类加载器环境中都是同一个,保证程序的稳定性。

如何判断两个类是否相等?

任意一个类都必须由类加载器和这个类本身共同确立其在虚拟机中的唯一性。
两个类只有由同一类加载器加载才有比较意义,否则即使两个类来源于同一个 Class 文件,被同一个 JVM 加载,只要类加载器不同,这两个类就必定不相等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值