几个挺有意思的Java疑问

记录下 Java 学习过程中产生的一些疑问。



日常对 class 的命令为什么几乎没出现过 $ ?

要知道 class 的命名是允许使用 $ 的,但是无论是网上各种博客的代码片段,框架源码和文档几乎没出现过 class 命名中带 $ 的情况。是单纯因为不好看吗?这个问题直到我看到内部类的编译的 .class 文件之后才被解开。

在一个类(Outer)内部定义的类(Inner),经过编译后产生的 .class 文件是带 $ 符号的(Outer$Inner.class),所以为了避免和经过编译的后产生的带 $ 符号的 .class 文件混淆,我们自己定义的类最好不带 $ 符号。


为什么在 Outer Class 的 method 里面实现的 Inner Class 里只能访问经过 final 修饰的 Outer Class 的 method 变量 ?

问题很绕,写段简单代码说明,为什么 innerMethod 访问的 i 必须是 final 修饰:

1
2
3
4
5
6
7
8
9
10
class Outer {
public void outerMethod() {
final int i = 10;
class Inner {
public void innerMethod() {
System.out.println(i);
}
}
}
}

因为 Inner Class 是 Class ,使用 new 操作符创建的对象存活在堆区。对象一旦被创建就会一直存活,直到被垃圾回收;而 Inner Class 所在的 outerMethod 是 method,里面的变量 i 是存活在栈区,随着 method 执行完会被马上回收;所以 Inner Class 的存活时间会被 i 要长,所以如果要在 Inner Class 安全使用 i 的话只能通过复制,而只有经过 final 修饰的变量(不可变)的复制使用才有意义。


不重写 toString ,直接打印对象得到的是包含对象的内存地址的字符串?

首先现代操作系统早已支持虚拟内存,所以至少不可能是物理地址。那么是虚拟地址吗?也不是,打印出来的只是对象的十六进制 hashCode,通过重写 hashCode 方法可以验证。

不同对象 hashCode 值可能相同(哈希碰撞),但是地址肯定不会相同。换句话说,通过打印对象观察后半部分的 hashCode 是否相同来判断是否为同一对象的做法并不严谨。


有了 == ,为什么还需要 equals ?

两者都是根类 Object 的方法,== 默认是比较变量栈中内容,所以导致了对于基本变量类型而言,== 是比较值(栈中直接是存储值);而对于 new 出来的引用对象类型而言,比较的是对象的堆引用地址(栈中是存储对象的堆引用地址),即比较两者是否为同一对象。

为什么还需要 equals ?有时候我们想根据对象中的某些字段(堆中的内容)决定两者是否相同,这时候就需要重写 equals 实现。继承自根类 Object 不重写 equals 默认就是使用 == 。


POJO & JavaBean

POJO: 简单 Java 类,没有继承任何类(非 Object 根类)和实现任何接口,没有注解,没有其他方法。如此简单的类,通常用来装载传递数据使用,不封装业务。

JavaBean: 规定所有的属性为 private,提供对应符合命名规范的 getter & setter ,有一个无参构造函数,同时类必须是可序列化的。可以有别的方法,允许封装部分业务。


为什么无法构建基本类型的泛型对象?

因为 Java 的泛型实现方法是擦拭法,即 JVM 并不知道泛型,泛型只是提供给编译器使用。我们所定义的 T 当被编译成 .class 文件后被 JVM 执行都会被转换成 Object,基本数据类型无法向上转型为 Object ,只有其包装类可以。

泛型的作用只是给编译器帮我们做安全的强制转型。


为什么有些异常不处理编译器会报错,有些不会?

首先 Java 异常分为两大类:Error & Exception,它们都继承自Throwable

Error 代表严重错误,通常只能避免,无法处理,如 OutOfMemoryErrorStackOverflowError

Exception 代表运行错误,应当被捕获并处理,如 FileNotFoundExceptionSocketException

同时 Exception 又分为两大类:

  • RuntimeException 类及其子类。
  • 继承自 Exception 的非 RuntimeException 类及其子类。

其中 Error 及其子类 & RuntimeException 及其子类可以不被捕获处理,其他(Exception 类和非 RuntimeException 类及其子类)必须要被捕获处理。


