Java常问面试题概要答案

1.JDK、JRE、JVM的区别

JDK:java标准开发包,包含java编译器、java运行时环境
JRE:java运行环境,用于运行java字节码文件,包含jvm及jvm所需类库
JVM:java虚拟机,负责运行字节码文件
简而言之,就是JDK包含JRE、JRE又包含了JVM

2.hashcode()与equals()之间的关系

java中,每个对象都可以通过hashcode()方法获取到自己的hash值,除了一些java提供的类中,自己重写了hashcode()以及equals()外,在我们新建对象时,有时候也需要重写这两个方法。

备注:对于基本数据类型而言使用==比较的是两者的值,
而对于对象类型而言则是比较的是对象的地址,所有类都是Object类的子类,不重写hashcode()方法时,使用是Object默认的hashCode()方法,导致对象内容一致但是地址不一样

Java API文档中关于hashCode方法有以下几点规定:
(1)在java应用程序执行期间,如果在equals方法比较中所用的信息没有被修改,那么在同一个对象上多次调用hashCode方法时必须一致地返回相同的整数。如果多次执行同一个应用时,不要求该整数必须相同。
(2)如果两个对象通过调用equals方法是相等的,那么这两个对象调用hashCode方法必须返回相同的整数。
(3)如果两个对象通过调用equals方法是不相等的,不要求这两个对象调用hashCode方法必须返回不同的整数。但是程序员应该意识到对不同的对象产生不同的hash值可以提供哈希表的性能。

重写以后我们可以通过该对象作为map的key做数据的存取、可以在集合、java8 stream等集合操作总进行比较、去重

3.String、StringBuffer、StringBuilder的区别

(1)String为常量字符串,修改时会生成新的字符串对象,StringBuffer和StringBuilder是可变的
(2)StringBuffer线程安全的(方法被synchronized修饰),StringBuilder是线程不安全的,单线程时StringBuilder的效率更高

4.Java泛型

(1)无限制类型擦除
当在类的定义时没有进行任何限制,那么在类型擦除后将会被替换成Object,例如、<?>都会被替换成Object
(2)有限制类型擦除
当类定义中的参数类型存在上下限(上下界),那么在类型擦除后就会被替换成类型参数所定义的上界或者下界,例如<? extend Person>会被替换成Person,而<? super Person>则会被替换成Object

5.ArrayList和LinkedList区别

(1)底层数据结构不同,ArrayList是基于数组实现的,LinkedList是基于链表实现的
(2)由于底层数据结构不同,ArrayList更适合随机查找,LinkedList更适合元素修改
(3)ArrayList和LinkedList都实现了List接口,但是LinkedList还实现了Deque接口,还可以当做队列使用
(4)由于底层数据结构不同,其物理存储空间不同,数组元素是连续空间,而链表则是通过指针指向来实现元素连接,其空间不一定连续

6.ConcurrentHashMap

(1)存储 :调用map.put(k, v)方法。
第一步:先将k, v封装成Node节点对象。

第二步:底层调用k类重写之后的hashCode()方法,得出k的int型哈希值。

第三步:拿到k的哈希值,通过哈希函数算法,将k的哈希值转换成数组的下标(如有16个槽位,根据算法得到下标为3,此时应该将这个key放在下标3的元素下,而这个元素可以是链表、树结构)。

第四步:拿到下标后,如果数组下标位置上没有链表,就把Node对象添加到这个位置上。
如果下标处有链表,此时会拿着Node对象的k和链表中的每一个节点的key,用equals()方法进行比对,如果发现key有相同的,直接用Node对象的v覆盖掉原来旧的value。如果比完了该链表,发现没有key相同的节点,直接存储在该链表的末尾位置。
在这里插入图片描述
jdk8版本之后
当HashMap底层数组中每个单向链表的元素超过8之后,底层会把该链表变成红黑树结构。
当红黑树节点小于6之后,会重新变成单向链表结构

(2)并发(线程安全)
JDK1.7版本:
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即 ConcurrentHashMap 把哈希桶数组切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成。
如下图所示,首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。
在这里插入图片描述

