最后
2020年在匆匆忙忙慌慌乱乱中就这么度过了,我们迎来了新一年,互联网的发展如此之快,技术日新月异,更新迭代成为了这个时代的代名词,坚持下来的技术体系会越来越健壮,JVM作为如今是跳槽大厂必备的技能,如果你还没掌握,更别提之后更新的新技术了。
更多JVM面试整理:
4: iload_1 //将局部变量表1号位的数放入操作数栈中
5: bipush 10 //将数字10放入操作数栈
7: if_icmpge 21 //比较操作数栈中的两个数,如果下面的数大于上面的数,就跳转到21。这里的比较是将两个数做减法。因为涉及运算操作,所以会将两个数弹出操作数栈来进行运算。运算结束后操作数栈为空
10: iload_2 //将局部变量2号位的数放入操作数栈中,放入的值为为0
11: iinc 2, 1 //将局部变量2号位的数加1,自增后,槽位中的值为1
14: istore_2 //将操作数栈中的数放入到局部变量表的2号位,2号位的值又变为了0
15: iinc 1, 1 //1号位的值自增1
18: goto 4 //跳转到第4条指令
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_2
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: return
2-4 构造方法
(1)cinit()V
Java代码:
public class Main {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
}
编译器会按从上至下的顺序,收集所有 static
静态代码块和静态成员赋值的代码,合并为一个特殊的方法 cinit()V
:
0: bipush 10
2: putstatic #2 // Field i:I
5: bipush 20
7: putstatic #2 // Field i:I
10: bipush 30
12: putstatic #2 // Field i:I
15: return
cinit()V
方法会在类加载的初始化阶段被调用
(2)init()V
Java代码:
public class Main {
private String a = “s1”;
{
b = 20;
}
private int b = 10;
{
a = “s2”;
}
public Main(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Main d = new Main(“s3”, 30);
System.out.println(d.a);//s3
System.out.println(d.b);//30
}
}
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在后
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // super.()V
4: aload_0
5: ldc #2 // <- “s1”
7: putfield #3 // -> this.a
10: aload_0
11: bipush 20 // <- 20
13: putfield #4 // -> this.b
16: aload_0
17: bipush 10 // <- 10
19: putfield #4 // -> this.b
22: aload_0
23: ldc #5 // <- “s2”
25: putfield #3 // -> this.a
28: aload_0 // ------------------------------
29: aload_1 // <- slot 1(a) “s3” |
30: putfield #3 // -> this.a |
33: aload_0 |
34: iload_2 // <- slot 2(b) 30 |
35: putfield #4 // -> this.b --------------------
38: return
2-5 方法调用
看一下几种不同的方法调用对应的字节码指令
package main;
public class Main {
public Main() {
}
private void test1() {
}
private final void test2() {
}
public void test3() {
}
public static void test4() {
}
public static void main(String[] args) {
Main m = new Main();
m.test1();
m.test2();
m.test3();
Main.test4();
}
}
不同方法在调用时,对应的虚拟机指令有所区别:
-
私有、构造、被final修饰的方法,在调用时都使用invokespecial指令,属于静态绑定
-
普通成员方法在调用时,使用invokevirtual指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定,属于动态绑定,即支持多态
-
静态方法在调用时使用invokestatic指令
对应的字节码文件:
Code:
stack=2, locals=2, args_size=1
0: new #2 // class main/Main
3: dup
4: invokespecial #3 // Method “”😦)V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: invokestatic #7 // Method test4:()V
23: return
-
new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈
-
dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合
invokespecial
调用该对象的构造方法"<init>":()V
(会消耗掉栈顶一个引用),另一个要配合astore_1
赋值给局部变量 -
成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】
2-6 多态的原理
因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用invokevirtual指令
在执行invokevirtual指令时,经历了以下几个步骤
-
先通过栈帧中对象的引用找到对象
-
分析对象头,找到对象实际的Class
-
Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
-
查询vtable找到方法的具体地址
-
执行方法的字节码
2-7 异常处理
(1)try-catch
public class Main {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
}
}
}
对应的字节码文件(为了抓住重点,下面的字节码省略了不重要的部分):
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2
9: bipush 20
11: istore_1
12: return
Exception table:
from to target type
2 5 8 Class java/lang/Exception
-
可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测2~4行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
-
8行的字节码指令 astore_2 是将异常对象引用存入局部变量表的2号位置(为e)
(2)多个 single-catch 块的情况
public class Main {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (ArithmeticException e) {
i = 20;
} catch (Exception e) {
i = 30;
}
}
}
对应的字节码文件:
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 19
8: astore_2
9: bipush 20
11: istore_1
12: goto 19
15: astore_2
16: bipush 30
18: istore_1
19: return
Exception table:
from to target type
2 5 8 Class java/lang/ArithmeticException
2 5 15 Class java/lang/Exception
- 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用
(3)finally
public class Main {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}
对应的字节码文件:
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1
//try块
2: bipush 10
4: istore_1
5: bipush 30
//try块执行完后,会执行finally
7: istore_1
8: goto 27
//catch块
11: astore_2
12: bipush 20
14: istore_1
//catch块执行完,会执行finally
15: bipush 30
17: istore_1
18: goto 27
//出现异常,但未被Exception捕获,会抛出其他异常,这时也需要执行finally块中的代码
21: astore_3
22: bipush 30
24: istore_1
25: aload_3
26: athrow //抛出异常
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any //剩余的异常类型,比如 Error
11 15 21 any //剩余的异常类型,比如 Erro
可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
注意:
虽然从字节码指令看来,每个块中都有finally块,但是finally块中的代码只会被执行一次
(4)finally中的return
public class Main {
public static void main(String[] args) {
int result = test();
System.out.println(result);//20
}
public static int test() {
try {
return 10;
} finally {
return 20;
}
}
}
对应的字节码文件:
Code:
stack=1, locals=2, args_size=0
0: bipush 10
2: istore_0
3: bipush 20
5: ireturn // 返回栈顶 int(20)
6: astore_1
7: bipush 20
9: ireturn // 返回栈顶 int(20)
Exception table:
from to target type
0 3 6 any
-
由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准
-
跟前一个中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常
运行下面的代码,不会抛出异常:
public class Main {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
try {
int i = 1 / 0;
return 10;
} finally {
return 20;
}
}
}
(5)finally不带return
public class Main {
public static void main(String[] args) {
int i = Main.test();
System.out.println(i);//输出为10
}
public static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
}
对应的字节码文件:
Code:
stack=1, locals=3, args_size=0
0: bipush 10
2: istore_0 //赋值给i 10
3: iload_0 //加载到操作数栈顶
4: istore_1 //加载到局部变量表的1号位置
5: bipush 20
7: istore_0 //赋值给i 20
8: iload_1 //加载局部变量表1号位置的数10到操作数栈
9: ireturn //返回操作数栈顶元素 10
10: astore_2
11: bipush 20
13: istore_0
14: aload_2 //加载异常
15: athrow //抛出异常
Exception table:
from to target type
3 5 10 any
2-8 synchronized
public class Main {
public static void main(String[] args) {
int i = 10;
Object lock = new Object();
synchronized (lock) {
System.out.println(i);
}
}
}
对应的字节码文件:
Code:
stack=2, locals=5, args_size=1
0: bipush 10
2: istore_1
3: new #2 // class java/lang/Object
6: dup
7: invokespecial #1 // Method java/lang/Object.“”😦)V
10: astore_2
11: aload_2
12: dup
13: astore_3
14: monitorenter //加锁
15: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_1
19: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
22: aload_3
23: monitorexit //解锁
24: goto 34
//异常操作
27: astore 4
29: aload_3
30: monitorexit //解锁
31: aload 4
33: athrow
34: return
Exception table:
from to target type
15 24 27 any
27 31 27 any
所谓的 语法糖 ,其实就是指 java 编译器把 *.java
源码编译为 *.class
字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃嘛)
3-1 默认构造器
public class Candy1 {
}
编译成class后的代码:
public class Candy1 {
// 这个无参构造是编译器帮助我们加上的
public Candy1() {
super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object.“”😦)V
}
}
3-2 自动拆装箱
这个特性是 JDK 5
开始加入的, 如下代码 :
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
这段代码在 JDK 5
之前是无法编译通过的,必须改写下面这样 :
public class Candy2 {
public static void main(String[] args) {
//基本类型转包装类型→装箱
Integer x = Integer.valueOf(1);
//包装类型转基本类型→拆箱
int y = x.intValue();
}
}
3-3 泛型集合取值
泛型也是在 JDK 5
开始加入的特性,但 java 在编译泛型代码后会执行泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:
public class Candy3 {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(10); // 实际调用的是 List.add(Object e)
Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
}
}
所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作:
// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);
如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:
// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();
对应字节码:
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList.“”😦)V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
//这里进行了泛型擦除,实际调用的是add(Objcet o)
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
//这里也进行了泛型擦除,实际调用的是get(Object o)
22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
//这里进行了类型转换,将Object转换成了Integer
27: checkcast #7 // class java/lang/Integer
30: astore_2
31: return
3-4 可变参数
可变参数也是 JDK 5
开始加入的新特性: 例如:
public class Candy4 {
public static void foo(String… args) {
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo(“hello”, “world”);
}
}
可变参数 String... args
其实是一个 String[] args
,从代码中的赋值语句中就可以看出来。 同 样 java 编译器会在编译期间将上述代码变换为:
public class Candy4 {
public static void foo(String[] args) {
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo(new String[]{“hello”, “world”});
}
}
注意: 如果调用了 foo() 则等价代码为 foo(new String[]{}) ,创建了一个空的数组,而不会 传递 null 进去
3-5 foreach循环
仍是 JDK 5 开始引入的语法糖,数组的循环:
public class Candy5_1 {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦
for (int e : array) {
System.out.println(e);
}
}
}
会被编译器转换为:
public class Candy5_1 {
public Candy5_1() {
}
public static void main(String[] args) {
int[] array = new int[]{1, 2, 3, 4, 5};
for(int i = 0; i < array.length; ++i) {
int e = array[i];
System.out.println(e);
}
}
}
如果是集合呢?
public class Candy5_2 {
public static void main(String[] args) {
List list = Arrays.asList(1,2,3,4,5);
for (Integer i : list) {
System.out.println(i);
}
}
}
实际被编译器转换为对迭代器的调用:
public class Candy5_2 {
public Candy5_2() {
}
public static void main(String[] args) {
List list = Arrays.asList(1, 2, 3, 4, 5);
Iterator iter = list.iterator();
while(iter.hasNext()) {
Integer e = (Integer)iter.next();
System.out.println(e);
}
}
}
注意 :foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其 中 Iterable 用来获取集合的迭代器( Iterator )
3-6 switch 字符串
从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:
public class Candy6_1 {
public static void choose(String str) {
switch (str) {
case “hello”: {
System.out.println(“h”);
break;
}
case “world”: {
System.out.println(“w”);
break;
}
}
}
}
注意: switch 配合 String 和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清楚
会被编译器转换为:
public class Candy6_1 {
public Candy6_1() {
}
public static void choose(String str) {
byte x = -1;
switch(str.hashCode()) {
case 99162322: // hello 的 hashCode
if (str.equals(“hello”)) {
x = 0;
}
break;
case 113318802: // world 的 hashCode
if (str.equals(“world”)) {
x = 1;
}
}
switch(x) {
case 0:
System.out.println(“h”);
break;
case 1:
System.out.println(“w”);
}
}
}
以看到,执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较。
问:为什么第一遍时必须既比较 hashCode,又利用 equals 比较呢?hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突。
例如
BM
和C.
这两个字符串的hashCode值都是 2123 ,如果有如下代码:
public class Candy6_1 {
public static void choose(String str) {
switch (str) {
case “BM”: {
System.out.println(“h”);
break;
}
case “C.”: {
System.out.println(“w”);
break;
}
}
}
}
会被编译器转换为:
public class Candy6_1 {
public Candy6_1() {
}
public static void choose(String var0) {
byte var2 = -1;
switch(var0.hashCode()) {
case 2123:
if (var0.equals(“C.”)) {
var2 = 1;
} else if (var0.equals(“BM”)) {
var2 = 0;
}
default:
switch(var2) {
case 0:
System.out.println(“h”);
break;
case 1:
System.out.println(“w”);
}
}
}
}
3-7 switch 枚举
public enum Sex {
MALE,FEMALE
}
public class Candy7 {
public static void foo(Sex sex){
switch (sex){
case MALE:
System.out.println(“男”);
break;
case FEMALE:
System.out.println(“女”);
break;
}
}
}
会被编译器转换为:
public class Candy7 {
/**
-
定义一个合成类(仅 jvm 使用,对我们不可见)
-
用来映射枚举的 ordinal 与数组元素的关系
-
枚举的 ordinal 表示枚举对象的序号,从 0 开始
-
即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/
static class $MAP {
// 数组大小即为枚举元素个数,里面存储case用来对比的数字
static int[] map = new int[2];
static {
map[Sex.MALE.ordinal()] = 1;
map[Sex.FEMALE.ordinal()] = 2;
}
}
public static void foo(Sex sex) {
int x = $MAP.map[sex.ordinal()];
switch (x) {
case 1:
System.out.println(“男”);
break;
case 2:
System.out.println(“女”);
break;
}
}
}
3-8 枚举类
JDK 7
新增了枚举类,以前面的性别枚举为例:
public enum Sex {
MALE,FEMALE
}
会被编译器转换为:
public final class Sex extends Enum {
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $VALUES;
static {
MALE = new Sex(“MALE”, 0);
FEMALE = new Sex(“FEMALE”, 1);
$VALUES = new Sex[]{MALE, FEMALE};
}
private Sex(String name, int ordinal) {
super(name, ordinal);
}
public static Sex[] values() {
return $VALUES.clone();
}
public static Sex valueOf(String name) {
return Enum.valueOf(Sex.class, name);
}
}
4-1 加载
-
将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
-
_java_mirror 即 java 的类镜像,例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用
-
_super 即父类
-
_fields 即成员变量
-
_methods 即方法
-
_constants 即常量池
-
_class_loader 即类加载器
-
_vtable 虚方法表
-
_itable 接口方法
-
如果这个类还有父类没有加载,先加载父类
-
加载和链接可能是交替运行的
-
instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror 是存储在堆中
-
instanceKlass和_java_mirror(java镜像类)互相保存了对方的地址
-
类的对象在对象头中保存了 *.class 的地址。让对象可以通过其找到方法区中的instanceKlass,从而获取类的各种信息
4-2 链接
(1)验证
验证类是否符合 JVM规范,安全性检查
(2)准备
为 static
变量分配空间,设置默认值
-
static变量在JDK 7以前是存储与instanceKlass末尾。但在JDK 7以后就存储在_java_mirror末尾了
-
static变量在分配空间和赋值是在两个阶段完成的。分配空间在准备阶段完成,赋值在初始化阶段完成
-
如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
-
如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
(3)解析
将常量池中的符号引用解析为直接引用
4-3 初始化
(1)cinit方法
初始化即调用 <cinit>()
,虚拟机会保证这个类的【构造方法】的线程安全
(2)发生的时机
类的初始化的懒惰的,以下情况会初始化:
-
main 方法所在的类,总会被首先初始化
-
首次访问这个类的静态变量或静态方法时
-
子类初始化,如果父类还没初始化,会引发
-
子类访问父类的静态变量,只会触发父类的初始化
-
Class.forName
-
new 会导致初始化
以下情况不会初始化:
-
访问类的 static final 静态常量(基本类型和字符串)
-
类对象.class 不会触发初始化
-
创建该类对象的数组
-
类加载器的.loadClass方法
-
Class.forName
的参数2为false时
验证类是否被初始化,可以看改类的静态代码块是否被执行
这里一个例子来验证:(实验时请先全部注释,每次只执行其中一个)
public class Load3 {
static {
System.out.println(“main init”);
}
public static void main(String[] args) throws ClassNotFoundException {
// 1. 静态常量(基本类型和字符串)不会触发初始化
System.out.println(B.b);
// 2. 类对象.class 不会触发初始化
System.out.println(B.class);
// 3. 创建该类的数组不会触发初始化
System.out.println(new B[0]);
// 4. 不会初始化类 B,但会加载 B、A
ClassLoader cl = Thread.currentThread().getContextClassLoader();
cl.loadClass(“cn.itcast.jvm.t3.B”);
// 5. 不会初始化类 B,但会加载 B、A
ClassLoader c2 = Thread.currentThread().getContextClassLoader();
Class.forName(“cn.itcast.jvm.t3.B”, false, c2);
// 1. 首次访问这个类的静态变量或静态方法时
System.out.println(A.a);
// 2. 子类初始化,如果父类还没初始化,会引发
System.out.println(B.c);
// 3. 子类访问父类静态变量,只触发父类初始化
System.out.println(B.a);
// 4. 会初始化类 B,并先初始化类 A
Class.forName(“cn.itcast.jvm.t3.B”);
}
}
class A {
static int a = 0;
static {
System.out.println(“a init”);
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println(“b init”);
最后
由于文案过于长,在此就不一一介绍了,这份Java后端架构进阶笔记内容包括:Java集合,JVM、Java并发、微服务、SpringNetty与 RPC 、网络、日志 、Zookeeper 、Kafka 、RabbitMQ 、Hbase 、MongoDB、Cassandra 、Java基础、负载均衡、数据库、一致性算法、Java算法、数据结构、分布式缓存等等知识详解。
本知识体系适合于所有Java程序员学习,关于以上目录中的知识点都有详细的讲解及介绍,掌握该知识点的所有内容对你会有一个质的提升,其中也总结了很多面试过程中遇到的题目以及有对应的视频解析总结。
子类初始化,如果父类还没初始化,会引发
-
子类访问父类的静态变量,只会触发父类的初始化
-
Class.forName
-
new 会导致初始化
以下情况不会初始化:
-
访问类的 static final 静态常量(基本类型和字符串)
-
类对象.class 不会触发初始化
-
创建该类对象的数组
-
类加载器的.loadClass方法
-
Class.forName
的参数2为false时
验证类是否被初始化,可以看改类的静态代码块是否被执行
这里一个例子来验证:(实验时请先全部注释,每次只执行其中一个)
public class Load3 {
static {
System.out.println(“main init”);
}
public static void main(String[] args) throws ClassNotFoundException {
// 1. 静态常量(基本类型和字符串)不会触发初始化
System.out.println(B.b);
// 2. 类对象.class 不会触发初始化
System.out.println(B.class);
// 3. 创建该类的数组不会触发初始化
System.out.println(new B[0]);
// 4. 不会初始化类 B,但会加载 B、A
ClassLoader cl = Thread.currentThread().getContextClassLoader();
cl.loadClass(“cn.itcast.jvm.t3.B”);
// 5. 不会初始化类 B,但会加载 B、A
ClassLoader c2 = Thread.currentThread().getContextClassLoader();
Class.forName(“cn.itcast.jvm.t3.B”, false, c2);
// 1. 首次访问这个类的静态变量或静态方法时
System.out.println(A.a);
// 2. 子类初始化,如果父类还没初始化,会引发
System.out.println(B.c);
// 3. 子类访问父类静态变量,只触发父类初始化
System.out.println(B.a);
// 4. 会初始化类 B,并先初始化类 A
Class.forName(“cn.itcast.jvm.t3.B”);
}
}
class A {
static int a = 0;
static {
System.out.println(“a init”);
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println(“b init”);
最后
由于文案过于长,在此就不一一介绍了,这份Java后端架构进阶笔记内容包括:Java集合,JVM、Java并发、微服务、SpringNetty与 RPC 、网络、日志 、Zookeeper 、Kafka 、RabbitMQ 、Hbase 、MongoDB、Cassandra 、Java基础、负载均衡、数据库、一致性算法、Java算法、数据结构、分布式缓存等等知识详解。
[外链图片转存中…(img-LVLvp5zS-1715096782367)]
本知识体系适合于所有Java程序员学习,关于以上目录中的知识点都有详细的讲解及介绍,掌握该知识点的所有内容对你会有一个质的提升,其中也总结了很多面试过程中遇到的题目以及有对应的视频解析总结。
[外链图片转存中…(img-sejzMbx5-1715096782367)]
[外链图片转存中…(img-AtMuuAJV-1715096782368)]