Java知识点整理

时间:2022-11-17 14:54:23

1) Java虚拟机的区域如何划分,每一个区功能。

Java知识点整理

 

1、程序计数器

程序计数器(Program Counter Register), 也有称作为PC寄存器。想必学过汇编语言的盆友对程序计数器这个概念并不陌生,在汇编语言中,程序计数器是指CPU中的寄存器,他保存的是当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令结束。

虽然说JVM中的程序计数器并不像汇编语言中的程序计数器一样是物理概念上的CPU寄存器,但是JVM中的程序计数器的功能跟汇编语言中的程序计数器的功能在逻辑上是等同的,也就是说是用来指示下一条执行的是哪一条指令的。

由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任意具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己的独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。

在JVM规范中规定,如果线程执行是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果执行的是native方法,则程序计数器的值是undefined。 
由于程序计数器中存储的数据所占空间大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。

 

2、虚拟机栈(栈内存)

Java栈也称作虚拟机栈(Java Vitual Machine Stack), 也就是我们常常所说的栈,跟C语言的数据段中的栈类似。Java栈是Java方法执行的内存模型。为什么这么说呢? 下面就来解释一下具体原因。

Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。

当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。讲到这里大家就会明白为什么在使用递归方法的时候容易导致栈内存溢出的现象了,以及为什么栈区的空间不用程序员去管理了(当然在Java中,程序员基本不用关系到内存分配和释放的事情,因为Java有自己的垃圾回收机制),这部分的空间的分配和释放都是由系统自动实施的。对于所有的程序设计语言来说,栈这部分的空间对于程序员来说是不透明的。

 

下图表示了一个Java栈的模型:

Java知识点整理

 

3、本地方法栈

本地方法栈与Java栈的作用和原理都非常相似。区别只不过是Java栈是为了执行Java方法服务的, 而本地方法栈则是为了执行本地方法(Native Method)服务的。 在JVM规范中,并没有对本地方法具体的实现方法以及数据结构的强制指定, 虚拟机可以*实现它。在HotSpot虚拟器中直接就把本地方法栈和Java栈合二为一。

 

4、堆(GC堆,堆内存)

在C语言中,堆这部分空间是唯一一个程序员可以管理的内存区域。程序员可以通过malloc函数和free函数在堆上申请和释放空间。 那么在Java中是怎么样的呢?

Java中的堆内存是用来存储对象本身以及数组(当然数组的引用是存放在Java栈中的)。 只不过和C语言中的不同,在Java中,程序员基本不用关心空间释放问题, Java的垃圾回收机制会自动进行处理。因此这部分空间也是Java垃圾收集器管理的主要区域。 另外,堆是被所有线程共享的,在JVM中只有一个堆。

 

5、方法区(非堆)

方法区在JVM也是一个非常重要的区域,它与堆一样,是被线程共享的区域。 在方法区中,存储了每个类的信息(包括类的名称,方法信息,字段信息)、静态变量、常量以及编译器编译后的代码等。 在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。

在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非只有Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。

在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为”永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过JDK7之后,HotSpot虚拟机便将运行时常量池的永久代移除了。

 

        1、直接内存不是Java内存区域 

        2、线程私有(程序计数器,虚拟机栈,本地方法栈);线程共享(堆,方法区)

        3、程序计数器指示当前正在执行的字节码指令地址

        4、GC堆根据垃圾收集器实现算法(分代收集算法)分为:Eden空间、From Survivor空间、To Survivor空间。(新生代,老年代)

 

科普直接内存

1、官方描述:

A byte buffer is either direct or non-direct. Given a direct byte buffer, the Java virtual machine will make a best effort to perform native I/O operations directly upon it. That is, it will attempt to avoid copying the buffer's content to (or from) an intermediate buffer before (or after) each invocation of one of the underlying operating system's native I/O operations.


2、对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作。而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先复制到直接内存,再利用本地IO处理,所以网络发送大量数据时,直接内存会具有更高的效率