Segment 是 ConcurrentHashMap 的一个内部类,Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。Segment 默认为 16,也就是并发度为 16。volatile 修饰了 HashEntry 的数据 value 和 下一个节点 next,保证了多线程环境下数据获取时的可见性

JDK1.8版本
Node数组(Node、TreeNode)+链表+红黑树结构;在锁的实现上,采用CAS + synchronized实现更加细粒度的锁。
在这里插入图片描述

TreeBin
TreeBin从字面含义中可以理解为存储树形结构的容器,而树形结构就是指TreeNode,所以TreeBin就是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制.

存储元素流程如下:
1.根据 key 计算出 hash 值;
2.判断是否需要进行初始化;
3.定位到 Node,拿到首节点 f,判断首节点 f:
4.如果为 null ,则通过 CAS 的方式尝试添加;
5.如果为 f.hash = MOVED = -1 ,说明其他线程在扩容,参与一起扩容;
6.如果都不满足 ,synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入;

常问面试题:
HashMap、HashTable、ConcurrentHashMap的区别、扩容机制、线程安全实现原理

7. B树和B+树

(1)B树(balance tree)多路平衡查找树
在这里插入图片描述
类似于普通的二叉树,允许1每个阶段有更多的叶子节点。
B树的特点:
1.所有键值分布在整颗树中(索引值和具体data都在每个节点里);
2.任何一个关键字出现且只出现在一个结点中;
3.搜索有可能在非叶子结点结束(最好情况O(1)就能找到数据);
4.在关键字全集内做一次查找,性能逼近二分查找;

B+树:
B+树是B-树的变体,也是一种多路搜索树, 它与 B- 树的不同之处在于:

所有关键字存储在叶子节点出现,内部节点(非叶子节点并不存储真正的 data)
为所有叶子结点增加了一个链指针

在这里插入图片描述

因为非叶子节点并不存储 data,所以一般B+树的叶节点和非叶子节点大小不同,而B-树的每个节点大小一般是相同的,为一页
在这里插入图片描述

为了增加 区间访问性,一般会对B+树做一些优化。
如下图带顺序访问的B+树, 带顺序可以提升检索的效率
在这里插入图片描述

** B-树和B+树的区别**
1.B+树内节点不存储数据,所有 data 存储在叶节点导致查询时间复杂度固定为 log n。而B-树查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1)。

如下所示B-树/B+树查询节点 key 为 50 的 data。

B-树:
在这里插入图片描述

从上图可以看出,key 为 50 的节点就在第一层,B-树只需要一次磁盘 IO 即可完成查找。所以说B-树的查询最好时间复杂度是 O(1)。

B+树:
在这里插入图片描述

由于B+树所有的 data 域都在根节点,所以查询 key 为 50的节点必须从根节点索引到叶节点,时间复杂度固定为 O(log n)。

总结:B树的由于每个节点都有key和data,所以查询的时候可能不需要O(logn)的复杂度,甚至最好的情况是O(1)就可以找到数据,而B+树由于只有叶子节点保存了data,所以必须经历O(logn)复杂度才能找到数据

  1. B+树叶节点两两相连(添加指针)可大大增加区间访问性,可使用在范围查询等(这就是为什么面试时有时会问为什么用b+树而不用hash作为索引的数据结构的原因之一),而B-树每个节点 key 和 data 在一起,则无法区间查找。
    在这里插入图片描述

根据空间局部性原理:如果一个存储器的某个位置被访问,那么将它附近的位置也会被访问。

B+树可以很好的利用局部性原理,若我们访问节点 key为 50,则 key 为 55、60、62 的节点将来也可能被访问,我们可以利用磁盘预读原理提前将这些数据读入内存,减少了磁盘 IO 的次数。
当然B+树也能够很好的完成范围查询。比如查询 key 值在 50-70 之间的节点。

总结:由于B+树的叶子节点的数据都是使用链表连接起来的,而且他们在磁盘里是顺序存储的,所以当读到某个值的时候,磁盘预读原理就会提前把这些数据都读进内存,使得范围查询和排序都很快

3.B+树更适合外部存储。由于内节点无 data 域,每个节点能索引的范围更大更精确

