八股Javaguide

JavaGuide上的一些

值传递 复制了值而已,对主函数中的值 没有改变
地址传递 复制了地址,所以对地址进行操作( *y 就看做指针就好) 改变了主函数中的值
引用传递 传递了地址, 效果和地址传递一样(在 c++ 中才有引用传递,c 语言中没有引用传递)

注意上述三行中的区别,引用传递和地址传递的区别在于 一个复制了地址,一个传递了地址,

反射:让我们在运行时分析类,以及执行类中方法的能力。

用户空间的程序不能直接访问内核空间。

用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间

浮点数没有办法用二进制精确表示,因此存在精度丢失的风险。

不过,Java 提供了BigDecimal 来操作浮点数。BigDecimal 的实现利用到了 BigInteger (用来操作大整数), 所不同的是 BigDecimal加入了小数位的概念。

  1. Java 集合常见知识点&面试题总结(上)
  2. Java 集合常见知识点&面试题总结(下)
  3. Java 容器使用注意事项总结

使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法, 它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。所以需要外面再包裹一层new ArrayList(Arrays.asList());

  1. Java 内存区域

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 方法区
  • 直接内存 (非运行时数据区的一部分)

对象的创建

Java 对象的创建过程我建议最好是能默写出来,并且要掌握每一步在做什么。

Step1:类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

Step2:分配内存

类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式“指针碰撞”“空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定

内存分配的两种方式 (补充内容,需要掌握):

  • 指针碰撞 :
    • 适用场合 :堆内存规整(即没有内存碎片)的情况下。
    • 原理 :用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
    • 使用该分配方式的 GC 收集器:Serial, ParNew
  • 空闲列表 :
    • 适用场合 : 堆内存不规整的情况下。
    • 原理 :虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
    • 使用该分配方式的 GC 收集器:CMS

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是”标记-清除”,还是”标记-整理”(也称作”标记-压缩”),值得注意的是,复制算法内存也是规整的。

内存分配并发问题(补充内容,需要掌握)

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

  • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配

Step3:初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

Step4:设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

Step5:执行 init 方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

对象的内存布局

在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头实例数据对齐填充

Hotspot 虚拟机的对象头包括两部分信息第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

  1. JVM 垃圾回收
IMG_2072.jpg
  1. JDK 监控和故障处理工具

postman用来测试服务。

jmeter用来做性能测试。

jconsole 堆内存的监控、CPU占用率等等。

jvisualvm一样,可以下载插件,查看GC。

进程

查看进程的问题

jps:查看当前系统的java进程,得到pid。 Jps, ups -l

jmap:查看对内存占用情况 jmap -heap 进程id

jconsole:堆内存的监控、CPU占用率等等。

jvisualvm:一样,可以下载插件,查看GC。

jstack 进程id,找到有问题的线程,可以定位到源码行(),也可以看到是否有锁

查看线程

top命令查看哪个进程对CPU的占用过高 (jps:查看当前系统的java进程,得到pid。 Jps, ups -l)

ps H -eo pid,tid,&cpu | grep 进程id

jstack 进程id,找到有问题的线程,可以定位到源码行(),也可以看到是否有死锁

查看对象运行时的内存结构等(HSDB工具)

jps得到进程id

首先进入jdk安装目录,进去后输入进程id

1
java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB

查找某个对象

1
select d from cn.itcast.jvm.t3.bytecode.Dog d

根据上面的到的地址查找到对象,然后对象头又16个字节,前8个字节是MarkWorld,后八个字节就是对象的Class指针。

IMG_2073.jpg

Java类加载机制的几个个阶段,加载、验证、准备、解析、初始化【jvm】

IMG_2074.jpg

  • instanceKlass保存在方法区。JDK 8以后,方法区位于元空间中,而元空间又位于本地内存中
  • java_mirror则是保存在堆内存中
  • InstanceKlass和*.class(JAVA镜像类)互相保存了对方的地址*
  • _类的对象在对象头中保存了.class的地址(也可以说是_java_mirror的地址)。让对象可以通过其,找到方法区(元空间)中的instanceKlass,从而获取类的各种信息。

加载

链接

  • 验证
  • 准备
  • 解析

初始化

还不明白Java从编译到执行的过程?看这一篇

编译 -> 加载 -> 解释 -> 执行

javac,编译,将源码文件(.java)编译成JVM可以解释的class文件

字节码存储在硬盘上,需要运行时,由类加载系统负责将累的信息加载到内存中(方法区),使用类加载器进行加载

解释就是 直接解释(JVM对字节码逐条解释的) + 即时编译器解释(对某段热点代码进行整体遍后执行,编译需要一定的时间,但是编译后运行时间很快)

执行

a++是在槽位上+的,所以与++a的区别是闲iload还是闲iinc的区别

而静态变量的自增需要放到操作数栈中。

原子性、可见性(太多次,直接从主存缓存到工作内存中去了)、有序性(JIT的优化)。

new singleinstance()的时候的时候,init和putstatic是顺序不固定的,所以单例模式中也要用volitile保证有序性。

红黑树

红黑树的应用 :TreeMap、TreeSet以及JDK1.8的HashMap底层都用到了红黑树。

为什么要用红黑树? 简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。详细了解可以查看 漫画:什么是红黑树?(也介绍到了二叉查找树,非常推荐)

红黑树的基本性质与操作

Java基础

对于异常重写方法不能抛出新的异常或者比被重写方法声明的检查异常更广的检查异常。但是可以抛出更少,更有限或者不抛出异常

WulzDmXEq1HxKCk

所以应该选abc,不能选d。

Java 之路 (七) – 复用类(组合、继承、代理、向上转型、final、再谈初始化和类的加载、方法覆盖)