不写权限修饰符代表的是 public 吗?

不是,不写权限修饰符代表的是 default 权限,不同情况下四种权限修饰符的访问权限如下:

public protected default private
同一个类 Y Y Y Y
同一个包 Y Y Y N
不同包子类 Y Y N N
不同包非子类 Y N N N

什么情况下使用 .setAccessible(true) 会失败?

我们知道当需要读取/修改一个私有变量的时候,可以先通过反射 cls.getDeclaredField("xxx") 获取到对应的 Field ,调用 field.setAccessible(true) 之后,再调用对应的 get 方法和 set 方法。(私有构造方法,成员方法同理)

但是调用 .setAccessible(true) 是会失败的,例如我们想要通过反射修改 JVM 核心库的类(java 和 javax 开头的 package 里的类)的时候,会有相应的 SecurityManager 进行规则检查,从而拒绝 .setAccessible(true) 执行,以保证 JVM 的安全。


为什么定义注解是 public @interface Annotation 的写法?

刚学注解觉得这样的写法很奇怪,注解到底还是不是接口。

注解是接口,自定义注解的写法只是快速定义注解接口的一种语法糖。定义一个最简单的注解如下:

1
2
// MyAnnoatioin.java
public @interface MyAnnoatioin {}

然后使用 javac 命令对其进行编译,得到 MyAnnoatioin.class 文件,再使用 javap 命令对得到的 class 文件进行反编译,得到没有使用语法糖的注解定义:

1
2
3
4
5
6
7
8
9
➜ javac MyAnnoatioin.java
➜ ll
total 16
-rw-r--r-- 1 Gra staff 156B 3 3 20:27 MyAnnoatioin.class
-rw-r--r-- 1 Gra staff 57B 3 3 20:13 MyAnnoatioin.java
➜ javap MyAnnoatioin.class
Compiled from "MyAnnoatioin.java"
public interface com.peterxx.MyAnnoatioin extends java.lang.annotation.Annotation {
}

可见,注解也是接口,只是通过 @interface 简化了接口继承自 java.lang.annotation.Annotation 的操作。


Java 注解是如何实现的?

因为注解也是接口,所以注解的“属性”也就是接口的方法。

当我们使用 MyAnnoatioin annotation = cls.getAnnotation(MyAnnoatioin.class); 获取某个注解的时候其实是 JVM 先在内存里创建了一个接口(该注解)的实现类,并使用该注解设置的属性值作为实现方法的返回值,最后将该实现类的实例返回。所以我们能够直接通过 annotation 对象调用方法获取到设置值。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// MyAnnoatioin.java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnoatioin {
String name() default "Peter";

int age() default 0;
}

...

// Main.java
@MyAnnoatioin(name = "Peter Gra", age = 19)
public class Main {
public static void main(String[] args) {
if (Main.class.isAnnotationPresent(MyAnnoatioin.class)) {
/*
* 获取某个注解相当于返回该注解的实现类对象,该实现类使用注解设置值作为方法的返回值
* 获取到 annotation 对象等效于 MyAnnoatioinImpl 类的实例
*/
MyAnnoatioin annotation = Main.class.getAnnotation(MyAnnoatioin.class);
System.out.println(annotation.name());
System.out.println(annotation.age());
}
}
}

...

// 调用 getAnnotation 获取注解时,JVM 会生成如下的实现类
class MyAnnoatioinImpl implements MyAnnoatioin {
@Override
public String name() {
return "Peter Gra";
}

@Override
public int age() {
return 21;
}

@Override
public Class<? extends Annotation> annotationType() {
return MyAnnoatioin.class;
}
}


子类能否覆写父类的静态方法?

子类可以创建相同签名的静态方法,但是不能称为覆写父类静态方法。称为隐藏(Hide)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Main {
public static void main(String[] args) {
Sup sub = new Sub();
sub.normalMethod();
sub.staticMethod();
}
}

class Sup {
public static void staticMethod() {
System.out.println("Sup.staticMethod");
}
public void normalMethod() {
System.out.println("Sup.normalMethod");
}
}