这是因为,由于B-树节点内部每个 key 都带着 data 域,而B+树节点只存储 key 的副本,真实的 key 和 data 域都在叶子节点存储。前面说过磁盘是分 block 的,一次磁盘 IO 会读取若干个 block,具体和操作系统有关,那么由于磁盘 IO 数据大小是固定的,在一次 IO 中,单个元素越小,量就越大。这就意味着B+树单次磁盘 IO 的信息量大于B-树,从这点来看B+树相对B-树磁盘 IO 次数少。

总结:由于B树的节点都存了key和data,而B+树只有叶子节点存data,非叶子节点都只是索引值,没有实际的数据,这就时B+树在一次IO里面,能读出的索引值更多。从而减少查询时候需要的IO次数!

常问面试
B+树是什么样的数据结构?
如上,略.
Mysql为什么用B+树作为索引而不用其他结构呢?

  1. 二叉树与B+数同属于树结构,但是如果在我们进行数据读取时,二叉树的深度就很深,会导致磁盘读写多次。而B+树的深度要远低于二叉树,并且一次性可以从磁盘中读取多条数据,这样能降低磁盘IO的次数。通俗的话来说就是把高瘦的树变成矮胖的树,磁盘IO次数就能减少。
  2. 为什么不用B树
    (1)每个节点中有key,也有data,但是每一个节点(磁盘页)的存储空间是有限的,如果data数据较大时会导致每个节点能存储的key的数据很小,最后导致B树的高度较大,磁盘IO次数增多。
    (2)索引和内容分散在整个B+树上,离根节点近搜索快,离根节点远搜索慢,花费的IO时间不平均。
    (3)B树不方便做范围搜索,例如要查找大于3小于10的数据,则需要先达到磁盘块5找到3,然后回到磁盘块2比较,在到磁盘块6找到9,这相当于进行了局部的中序遍历,情况更糟的情况下可能还要跨层访问。
    3.为什么不用红黑树
    (1)数据是单边增长的情况 那么出现的就是和链表一样的数据结构,树高度大,红黑树的目的主要是解决这个问题。
    (2)B树和B+树克服了红黑树克服不了的困难,因为随着数据量增多,红黑树的高度必然也会变得很高,造成磁盘IO次数的增加
    4.为什么不适用hash
    如果只是查找一条数据那确实效率很快(进行一次哈希运算即可),但我们需要考虑查询多条数据的情况,B+树索引有序,加上B+树有链表相连,这时候效率就比hash高,并且支持范围查找。

4.B+树的优势
接下来我们来看一下使用B+树作为MySQL索引存储的数据结构的示例图。所有数据保存在叶子节点上,叶子节点有链表相连。
在这里插入图片描述

1.跟B树相比,B+树的叶子结点是一条链表,会将搜有的数据连接在一起,做整表遍历和区间查找是非常容易的,例如查找3到15范围内的数据,通过链表就能把所有数据取出来了(即磁盘块4和磁盘块5是相连的)。
2.每个非叶子节点只存放key值,这样一个节点就能存放更多的key值,从而降低树的层高。
3.B+树所有的数据都存在叶子节点上,找到对应数据的时间比较平均的。

8.负载均衡常见策略