上下转型问题

Java 之路 (八) – 多态(向上转型、多态、绑定、构造器与多态、协变返回类型、向下转型)

静态方法只能被继承,不能被重写,但是子类可以写一个一样的静态的,但是不是重写。

非静态方法也不能被静态方法重写。

子类与父类中同名同参数的方法必须同时声明为非静态的 (即为重写) ,或者同时声明为静态的(不是重写)。

如果子类声明了与父类同名同参数的静态方法,那么父类的静态方法将会被隐藏,对于子类不可见,但子类没有重写父类的静态方法。多态调用时会调用父类的静态方法。

注意,java 的重载,是不能只有 返回值 不同的,是会报错的。

所以说,如果有父类跟子类同样名字同样返回值,不同参数也是可以的(编译器不报错),但是这个时候如果是向上转型的调用的话,父类的声明的变量只能用到父类自己的,用不了子类的(因为同名了)。
同时,如果父类跟子类一样名字一样参数,但是返回值不同,编译器直接报错

当进行向上转型时,不能使用扩展部分(基类无法访问导出类的扩展部分)。

导出类 覆盖(即重写) 基类 方法时,返回的类型可以是基类方法返回类型的子类。

多态

重写和重载都是多态的一种

此处考虑的是,向上转型,父类的声明,子类的创建。

只有普通的方法调用可以是多态的,静态方法 / 域(包括普通) 不具有多态性。

也就是说,父类的声明子类的创建:

普通方法,从下面往上面找;找普通属性,声明的这个有就有,没有就报错(当然,如果通过调用get这个普通方法访问field,那么拿到的是子类的);

找静态方法和静态属性,看声明的那一个,它有就有,没有就是报错,当然最好直接通过类名访问,否则狠狠地警告。

类加载阶段(加载、链接、初始化)

初始化时机

注意:访问该类的静态变量或者静态方法的时候会初始化的

构造器也是 static 方法,尽管没有显式写出 static 关键字。因此可以更准确地说,类是在其任何 static 成员被访问时被加载。

OieX6P7LgfonkC9

初始化顺序(在类加载阶段,第二行不一定对)

因为子类初始化的话,父类如果没有初始化,也会被初始化。

所以,首先①父类static -> ②子类static -> ③父类非static -> ④子类非static (这个不一定正确)

但是子类访问父类的静态变量,只会触发父类的初始化。

下面这个一定正确

静态代码块和静态属性直接初始化的执行顺序?静态方法什么时候执行?_阿里面试题——Java对象初始化…

核心理念

  1. 静态属性和静态代码块都是在类加载的时候初始化和执行,两者的优先级别是一致的,且高于非静态成员,执行按照编码顺序。
  2. 非静态属性和匿名构造器(就是变量后面加个括号的)一定在所有的构造方法之前执行,两者的优先级别一致,执行按照编码顺序。
  3. 以上执行完毕后执行构造方法中的代码。
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
public class InitializeDemo {
private static int k = 1;
private static InitializeDemo t1 = new InitializeDemo("t1");
private static InitializeDemo t2 = new InitializeDemo("t2");
private static int i = print("i");
private static int n = 99;
static {
print("静态块");
}
private int j = print("j");
{
print("构造块");
}
public InitializeDemo(String str) {
System.out.println((k++) + ":" + str + " i=" + i + " n=" + n + " 构造方法");
++i;
++n;
}
public static int print(String str) {
System.out.println((k++) + ":" + str + " i=" + i + " n=" + n);
++n;
return ++i;
}
public static void main(String args[]) {
new InitializeDemo("init");
}
}

// 输出
1:j i=0 n=0
2:构造块 i=1 n=1
3:t1 i=2 n=2 构造方法
4:j i=3 n=3
5:构造块 i=4 n=4
6:t2 i=5 n=5 构造方法
7:i i=6 n=6
8:静态块 i=7 n=99
9:j i=8 n=100
10:构造块 i=9 n=101
11:init i=10 n=102 构造方法

Process finished with exit code 0

注意,非常深刻,若1中顺序下来的时候,静态属性创建了对象实例需要调用构造方法进行构造的;由于核心理念2的作用,所有的非静态属性和匿名构造器会在前面先顺序执行一遍,(此时前面的静态变量和块,没有被运行到的就先都是默认值了(final不知道))。

zrXEonhqwfSNY5J

再来一个案例

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
class Glyph {
void draw() { print("Glyph.draw()"); }
Glyph() {
print("Glyph() before draw()");
draw();
print("Glyph() after draw()");
}
}

class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
print("RoundGlyph.RoundGlyph(), radius = " + radius);
}
void draw() {
print("RoundGlyph.draw(), radius = " + radius);
}
}

public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
} /* Output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*/

// 由于父类的静态(静态块=静态属性),非静态(构造块=普通属性),先于父类构造方法
// 并且子类的静态(),非静态,也先于构造方法
// 但是此处,初始化子类,需要先初始化父类,父类在构造器中调用了draw,由于多态调用的是子类的draw,而子类的radius还没有赋值只是初始值0。

带有父类的案例

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
public class Main1 {
public static void main(String[] args) {
new B();
}

public static class A {
A() {
System.out.println("AAA");
}
{
System.out.println("func aaa");
}
static {
System.out.println("static AAA");
}
}

public static class B extends A {
B() {
System.out.println("BBB");
}
{
System.out.println("func BBB");
}
static {
System.out.println("static BBB");
}
}
}

/**
static AAA
static BBB
func aaa
AAA
func BBB
BBB
**/

Author: Jcwang

Permalink: http://example.com/2022/07/18/%E5%85%AB%E8%82%A1javaguide/