JVM

JVM

阿里二面试题:JVM 方法区和元空间什么关系?6大角度带你了解方法区

阿里二面:JVM方法区和元空间的关系到底是怎样的

方法区和永久代以及元空间有什么关系?

方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。

并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现便成为元空间。

JDK 1.7 为什么要将字符串常量池移动到堆中?

主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。

注意

Java的堆中 1.8及以后有new出来的对象实例,静态变量、字符串常量池

而1.6及以前静态变量和字符串常量池都是在方法去中(永久带中)。

0acb7c38c148e32c78451ff7a9cbe5f3.webp.png

Screen-Shot-2022-06-13-at-16.06.01.png

JVM内存结构、内存模型、对象模型的区别

  1. JVM内存结构和Java虚拟机运行时区域有关。
  2. Java内存模型和Java并发编程有关。
  3. Java对象模型和Java对象在虚拟机中表现形式有关。

JMM定义了一套在多线程读写共享数据时(成员变量,数组)时,对数据的可见性、有序性、和原子性的规则和保障

Jvm知识

里面有GC、调优等等

GC

JVM: 垃圾回收

一道关于GC的题目

kRD15MXms9buY7h

jdk创建对象的速度远大于cglib,这是由于cglib创建对象时需要操作字节码。cglib执行速度略大于jdk,所以比较适合单例模式。另外由于CGLIB的大部分类是直接对Java字节码进行操作,这样生成的类会在Java的永久堆中。如果动态代理操作过多,容易造成永久堆满,触发OutOfMemory异常。spring默认使用jdk动态代理,如果类没有接口,则使用cglib。

选C

CGLIB是一个强大的、高性能的代码生成库。

CGLIB代理主要通过对字节码的操作,为对象引入间接级别,以控制对象的访问。我们知道Java中有一个动态代理也是做这个事情的,那我们为什么不直接使用Java动态代理,而要使用CGLIB呢?答案是CGLIB相比于JDK动态代理更加强大,JDK动态代理虽然简单易用,但是其有一个致命缺陷是,只能对接口进行代理。如果要代理的类为一个普通类、没有接口,那么Java动态代理就没法使用了。

CGLIB和Java动态代理的区别

  1. Java动态代理只能够对接口进行代理,不能对普通的类进行代理(因为所有生成的代理类的父类为Proxy,Java类继承机制不允许多重继承);CGLIB能够代理普通类;
  2. Java动态代理使用Java原生的反射API进行操作,在生成类上比较高效;CGLIB使用ASM框架直接对字节码进行操作,在类的执行过程中比较高效

GC种类,GC的STW

ScreenShot2022-09-11at11.56.56

【大厂突击】六、Minor GC、Young GC、Full GC、Old GC、Major GC、Mixed GC 一文搞懂

ScreenShot2022-09-11at11.57.27

面试官:并发标记时如何标记垃圾 & 垃圾回收之三色标记法详解

并发标记,适用于CMS和G1,并发标记的意思就是 **可以在不暂停用户线程的情况下对其进行标记**,那么实现这种并发标记的算法就是 三色标记法,三色标记法最大的特点就是可以异步执行,从而可以以中断时间极少的代价或者完全没有中断来进行整个GC。

CMS垃圾回收器存在的问题及解决方案

hMOlustCV2EJQIK

空间碎片问题
由于CMS使用的是标记-清理算法,因此会导致产生大量的内存碎片,如果不整理的话,将会有大量的不连续的内存空间存在,因此就无法存放大对象,导致频繁GC。
解决方案:
CMS存在一个默认的参数 “-XX:+UseCMSCompactAtFullCollection”,意思是在Full GC之后再次STW,停止工作线程,整理内存空间,将存活的对象移到一边。
还有一个参数是“-XX:+CMSFullGCsBeforeCompaction”,表示在进行多少次Full GC之后进行内存碎片整理,默认为0,即每次Full GC之后都进行内存碎片整理。

c25VQSdEOLl9CKh

D,execute可以抛出异常,

初始化

时机

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

构造器也是 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++; // 此处++i,上面的i为7,如果i++,那么上面得到的i是6
}
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

有关i++的问题

首先如果不是静态变量的话,i++直接再slot中执行

所以只有++i + a

1
2
iinc 1,1
iload_1 // 注意这个load的是给外面的别的操作i用的,比如i++ + a,然后就是后面操作+a的时候用的吗,放到的操作数栈中。

++i的话正好相反。

而static 的i++的话,是四句话

1
2
3
4
getstatic i
iconst_1
iadd
putstatic i // 注意这里的只有i++,而不像上面的,+a,所以如果有+a的话,我觉得i++的话getstatic i再在最上面放一个,而++i在最下面放一个

注意,在下面的句子中,i最后均为0(不论是否static);注意,如果都是++i的话,那就是1了。

1
2
i = 0;
i = i++;

两个线程分别对int i=0进行i++一百次

注意上面的这是考虑主存和工作内存的原因,2~200

如果一个线程++,另一个–,那么就-100~100之间呗。当然我看的JVM课程是从字节码角度分析的。

Runnable 与 Callable的不同点

Runnable没有返回值;Callable可以返回执行结果
Callable接口的call()允许抛出异常;Runnable的run()不能抛出

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

// 1 编写类实现Callable接口 , 实现call方法
class XXX implements Callable {
@Override
public call() throws Exception {
return T;
}
}
// 2 创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask future = new FutureTask<>(callable);
// 3 通过Thread,启动线程
new Thread(future).start();
// 4 此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
future.get()

同步容器、并发容器、免锁容器

同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调 用同步容器的方法,它们将会串行执行。比如 Vector, Hashtable,以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。可以通过查看 Vector,Hashtable 等这些同步容器的实现代码, 可以看到这些容器实现线程安全的方式就 是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized。

并发容器:使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁(当然jDK1.8之后使用的是Node数组+链表+红黑树,而且对元素的头+synchnorize或者该数组中没有的时候用CAS写入),在 这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发 地修改 map,所以它可以在并发环境下实现更高的吞吐量。

免锁容器:CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时, 不会抛出 ConcurrentModificationException。在 CopyOnWriteArrayList 中,写入将导致 创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可 以安全地执行。

免锁容器缺点

1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可 能导致 young gc 或者 full gc;

2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后, 读取到数据可能还是旧的,虽然 CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足 实时性要求;

CopyOnWriteArrayList 透露的思想

1、读写分离,读和写分开

2、最终一致性

3、使用另外开辟空间的思路,来解决并发冲突

Java:ReentrantLock可重入锁的深入学习

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

一些垃圾回收器

MZNnqc

Author: Jcwang

Permalink: http://example.com/2022/06/13/%E5%85%AB%E8%82%A1JVM/