1.轮循:如有AB两个同种服务,这次访问A,下次就访问B
2.加权轮循:如A服务的服务器 16G B服务的服务器32G,我们可以配置1:2的权重,这意味着在服务器 A 接收到第一个请求后,服务器 B 会连续的接收到 2 个请求
3.最少连接数
以上两种方法都没有考虑的是系统不能识别在给定的时间里保持了多少连接。因此可能发生,服务器 B 服务器收到的连接比服务器 A 少但是它已经超载,因为 服务器 B 上的用户打开连接持续的时间更长。这就是说连接数即服务器的负载是累加的。这种潜在的问题可以通过 “最少连接数” 算法来避免:传入的请求是根据每台服务器当前所打开的连接数来分配的。即活跃连接数最少的服务器会自动接收下一个传入的请求。基本上和简单轮询的原则相同:所有拥有虚拟服务的服务器资源容量应该相近。值得注意的是,在流量率低的配置环境中,各服务器的流量并不是相同的,会优先考虑第一台服务器。这是因为,如果所有的服务器是相同的,那么 第一个服务器优先,直到第一台服务器有连续的活跃流量,否则总是会优先选择第一台服务器。
4.源 IP 哈希
这种方式通过生成请求源 IP 的哈希值,并通过这个哈希值来找到正确的真实服务器。这意味着对于同一主机来说他对应的服务器总是相同。使用这种方式,你不需要保存任何源 IP。但是需要注意,这种方式可能导致服务器负载不平衡。
5.最少连接数慢启动时间
对最少连接数和带权重的最小连接数调度方法来说,当一个服务器刚加入线上环境时,可以为其配置一个时间段,在这段时间内连接数是有限制的而且是缓慢增加的。这为服务器提供了一个‘过渡时间’以保证这个服务器不会因为刚启动后因为分配的连接数过多而超载。这个值在 L7 配置界面设置。
6.加权最少连接
如果服务器的资源容量各不相同,那么 “加权最少连接” 方法更合适:由管理员根据服务器情况定制的权重所决定的活跃连接数一般提供了一种对服务器非常平衡的利用,因为他它借鉴了最少连接和权重两者的优势。通常,这是一个非常公平的分配方式,因为它使用了连接数和服务器权重比例;集群中比例最低的服务器自动接收下一个请求。但是请注意,在低流量情况中使用这种方法时,请参考 “最小连接数” 方法中的注意事项。
7.基于代理的自适应负载均衡
负载主机包含一个自适用逻辑用来定时监测服务器状态和该服务器的权重。对于非常强大的 “基于代理的自适应负载均衡” 方法来说,负载主机以这种方式来定时检测所有服务器负载情况:每台服务器都必须提供一个包含文件,这个文件包含一个 0~99 的数字用来标明改服务器的实际负载情况 (0 = 空前,99 = 超载,101 = 失败,102 = 管理员禁用),而服务器同构 http get 方法来获取这个文件;同时对集群中服务器来说,以二进制文件形式提供自身负载情况也是该服务器工作之一,然而,并没有限制服务器如何计算自身的负载情况。根据服务器整体负载情况,有两种策略可以选择:在常规的操作中,调度算法通过收集的服务器负载值和分配给该服务器的连接数的比例计算出一个权重比例。因此,如果一个服务器负载过大,权重会通过系统透明地做调整。和加权轮循调度方法一样,不正确的分配可以被记录下来使得可以有效地为不同服务器分配不同的权重。然而,在流量非常低的环境下,服务器报上来的负载值将不能建立一个有代表性的样本;那么基于这些值来分配负载的话将导致失控以及指令震荡。 因此,在这种情况下更合理的做法是基于静态的权重比来计算负载分配。当所有服务器的负载低于管理员定义的下限时,负载主机就会自动切换为加权轮循方式来分配请求;如果负载大于管理员定义的下限,那么负载主机又会切换回自适应方式。
8.固定权重
最高权重只有在其他服务器的权重值都很低时才使用。然而,如果最高权重的服务器下降,则下一个最高优先级的服务器将为客户端服务。这种方式中每个真实服务器的权重需要基于服务器优先级来配置。
9.加权响应
流量的调度是通过加权轮循方式。加权轮循中 所使用的权重 是根据服务器有效性检测的响应时间来计算。每个有效性检测都会被计时,用来标记它响应成功花了多长时间。但是需要注意的是,这种方式假定服务器心跳检测是基于机器的快慢,但是这种假设也许不是总能够成立。所有服务器在虚拟服务上的响应时间的总和加在一起,通过这个值来计算单个服务物理服务器的权重;这个权重值大约每 15 秒计算一次。

