大端是高字节存放到内存的低地址
小端是高字节存放到内存的高地址
假如现有一32位int型数0x12345678,那么其MSB(Most Significant Byte,最高有效字节)为0x12,其LSB (Least Significant Byte,最低有效字节)为0x78,在CPU内存中有两种存放方式:(假设从地址0x4000开始存放)
包括java字节序也是这样
java字节序
字节序分为两种:
BIG-ENDIAN—-大字节序
LITTLE-ENDIAN—-小字节序
BIG-ENDIAN就是最低地址存放最高有效字节。(对应大端)
LITTLE-ENDIAN是最低地址存放最低有效字节。(对应小端)
java字节序:JAVA虚拟机中多字节类型数据的存放顺序,JAVA字节序也是BIG-ENDIAN。
帧
在网络中,网络设备将“位”组成一个个的字节,然后这些字节“封装”成帧,在网络上传输。为什么要把数据“封装”成帧呢?因为用户数据一般都比较大,有的可以达到MB字节,一下子发送出去十分困难,于是就需要把数据分成许多小份,再按照一定的次序发送出去。
以太网的帧值总是在一定范围内浮动,最大的帧值是1518字节,最小的帧值是64字节。在实际应用中,帧的大小是由设备的MTU(最大传输单位)即设备每次能够传输的最大字节数自动来确定的。
帧是当计算机发送数据时产生的,确切地说,是由计算机中安装的网卡产生的。帧只对于能够识别它的设备才有意义。
数据在网络上是以很小的称为帧(Frame)的单位传输的,帧由几部分组成,不同的部分执行不同的功能。帧通过特定的称为网络驱动程序的软件进行成型,然后通过网卡发送到网线上,通过网线到达它们的目的机器,在目的机器的一端执行相反的过程。接收端机器的以太网卡捕获到这些帧,并告诉操作系统帧已到达,然后对其进行存储。就是在这个传输和接收的过程中,嗅探器会带来安全方面的问题 。
1字=2字节(1 word = 2 byte) ,1字节=8位(1 byte = 8bit) ,一个字的字长为16 ,一个字节的字长是8。
1、字节
字节来自英文Byte,音译为“拜特”,习惯上用大写的“B”表示。 字节是计算机中数据处理的基本单位。计算机中以字节为单位存储和解释信息,规定一个字节由八个二进制位构成,即1个字节等于8个比特(1Byte=8bit),八位二进制数最小为00000000,最大为11111111;通常1个字节可以存入一个ASCII码,2个字节可以存放一个汉字国标码。
2、位
来自英文bit,音译为“比特”,表示二进制位。位是计算机内部数据储存的最小单位,一个二进制位只可以表示0和1两种状态(21);两个二进制位可以表示00、01、10、11四种(22)状态;三位二进制数可表示八种状态(23),以此类推。
3、英文字母和中文
一般来说,字母占一个字节,中文占2个字节
TCP中的ACK确认序号一般是指定对方发送下一条信息的序号,比如服务端ACK=x+1,则代表客户端发送下一条信息的序号为x+1
TCP交互图:
四次挥手和三次握手的区别在于四次挥手时,服务端的断链请求和确认请求是分开发的,而三次握手时,服务端建立链接请求和确认请求是合在一起发送的
造成粘包和拆包现象的原因:
1、TCP 发送缓冲区剩余空间不足以发送一个完整的数据包,将发生拆包;
2、要发送的数据超过了最大报文长度的限制,TCP 传输数据时进行拆包;
3、要发送的数据包小于 TCP 发送缓冲区剩余空间,TCP 将多个数据包写满发送缓冲区一次发送出去,将发生粘包;
4、接收端没有及时读取 TCP 发送缓冲区中的数据包,将会发生粘包。
粘包拆包的解决方法:
1、发送端给数据包添加首部,首部中添加数据包的长度属性,这样接收端通过首部中的长度字段
就可以知道数据包的实际长度啦;
2、针对发送的数据包小于缓冲区大小的情况,发送端可以将不同的数据包规定成同样的长度,不
足这个长度的补充 0,接收端从缓冲区读取固定的长度数据这样就可以区分不同的数据包;
3、发送端通过给不同的数据包添加间隔符号确定边界,接收端通过这个间隔符号就可以区分不同
的数据包。
UDP 是基于数据包传输数据的,UDP 首部也记录了数据包的长度,可以轻易的区分出不同的数据包的边界,不会出现粘包和拆包现象
HTTP1.0、HTTP1.1、HTTP2.0的关系和区别:
在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连
接,任务结束就中断连接。
而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加
入这行代码:Connection:keep-alive
HTTP2.0的服务器推送,是针对静态资源,无法自定义数据
大多数聊天推送场景下还是会使用:短连接(心跳包测试)+ 长链接(接收消息) 的方式来实现。
长连接和短连接的优缺点?
长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间 。对于频繁请求资源的客户来说,较适用长连接。不过这里存在一个问题,存活功能的探测周期太长,还有就是它只是探测TCP连接的存活,属于比较斯文的做法,遇到恶意的连接时,保活功能就不够使了。在长连接的应用场景下,client端一般不会主动关闭它们之间的连接,Client与server之间的连接如果一直不关闭的话,会存在一个问题,随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接,这样可 以避免一些恶意连接导致server端服务受损;如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,这样可以完全避免某个蛋疼的客户端连累后端服务。
短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。但如
果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽。
cookie和session
cookie存放在浏览器,数据量小,不安全,只能存储字符串,可实现自动登录等功能
session存放在服务端,数据量没有限制,较为安全,存储结构类似于hashtable
的结构,可以存放任何类型,可实现购物车未登录状态下存放商品的功能
注:cookies过期是浏览器行为,cookies内信息过期是服务器行为,sessionId通常以cookie形式返回给客户端
直接缓冲区与非直接缓冲区:
非直接缓冲区 : 通过allocate()方法分配缓冲区,将缓冲区建立在JVM的内存中。
直接缓冲区 : 通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率。
在应用程序中开辟一个直接缓冲区是比较耗费资源的。还有一点:应用程序将文件写入直接缓冲区后,这个文件的数据就不归应用程序所管了。至于直接缓冲区中的数据在何时写入到磁盘中,那就由操作系统决定了。在销毁的时候 需要断开应用程序和 物理内存之间的引用,然后让垃圾回收机制进行回收,这个也是比较耗资源的。
内存映射( Memory mapped I/O )是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件(修改内存的同时,磁盘文件也会发生相同修改)。
ACID:atomicity, consistency, isolation, and durability 原子性、一致性、隔离性和持久性
实现跨域的方式:JSONP方式、CORS方式、代理方式
1、jsonp方式:script、img、iframe、link、video、audio 等带有 src 属性的标签可以跨域请求和执行资源,jsonp 利用这一点“漏洞”实现跨域。
jsonp 实现跨域很简单但是只支持 GET 请求方式。而且在服务器端接受到 JSONP 请求后需要设置请求头,添加 Access-Control-Allow-Origin 属性,属性值为 * ,表示允许所有域名访问,这样浏
览器才会正常解析,否则会报 406 错误。
2、cors(Cross-Origin Resource Sharing)方式:即跨域资源共享,需要浏览器和服务器同时支持,这种请求方式分为简单请求和非简单请求。
注:简单请求需要满足以下条件:
1、只使用以下HTTP方法之一:GET、HEAD或POST。
2、只使用以下HTTP头部:Accept、Accept-Language、Content-Language、Content-Type。
3、Content-Type的值仅限于:application/x-www-form-urlencoded、multipart/form-data或text/plain。
如果一个跨域请求不满足以上所有条件,那么它被认为是非简单请求。对于非简单请求,浏览器会在实际请求(例如PUT
、DELETE
、PATCH
或具有自定义头部和其他Content-Type
的POST
请求)之前发送OPTIONS
请求(预检请求)。
对于简单的请求,浏览器会在请求头中添加 Origin 属性,标明本次请求来自哪个源(协议 + 域名 +端口)。
另外一种是非简单请求,这种请求在浏览器正式发出 XMLHttpRequest 请求前会先发送一个预检 HTTP 请求,询问服务器当前网页的域名是否在服务器的许可名单之中,只有得到服务器的肯定后才会正式发出通信请求。如果服务器回应预检请求的响应头中没有任何 CORS 相关的头信息的话表示不支持跨域,如果允许跨域就会做出响应,接着浏览器会像简单请求一样,发送一个 CORS 请求,请求头中一定包含 Origin 属性,服务器的响应头中也一定得包含 Access-Control-Allow-Origin 属性
3、代理方式
跨域限制是浏览器的同源策略导致的,使用 nginx 当做服务器访问别的服务的 HTTP 接口是不需要执行 JS 脚步不存在同源策略限制的,所以可以利用 Nginx 创建一个代理服务器,这个代理服务器的域名跟浏览器要访问的域名一致,然后通过这个代理服务器修改 cookie 中的域名为要访问的 HTTP接口的域名,通过反向代理实现跨域。
JVM结构(包括内存结构)如下图:
JVM包括类加载器、堆、栈、方法区(常量池在方法区中)、程序计数器、本地方法栈 等
1、栈(java栈也称为虚拟机栈):
Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、
指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、
**方法返回地址(Return Address)**和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。
操作数栈:
操作数栈是弹栈/压栈来访问。操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区。数据运算的地方,大多数指令都在操作数栈弹栈运算,然后结果压栈。java虚拟机栈是方法调用和执行的空间,每个方法会封装成一个栈帧压入占中。其中里面的操作数栈用于进行运算,当前线程只有当前执行的方法才会在操作数栈中调用指令(可见java虚拟机栈的指令主要取于操作数栈)。
2、堆:
Java中的堆是用来存储对象本身的以及数组(数组引用是存放在Java栈中的)。堆是被所有线程共享的,在JVM中只有一个堆。
3、方法区:
与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
方法区逻辑上属于堆的一部分,但是为了与 堆
进行区分,通常又叫 非堆
在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。
在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。
年轻代
年轻代又划分为Eden空间和Survivor空间
PermGen(永久代)
PermGen , 就是 PermGen space ,全称是 Permanent Generation space ,是指内存的永久保存区域。这块内存主要是被JVM存放Class和Meta信息的, Class 在被 Loader 时就会被放到 PermGen space 中。
- 方法区 是 JVM 的规范,所有虚拟机 必须遵守的。常见的JVM 虚拟机 Hotspot 、 JRockit(Oracle)、J9(IBM)
- PermGen space 则是 HotSpot 虚拟机 基于 JVM 规范对 方法区 的一个落地实现, 并且只有 HotSpot 才有 PermGen space。而如 JRockit(Oracle)、J9(IBM) 虚拟机有 方法区 ,但是就没有 PermGen space。PermGen space 是 JDK7及之前, HotSpot 虚拟机 对 方法区 的一个落地实现。在JDK8被移除。
- Metaspace(元空间)是 JDK8及之后,废弃了 PermGen space ,取而代之的是 Metaspace , 这是 HotSpot 虚拟机 对 方法区 的新的落地实现。
Metaspace(元空间)和 PermGen(永久代)类似,都是对 JVM规范中方法区的一种落地实现。
不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
JDK版本 | 方法区的实现 | 运行时常量池所在的位置 |
JDK6 | PermGen space(永久代) | PermGen space(永久代) |
JDK7 | PermGen space(永久代) | Heap(堆) |
JDK8 | Metaspace(元空间) | Heap(堆) |
JVM内存溢出的情况
java类的符号引用和直接引用
Java类从加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括,加载 ,验证 , 准备 , 解析 , 初始化 , 卸载 ,总共七个阶段。其中验证 ,准备 , 解析 统称为连接。
而在解析阶段会有一个步将常量池当中二进制数据当中的符号引用转化为直接引用的过程。
符号引用 :符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。个人理解为:在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,多以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。
JVM加载class文件的原理机制
类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。 类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个类加载器的说明:
- Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
- Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;(jre/lib/ext)
- System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。(一般都是自己编写的代码)
双亲委派机制中类加载器的关系如下:
双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换。
代码层面的类加载器关系如下:
从图可以看出顶层的类加载器是ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器),这里我们主要介绍ClassLoader中几个比较重要的方法。
loadClass(String)
该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先从缓存查找该class对象,找到就不用重新加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果找不到,则委托给父类加载器去加载
c = parent.loadClass(name, false);
} else {
//如果没有父类,则委托给启动加载器去加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// 如果都没有找到,则通过自定义实现的findClass去查找并加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {//是否需要在加载时进行解析
resolveClass(c);
}
return c;
}
}
loadClass方法所展示的,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载(关于findClass()稍后会进一步介绍)。从loadClass实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass("className"),这样就可以直接调用ClassLoader的loadClass方法获取到class对象。
findClass(String)
在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的(稍后会分析),ClassLoader类中findClass()方法源码如下:
//直接抛出异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
defineClass(byte[] b, int off, int len)
defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象,简单例子如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类的字节数组
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//使用defineClass生成class对象
return defineClass(name, classData, 0, classData.length);
}
}
需要注意的是,如果直接调用defineClass()方法生成类的Class对象,这个类的Class对象并没有解析(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段进行。
resolveClass(Class≺?≻ c)
使用该方法可以使用类的Class对象创建完成也同时被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。
Java自带的加载器加载的类,在虚拟机的生命周期中是不会被卸载的,只有用户自定义的加载器加载的类才可以被卸.
即时编译器(JIT)和解释器
解释器的执行,抽象的看是这样的:输入的代码 -> [ 解释器 解释执行 ] -> 执行结果
而要JIT编译然后再执行的话,抽象的看则是:输入的代码 -> [ 编译器 编译 ] -> 编译后的代码 -> [ 执行 ] -> 执行结果
即时编译建立在程序符合二八定律的假设上,也就是百分之二十的代码占据了百分之八十的计算资源。
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,下次可以重复调用,以达到理想的运行速度。
说JIT比解释快,其实说的是“执行编译后的代码”比“解释器解释执行”要快,并不是说“编译”这个动作比“解释”这个动作快。JIT编译再怎么快,至少也比解释执行一次略慢一些,而要得到最后的执行结果还得再经过一个“执行编译后的代码”的过程。所以,对“只执行一次”的代码而言,解释执行其实总是比JIT编译执行要快。怎么算是“只执行一次的代码”呢?粗略说,下面两个条件同时满足时就是严格的“只执行一次”
1、只被调用一次,例如类的构造器(class initializer,<clinit>())
2、没有循环
对只执行一次的代码做JIT编译再执行,可以说是得不偿失。对只执行少量次数的代码,JIT编译带来的执行速度的提升也未必能抵消掉最初编译带来的开销。只有对频繁执行的代码,JIT编译才能保证有正面的收益。
对一般的Java方法而言,编译后代码的大小相对于字节码的大小,膨胀比达到10x是很正常的。同上面说的时间开销一样,这里的空间开销也是,只有对执行频繁的代码才值得编译,如果把所有代码都编译则会显著增加代码所占空间,导致“代码爆炸”。这也就解释了为什么有些JVM会选择不总是做JIT编译,而是选择用解释器+JIT编译器的混合执行引擎。
Java的执行过程整体可以分为两个部分,第一步由javac将源码编译成字节码,在这个过程中会进行词法分析、语法分析、语义分析,编译原理中这部分的编译称为前端编译。接下来无需编译直接逐条将字节码解释执行,在解释执行的过程中,虚拟机同时对程序运行的信息进行收集,在这些信息的基础上,编译器会逐渐发挥作用,它会进行后端编译——把字节码编译成机器码,但不是所有的代码都会被编译,只有被JVM认定为的热点代码,才可能被编译。
图中IR为Intermediate Representation的缩写,意思为中间表达形式
JVM中集成了两种编译器,Client Compiler和Server Compiler,它们的作用也不同。Client Compiler注重启动速度和局部的优化,Server Compiler则更加关注全局的优化,性能会更好,但由于会进行更多的全局分析,所以启动速度会变慢。两种编译器有着不同的应用场景,在虚拟机中同时发挥作用。
HotSpot VM带有一个Client Compiler C1编译器。这种编译器启动速度快,但是性能比较Server Compiler来说会差一些。
在Hotspot VM中,默认的Server Compiler是C2编译器。
一、中间表达形式
在编译原理中,通常把编译器分为前端和后端,前端编译经过词法分析、语法分析、语义分析生成中间表达形式(Intermediate Representation,以下称为IR),后端会对IR进行优化,生成目标代码。
Java字节码就是一种IR,但是字节码的结构复杂,字节码这样代码形式的IR也不适合做全局的分析优化。现代编译器一般采用图结构的IR,静态单赋值(Static Single Assignment,SSA)IR是目前比较常用的一种。这种IR的特点是每个变量只能被赋值一次,而且只有当变量被赋值之后才能使用。
SSA IR的作用:1、去除冗余的赋值(未使用到的)2、删除和优化死代码(永远不会走到的代码)
C1编译器优化大部分都是在HIR之上完成的。当优化完成之后它会将HIR转化为LIR,LIR和HIR类似,也是一种编译器内部用到的IR,HIR通过优化消除一些中间节点就可以生成LIR,形式上更加简化。
Sea-of-Nodes IR
C2编译器中的Ideal Graph采用的是一种名为Sea-of-Nodes中间表达形式,同样也是SSA形式的。它最大特点是去除了变量的概念,直接采用值来进行运算。
Phi And Region Nodes
Phi Nodes中保存不同路径上包含的所有值,Region Nodes根据不同路径的判断条件,从Phi Nodes取得当前执行路径中变量应该赋予的值。
Global Value Numbering(GVN)
Global Value Numbering(GVN) 是一种因为Sea-of-Nodes变得非常容易的优化技术 。
可以将GVN理解为在IR图上的公共子表达式消除(Common Subexpression Elimination,CSE)。两者区别在于,GVN直接比较值的相同与否,而CSE是借助词法分析器来判断两个表达式相同与否。
二、方法内联
方法内联,是指在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。JIT大部分的优化都是在内联的基础上进行的,方法内联是即时编译器中非常重要的一环。
方法内联的条件
编译器的大部分优化都是在方法内联的基础上。所以一般来说,内联的方法越多,生成代码的执行效率越高。但是对于即时编译器来说,内联的方法越多,编译时间也就越长,程序达到峰值性能的时刻也就比较晚。
虚函数内联
关于虚函数:Java中其实没有虚函数的概念,它的普通函数就相当于C++的虚函数,动态绑定是Java的默认行为。如果Java中不希望某个函数具有虚函数特性,可以加上final关键字变成非虚函数。
C++虚函数 == Java普通函数(方法)
C++纯虚函数 == Java抽象函数(方法)
C++抽象类 == Java抽象类
C++虚基类 == Java接口
三、逃逸分析(不逃逸的话会进行优化)
逃逸分析是“一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针”。Java虚拟机的即时编译器会对新建的对象进行逃逸分析,判断对象是否逃逸出线程或者方法。即时编译器判断对象是否逃逸的依据有两种:
- 对象是否被存入堆中(静态字段或者堆中对象的实例字段),一旦对象被存入堆中,其他线程便能获得该对象的引用,即时编译器就无法追踪所有使用该对象的代码位置。
- 对象是否被传入未知代码中,即时编译器会将未被内联的代码当成未知代码,因为它无法确认该方法调用会不会将调用者或所传入的参数存储至堆中,这种情况,可以直接认为方法调用的调用者以及参数是逃逸的。
Java 对象由低到高的逃逸程度即为1、不逃逸2、方法逃逸3、线程逃逸
锁消除
在学习Java并发编程时会了解锁消除,而锁消除就是在逃逸分析的基础上进行的。
如果即时编译器能够证明锁对象不逃逸,那么对该锁对象的加锁、解锁操作没就有意义。因为线程并不能获得该锁对象。在这种情况下,即时编译器会消除对该不逃逸锁对象的加锁、解锁操作。实际上,编译器仅需证明锁对象不逃逸出线程,便可以进行锁消除。包括相关的synchronized
同步锁可以将其消除。
栈上分配
我们都知道Java的对象是在堆上分配的,而堆是对所有对象可见的。同时,JVM需要对所分配的堆内存进行管理,并且在对象不再被引用时回收其所占据的内存。如果逃逸分析能够证明某些新建的对象不逃逸,那么JVM完全可以将其分配至栈上,并且在new语句所在的方法退出时,通过弹出当前方法的栈桢来自动回收所分配的内存空间。这样一来,我们便无须借助垃圾回收器来处理不再被引用的对象。不过Hotspot虚拟机,并没有进行实际的栈上分配,而是使用了标量替换这一技术。
所谓的标量,就是仅能存储一个值的变量,比如Java代码中的基本类型。与之相反,聚合量则可能同时存储多个值,其中一个典型的例子便是Java的对象。编译器会在方法内将未逃逸的聚合量分解成多个标量,以此来减少堆上分配。
四、Loop Transformations
C2编译器在构建Ideal Graph后会进行很多的全局优化,其中就包括对循环的转换,最重要的两种转换就是循环展开和循环分离。
循环展开
循环展开通过减少或消除控制程序循环的指令,来减少计算开销,这种开销包括增加指向数组中下一个索引或者指令的指针算数等。如果编译器可以提前计算这些索引,并且构建到机器代码指令中,那么程序运行时就可以不必进行这种计算。也就是说有些循环可以写成一些重复独立的代码。
public void loopRolling(){
for(int i = 0;i<200;i++){
delete(i);
}
}
上面的代码循环展开以后得到:
public void loopRolling(){
for(int i = 0;i<200;i+=5){
delete(i);
delete(i+1);
delete(i+2);
delete(i+3);
delete(i+4);
}
}
这样展开就可以减少循环的次数,每次循环内的计算也可以利用CPU的流水线提升效率。当然这只是一个示例,实际进行展开时,JVM会去评估展开带来的收益,再决定是否进行展开。
循环分离
把循环中一次或多次的特殊迭代分离出来,在循环外执行。
int a = 10;
for(int i = 0;i<10;i++){
b[i] = x[i] + x[a];
a = i;
}
可以看出这段代码除了第一次循环a = 10以外,其他的情况a都等于i-1。所以可以把特殊情况分离出去,变成下面这段代码:
b[0] = x[0] + 10;
for(int i = 1;i<10;i++){
b[i] = x[i] + x[i-1];
}
这种等效的转换消除了在循环中对a变量的需求,从而减少了开销。
五、窥孔优化与寄存器分配
窥孔优化是优化的最后一步,这之后就会程序就会转换成机器码,窥孔优化就是将编译器所生成的中间代码(或目标代码)中相邻指令,将其中的某些组合替换为效率更高的指令组,常见的比如强度削减、常数合并等,看下面这个例子就是一个强度削减的例子:
强度削减
y1=x1*3 经过强度削减后得到 y1=(x1<<1)+x1
编译器使用移位和加法削减乘法的强度,使用更高效率的指令组。
寄存器分配也是一种编译的优化手段,在C2编译器中普遍的使用。它是通过把频繁使用的变量保存在寄存器中,CPU访问寄存器的速度比内存快得多,可以提升程序的运行速度。
寄存器分配和窥孔优化是程序优化的最后一步。经过寄存器分配和窥孔优化之后,程序就会被转换成机器码保存在codeCache中。
JVM回收
垃圾回收器有以下这些:
- Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
- ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
- Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
- Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
- CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
- G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。(新生代基于复制算法)
- ZGC(Z Garbage Collector)是一款由Oracle公司研发的,以低延迟为首要目标的一款垃圾收集器。它是基于动态Region内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的收集器。在JDK 11新加入,还在实验阶段,主要特点是:回收TB级内存(最大4T),停顿时间不超过10ms。优点:低停顿,高吞吐量,ZGC收集过程中额外耗费的内存小。缺点:浮动垃圾(新生代基于复制算法)
新生代收集器:Serial、ParNew、Parallel Scavenge 新生代的算法基本都基于复制算法
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器:G1,ZGC(因为不涉年代不在图中)。
CMS全称 Concurrent Mark Sweep(并发标记清扫)
CMS工作地点在老年代,ParNew为了配合CMS一起使用主要解决CMS不能收集新生代的问题,ParNew的工作地点在新生代。
在Java语言里,可作为GC Roots对象的包括如下几种:
1. 虚拟机栈(栈桢中的本地变量表)中的引用的对象 ;
2. 方法区中的类静态属性引用的对象 ;
3. 方法区中的常量引用的对象 ;
4. 本地方法栈中JNI的引用的对象;
CMS 处理过程有七个步骤:
1. 初始标记(CMS-initial-mark) ,会导致swt;(找出存活的对象,标记一下GC Roots能直接关联到的对象,大量标记)
2. 并发标记(CMS-concurrent-mark),与用户线程同时运行;(根据存活的对象找其相关引用的对象以及对在此期间新生代和老年代发生变化的对象和对象引用进行标记,Card标识为Dirty)
3. 预清理(CMS-concurrent-preclean),与用户线程同时运行;
4. 可被终止的预清理(CMS-concurrent-abortable-preclean) 与用户线程同时运行;
5. 重新标记(CMS-remark) ,会导致swt;
6. 并发清除(CMS-concurrent-sweep),与用户线程同时运行;
7. 并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;
注:Minor GC发生在Eden区;Young GC发生在Eden、S0、S1区;Major GC发生在Old区
上面的card是指卡表,卡表是记录一块内存区域是否有对象含有跨代指针,通过write barrier来维护卡表状态
write barrier 写屏障就是对一个对象引用进行写操作(即引用赋值)之前或之后附加执行的逻辑,相当于为引用赋值挂上的一小段钩子代码。
跨代引用
定义:新生代中的对象持有了老年代中的对象的引用 或 老年代中的对象持有了新生代中对象的引用。
对象引用标记算法
1、引用计数法 缺点:无法解决对象循环引用问题。
2、可达性分析法
这个算法首先需要按照规则查找当前活跃的引用,将其称为 GC Roots
。接着将 GC Roots
作为根节点出发,遍历对象引用关系图,将可以遍历(可达)的对象标记为存活,其余对象当做无用对象。
主流GC算法:
1、标记-清除 算法
2、复制算法
3、标记-整理 算法
标记-清除算法
这是一个最为基础也是最容易实现的算法,主要实现步骤分为两步:标记,清除。
- 标记:通过上述
GC Roots
标记出可达对象。 - 清除:清理未标记对象。
缺点:虽然堆空间被清理出来,但是也产生很多空间碎片。效率比较低。这就竟会导致 GC
占用时间过长,影响正常程序使用。
复制算法
为了解决上述效率问题,诞生复制算法。这个算法将可用内存分为两块,每次只使用其中一块,当这一块内存使用完毕,触发 GC
,将会把存活的对象依次复制到另外一块上,然后再把已使用过的内存一次性清理。
另外对象存活率也会影响复制算法效率。如果对象大部分都是朝生夕死,只需要移动少量存活对象,就能腾出大部分空间。反而如果对象存活率高,这就需要进行较多的复制操作,回收之后也并没有多余内存,这就可能导致频繁触发 GC
。
缺点:空间开销大
CMS新生代的Young GC、G1和ZGC都基于标记-复制算法,但算法具体实现的不同就导致了巨大的性能差异。标记-复制算法应用在CMS新生代(ParNew是CMS默认的新生代垃圾回收器)和G1垃圾回收器中。
标记-整理算法(也叫标记-压缩)
标记-整理算法可以说是标记-清除算法的改进版,改进了清除导致的空间碎片问题。这个算法分为两步:
- 标记:也是通过
GC Roots
标记存活对象。 - 整理:将存活对象往一端移动,按照内存地址一次排序,然后将末端边界之外内存直接清理。
缺点:虽然标记-整理算法解决了标记-清除算法空间碎片问题,也完整利用整个内存空间,但是这个算法问题效率并不高。相较于标记-清除算法,标记-整理算法多增加整理这一步,所以该算法效率还低于标记-清除算法。
从上面三种 GC
算法可以看到,并没有一种空间与时间效率都是比较完美的算法,所以只能做的是综合利用各种算法特点将其作用到不用的内存区域。
新生代比较适合复制算法,老年代适合使用标记-清除或标记-整理算法。
STAB三色标记法
将所有对象分为三种颜色:
- 白色:没有检查
- 灰色:自身被检查了,成员没被检查完(可以认为访问到了,但是正在被检查,就是图的遍历里那些在队列中的节点)
- 黑色:自身和成员都被检查完了
漏标问题的产生条件(两个条件同时存在的时候产生,漏标的白色对象回收可能导致JVM crash):
1.灰色不再指向白色
2.黑色指向这个白色
CMS是从条件2入手搞的;G1是从条件1入手搞的;
CMS:采用IncrementalUpdate(增量更新)算法,在并发标记阶段时如果一个白色对象被一个黑色对象引用时,会将黑色对象重新标记为灰色,让垃圾收集器在重新标记阶段重新扫描。
G1:采用SATB(snapshot-at-the-beginning),在初始标记时做一个快照,当B和C之间的引用消失时要把这个引用推到GC的堆栈,保证C还能被GC扫描到,在最终标记阶段扫描STAB记录。
对象分配规则
- 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
- 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
- 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
MESI协议(缓存一致性协议)
MESI中每个缓存行都有四个状态,分别是E(exclusive)、M(modified)、S(shared)、I(invalid)。下面我们介绍一下这四个状态分别代表什么意思。
M:代表该缓存行中的内容被修改了,并且该缓存行只被缓存在该CPU中。这个状态的缓存行中的数据和内存中的不一样,在未来的某个时刻它会被写入到内存中(当其他CPU要读取该缓存行的内容时。或者其他CPU要修改该缓存对应的内存中的内容时(个人理解CPU要修改该内存时先要读取到缓存中再进行修改),这样的话和读取缓存中的内容其实是一个道理)。
E:E代表该缓存行对应内存中的内容只被该CPU缓存,其他CPU没有缓存该缓存对应内存行中的内容。这个状态的缓存行中的内容和内存中的内容一致。该缓存可以在任何其他CPU读取该缓存对应内存中的内容时变成S状态。或者本地处理器写该缓存就会变成M状态。
S:该状态意味着数据不止存在本地CPU缓存中,还存在别的CPU的缓存中。这个状态的数据和内存中的数据是一致的。当有一个CPU修改该缓存行对应的内存的内容时会使该缓存行变成 I 状态。
I:代表该缓存行中的内容时无效的。
内存屏障、读写屏障 待补充
内存屏障也叫内存栅栏
内存屏障分为四种:
StoreStore屏障、StoreLoad屏障、LoadLoad屏障、LoadStore屏障。
Store相当于是写屏障,Load相当于是读屏障。
写屏障就是对一个对象引用进行写操作(即引用赋值)之前或之后附加执行的逻辑,相当于为引用赋值挂上的一小段钩子代码。读屏障同理。
Volatile的底层实现原理就是内存屏障。
HashMap的数据结构:
HashMap 就是以 Key-Value 的方式进行数据存储的一种数据结构,在我们平常开发中非常常用,它在 JDK 1.7 和 JDK 1.8 中底层数据结构是有些不一样的。总体来说,JDK 1.7 中 HashMap 的底层数据结构是数组 + 链表,使用 Entry 类存储 Key 和 Value;JDK 1.8 中 HashMap 的底层数据结构是数组 + 链表/红黑树,使用 Node 类存储 Key 和 Value。
LinkedHashMap:
LinkedHashMap重写了它的父类HashMap的addEntry和createEntry方法。当要插入一个键值对的时候,首先会调用它的父类HashMap的put方法。在put方法中会去检查一下哈希表中是不是存在了对应的key,如果存在了就直接替换它的value就行了,如果不存在就调用addEntry方法去新建一个Entry。注意,这时候就调用到了LinkedHashMap自己的addEntry方法。我们看到上面的代码,这个addEntry方法除了回调父类的addEntry方法之外还会调用removeEldestEntry去移除最老的元素,这步操作主要是为了实现LRU算法,下面会讲到。我们看到LinkedHashMap还重写了createEntry方法,当要新建一个Entry的时候最终会调用这个方法,createEntry方法在每次将Entry放入到哈希表之后,就会调用addBefore方法将当前结点插入到双向链表的尾部。这样双向链表就记录了每次插入的结点的顺序,获取元素的时候只要遍历这个双向链表就行了,下图演示了每次调用addBefore的操作。由于是双向链表,所以将当前结点插入到头结点之前其实就是将当前结点插入到双向链表的尾部。
String、StringBuffer和StringBuilder的异同:
相同点:底层都是通过char数组实现的
不同点:
String对象一旦创建,其值是不能修改的,如果要修改,会重新开辟内存空间来存储修改之后的对象;而StringBuffer和StringBuilder对象的值是可以被修改的;
StringBuffer几乎所有的方法都使用synchronized实现了同步,线程比较安全,在多线程系统中可以保证数据同步,但是效率比较低;而StringBuilder 没有实现同步,线程不安全,在多线程系统中不能使用 StringBuilder,但是效率比较高。
如果我们在实际开发过程中需要对字符串进行频繁的修改,不要使用String,否则会造成内存空间的浪费;当需要考虑线程安全的场景下使用 StringBuffer,如果不需要考虑线程安全,追求效率的场景下可以使用 StringBuilder。
ByteBuffer 与 StringBuffer的区别:
ByteBuffer 是 NIO 中的一个类,是用于读写字节数据的缓冲区。它的主要作用是提高 I/O 操作的效率,避免每次读写操作都要从磁盘或网络中读写数据,而是将数据先缓存到内存中,等到需要时再进行读写操作。与 StringBuffer 不同,ByteBuffer 中存储的是二进制数据,而非字符数据,因此适用于处理图片、音视频等二进制数据。
StringBuffer 是 Java 中用于处理字符串的一个类,它提供了对字符串的插入、删除、替换等操作,适用于处理文本数据。与 ByteBuffer 不同,StringBuffer 中存储的是字符数据,而非二进制数据,因此适用于处理文本数据。
总的来说,ByteBuffer 适用于处理二进制数据,而 StringBuffer 适用于处理字符串数据。它们的作用和使用场景不同,应根据实际情况选择合适的类来处理数据。
java.util.Date 与 java.sql.Date 有什么区别:
1、java.sql.Date是java.util.Date的子类
2、java.sql.Date类没有时分秒,只有年月日
3、java.util.Date类转java.sql.Date类,向下转型,需要调用java.util.Date类的getTime()方法,取得long类型返回值,作为参数转换。
4、java.sql.Date类转java.util.Date类,向上转型,会自动转换,但是数值我们可以很明显的看到,也没有了时分秒。
5、具体使用根据业务需求决定,有的业务时间不要精确到时分秒,有的需要,根据需求使用相应的时间类。
Junit中的@Before、@After、@BeforeClass以及@AfterClass 的区别
@Before:所有测试用例都会各执行一次@Before部分的代码。(每个用例执行的时候都要执行)
@Beforeclass: 在类中只会被执行一次(总共就一次)
@After:释放资源 对于每一个测试方法都要执行一次(每个用例执行的时候都要执行)
@Afterclass:所有测试用例执行完才执行一次(总共就一次)
PowerMock是基于Junit的,用于测试静态方法和私有方法
Comparable和Comparator:
package java.lang;
import java.util.*;
public interface Comparable<T> {
public int compareTo(T o);
}
package java.util;
public interface Comparator<T>
{
int compare(T o1, T o2);
boolean equals(Object obj);
}
Comparable是排序接口,若一个类实现了Comparable接口,就意味着“该类支持排序”。而Comparator是比较器,我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。
Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。
两种方法各有优劣, 用Comparable 简单, 只要实现Comparable 接口的对象直接就成为一个可以比较的对象,但是需要修改源代码。 用Comparator 的好处是不需要修改源代码, 而是另外实现一个比较器, 当某个自定义的对象需要作比较的时候,把比较器和对象一起传递过去就可以比大小了, 并且在Comparator 里面用户可以自己实现复杂的可以通用的逻辑,使其可以匹配一些比较简单的对象,那样就可以节省很多重复劳动了。
Comparable中的compareTo和Comparator中的compare这两个方法的返回说明:
o1和o2在Comparable中的compareTo中分别代表本类对象o1和传入对象o2
如果要按照升序排序,则o1 小于o2,返回-1(负数),相等返回0,o1大于o2返回1(正数)
如果要按照降序排序,则o1 小于o2,返回1(正数),相等返回0,o1大于o2返回-1(负数)
- return 0:不交换位置,不排序
- return 1:交换位置
- return -1:不交换位置
- return o1-o2:升序排列
- return o2-o1:降序排列
compare的底层源码:
//----------------------------Collections.sort------------------------------------------
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}
//----------------------------legacyMergeSort ------------------------------------------
private static <T> void legacyMergeSort(T[] a, Comparator<? super T> c) {
T[] aux = a.clone();
if (c==null)
mergeSort(aux, a, 0, a.length, 0);
else
mergeSort(aux, a, 0, a.length, 0, c);
}
//----------------------------mergeSort ------------------------------------------
private static void mergeSort(Object[] src,
Object[] dest,
int low, int high, int off,
Comparator c) {
int length = high - low;
// Insertion sort on smallest arrays
if (length < INSERTIONSORT_THRESHOLD) {
for (int i=low; i<high; i++)
for (int j=i; j>low && c.compare(dest[j-1], dest[j])>0; j--)
swap(dest, j, j-1);
return;
}
// Recursively sort halves of dest into src
int destLow = low;
int destHigh = high;
low += off;
high += off;
int mid = (low + high) >>> 1;
mergeSort(dest, src, low, mid, -off, c);
mergeSort(dest, src, mid, high, -off, c);
// If list is already sorted, just copy from src to dest. This is an
// optimization that results in faster sorts for nearly ordered lists.
if (c.compare(src[mid-1], src[mid]) <= 0) {
System.arraycopy(src, low, dest, destLow, length);
return;
}
// Merge sorted halves (now in src) into dest
for(int i = destLow, p = low, q = mid; i < destHigh; i++) {
if (q >= high || p < mid && c.compare(src[p], src[q]) <= 0)
dest[i] = src[p++];
else
dest[i] = src[q++];
}
}
//*****************************************重点*************************************************
for (int j=i; j>low && c.compare(dest[j-1], dest[j])>0; j--)
swap(dest, j, j-1);
把上面的 return o1-o2和return o2-o1套进compare源码中的c.compare(dest[j-1], dest[j])>0来看,应该能比较好的理解了
如果返回负数,第一个参数放前面;
按照官方默认来看:
当前值<传递过来的值,返回-1,则返回小的那个数放在前边,这样就是升序排列
同样当当前值>传递过来的值是返回-1的话,大的那个数就在前边,这样就是降序排列了
这样我们不管降序升序只要保证返回是-1的那个条件符合你的需求就行了。
关于命令执行展示出的内容过多,可以通过将内容导入文件的方式来查看全部内容,例如jstack pid命令展示出很多堆栈信息,可以通过jstack -l pid >> 123.txt 将堆栈信息导入123.txt中
countdownlatch、cyclicbarrier、Semaphore区别
countdownlatch和cyclicbarrier都是thread挡板的作用,即线程达到一定数量开始执行后续操作,不同点在于cyclicbarrier可以复用,countdownlatch是一次性的
Semaphore作用限制thread的执行个数,超过执行个数后,thread会进行阻塞
Executors、threadpoolexecutor:
Executors是基于threadpoolexecutor来做实现的,一般推荐直接使用threadpoolexecutor来对thread池进行自定义操作和编写具体功能
在 Java 中使用线程的最佳实践:
- 避免使用 Thread 构造函数创建线程:使用 Thread 构造函数创建线程会使代码缺乏可读性和可维护性,并且可能会导致资源泄漏。相反,建议使用 ExecutorService 或者 ThreadPoolExecutor 来创建线程。
- 使用 Callable 接口取代 Runnable 接口:Callable 接口可以返回一个结果或者抛出一个异常,而 Runnable 接口只能执行任务而无法返回结果。如果需要返回结果,则应该使用 Callable 接口。
- 使用 join() 方法等待线程完成:如果需要在主线程中等待某个子线程完成后再继续执行后面的代码,可以使用 join() 方法来等待线程完成。
- 避免使用 synchronized 关键字:synchronized 关键字可以帮助同步多个线程之间的共享状态,但是它可能导致死锁、性能下降等问题。推荐使用 java.util.concurrent 包中提供的并发类来处理多线程并发问题。
- 不要滥用线程:创建过多的线程会导致系统资源消耗过大,严重时可能会导致系统崩溃。因此,在使用线程时,应该根据具体情况进行合理的优化和调整。
- 使用 volatile 关键字确保线程安全:volatile 可以保证变量在线程之间的可见性和有序性,从而避免出现线程安全问题。
- 使用 ThreadLocal 类来保证线程安全:ThreadLocal 可以为每个线程创建一个独立的变量副本,从而避免多个线程之间的竞争条件。
- 使用锁(Lock)机制代替 synchronized 关键字:锁机制能够更加灵活地控制多线程之间的同步问题,并且可以提供更好的性能和可伸缩性。
hashCode作用:
它返回的就是根据对象的内存地址换算出的一个值。这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。(equals方法比较消耗性能和时间)
equals和==的区别:
在Object类中,两者的作用是相同的,都代表比较两个对象是否相等,比较的两个引用变量指向同一个对象(即指向同一个地址),一般在object子类中,可能会重写equals方法,来比较对象的内容是否相同
== 的作用:
基本类型:比较的就是值是否相同
引用类型:比较的就是地址值是否相同
equals 的作用:
引用类型:默认情况下,比较的是地址值,重写该方法后比较对象的成员变量值是否相同
浅拷贝和深拷贝
浅拷贝
- 对于数据类型是基本数据类型及string类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。“string”属于Java中的字符串类型,也是一个引用类型,并不属于基本的数据类型。
- 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值
深拷贝
- 复制对象的所有基本数据类型的成员变量值
- 为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象(包括对象的引用类型)进行拷贝
要实现序列化,需要让一个类实现 Serializable 接口,序列化除了能够实现对象的持久化之外,还能够用于对象的深度克隆,对于不想进行序列化的变量,使用 transient 关键字修饰
Serializable序列转化性能较差
gc和finalize:
jvm gc机制会自动调用object的finalize方法