3、直接内存的使用场景:一、有很大的数据需要存储,它的生命周期又很长 二、适合频繁的IO操作,比如网络并发场景

 

2)  双亲委派模型中,从顶层到底层,都是哪些类加载器,分别加载哪些类?

    1、启动类加载器(Bootstrap ClassLoader), 负责将 Java_Home/lib下面的类库加载到内存中(比如rt.jar)

    2、扩展类加载器(Extension ClassLoader),负责将Java_Home/lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中

    3、应用程序类加载器(Application ClassLoader),负责将系统类路径(CLASSPATH)中指定的类库加载到内存中

 

          除此之外,还有自定义的类加载器,它们之间的层次关系被称为类加载器的双亲委派模型。

         双亲委派模型最大的好处就是让Java类同其类加载器一起具备了一种带优先级的层次关系。这句话可能不好理解,我们举个例子。比如我们要加载顶层的Java类——java.lang.Object类,无论我们用哪个类加载器去加载Object类,这个加载请求最终都会委托给Bootstrap ClassLoader,这样就保证了所有加载器加载的Object类都是同一个类。如果没有双亲委派模型,那就乱套了。

 

3)HashMap结构,get和put是如何实现, HashMap有哪些问题?

存储实现:put(key,vlaue),get类似

1) 计算key的hash值,该方法为一个纯粹的数学计算,就是计算h的hash值, int hash = hash(key.hashCode());
2) 计算key hash 值在 table 数组中的位置, int i = indexFor(hash, table.length); h & (length-1); 对length取模
3) 从i出开始迭代 e,找到 key 保存的位置

当我们想一个HashMap中添加一对key-value时,系统首先会计算key的hash值,然后根据hash值确认在table中存储的位置。若该位置没有元素,则直接插入。否则迭代该处元素链表并依此比较其key的hash值。

如果两个hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),则用新的Entry的value覆盖原来节点的value。如果两个hash值相等但key值不等 ,则将该节点插入该链表的链头。

一、链的产生。
这是一个非常优雅的设计。系统总是将新的Entry对象添加到bucketIndex处。如果bucketIndex处已经有了对象,那么新添加的Entry对象将指向原有的Entry对象,形成一条Entry链,但是若bucketIndex处没有Entry对象,也就是e==null,那么新添加的Entry对象指向null,也就不会产生Entry链了。

二、扩容问题。
随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。该临界点在当HashMap中元素的数量等于table数组长度*加载因子。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

三、HashMap不适合多线程。默认负载因子大小为0.75,也就是说当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing。当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在LinkedList中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在LinkedList的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。

 

4) ConcurrentHashMap的get和put如何实现的?ConcurrentHashMap有哪些问题? ConcurrentHashMap的锁是读锁还是写锁?

concurrencyLevel:并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。

1)Segment 数组长度为 16,不可以扩容。
2)Segment[i] 的默认大小为 2,负载因子是 0.75,得出初始阈值为 1.5,也就是以后插入第一个元素不会触发扩容,插入第二个会进行第一次扩容
3)segmentShift 的值为 32 – 4 = 28,segmentMask 为 16 – 1 = 15,姑且把它们简单翻译为移位数和掩码,这两个值在定位segment数组下标位置时用到
4)Segment 内部是由 数组+链表 组成的。Segment 是一种可重入锁(继承ReentrantLock)

put过程分析,get过程类似
1)  计算 key 的 hash 值 int hash = hash(key);
2)  根据 hash 值找到 Segment 数组中的位置 j,hash 是 32 位,无符号右移 segmentShift(28) 位,剩下低 4 位,然后和 segmentMask(15) 做一次与操作,也就是说 j 是 hash 值的最后 4 位,也就是槽的数组下标int hash = hash(key); int j = (hash >>> segmentShift) & segmentMask;
3) int index = (tab.length - 1) & hash,segment 内部的数组tab,利用 hash 值,求应该放置的数组下标
4) 如果任何元素和已经存在一个链表,覆盖旧值;否则,将它设置为链表表头;如果是null,初始化并设置为链表表头