Dubbo 中使用的5种负载均衡:
RandomLoadBalance 是一种比较容易实现的负载均衡策略,也是Dubbo 默认使用的负载均衡策略。就是通过加权的随机,负载均衡分发请求。
LeastActiveLoadBalance 最小活跃度轮询,也就是优先选择活跃度最小的服务进行调用,活跃度简单来说就是服务调用的次数,通过一个 ConcurrentHashMap存储调用服务的次数,获取最小的调用,如果存在多个最小的则通过上面随机的方式调用。
ConsistentHashLoadBalance 一致性Hash 就是通过请求参数来具体定位服务的方式,Dubbo 通过一致性Hash 算法得到具体的服务地址,为了防止资源倾斜,又加入了虚拟节点。
RoundRobinLoadBalance 加权重的轮询算法,通过权重来模拟实现轮询。每个服务会维护一个静态的权重,以及不断变化的动态权重。每次服务会选择动态权重最大的服务,然后将该服务的动态权重剪去总权重,再下次计算动态权重就是通过【原权重】+ 【动态权重】得到,也就是权重高的经过选择之后,权重会变低,而没有被选择的服务权重会慢慢变高,起到加权以及轮询的作用。
ShortestResponseLoadBalance 最短响应负载均衡,也就是调用负载均衡响应时间最多的服务,如果有多个就通过加权的随机来选择,和 LeastActiveLoadBalance 类似,都是去一个 ConcurrentHashMap 中取值,取得历次响应时间的平均,然后比较。

9.Java类加载机制与双亲委派

前置条件
编译器把Java源文件编译成.class文件,再由JVM装载.class文件到内存中,JVM装载完成后得到一个Class对象字节码。有了字节码对象,接下来就可以实例化使用了。
在这里插入图片描述

1.类加载器
类的加载器主要有启动类加载器、扩展类加载器、应用类加载器、用户自定义加载器。
在这里插入图片描述
启动类加载器:是用来加载jdk\jre\lib下的核心类库,比如rt.jar、resources.jar等。
扩展类加载器:是用来加载jdk\jre\lib\ext下的扩展类库中的jar包和.class文件。
应用类加载器:则用来加载classpath下的jar包和.class文件。还有自定义加载器,也属于应用类加载器。

在这里插入图片描述
2.双亲委派机制
按照加载器的层级关系,逐层进行委派。
在这里插入图片描述
要加载一个类User.class,从低层级到高层级一级一级委派,先由应用层加载器委派给扩展类加载器,再由扩展类委派给启动类加载器;启动类加载器载入失败,再由扩展类加载器载入,扩展类加载器载入失败,最后由应用类加载器载入,如果应用类加载器也找不到那就报ClassNotFound异常了

双亲委派机制的优点:
1.保证安全性,层级关系代表优先级,也就是所有类的加载,优先给启动类加载器(不是全都给启动类加载哈,比如有些类可能在Application Class Loader加载过了,这时候发现在Application Class Loader加载过了,就不会再请求父类加载器加载了),这样就保证了核心类库类。
2.避免重复,如果父类加载器加载过了,子类加载器就没有必要再去加载了。

10.JVM内存模型

(本节内容摘录自JavaGuide,仅做记录使用,如有侵权,联系删除)
运行时数据区域
JDK 1.7
在这里插入图片描述
JDK 1.8 之后
在这里插入图片描述

线程私有的:

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

线程共享的:


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

Java 虚拟机规范对于运行时数据区域的规定是相当宽松的。以堆为例:堆可以是连续空间,也可以不连续。堆的大小可以固定,也可以在运行时按需扩展 。虚拟机实现者可以使用任何垃圾回收算法管理堆,甚至完全不进行垃圾收集也是可以的。

(1)程序计数器

程序计数器程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。从上面的介绍中我们知道了程序计数器主要有两个作用:字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。⚠️ 注意 :程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

(2)Java 虚拟机栈

与程序计数器一样,Java 虚拟机栈(后文简称栈)也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。栈绝对算的上是 JVM 运行时数据区域的一个核心,除了一些 Native 方法调用是通过本地方法栈实现的(后面会提到),其他所有的 Java 方法调用都是通过栈来实现的(也需要和其他运行时数据区域比如程序计数器配合)。方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。
在这里插入图片描述
局部变量表 主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

在这里插入图片描述
操作数栈 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。动态链接 主要服务一个方法需要调用其他方法的场景。在 Java 源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在 Class 文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用。

在这里插入图片描述