class Sub extends Sup {
public static void staticMethod() {
System.out.println("Sub.staticMethod");
}
@Override
public void normalMethod() {
System.out.println("Sub.normalMethod");
}
}

这时候输出为:

1
2
>>> Sub.normalMethod
>>> Sup.staticMethod

产生这样的输出结果,是因为一个实例对象有两个类型:表面类型(Apparent Type)和实际类型(Actual Type)。

表面类型是声明时的类型,实际类型是对象产生时的类型。

对于非静态方法,它是根据对象的实际类型来执行的。而对于静态方法来说,如果是通过对象调用静态方法,JVM 则会通过对象的表面类型查找到对应的类,再使用对应的类名进行调用。

也就是说执行 sub.staticMethod(); 时会因为 sub 是使用 Sup 类进行声明,变成执行 Sup.staticMethod();

所以,在调用静态方法的时候最好使用类名调用,而不是使用对象调用。


变量的“声明”和“使用”顺序能颠倒吗?

实例变量可以,但是静态变量不行。为了避免不必要的混淆,最好统一遵循“变量先声明再使用”的原则。

因为静态变量是在类加载的时候分配空间,之后随着静态代码块或者静态变量声明赋值的先后顺序执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main 

// public static int i = 100;
static {
i = 1000;
}
// public static int i = 100;

public static void main(String[] args) {
// 静态变量的声明赋值如果出现在静态代码块赋值之前 打印1000
// 静态变量的声明赋值如果出现在静态代码块赋值之后 打印100
System.out.println(i);
}
}

而实例变量是在调用构造函数前被初始化。而且当子类实例化时,首先会先初始化父类。


使用 synchronized 修饰方法和使用 synchronized 来构建同步代码块有何区别?

使用 synchronized 修饰方法和使用 synchronized 来构建同步代码块底层原理是一样。

只是使用 synchronized 修饰方法默认会使用 this 作为锁对象。


synchronized 方法 & 同步代码块如何选择?

建议使用同步代码块。

出于并发性能的考虑,被 synchronized 包裹的代码越少越好。使用 synchronized 方法默认会使用 synchronized 包裹整个方法,而使用同步代码块则能自己控制同步作用范围。

而且 synchronized 方法只能使用 this 作为锁对象,方法调用者本身的作用域通常较大,不能保证不会被其他线程作为锁使用;而同步代码块能够自己决定锁对象,通常可以使用范围特定的专门对象作为锁,更加安全灵活。


为什么 Java 数组对象的 getClass() 方法不显示为 class Array ?

