JVM调优
GC调优步骤:
1>.打印GC日志
-xx:+PrintGCDetails -xx:+PrintGCTimeStamps -xx:+PrintGCDateStamps -xloggc:./gc.log
Tomcat可以直接夹在JAVA_OPTS变量里
2>分析日志得到关键性指标
判断这个项目为什么产生FullGC,为什么需要去调优
3>分析GC原因,调优JVM参数
1.Parallel Scavenge收集器(默认)
分析parallel-gc.log
第一次调优,设置Metaspace,增大元空间大小
-XX:MetaspaceSize=64M -XXMaxMetaspaceSize=64M
-XX:MetaspaceSize=128M -XXMaxMetaspaceSize=128M
比较一下几次调优的结果
吞吐量 | 最大停顿 | 平均停顿 | YGC | FGC |
97.647% | 360ms | 67 | 16 | 2 |
98.205% | 200ms | 69 | 11 | 0 |
2.配置CMS收集器
分析gc-cms.log
-XX:+UseConcMarkSweepGC
3.配置G1收集器
分析gc-g1.log
-XX:+UseG1GC
young GC [GC pause]
initial-mark (参数 initiatingHeapOccupancyPercent)
mixed GC (参数 G1HeapWastePercent)
Full GC (无可用region)
G1的目标就是为了减少Full GC,满足应用性能指标。
四、HashMap【不太全面,部分总结】
众所周知,HashMap底层结构,1.8之前是数组+链表,从1.8开始则是数组+链表+红黑树(链表长度大于8),线程不安全。
hash冲突:HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。详情如下:
随着存储的对象数量不断增加,可能会导致以下几种情况:
1.存储对象key 值相同(hash值一定相同),导致冲突;
2.存储对象key 值不同,由于 hash 函数的局限性导致hash 值相同,冲突;
3.存储对象key 值不同,hash 值不同,但 hash 值对数组长度取模后相同,冲突;
HashMap里面解决hash冲突的方法就是链地址法,
链地址法的基本思想是:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向 链表连接起来,如: 键值对k2, v2与键值对k1, v1通过计算后的索引值都为2,这时及产生冲突,但是可以通道next指针将k2, k1所在的节点连接起来,这样就解决了哈希的冲突问题 。
讲到HashMap,面试里面指定会被问与Hashtable的区别:
区别名称 | hashMap | hashtable |
继承 | AbstractMap | Dictionary |
线程是否安全 | 非synchronized,不安全,但效率好 | synchronized,安全,效率低 |
默认初始容量 | 16 | 11 |
扩容方式 | 2n,原来的2倍 | 2*oldCapacity +1 |
key值 | 可为null,但至多1个 | 不能为null |
遍历 | Iterator | enumerator |
五、基础必备
<1> String、StringBuffer、StringBuilder的区别
区别名称 | String | StringBuffer | StringBuilder |
字符串是否可变 | 字符串常量,不可变,因为是final | 字符串变量,可变 | 字符串变量,可变 |
线程是否安全 | 多线程、安全 | 多线程、安全 | 单线程、不安全 |
执行效率 | 慢 | 中 | 快 |
String s1 = "abc" ;
String s2 = "abc" ;
String s3= new String("abc");
用s1、s2这种方式创建字符串对象的时候,首先会去字符串常量池中查找看有没有“abc”字符串,如果有则返回它的地址给s1,如果没有则在常量池中创建“abc”字符串,并将地址返回给s1. 所以s1 == s2 ;
用s3这种方式创建字符串对象的时候,首先会去字符串常量池中查找看有没有“abc字符串,如果没有则在常量池中创建“abc”字符串,然后在堆内存中创建“Cat”字符串,并将堆内存中的地址返回给s3.
也就是说,s3这种方式,常量池与堆内存都要保存,并且返回的地址是堆内存的地址,而不是常量池中的地址。
String s1 = "abc" ;
String s4 = "ab" + "c";
String s5 = "a" + "bc";
String s6 = "a";
String s7 = "b";
String s8 = "c";
String s9 = s6 + s7 +s8;
s1 、s4、s5都是指向常量池中字符串“abc”地址的一个引用,所以s1 == s4 == s5,
s9也是跟s1他们相等的,但是他们开辟的空间缺很浪费,s1 :直接在常量池中放入“abc”
s4:在常量池,先开辟一段空间放“ab”, 再开辟一段空间放“c”,最后再开辟一段空间放“abc”【假如常量池中没有“abc”】
s9:更复杂,在常量池分别开辟“a” "b" "c" ,最后再开辟一段空间放“abc”【假如常量池中没有“abc”】。
结果都一样,引用指向常量池中的“abc”.
<2> 值传递与引用传递
值传递:在方法被调用时,实参通过形参把它的内容副本传入方法内部,此时形参接收到的内容是实参值的一个拷贝,因此在方法内对形参的任何操作,都仅仅是对这个副本的操作,不影响原始值的内容。
1public static void valueCrossTest(int age,float weight){
2 System.out.println("传入的age:"+age);
3 System.out.println("传入的weight:"+weight);
4 age=33;
5 weight=89.5f;
6 System.out.println("方法内重新赋值后的age:"+age);
7 System.out.println("方法内重新赋值后的weight:"+weight);
8 }
9
10//测试
11public static void main(String[] args) {
12 int a=25;
13 float w=77.5f;
14 valueCrossTest(a,w);
15 System.out.println("方法执行后的age:"+a);
16 System.out.println("方法执行后的weight:"+w);
17}
运行之后的结果 ,毫无疑问
1传入的age:25
2传入的weight:77.5
3
4方法内重新赋值后的age:33
5方法内重新赋值后的weight:89.5
6
7方法执行后的age:25
8方法执行后的weight:77.5
a和w作为实参传入valueCrossTest之后,无论在方法内做了什么操作,最终a和w都没变化。
值传递传递的是真实内容的一个副本,对副本的操作不影响原内容,也就是形参怎么变化,不会影响实参对应的内容。
引用传递:”引用”也就是指向真实内容的地址值,在方法调用时,实参的地址通过方法调用被传递给相应的形参,在方法体内,形参和实参指向通愉快内存地址,对形参的操作会影响的真实内容。
// 例子1
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public static void PersonCrossTest(Person person){
System.out.println("传入的person的name:"+person.getName());
person.setName("隔壁老王");
System.out.println("方法内重新赋值后的name:"+person.getName());
}
//测试
public static void main(String[] args) {
Person p=new Person();
p.setName("我是邻居老张");
p.setAge(45);
PersonCrossTest(p);
System.out.println("方法执行后的name:"+p.getName());
}
执行后得到的是:
1传入的person的name:邻居老张
2方法内重新赋值后的name:隔壁老王
3方法执行后的name:隔壁老王
可以看出,person经过personCrossTest()方法的执行之后,内容发生了改变,对形参的操作,改变了实际对象的内容。
这样还没完事,继续看,假如将PersonCrossTest()方法改成如下:
// 例子2
1public static void PersonCrossTest(Person person){
2 System.out.println("传入的person的name:"+person.getName());
3 person=new Person();//加多此行代码
4 person.setName("隔壁老王");
5 System.out.println("方法内重新赋值后的name:"+person.getName());
6 }
其他条件不变,则得到的结果为:
1传入的person的name:邻居老张
2方法内重新赋值后的name:隔壁老王
3方法执行后的name:邻居老张
为啥跟上面的不一样呢,不就多建了次对象吗?
Person p=new Person();
p.setName("我是邻居老张");
p.setAge(45);
执行这段代码的时候,
JVM会在堆内开辟一块内存,用来存储p对象的所有内容,同时在main()方法所在线程的栈区中创建一个引用p存储堆区中p对象的真实地址,
当执行到PersonCrossTest()方法时,因为方法内有这么一行代码:
person=new Person();//加多此行代码
JVM需要在堆内另外开辟一块内存来存储new Person(),那此时形参person指向了这个地址。引用传递中形参实参指向同一个对象,形参的操作会改变实参对象的改变。
无论是基本类型和是引用类型,在实参传入形参时,都是值传递,也就是说传递的都是一个副本,而不是内容本身。
因此在第一个例子中,对形参p的操作,会影响到实参对应的对象内容。而在第二个例子中,当执行到new Person()之后,JVM在堆内开辟一块空间存储新对象,并且把person改成指向新对象的地址,此时:
p依旧是指向旧的对象,person指向新对象的地址。
所以此时对person的操作,实际上是对新对象的操作,于实参p中对应的对象毫无关系。
六、多线程
线程:单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位。
进程:是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。
如何实现线程?
1、extends Thread class
2、implement Runable interface
3、implement Callable interface
线程池:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效 率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。
为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池
使用线程池的好处:
1、降低资源消耗。重复利用已创建线程,降低线程创建与销毁的资源消耗。
2、提高响应效率。任务到达时,不需等待创建线程就能立即执行。
3、提高线程可管理性。
4、防止服务器过载。内存溢出、CPU耗尽
Thread 类中的 start 和 run 方法有什么区别?
start 方法被用来启动新创建的线程,而且 start 内部调用了 run 方法
当你调用 run 方法的时候,只会是在原来的线程中调用,没有新的线程启动
一个线程运行时发生异常会怎样?
如果异常没有被捕获该线程将会停止执行。
线程调度算法:抢占式,一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
synchronized和Lock有什么区别?
区别 | synchronized | Lock |
JVM层面实现的,java提供的关键字 | API层面的锁 | |
底层会自动释放 | 手动释放锁 | |
等待不可中断,除非抛出异常或者执行完成 | 可以中断,通过interrupt()可中断 | |
不可绑定多个条件 | 可实现分组唤醒需要唤醒的锁 |
乐观锁、悲观锁:
悲观锁:
还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
乐观锁:
就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。【乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。】
sleep 、wait的区别【 sleep()睡眠时,保持对象锁,仍然占有该锁;而wait()睡眠时,释放对象锁。
】
sleep | wait | |
没有释放锁,不放弃监视器 | 释放锁,放弃监视器【
使得其他线程可以使用同步控制块或者方法(锁代码块和方法锁)。 】 |
|
可以在任何地方使用(使用范围) | wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用 | |
必须捕获异常 | 不需要捕获异常 | |
yield、join方法
yield()方法是停止当前线程,让同等优先权的线程或更高优先级的线程有执行的机会。如果没有的话,那么yield()方法将不会起作用,并且由可执行状态后马上又被执行。
join()方法是用于在某一个线程的执行过程中调用另一个线程执行,等到被调用的线程执行结束后,再继续执行当前线程。
简述线程的五种状态,同样也是线程的生命周期
(1)新建(new):当一个线程处于新建状态时,它仅仅是一个空的线程对象,系统不为它分配资源。Tread t = new Tread(new Runner());
(2)就绪(Runable):此时线程处在随时可以运行的状态,在随后的任意时刻,都可能进入运行状态。t.star( );
(3)运行(Running):处于这个状态的线程占用CPU,执行程序代码。
(4)阻塞(Blocked):阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行,直到线程重新进入就绪状态。wait、sleep、同步锁被占用;
(5)死亡(Dead):当线程退出run()方法时,就进入死亡状态,该线程生命周期结束。可能正常执行完run()方法退出,也可能是遇到异常。
线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
请说出你所知道的线程同步的方法:
wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
Allnotity():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
七、网络协议HTTP、TCP
HTTP:HyperText Transfer Protocol,超文本传输协议
TCP:Transmission Control Protocol,传输控制协议
HTTP的工作原理:
HTTP 协议采用请求/响应模型。客户端向服务器发送一个请求报文,服务器以一个状态作为响应。
以下是 HTTP 请求/响应的步骤:
1、客户端连接到web服务器:HTTP 客户端与web服务器建立一个 TCP 连接;
2、客户端向服务器发起 HTTP 请求:通过已建立的TCP 连接,客 户端向服务器发送一个请求报文;
3、服务器接收 HTTP 请求并返回 HTTP 响应:服务器解析请求,定位请求资源,服务器将资源副本写到 TCP 连接,由客户端读取;
4、释放 TCP 连接:若connection 模式为close,则服务器主动 关闭TCP 连接,客户端被动关闭连接,释放TCP 连接;若
connection 模式为keepalive,则该连接会保持一段时间,在该 时间内可以继续接收请求;
5、客户端浏览器解析HTML内容:客户端将服务器响应的 html 文 本解析并显示;
例如:在浏览器地址栏键入URL,按下回车之后会经历以下 流程:
1、浏览器向 DNS 服务器请求解析该 URL 中的域名所对应 的 IP 地址;
2、解析出 IP 地址后,根据该 IP 地址和默认端口 80或443,和服务器建立 TCP 连接;
3、浏览器发出读取文件(URL 中域名后面部分对应的文件) 的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;(若在https下会有12个包,SSL握手有8个包)
4、服务器对浏览器请求作出响应,并把对应的 html 文本 发送给浏览器;
5、释放 TCP 连接;
6、浏览器将该 html 文本并显示内容;
HTTP、HTTPS之间的区别
区别 | HTTP | HTTPS |
证书 | 不需要 | 需要ca申请证书,一般免费证书较少,因而需要一定费用。 |
传输方式 | 明文传输 | ssl加密传输协议 |
端口 | 80 | 443 |
连接 | 简单,无状态 | SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议 |
页面响应 | 速度快,HTTP 使用 TCP 三次握手建立连接,客户端和服务器需要交换 3 个包 | 速度慢, HTTPS除了 TCP 的三个包,还要加上 ssl 握手需要的 9 个包,所以一共是 12 个包。 |
TCP:说到TCP,不得不讲的就是传说中的三次握手,四次挥手。
ACK:确认序号的标志,ACK=1表示确认号有效,ACK=0表示报文不含确认序号信息
SYN:连接请求序号标志,用于建立连接,SYN=1表示请求连接
FIN:结束标志,用于释放连接,为1表示关闭本方数据流
建立连接的三次握手:
握手之前主动打开连接的客户端结束CLOSED阶段,被动打开的服务器端也结束CLOSED阶段,并进入LISTEN阶段。随后开始“三次握手”:
- 第一次握手:客户端发送syn包(syn=x)的数据包到服务器,并进入
SYN_SENT
状态,等待服务器确认; - 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入
SYN_RCVD
状态; - 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入
ESTABLISHED
状态,完成三次握手。
关闭连接的四次挥手:
- 第一次挥手:客户端发出释放FIN=1,自己序列号seq=u,进入FIN-WAIT-1状态
- 第二次挥手:服务器收到客户端的后,发出ACK=1确认标志和客户端的确认号ack=u+1,自己的序列号seq=v,进入CLOSE-WAIT状态
- 第三次挥手:客户端收到服务器确认结果后,进入FIN-WAIT-2状态。此时服务器发送释放FIN=1信号,确认标志ACK=1,确认序号ack=u+1,自己序号seq=w,服务器进入LAST-ACK(最后确认态)
- 第四次挥手:客户端收到回复后,发送确认ACK=1,ack=w+1,自己的seq=u+1,客户端进入TIME-WAIT(时间等待)。客户端经过2个最长报文段寿命后,客户端CLOSE;服务器收到确认后,立刻进入CLOSE状态。
为什么建立连接三次握手,关闭连接要四次挥手?
- 三次握手时,服务器同时把ACK和SYN放在一起发送到了客户端那里
- 四次挥手时,当收到对方的 FIN 报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方是否现在关闭发送数据通道,需要上层应用来决定,因此,己方 ACK 和 FIN 一般都会分开发送。