栈空间虽然不是无限的,但一般正常调用的情况下是不会出现问题的。不过,如果函数调用陷入无限循环的话,就会导致栈中被压入太多栈帧而占用太多空间,导致栈空间过深。那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。 Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。 除了 StackOverFlowError 错误之外,栈还可能会出现OutOfMemoryError错误,这是因为如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。简单总结一下程序运行中栈可能会出现两种错误:StackOverFlowError: 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。OutOfMemoryError: 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

(3)本地方法栈

和虚拟机栈所发挥的作用非常相似,区别是:** 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。

(4)堆

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。Java 世界中“几乎”所有的对象都在堆中分配,但是,随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。
在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:
新生代内存(Young Generation)
老生代(Old Generation)
永久代(Permanent Generation)
下图所示的 Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间一层属于老年代,最下面一层属于永久代。
在这里插入图片描述
JDK 8 版本之后 PermGen(永久) 已被 Metaspace(元空间) 取代,元空间使用的是直接内存 (我会在方法区这部分内容详细介绍到)。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

(5)方法区

方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,方法区到底要如何实现那就是虚拟机自己要考虑的事情了。也就是说,在不同的虚拟机实现上,方法区的实现是不同的。当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。方法区和永久代以及元空间是什么关系呢? 方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。
在这里插入图片描述

1、整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。当元空间溢出时会得到如下错误: java.lang.OutOfMemoryError: MetaSpace你可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
2、元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
3、在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。

(6)运行时常量池

Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的 常量池表(Constant Pool Table) 。字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量,符号引用包括类符号引用、字段符号引用、方法符号引用和接口方法符号引用。常量池表会在类加载后存放到方法区的运行时常量池中。运行时常量池的功能类似于传统编程语言的符号表,尽管它包含了比典型符号表更广泛的数据。既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。

11.JVM垃圾回收

(本节内容摘录自JavaGuide,仅做记录使用,如有侵权,联系删除)

1.垃圾回收

Java垃圾回收主要工作的区域是堆
JDK1.8之前
收集器基本都采用分代垃圾收集算法,所以 Java 堆被划分为了几个不同的区域,基于此,堆主要被划分为以下几块
1.新生代
2.老年代
3.永久代
而从1.8开始,永久代被元空间替代,作用是一致的

(1)一般来说(大对象直接进入永久代/元空间,配置了-XX:PretenureSizeThreshold,大于该值的对象会直接进入老年代),对象的产生从新生代开始,
(2)当新生代满了了以后,会把存活的对象复制到form(survivor区的1份中),如果放不下时,会直接进入老年代,
(3)在每经过一次gc后,会把存活的对象从from区复制到to区,并且其中的对象的生存时间加1,到了某个数值(如15)后进入老年代
(4)当老年代满了或者存放不下将要进入老年代的存活对象的时候,就会发生一次Full GC(这个是最需要避免的,因为耗时较为严重)
在这里插入图片描述

2.死亡对象判断

1.引用计数法
每当有一个地方引用它,计数器就加 1;
当引用失效,计数器就减 1;
任何时候计数器为 0 的对象就是不可能再被使用的。
2.可达性分析算法
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

3.垃圾回收算法

1.标记-清除算法
该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。

在这里插入图片描述

效率问题
空间问题(标记清除后会产生大量不连续的碎片)

2.标记-复制算法
为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
在这里插入图片描述

3.标记-整理算法
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
在这里插入图片描述

分代收集算法当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

12.死锁

1.死锁产生的条件
(1)互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
(2)请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
(4)环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
2.死锁的预防
(1)资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
(2)只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
(3)可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
(4)资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

13.双重检测单例

package test;

public class Goods {
    private static Goods goods = null;

    private Goods(){}

    public Goods getGoods(){
        if(goods==null){
          synchronized (Goods.class){
          //当一个对象已经创建时,锁释放,其他线程直接获取到了锁,进入到此处,此时就有一个问题就是他会再次创建一个对象
            if(goods==null){
                goods = new Goods();
            }
          }
        }
        return goods;
    }
}

以上内容整理自互联网,如有侵权,联系删除

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SinceThenLater

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值