ConcurrentHashMap支持允许多个修改同时并发进行,原因就是采用的Segment分段锁功能,每一个Segment 都想的于小的hash table并且都有自己锁,只要修改不再同一个段上就不会引起并发问题。

使用ConcurrentHashMap时候 有时候会遇到跨段的问题,跨段的时候【size()、 containsValue()】,可能需要锁定部分段或者全段,当操作结束之后,又回按照 顺序进行释放每一段的锁。

 ConcurrentHashMap支持完全并发的读以及一定程度并发的写。如果使用传统的技术,如HashMap中的实现,如果允许可以在hash链的中间添加或删除元素,读操作不加锁将得到不一致的数据。但是ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。HashEntry代表每个hash链中的一个节点,其结构如下所示: 

 Java知识点整理

可以看到除了value不是final的,其它值都是final的,这意味着不能从hash链的中间或尾部添加或删除节点,因为这需要修改next引用值,所有的节点的修改只能从头部开始。对于put操作,可以一律添加到Hash链的头部。但是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制一遍,最后一个节点指向要删除结点的下一个结点。为了确保读操作能够看到最新的值,将value设置成volatile,这避免了加锁。 remove操作要注意一个问题:如果某个读操作在删除时已经定位到了旧的链表上,那么此操作仍然将能读到数据,只不过读取到的是旧数据而已,这在多线程里面是没有问题的。

HashEntry 类的 value 域被声明为 Volatile 型,Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程“看”到。在 ConcurrentHashMap 中,不允许用 null作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。 

 在看源码实现时,对HashEntry 的 value 域的值可能为 null有些疑惑,网上都是说发生了重排序现象,后来仔细想想不完全正确,重排序发生在删除操作时,这只是其中的一个原因,尽管ConcurrentHashMap不允许将value为null的值加入,但现在仍然能够读到一个为空的value就意味着此值对当前线程还不可见,主要因为HashEntry还没有完全构造完成导致的,所以添加和删除对链表的结构性修改都可能会导致value为null。

 

5)  HashMap与HashTable的异同

a) 两个类的继承体系有些不同。虽然都实现了Map、Cloneable、Serializable三个接口。但是HashMap继承自抽象类AbstractMap,而HashTable继承自抽象类Dictionary。
b) HashMap是支持null键和null值的,而HashTable在遇到null时,会抛出NullPointerException异常
c) HashMap/HashTable内部用Entry数组实现哈希表,而对于映射到同一个哈希桶(数组的同一个位置)的键值对,使用Entry链表来存储(解决hash冲突)
d) HashTable是同步的,HashMap不是

 

6)await/wait 与sleep、yield间的区别

 Java知识点整理

 

7) 什么是线程池?如果让你设计一个动态大小的线程池,如何设计,应该有哪些方法?

官方描述:

In computer programming, a thread pool is a software design pattern for achieving concurrency of execution in a computer program. Often also called a replicated workers or worker-crew model,a thread pool maintains multiple threads waiting for tasks to be allocated for concurrent execution by the supervising program. By maintaining a pool of threads, the model increases performance and avoids latency in execution due to frequent creation and destruction of threads for short-lived tasks.The number of available threads is tuned to the computing resources available to the program, such as parallel processors, cores, memory, and network sockets.

实现线程池主要包括以下4个基本组成部分:

1)线程池管理类
主要用于实现创建线程和添加客户端请求的新任务,执行任务以及如何回收已经执行完任务的线程。

2)工作线程类
线程池中的线程,它主要用于处理任务队列中的任务。

3)任务类
定义任务的各种属性,以及要完成的操作

4)任务队列

按先来先服务的顺序用于存放新加入的任务,以便让工作线程来执行