Java 里如果是基本类型的数组会显示 class [ + 基本类型首字母大写。如 int[] 对象的 getClass() 会显示 class [I ;double[] 显示 class [D ;char[] 显示 class [C 。

如果是引用类型,则会显示 class [ + 引用类型的全类名。Integer[] 显示为 class [Ljava.lang.Integer ;String[] 显示为 class [Ljava.lang.String ;

而非数据类型,则会显示 class + 数据类型类名。如 ArrayList 对象的 getClass() 会显示为class java.util.ArrayList。为什么 Java 数组对象的 getClass() 不直接显示 class Array 呢?

答案是因为Array是属于 java.lang.reflect 包,它是通过反射访问数组元素的工具类,而不是具体的一个数组类。真正 class [ 类在编译期间生成的。


基本类型数组转 List 的问题

先来看一段代码:

1
2
3
4
5
public static void main(String[] args) {
int[] ints = {1, 2, 3, 4, 5};
List list = Arrays.asList(ints);
System.out.println(list.size());
}

请思考以上代码输出是多少?答案是 1 ,而不是 5 。

明明是一个有 5 个成员的 int 类型数组,通过 asList 转成列表之后则变成了 1 个?

这是因为基本类型不能泛型化,所以 int 类型的数组转成 List 时候,int 数组的成员 int 不能作为 List 的成员,但是 int[] 可以被泛型化,所以最终整体的 int 数组作为成员转成了 List 对象。也就是说转成的 List 对象其实是 List<int[]> 类型的,所以打印结果为 1 。

1
2
3
4
5
public static void main(String[] args) {
int[] ints = {1, 2, 3, 4, 5};
List<int[]> list = Arrays.asList(ints);
System.out.println(list.get(0).equals(ints)); // 输出 true
}


asList 方法产生的 List 对象不可修改

除了上面说到的基本类型数组通过 asList 方法转 List 需要注意基本类型不能泛型化的问题以外,asList 方法还有一个需要注意的地方,先看以下代码:

1
2
3
4
5
public static void main(String[] args) {
Integer[] ints = {1, 2, 3, 4, 5};
List<Integer> list = Arrays.asList(ints);
list.add(6);
}

这段代码会抛 java.lang.UnsupportedOperationException 异常,不支持 add 操作。

通过查看 Arrays 的源码发现,通过 asList 方法返回的不是我们平时使用的 java.util.ArrayList 对象,而是一个 Arrays 内部自己实现的 ArrayList 类,该对象也是继承自 AbstractList ,但是自身没有覆写 AbstractList 的 add(int index, E element) 方法,所以当调用 asList 返回的 List 对象的 add 方法时,会由 AbstractList 抛出 java.lang.UnsupportedOperationException 异常。即 asList 方法产生的 List 对象不可修改

注意,这里说的不能修改除了不能添加元素还包括不能移除元素。因为 Arrays 内部的 ArrayList 类不仅仅没有实现 add 方法,也没有实现 remove 方法。

也由此引出,如果想在定义 List 的同时完成初始化工作,使用 Arrays.asList 不是一个好点习惯,因为返回的是一个不可操作的 ArrayList 对象,可以使用匿名类 + 构造代码块的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
// 错误
// List<Integer> readOnlyList = Arrays.asList(1,2,3);
// readOnlyList.add(4);
// System.out.println(readOnlyList);

// 正确
List<Integer> normalList = new ArrayList<Integer>() {
{
add(1);
add(2);
add(3);
}
};
normalList.add(4);
System.out.println(normalList);
}


实现排序,是使用 Comparable 接口 还是 Comparator 接口

Comparable 接口的类表明自身是可比较的,有了比较才能进行排序;

Comparator 接口是一个工具类接口,它的名字(比较器)也已经表明了它的作用:用作比较,它与原有类的逻辑没有关系,只是实现两个类的比较逻辑。

从这方面来说,一个类可以有很多的比较器,只要有业务需求就可以产生比较器,有比较器就可以产生 N 多种排序,而 Comparable 接口的排序只能说是实现类的默认排序算法,一个类稳定、成熟后其 compareTo 方法基本不会改变,也就是说一个类只能有一个固定的、由 compareTo 方法提供的默认排序算法。

Comparable 接口可以作为实现类的默认排序法,Comparator 接口则是一个类的扩展排序工具。


为什么在接口定义中,推荐使用枚举作为入参,但是不推荐枚举作为返回值

这是为了更好实现接口兼容所定的原则。

这在二方库和三方库里接口设计中尤为重要,遵循该原则设计的接口能够保证当“服务使用方的本地枚举”和“服务提供方的最新枚举”版本不一致时,服务还能够正常工作。

我们可以列举一个场景来说明问题,假如有以下接口和枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 服务方提供
enum OperationActionEnum {
LOGIN,
LOGOUT,
REFRESH;
}

enum OperationResultCodeEnum {
SUCCESS,
FAILURE;
}

interface ServiceApi {
OperationResultCodeEnum doSomething(OperationActionEnum action);
}

// 使用方使用
public static void main(String[] args) {
...
if (OperationResultCodeEnum.success == serviceApi.doSomething(action)) {
// 成功逻辑
} else {
// 失败逻辑
}
...
}

  • 为什么推荐使用枚举做为入参?

    后续添加枚举类型( 假如添加了一个 OperationActionEnum 枚举 FREEZE ),并不影响接口兼容。

    “服务使用方”调用服务的时候只传其本地有的枚举类型,新增的枚举本地根本没有,也就不会传递。换句话说,这时候新增什么内容不影响原有逻辑。

    这时候入参使用枚举甚至还起到了参数校检的作用,只有支持的枚举才能够被传递。服务的调用也是以“服务使用方”的本地枚举为准。


  • 为什么不推荐使用枚举作为返回值?

    后续添加枚举类型( 假如添加了一个 OperationResultCodeEnum 枚举 SUCCESS_AND_USING_CACHE ,代表成功并且使用到了缓存),这时候如果出现“服务使用方的本地枚举”和“服务提供方的最新枚举”版本不一致,可能会引发灾难。

    想象一下假如“服务使用方”原本只处理了 OperationResultCodeEnum 的 SUCCESS 枚举,其他情况都走失败逻辑,这时候“服务提供方”新增一个细化成功原因的枚举,直接导致了“服务使用方”原来成功的用户走到了失败的逻辑,这显然没有做到接口兼容。

    这时候的服务调用是以“服务提供方”的最新枚举为准。

    这是“服务使用方”处理原有枚举类型不严谨的错吗?不完全是。更多的责任落在“服务提供方”设计接口时的考虑不全。

在进行接口设计时,推荐使用枚举作为入参,但是不推荐枚举作为返回值

注意:这里指的枚举除了枚举本身还包括使用了枚举的 POJO 。


枚举还是常量类

能用枚举就用枚举,不能用枚举才考虑使用常量类或接口常量。

使用枚举有如下优点:

  • 更加纯粹。有时候我们定义一个常量,通常只强调其枚举项是什么,而不关心甚至不需要指定其值;但是用常量类或者接口常量通常需要和 int 类型进行绑定。
  • 使用 switch 的时候不需要进行越界检查。如果使用常量类或者接口常量的方式,因为通常会跟 int 或者 String 进行绑定,如果和 int 进行绑定之后,免不了先对枚举使用到的 int 范围进行检查;但是使用枚举类则不需要。
  • 枚举类提供了一系列的内置方法。例如使用枚举类可以很方便的列出所有枚举项;使用常量类或者接口常量则需要自己利用反射实现。

List<T>、List<?> 和 List<Object> 的区别

List<T>、List<?> 和 List<Object> 这三者都可以容纳所有的对象,但使用的顺序应该是 List<T>、List<?> 和 List<Object> 。

List<T> 表示的是 List 集合中的元素都为 T 类型,具体类型在运行期决定。使用 List<T> 可以进行读写操作;

List<?> 表示的是任意类型,与 List<T> 类似。但使用 List<?> 只能进行读操作;

List<Object> 表示 List 集合中的所有元素为 Object 类型,由于多态,所有对象都可以装载进 List<Object> 。List<Object> 可以进行读写,但是执行写入操作时需要向上转型(Up cast),在读取数据后需要向下转型(Downcast)。


反射时 Accessible 设为 true 的真实含义

网上不少资料说反射时调用是 setAccessible(true) 是为了能够访问私有类型的方法和字段。其实不完全正确。

因为 public 的方法也会存在 Accessible 为 false 的情况,例如:

1
2
3
4
5
6
7
8
9
10
class AccessibleTest {
public final void method() {
System.out.println("AccessibleTest.method");
}

public static void main(String[] args) throws NoSuchMethodException {
Method method = AccessibleTest.class.getMethod("method");
System.out.println(method.isAccessible()); // 打印为 flase
}
}

所以设置 Accessible 为 true 并不是单纯的因为访问权限。那么设置 Accessible 到底改变什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void setAccessible(boolean flag) throws SecurityException {
SecurityManager sm = System.getSecurityManager();
if (sm != null) sm.checkPermission(ACCESS_PERMISSION);
setAccessible0(this, flag);
}

private static void setAccessible0(AccessibleObject obj, boolean flag)
throws SecurityException
{
if (obj instanceof Constructor && flag == true) {
Constructor<?> c = (Constructor<?>)obj;
if (c.getDeclaringClass() == Class.class) {
throw new SecurityException("Cannot make a java.lang.Class" +
" constructor accessible");
}
}
// 设置 Accessible 为 true 本质是改变 override 属性
obj.override = flag;
}

我们知道 Accessible 为 false 的 Method 在不设置 setAccessible(true) 是无法被调用的(通过 invoke 方法),这意味着在 invoke 源码中必然存在对 Accessible 的检查逻辑。

深入源码可以发现,设置 Accessible 为 true 是为了实现快速调用,忽略安全检查,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
// 如果 override 为 false 要先进行各项检查
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}

// 调用方法
MethodAccessor ma = methodAccessor;
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}

设置 Accessible 为 true 并不是单纯的因为访问权限。出于对性能的考虑,使用反射时务必要设置 Accessible 为 true。


Java 的几种引用类型有何区别

  1. 普通引用:就是日常使用得最多的引用。这类引用的特点是:只有当对象的普通引用数为 0 ,对象才能被 GC 回收。

1
Object o = new Object();

  1. 弱引用:对于弱引用,最经典的场景是用作 ThreadLocal 内部类 ThreadLocalMap 中的 Entry 。使用弱引用的目的是保证 ThreadLocal 置空的时候,能够被顺利回收(因为 ThreadLocal 本身作为了 Map 的 key 使用)如果一个对象只有弱引用指向,触发 GC 时必然会被回收。

1
WeakReference<Object> stringWeakReference = new WeakReference<Object>(new Object());

  1. 软引用:如果一个对象只有软引用指向,触发 GC 时有可能会被回收。

1
SoftReference<Object> objectSoftReference = new SoftReference<Object>(new Object());

  1. 虚引用:基本上用不上,通常用作管理“堆外内存”。一旦虚引用对象被回收,将会把消息通知到某个队列。

1
2
ReferenceQueue<Object> queue = new ReferenceQueue<Object>();
PhantomReference<Object> objectPhantomReference = new PhantomReference<Object>(new Object(), queue);


volatile 的作用和原理

volatile 的作用是保证线程可见性和禁止指令重排序

  1. 什么叫保证线程可见性?

在 Java 线程模型中,每个线程有一个独立的工作内存空间(里面的数据从主存中复制而来),在线程执行时,使用的是工作内存中的变量值。假如两个线程同时读取了主存中的某个数据到工作内存,当其中一个线程在工作内存中对变量进行修改,另外一个线程是没法及时知道变量被修改了。保证线程可见性的意思是希望变量的修改能够及时被其他线程知道

  1. volatile 如何保证线程可见性?

通过 store 指令和 load 指令实现。如果一个变量被 volatile 修饰了,那么 JVM 会在读这个变量的指令之前插入一条 load 指令,表示重新从主存中读取该变量到工作内存;在写这个变量的指令之后插入一条 store 指令,表示将工作内存中的值更新到主存。

  1. 什么是禁止指令重排序?

CPU 为了提高执行效率,对于大多数指令其实都是允许乱序执行的。允许乱序执行的意思不是指 CPU 会去调整先执行哪条指令再执行哪条指令,指令仍然是按照顺序开始执行,只不过允许乱序是指一条执行的开始不一定要等待上一条指令的结束。也就是 CPU 总是顺序取指令,最终可能乱序执行完成(即只有编译器的乱序才是真正的乱序,CPU 的乱序严格意义上不算乱序)。

假如有 a = x;b = y; 前后两行指令,在 a = x; 执行完之前(可能需要等待 IO),会执行 b = y; 。导致的结果可能是虽然 b = y;a = x; 后面,但是比 a = x; 先执行完。禁止指令重排序的意思是必须在 a = x; 执行完了之后再开始执行 b = y;

  1. volatile 如何实现禁止指令重排序?

通过添加内存屏障来实现。在读取 volatile 变量的指令前添加 LoadLoadBarrier,在后添加 LoadStoreBarrier;在写 volatile 变量的指令前添加 StoreStoreBarrier,在后添加 StoreLoadBarrier。


Java 是编译执行还是解释执行

既有编译执行也有解释执行。在 Hotspot 实现中,默认是混合执行。混合执行的意思是刚开始代码是解释执行,而对于一些反复被执行的热点代码会采取编译执行的方式。

更为具体的解释为:代码刚开始是被解释执行,同时每个方法有一个调用计算器,当调用次数达到阈值,JVM 会认为这属于热点代码,会将该方法编译成 C++ 本地代码。随后执行将不再采用解释执行方式,而是直接执行编译好的 C++ 本地代码,由此提高执行效率。

JVM 也提供了指定执行模式和热点方法调用阈值的参数(JDK 1.8):

-Xmixed :混合执行模式。为默认值。

-Xint :纯解释模式。启动很快,执行很慢。

-Xcomp :纯编译模式。启动很慢(当类数目较多的时候),执行很快。

-XX:CompileThreshold :指定方法调用次数超过多少被定义为热点方法,默认为 10000。


Java 对象的创建过程

  1. 加载类

    1. class loading。将 class 文件加载进内存,并创建一个 Class 对象指向它。
    2. class linking ( verification, preparation, resolution )。在 preparation 阶段为静态变量分配内存,并赋默认值。
    3. class initializing。执行静态内容(包括静态变量赋起始值和执行静态代码块,按编写顺序执行)。
  2. 在堆内存里申请空间。包括为实例变量分配内存,并赋默认值;此时对象处于“半初始化”状态。

  3. 执行非静态内容(包括实例变量赋起始值和执行构造代码块,按编写顺序执行)。

  4. 执行构造方法。

  5. 将栈变量指向堆内存创建好的对象。

注意:不考虑 CPU 乱序执行的情况下的对象创建过程。实际情况中,第 5 步可能会先于第 3 步或者第 4 步完成,即栈变量指向一个“半初始化”状态的堆对象。


内存溢出和内存泄露的区别

内存泄露(Memory Leak):是指某一块内存分配给某个对象后,该对象已经不被使用,但是这块内存一直被占着,没法释放。

内存溢出(Out Of Memory ):通常指内存不足。


sleep() & awit() & yield()

sleep() 是 Thread 类的静态方法。目的是将当前线程的 CPU 时间让出,但不会让出锁,所以在能在任意地方执行。调用了 sleep() 方法之后,线程会从 RUNNABLE 状态变为 TIME_WAITING 状态,时间达到之后,自动切换回 RUNNABLE 状态。也就是等待时间结束之后,一旦得到 CPU 时间片将会继续执行。

awit() 是 Object 的实例方法。目的是让出锁,并将线程放入等待队列(从 Runnable 变为 Waiting 状态或者 TIMED_WAITING 状态),所以只能在同步方法或者同步代码块中执行(已经获取到了锁)。当调用 awit() 之后只有调用了 notify() 或者 notifyAll() 才能重新回到 Runnable 状态,才有可能被执行。

yield() 是 Thread 类的静态方法。和 sleep() 类似,会将当前 CPU 时间让出,但和 sleep() 不同的是,sleep() 让出的 CPU 时间会被其他线程抢夺,而 yield() 让出的 CPU 时间只会被优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程所获得。sleep() 方法会使线程从 RUNNABLE 状态变为 WAITING 状态,而 yield() 只会将线程从 RUNNING 状态变为 READY 状态(仍然是 RUNNABLE 状态)。


Java GC 的时候的 STW 是如何让用户线程停下来的

JVM 通过设定 Safe Point 作为用户线程是否暂停的检查点。Safe Point 是指 Java 代码中的一些特殊位置(如方法返回处、有界循环结束处、无界循环回跳处、异常抛出处…),当用户线程到达 Safe Point 的时候会检查 JVM 的标识位,如果标识位为真则暂停执行。

当所有的用户线程都到达了 Safe Point 之后,JVM 再将其统一挂起。所以 Java GC 的过程中有一个等到所有线程到达 Safe Point 的过程。


Java 是如何确定哪些对象是 GC Roots 的

查找 GC Roots 的方法有两种:

  • 保守式 GC:遍历方法区和栈区进行查找

  • 准确式 GC:使用数据结构来记录 GC Roots 的引用位置

HotSpot 使用的是准确式 GC 的方式,采用的数据结构为 OopMap。


什么是 OopMap

在源码中每个变量都是有类型的,但是编译之后的代码就只有变量在栈上的位置了。所以会出现无法确定某个位置里的内容是指针还是整型的“疑似指针”问题,OopMap 的出现是为了解决“疑似指针”问题。

在 HotSpot 中,对象的类型信息里有记录自己的 OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据。从而可以完全确定某个对象上记录的类型信息。

操作系统和内核的关系 内存区域划分

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×