JDK,JRE,JVM区别
一、JDK
JDK(Java SE Development Kit),Java标准开发包,它提供了编译、运行Java程序所需的各种工具和资源,包括Java编译器、Java运行时环境,以及常用的Java类库等。
下图是JDK的安装目录:
二、JRE
JRE( Java Runtime Environment) 、Java运行环境,用于解释执行Java的字节码文件。普通用户而只需要安装 JRE(Java Runtime Environment)来运行 Java 程序。而程序开发者必须安装JDK来编译、调试程序。
下图是JRE的安装目录:里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre。
三、JVM
JVM(Java Virtual Mechinal),Java虚拟机,是JRE的一部分。它是整个java实现跨平台的最核心的部分,负责解释执行字节码文件,是可运行java字节码文件的虚拟计算机。所有平台的上的JVM向编译器提供相同的接口,而编译器只需要面向虚拟机,生成虚拟机能识别的代码,然后由虚拟机来解释执行。
当使用Java编译器编译Java程序时,生成的是与平台无关的字节码,这些字节码只面向JVM。不同平台的JVM都是不同的,但它们都提供了相同的接口。JVM是Java程序跨平台的关键部分,只要为不同平台实现了相应的虚拟机,编译后的Java字节码就可以在该平台上运行。
即时编译器
在Java编程语言和环境中,即时编译器(JIT compiler,just-in-timecompiler)是一个把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器(processor)的指令的程序。当你写好一个Java程序后,源语言的语句将由Java前端编译器(javac或者Eclipse JDT中的增量式编译器)编译成字节码,而不是编译成与某个特定的处理器硬件平台对应的本地指令代码(比如,Intel的Pentium微处理器或IBM的System/390处理器)。字节码是可以发送给任何平台并且能在那个平台上运行的独立于平台的代码。
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据( Instance Data)和对齐填充(Padding)。
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
迭代器
参数 | 抽象类 | 接口 |
---|---|---|
默认的方法实现 | 它可以有默认的方法实现 | 接口完全是抽象的。它根本不存在方法的实现 |
实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现 |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
与正常Java类的区别 | 除了你不能实例化抽象类之外,它和普通Java类没有任何区别 | 接口是完全不同的类型 |
访问修饰符 | 抽象方法可以有public、protected和default这些修饰符 | 接口方法默认修饰符是public。你不可以使用其它修饰符。 |
main方法 | 抽象方法可以有main方法并且我们可以运行它 | 接口没有main方法,因此我们不能运行它。 |
多继承 | 抽象方法可以继承一个类和实现多个接口 | 接口只可以继承一个或多个其它接口 |
速度 | 它比接口速度要快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。 |
添加新方法 | 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。 | 如果你往接口中添加方法,那么你必须改变实现该接口的类。 |
Java中的代码块
代码块分类
位置 | 作用 | |
---|---|---|
局部代码块 | 在方法中 | 给变量限定生命周期,局部代码块的变量在执行结束后会被Java回收 |
构造代码块 | 在类的成员位置 | 在每次执行构造方法前先执行构造代码块,可以将多个构造方法中的相同的代码放到构造代码块中,对对象进行初始化. |
静态代码块 | 在类的成员位置 | 一般用于给类初始化,被静态修饰的代码块仅执行一次. |
加载顺序
public class Test {
{
System.out.println("C");
}
public static void main(String[] args) {
{
System.out.println("E");
}
System.out.println("A");
new Test();
new Test().search();
}
Test(){
System.out.println("B");
}
static {
System.out.println("D");
}
public void search(){
{
System.out.println("e");
}
}
}
打印结果:DEACBCBe
加载顺序是:创建静态代码块 ==> 执行静态方法 ==> 按顺序执行代码块 ==> 在初始化构造函数之前先执行构造代码块 ==> 执行造代码方法
1.1Java中各个情况的父类和子类的各项加载顺序
class A {
private static int numA;
private int numA2;
static {
System.out.println("A的静态字段 : " + numA);
System.out.println("A的静态代码块");
}
{
System.out.println("A的成员变量 : " + numA2);
System.out.println("A的非静态代码块");
}
public A() {
System.out.println("A的构造器");
}
}
class B extends A {
private int numB2;
private static int numB;
static {
System.out.println("B的静态字段 : " + numB);
System.out.println("B的静态代码块");
}
{
System.out.println("B的成员变量 : " + numB2);
System.out.println("B的非静态代码块");
}
public B() {
System.out.println("B的构造器");
}
}
public class Test {
public static void main(String[] args) {
A ab = new B();
System.out.println("---");
ab = new B();
}
}
最终确定的加载顺序为:
父类的静态代方法和成员变量 ==> 父类的成员变量和构造代码块 ==> 父类的构造函数 ==> 子类的静态代码块和成员变量 ==> 子类的成员变量 ==> 子类的构造代码块 ==> 子类的构造方法
如果存在使用子类静态方法调用父类的情况,只会初始化父类中的相关数据。子类不会初始化,也就是说子类中的静态方法和静态代码块不会执行。
2.那些创建方法不会初始化类
在java核心知识点上面背诵
3.final存在的时候java继承类的加载顺序
static:
1.static修饰一个属性字段,那么这个属性字段将成为类本身的资源,public修饰为共有的,可以在类的外部通过test.a来访问此属性;在类内部任何地方可以使用。如果被修饰为private私有,那么只能在类内部使用。
2.如果属性被修饰为static静态类资源,那么这个字段永远只有一个,也就是说不管你new test()多少个类的对象,操作的永远都只是属于类的那一块内存资源。
final:
1.final 用于声明属性、方法和类,分别表示属性一旦被分配内存空间就必须初始化并且以后不可变;方法一旦定义必须有实现代码并且子类里不可被覆盖;类一旦定义不能被定义为抽象类或是接口,因为不可被继承。
2.被final修饰而没有被static修饰的类的属性变量只能在两种情况下初始化:
- 在它被定义的时候
public class Test{
public final int a=0;
private Test(){
}
}
2)在构造函数里初始化
public class Test{
public final int a;
private Test(){
a=0;
}
}
当这个属性被修饰为final,而非static的时候,它属于类的实例对象的资源,当类被加载进内存的时候这个属性并没有给其分配内存空间,而只是定义了一个变量a,只有当类被实例化的时候这个属性才被分配内存空间,而实例化的时候同时执行了构造函数,所以属性被初始化了,也就符合了当它被分配内存空间的时候就需要初始化,以后不再改变的条件。
3.同时被final和static修饰的类的属性变量只能在两种情况下初始化:
- 在它被定义的时候
public class Test{
public static final int a=0;
private Test(){
}
}
2)在类的静态块里初始化
public class Test{
public static final int a;
static{a=0;}
}
当类的属性被同时被修饰为static和final的时候,他属于类的资源,那么就是类在被加载进内存的时候(也就是应用程序启动的时候)就要为属性分配内存,所以此时属性已经存在,它又被final修饰,所以必须在属性定义了以后就给其初始化值。而构造函数是在当类被实例化的时候才会执行,所以不能用构造函数。而static块是类被加载的时候执行,且只执行这一次,所以在static块中可以执行初始化。
3.1总结
只有final修飾的時候該成員變量是類的資源他的初始化隨著類的初始化一起執行。也就是運行構造方法的時候會對final修飾的變量初始化,如果只是定義在勒種,那他會在構造靜態方法之後,構造方法之前執行。
如果static和final同時修飾一個數據,那麼,改數據是在類加載到內存的時候就已經開闢好空間了,如果是在類中複製,那麼直接可以調用,如果在靜態構造體中初始化,會初始化類的靜態方法後再初始化該值。
如果使用static和final同時修飾,在類中初始化,但是賦值對象是需要創建的。那麼還是會初始化類中的靜態方法
5.Integer和int怎么计算,Integer传递的方式是指针还是值传递
Integer对象在函数中传递的方法和int相同都是值的传递,不传递引用
public class Test {
private static int x = 10;
private static Integer y = 10;
public static void updateX(int value) {
value = 3 * value;
}
public static void updateY(Integer value) {
value = 3 * value;
}
public static void main(String[] args) {
updateX(x);
updateY(y);
System.out.println("null");
}
}
结果: x,y的值都为10
12.递归和递推的特点和区别
递归是递推在计算机行的实现。递推是数学算法。
递归需要做许多函数调用,每个函数调用都需要设置有一个栈帧,并传递参数,这些都增加了时间开销,而这些开销循环中没有。绝大多数情况下,现代计算机
中这些开销影响并不显著。但如果你的代码频繁执行(比如短时间内执行百万次甚至上亿次),你必须关注函数调用性能的问题
递归比循环更加强大的地方在于,递归函数维持着一个保存每次递归调用当前状态的栈,允许函数获得子问题的结果后继续处理。
什么是Java中的自动拆装箱
基本数据类型运算结果查过该数据类型的上限的时候
**这就是发生了溢出,溢出的时候并不会抛异常,也没有任何提示。**所以,在程序中,使用同类型的数据进行运算的时候,一定要注意数据溢出的问题。
包装类型
Java语言是一个面向对象的语言,但是Java中的基本数据类型却是不面向对象的,这在实际使用时存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。
包装类均位于java.lang包,包装类和基本数据类型的对应关系如下表所示
基本数据类型 | 包装类 |
---|---|
byte | Byte |
boolean | Boolean |
short | Short |
char | Character |
int | Integer |
long | Long |
float | Float |
double | Double |
在这八个类名中,除了Integer和Character类以后,其它六个类的类名和基本数据类型一致,只是类名的第一个字母大写即可。
为什么需要包装类
很多人会有疑问,既然Java中为了提高效率,提供了八种基本数据类型,为什么还要提供包装类呢?
这个问题,其实前面已经有了答案,因为Java是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将int 、double等类型放进去的。因为集合的容器要求元素是Object类型。
为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
哪些地方会自动拆装箱
我们了解过原理之后,在来看一下,什么情况下,Java会帮我们进行自动拆装箱。前面提到的变量的初始化和赋值的场景就不介绍了,那是最简单的也最容易理解的。
我们主要来看一下,那些可能被忽略的场景。
场景一、将基本数据类型放入集合类
场景二、包装类型和基本类型的大小比较
场景三、包装类型的运算
我们发现,两个包装类型之间的运算,会被自动拆箱成基本类型进行。
场景四、三目运算符的使用
场景五、函数参数与返回值
自动拆装箱与缓存
Java SE的自动拆装箱还提供了一个和缓存有关的功能,我们先来看以下代码,猜测一下输出结果:
我们普遍认为上面的两个判断的结果都是false。虽然比较的值是相等的,但是由于比较的是对象,而对象的引用不一样,所以会认为两个if判断都是false的。
在Java中,==比较的是对象应用,而equals比较的是值。
所以,在这个例子中,不同的对象有不同的引用,所以在进行比较的时候都将返回false。奇怪的是,这里两个类似的if条件判断返回不同的布尔值。
上面这段代码真正的输出结果:
integer1 == integer2
integer3 != integer4
**原因就和Integer中的缓存机制有关。**在Java 5中,在Integer的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。
适用于整数值区间-128 至 +127。
只适用于自动装。使用构造函数创建对象不适用。
具体的代码实现可以阅读Java中整型的缓存机制一文,这里不再阐述。
我们只需要知道,当需要进行自动装箱时,如果数字在-128至127之间时,会直接使用缓存中的对象,而不是重新创建一个对象。
其中的javadoc详细的说明了缓存支持-128到127之间的自动装箱过程。最大值127可以通过-XX:AutoBoxCacheMax=size
修改。
实际上这个功能在Java 5中引入的时候,范围是固定的-128 至 +127。后来在Java 6中,可以通过java.lang.Integer.IntegerCache.high
设置最大值。
这使我们可以根据应用程序的实际情况灵活地调整来提高性能。到底是什么原因选择这个-128到127范围呢?因为这个范围的数字是最被广泛使用的。 在程序中,第一次使用Integer的时候也需要一定的额外时间来初始化这个缓存。
在Boxing Conversion部分的Java语言规范(JLS)规定如下:
如果一个变量p的值是:
-128至127之间的整数(§3.10.1)
true 和 false的布尔值 (§3.10.3)
‘\u0000’至 ‘\u007f’之间的字符(§3.10.4)
范围内的时,将p包装成a和b两个对象时,可以直接使用a==b判断a和b的值是否相等。
自动拆装箱带来的问题
当然,自动拆装箱是一个很好的功能,大大节省了开发人员的精力,不再需要关心到底什么时候需要拆装箱。但是,他也会引入一些问题。
包装对象的数值比较,不能简单的使用
==
,虽然-128到127之间的数字可以,但是这个范围之外还是需要使用equals
比较。前面提到,有些场景会进行自动拆装箱,同时也说过,由于自动拆箱,如果包装类对象为null,那么自动拆箱时就有可能抛出NPE。
如果一个for循环中有大量拆装箱操作,会浪费很多资源。
JDK动态代理实现流程
1. 什么是代理
代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。
2.JDK动态接口代理
使用两个类:Proxy和InvocationHandler。
InvocationHandler:通过反射机制调用类的代码。
Proxy:利用InvocationHandler动态创建一个符合某一个接口的实例。生成代理目标对象
3.步骤
1.创建一个实现接口InvocationHandler的类,它必须实现invoke方法
2.创建被代理的类以及接口
3.通过Proxy的静态方法
newProxyInstance(ClassLoaderloader, Class[] interfaces, InvocationHandler h)创建一个代理
4.通过代理调用方法
4.使用步骤
1.创建一个接口类
package src;
public interface Student {
public void call();
}
2.创建一个接口类的实现类
package src;
public class StudentImpl implements Student {
@Override
public void call() {
System.out.println("上课");
}
}
3.创建一个invocationHanler的实现类
package src;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class InvocationHandlerImpl implements InvocationHandler {
private Object student;
public InvocationHandlerImpl(Object student){
this.student = student;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object invoke = method.invoke(student, args);
return invoke;
}
}
4.创建测试类
package src;
import java.lang.reflect.Proxy;
public class Test {
public static void main(String[] args) {
//需要被代理的对象
StudentImpl student = new StudentImpl();
//创建handler对象
InvocationHandlerImpl handler = new InvocationHandlerImpl(student);
Student proxyClass =(Student) Proxy.newProxyInstance(student.getClass().getClassLoader(), student.getClass().getInterfaces(),handler);
proxyClass.call();
}
}
CGLIB动态代理
1.原理
CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。
2.步骤
1.创建一个业务层
public class HelloService {
public HelloService() {
System.out.println("HelloService构造");
}
/**
* 该方法不能被子类覆盖,Cglib是无法代理final修饰的方法的
*/
final public String sayOthers(String name) {
System.out.println("HelloService:sayOthers>>"+name);
return null;
}
public void sayHello() {
System.out.println("HelloService:sayHello");
}
}
2.创建一个类事件MethodInterceptor
public class MyMethodInterceptor implements MethodInterceptor{
/**
* sub:cglib生成的代理对象
* method:被代理对象方法
* objects:方法入参
* methodProxy: 代理方法
*/
@Override
public Object intercept(Object sub, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("======插入前置通知======");
Object object = methodProxy.invokeSuper(sub, objects);
System.out.println("======插入后者通知======");
return object;
}
}
3.测试
public class Test {
public static void main(String[] args) {
// 代理类class文件存入本地磁盘方便我们反编译查看源码
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\code");
// 通过CGLIB动态代理获取代理对象的过程
Enhancer enhancer = new Enhancer();
// 设置enhancer对象的父类
enhancer.setSuperclass(HelloService.class);
// 设置enhancer的回调对象
enhancer.setCallback(new MyMethodInterceptor());
// 创建代理对象
HelloService proxy= (HelloService)enhancer.create();
// 通过代理对象调用目标方法
proxy.sayHello();
}
}
Java集合
collection接口的主要继承接口和主要实现类
List(有序的Collection)
- ArrayList
- 排列有序可重复
- 底层使用数组
- 查找快,增删慢,因为不能有间隔,所有增删都需要移动数据
- 线程不安全
- 当容量不够的时候,当前容量*1.5+1
- Vector
- 排列有序,可重复
- 底层使用数组
- 线程安全效率低
- 容量不够*2
- LinkedList
- 排列有序课重复
- 底层使用双向链表
- 查询慢,增删快
- 线程不安全。
Set(无序的Collection)想要判断set中的对象是否相等,必须重写hashCode和equals方法
- HashSet
- 排列无序,不可重复
- 底层使用hash表来实现,通过HashCode获取哈希值然后按照hashCode进行排列的,一个HashCode位置上可以存放多个元素。
- 存取快,内部是HashMap
- TreeSet
- 排列无序,不可重复
- 底层使用二叉树实现
- 排列存储,Integer和String对象可以进行默认的排序,自己写的需要复写compareTo()函数,使用二叉树存储的时候按照某种规则进行了排序
- 内部是TreeMap的SortedSet
- LinkedHashSet
- 采用hash表存储,并用双向链表记录插入顺序
- 内部是LinkedHashMap
- 继承于HashSet,操作方法与HashSet相同
Queue在两端出入的List,所以也可以用数组或者链表来实现。
Map接口
HashMap
1.7
java7中使用数组和链表的形式实现的,扩容因子是0.75,扩容时*2。
1.8
java8中使用数组+链表+红黑树的形式实现,链表中元素超过8个自动变为红黑树,链表中元素从8个以上变为6个就重新变回链表
特性
- 键不可重复,值可重复
- 底层哈希表
- 线程不安全
- 允许key值为null,value也可以为null
hashmap详细的底层源码分析过程
hashtable
- 继承自Dictionary
- 线程安全的,并发性不如CurrentHashMap
- 是一个遗留类
TreeMap
- 实现SortedMap接口
- 默认按键值对排序,也可以指定排序比较器。
ConcurrentHashMap
segment继承ReentrantLock进行加锁,每次锁住一个segment。在1.6中规定了初始化有16个segment,这个值可以更改,一旦初始化后将不能被扩容
面试题
HashMap与HashTable的区别
- Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。
- hashtable线程安全hashmap线程不安全
- Hashtable中,key和value都不允许出现null值。HashMap中允许出现一个键的空值,和值的空值
- 哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
- Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值,Hashtable在求hash值对应的位置索引时,用取模运算,而HashMap在求位置索引时,则用与运算,且这里一般先用hash&0x7FFFFFFF后,再对length取模,&0x7FFFFFFF的目的是为了将负的hash值转化为正值,因为hash值有可能为负数,而&0x7FFFFFFF后,只有符号外改变,而后面的位都不变。
- HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。 Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。 Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。
Tip:建议hashmap和ConcurrentHashMap去网上搜一搜源码去学习
String,StringBuild,StringBuffer之间的共同点和区别
- 建议去网上搜索源码来学习
JVM虚拟机知识点
JVM内存模型图
JVM结构(按照线程共享机制分类)
1.线程共享区域
方法区域
- 描述:
- 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 所谓的方法区为永久代(Permanent Generation)的说法,仅仅是因为HotSpot虚拟机将GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机是不存在永久代的说法的。
- 包含
- 运行时常量池
Tip:运行时常量池
- 运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量和符号引用,一般还会存放翻译出来的直接引用。这部分内容将在类加载后进入方法区的运行时常量池中存放。
- 当常量池无法再申请到内存时抛出OutOfMemoryError异常。
Tip:元空间和永久代
- java8中,取消永久代,方法存放于元空间(Metaspace),元空间并不在虚拟机中,使用本地内存,元空间存储类的元信息
- 永久代在1.7的时候存在。因为不是GC(垃圾回收器)收集的区域,内存会主键变得越来越大,当垃圾存的足够多的时候就会爆出outofmemoryError异常。
- 元空间并不存在于虚拟机中,而是使用本地内存,因此元空间的大小只限制与本地内存。
堆
- 描述
- java堆是Java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建,其唯一目的就是存放对象实例:所有的对象实例以及数组都要在堆上分配(但随着JIT编译器的发展与逃逸分析技术逐渐成熟,所有的对象分配在堆上也渐渐不是那么绝对了)。
- Java堆是垃圾收集器管理的主要区域,现在收集器基本采用分代收集算法,所以Java堆还可细分为:新生代和老年代;再细致点分为Eden空间,From Survivor空间,To Survivor空间等。
- 根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。
- 如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
- 包含
- 字符串常量池(在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;在JDK7.0版本,字符串常量池被移到了堆中了)
2.线程独占区域
程序计数器
- 程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
虚拟机栈
- Java虚拟机栈与线程生命周期相同。其描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 局部变量表存放了编译器可知的各种基本数据类型(boolean, byte, char, short, int, float, long, double),对象引用(reference类型)和returnAddress类型(指向了一条字节码指令的地址)。
- 在这个区域中,Java虚拟机规范规定了两种异常情况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常;
- 如果虚拟机栈可以动态扩展(当前大部分Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),并且扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈
- 本地方法栈与虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,本地方法栈为虚拟机使用到的Native方法服务。
- 由于虚拟机规范中没有对本地方法栈中的语言、使用方式与数据结构进行强制规定,有的虚拟机(如Sun HotSpot虚拟机)直接把本地方法栈和虚拟机栈合二为一。
- 本地方法栈也会抛出StackOverflowError异常和OutOfMemoryError异常。
3.直接内存
直接内存并不是 JVM 运行时数据区的一部分, 但也会被频繁的使用: 在 JDK 1.4 引入的 NIO 提供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后使用DirectByteBuffer 对象作为这内存的引用进行操作(详见: Java I/O 扩展), 这样就避免了在 Java堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。
类的加载机制
1.阶段
加载阶段
- 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的入口
- 这个类可以从java内置的包中加载,也可以从zip包中获取,也可以通过动态代理生成,也可以用其他文件生成。
验证阶段
- 验证class文件字节流中包含的信息是符合当前虚拟机的要求。并且不会损害虚拟机本身
准备
- 为类变量分配内存,并设置类变量的初始值
- 在方法区中分配这些变量所使用的的内存空间,如果静态类没有加final则准备阶段并不会被赋值,如果加上了final则编译阶段会对变量生成constantvalue属性,然后根据这个属性把值赋给变量。
解析
- 虚拟机奖常量池中的符号引用替换为直接引用
- 符号引用:符号引用并不一定会存在内存中。各个虚拟机的符号引用的字面量一定相同,引用的是java虚拟机中规范的class文件格式中
- 直接引用:指向目标的一个指针,能够间接定位的一个句柄,直接引用的对象一定存在于内存中。
初始化
- 开始执行真正的java代码,初始化阶段是执行类构造器的过程
2. 类加载器
- 启动类加载器(Bootstrap ClassLoader)
- 加载JAVA_HOME\lib目录下的类
- -Xbootclasspath指定加载路径,且被虚拟机认可
- 扩展类加载器(Extension ClassLoader)
- 加载JAVA_HOME\lib\ext目录中的类
- 或者系统变量中指定的类库
- 应用程序类加载器(Application ClassLoader)
- 负责加载用户路径上的类库
3.双亲委派模型
- 当收到一个类加载的请求的时候,他不会自己去首先加载这个类,而是把这个请求委派给父类去完成。每一层都是如此,只要当父类无法加载这个类的时候,子类加载器才会尝试自己去加载。
垃圾回收机制
分代
-
新生代(占据三分之一的空间,新建的对象都存放到新生代,特别大的存不下的除外)
- Eden区域(占据新生代8/10区域)
- from server 区域(占据新生代1/10的区域)
- to server 区域 (占据新生代1/10的区域)
-
老年代(占据空间的2/3,用于存放不轻易被垃圾回收的对象,和新生代存不下的对象)
-
MinorGC过程:
- java对象在eden区域出生,当eden区域内存不够的时候触发MinorGC对新生代进行一次垃圾回收
- 当进行垃圾回收的时候先把eden中和servivorFrom中的区域中存活的对象复制到ServiviorTo区域中。(如果有达到老年区标准,即年龄达到15,就会被移入老年区)同时把所有对象年龄加一
- 清空eden和servicorFrom中的数据,然后把to和from进行交换。
- MinorGC采用复制算法
垃圾回收算法
垃圾回收的时候需要进行可达性分析:
通过一系列的GC root和一个对象之间有没有可达路径,判断这个对象是否需要进行回收,但是不可达对象要进行回收需要至少需要经过两次标记过程,两次都为可回收对象在进行回收。
分代收集算法
- 标记清除算法
- 优点:速度快
- 缺点:容易产生碎片化的内存
- 复制算法
- 优点:解决了内存在使用标记清除算法后产生的内存碎片
- 缺点:会有一半的内存区域是大部分时间是被浪费的。
- 标记整理算法
- 优点:不会产生碎片化内存,也不会有一半的内存在大部分时间被浪费
- 缺点:如果需要移动的数据量过多,会消耗大量的性能和时间
Tip:新生代最好使用复制算法,老年代最好使用标记清除算法或者标记整理算法
分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(YoungGeneration)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
分区收集算法
把整个堆划分成多个连续的不同小区间,每个小区间独立使用独立回收,可以控制一次回收多个小区间。可以减少GC停顿的时间。
垃圾收集器
新生代
- serial垃圾收集器
- 单线程收集器,使用复制算法,现在是java虚拟机运行在client模式下的默认新生代垃圾回收器
- 进行垃圾回收的时候需要暂停其他所有的工作线程。
- ParNew垃圾收集器
- Serial收集器的多线程版本,使用的是复制算法。
- 进行垃圾回收的时候使用多条线程进行,同样需要暂停其他工作线程。
- java虚拟机运行在server模式下的默认新生代垃圾回收器
- 默认开启cpu树木相同的线程数,-XX:ParqallelGcThreads参数限制垃圾回收线程数量
- ParallelScavenge收集器
- 使用复制算法,多线程垃圾回收器
- 和ParNew垃圾回收器关注点不同,关注程序的吞吐量,最高效的利用cpu来完成程序的运算任务。
老年代
- Serial Old收集器
- 标记整理算法
- 单线程,运行的时候需要停掉所有用户线程
- 默认的client默认的老年代垃圾回收器
- Parallel Old收集器
- 标记整理算法
- 为老年代提供吞吐量优先的多线程算法
- CMS收集器
- 多线程的标记清除算法
- 工作流程
- 初始标记:先暂停其他工作线程,然后值标记一些GC Root能够直接标记的对象
- 并发标记:通过多线程的方法,在不暂停工作线程的情况下跟踪GC Root
- 重新标记:修正并发标记期间可能变动的标记
- 并发清除:清除标记的对象。和用户线程一起执行不暂停工作
全局收集器
-
标记整理算法
-
优点:
- 给予标记整理算法不会产生内存碎片
- 可以控制停顿时间,在不牺牲吞吐量的前提下,实现低停顿的垃圾回收。
-
把垃圾回收区域分成小块,然后检测每块的垃圾回收进度,并且保存了一张垃圾回收优先级的表,优先回收垃圾最多的区域。
引用类型
-
强引用:把一个对象赋值给一个引用变量,这个引用变量就是一个强引用,强引用对象在垃圾回收里面是可以被标记的处于可以达到的撞他,所以他不会被回收
-
软引用:软应用使用softRefence类来实现,软引用对象在系统内存充足的时候不会被回收,不足的时候被回收。
-
弱引用:使用weakReference类来实现,只要垃圾回收机制启动就会被回收
-
虚引用:使用phantomreference类来实现,不能单独使用,需要和引用队列联合使用,主要用于跟踪对象被垃圾回收的状态。
JVM 常见面试题进阶
提示:
再把“Java核心知识点整理”的jvm部分加上,也看看,整合到一起。
1.1 Java 语言怎么实现跨平台的?
我们编写的 Java 源码,编译后会生成一种 .class 文件,称为字节码文件。字节码不能直接运行,必须通过 JVM 翻译成机器码才能运行。
JVM 是一个”桥梁“,是一个”中间件“,是实现跨平台的关键。Java 代码首先被编译成字节码文件,再由 JVM 将字节码文件翻译成机器语言,从而达到运行 Java 程序的目的。
1.2 JVM 数据运行区,哪些会造成 OOM 的情况?
除了数据运行区,其他区域均有可能造成 OOM 的情况。
堆溢出:java.lang.OutOfMemoryError: Java heap space
栈溢出:java.lang.StackOverflowError
永久代溢出:java.lang.OutOfMemoryError:PermGen space
1.3 详细介绍一下对象在分代内存区域的分配过程?
- JVM 会试图为相关 Java 对象在 Eden 中初始化一块内存区域。
- 当 Eden 空间足够时,内存申请结束;否则到下一步。
- JVM 试图释放在 Eden 中所有不活跃的对象(这属于 1 或更高级的垃圾回收)。释放后若 Eden 空间仍然不足以放入新对象,则试图将部分 Eden 中活跃对象放入 Survivor 区。
- Survivor 区被用来作为 Eden 及 Old 的中间交换区域,当 Old 区空间足够时,Survivor 区的对象会被移到 Old 区,否则会被保留在 Survivor 区。
- 当 Old 区空间不够时,JVM 会在 Old 区进行完全的垃圾收集。
- 完全垃圾收集后,若 Survivor 及 Old 区仍然无法存放从 Eden 复制过来的部分对象,导致 JVM 无法在 Eden 区为新对象创建内存区域,则出现 “ out of memory ” 错误。
1.4 G1 与 CMS 两个垃圾收集器的对比
细节方面不同
- G1 在压缩空间方面有优势。
- G1 通过将内存空间分成区域(Region)的方式避免内存碎片问题。
- Eden, Survivor, Old 区不再固定、在内存使用效率上来说更灵活。
- G1 可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象。
- G1 在回收内存后会马上同时做合并空闲内存的工作、而 CMS 默认是在 STW(stop the world)的时候做。
- G1 会在 新生代和年老代 中使用、而 CMS 只能在 O 区使用。
- 整体内容不同
吞吐量优先:G1
响应优先:CMS
CMS 的缺点是对 cpu 的要求比较高。G1 是将内存化成了多块,所有对内段的大小有很大的要求。
CMS 是清除,所以会存在很多的内存碎片。G1 是整理,所以碎片空间较小。
1.5 线上常用的 JVM 参数有哪些?
数据区设置
Xms:初始堆大小
Xmx:最大堆大小
Xss:Java 每个线程的Stack大小
XX:NewSize=n:设置年轻代大小
XX:NewRatio=n:设置年轻代和年老代的比值。如:为 3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代年老代和的 1/4。
XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。如:3,表示 Eden:Survivor=3:2,一个 Survivor 区占整个年轻代的 1/5。
XX:MaxPermSize=n:设置持久代大小。
JvM 参数调优:
一 xms < size >表示jvM 初始化堆的大小, : Xmx < size >表示 JvM 堆的最大值。这两个值的大小一般根据需要进行设置。当应用程序需要的内存超出堆的最大值时虚拟机就会提示内存溢出,并且导致应用服务崩溃。因此一般建议堆的最大值设置为可用内存的最大值的 80 %。
在 catalina . bat 中,设置 JAvA _ OPTS = ’一 Xms256m - Xmxs 12m ' ,表示初始化内存为 256MB ,可以使用的最大内存为 51 ZMB 。
收集器设置
XX:+UseSerialGC:设置串行收集器
XX:+UseParallelGC::设置并行收集器
XX:+UseParalledlOldGC:设置并行年老代收集器
XX:+UseConcMarkSweepGC:设置并发收集器
GC日志打印设置
XX:+PrintGC:打印 GC 的简要信息
XX:+PrintGCDetails:打印 GC 详细信息
XX:+PrintGCTimeStamps:输出 GC 的时间戳
1.6 对象什么时候进入老年代?
对象优先在 Eden 区分配内存
当对象首次创建时, 会放在新生代的 eden 区, 若没有 GC 的介入,会一直在 eden 区,GC 后,是可能进入 survivor 区或者年老代
大对象直接进入老年代
所谓的大对象是指需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组,大对象对虚拟机的内存分配就是坏消息,尤其是一些朝生夕灭的短命大对象,写程序时应避免。
长期存活的对象进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器,对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1,当他的年龄增加到一定程度(默认是 15 岁), 就将会被晋升到老年代中。
1.7 什么是内存溢出, 内存泄露? 他们的区别是什么?
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现 out of memory;
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。
什么情况下会出现栈溢出
- 方法创建了一个很大的对象,如 List,Array。
- 是否产生了循环调用、死循环。
- 是否引用了较大的全局变量。
1.8 引起类加载操作的行为有哪些?
- 遇到 new、getstatic、putstatic 或 invokestatic 这四条字节码指令。
- 反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 子类初始化的时候,如果其父类还没初始化,则需先触发其父类的初始化。
- 虚拟机执行主类的时候(有 main( string[] args))。
- JDK1.7 动态语言支持。
1.9 介绍一下 JVM 提供的常用工具
jps:用来显示本地的 Java 进程,可以查看本地运行着几个 Java 程序,并显示他们的进程号。 命令格式:jps
jinfo:运行环境参数:Java System 属性和 JVM 命令行参数,Java class path 等信息。 命令格式:jinfo 进程 pid
jstat:监视虚拟机各种运行状态信息的命令行工具。 命令格式:jstat -gc 123 250 20
jstack:可以观察到 JVM 中当前所有线程的运行情况和线程当前状态。 命令格式:jstack 进程 pid
jmap:观察运行中的 JVM 物理内存的占用情况(如:产生哪些对象,及其数量)。 命令格式:jmap [option] pid
1.10 Full GC 、 Major GC 、Minor GC 之间区别?
Minor GC: 从新生代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。
Major GC: 清理 Tenured 区,用于回收老年代,出现 Major GC 通常会出现至少一次 Minor GC。
Full GC: Full GC 是针对整个新生代、老年代、元空间(metaspace,java8 以上版本取代 perm gen)的全局范围的 GC。
1.11 什么时候触发 Full GC ?
- 调用 System.gc 时,系统建议执行 Full GC,但是不必然执行。
- 老年代空间不足。
- 方法区空间不足。
- 通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存。
- 由 Eden 区、survivor space1(From Space)区向 survivor space2(To Space)区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
1.12 什么情况下会出现栈溢出
- 方法创建了一个很大的对象,如 List,Array。
- 是否产生了循环调用、死循环。
- 是否引用了较大的全局变量。
1.13 说一下强引用、软引用、弱引用、虚引用以及他们之间和 gc 的关系
强引用:new 出的对象之类的引用,只要强引用还在,永远不会回收。
软引用:当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。
弱引用:只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
虚引用:主要作用是跟踪对象被垃圾回收的状态。
1.14 Eden 和 Survivor 的比例分配是什么情况?为什么?
默认比例 8:1。 大部分对象都是朝生夕死。 复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
实战
2.1 CPU 资源占用过高
-
top 查看当前 CPU 情况,找到占用 CPU 过高的进程 PID=123。
-
top -H -p123 **可以找出两个 CPU 占用较高的线程,**记录下来 PID=2345, 3456 转换为十六进制。
-
jstack -l 123 > temp.txt 打印出当前进程的线程栈。
可以观察到 JVM 中当前所有线程的运行情况和线程当前状态。
-
查找到对应于第二步的两个线程运行栈,分析代码。
2.2 OOM 异常排查
1.使用 top 指令查询服务器系统状态。
\2. ps -aux|grep java 找出当前 Java 进程的 PID。
\3. jstat -gcutil pid interval 查看当前 GC 的状态。
\4. jmap -histo:live pid 可用统计存活对象的分布情况,从高到低查看占据内存最多的对象。
\5. jmap -dump:format=b,file= 文件名 [pid] 利用 Jmap dump。
\6. 使用性能分析工具对上一步 dump 出来的文件进行分析,工具有 MAT 等。
[JVM常见面试题基础](
进程和线程
线程和进程定义
进程:进程是并发执行程序在执行过程中资源分配和管理的基本单位(资源分配的最小单位)。进程可以理解为一个应用程序的执行过程,应用程序一旦执行,就是一个进程。每个进程都有自己独立的地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段。
线程:程序执行的最小单位。
线程和进程的区别
1.地址空间: 同一进程的所有线程共享本进程的地址空间,而不同的进程之间的地址空间是独立的。
2.资源拥有: 同一进程的所有线程共享本进程的资源,如内存,CPU,IO等。进程之间的资源是独立的,无法共享。
3.执行过程:每一个进程可以说就是一个可执行的应用程序,每一个独立的进程都有一个程序执行的入口,顺序执行序列。但是线程不能够独立执行,必须依存在应用程序中,由程序的多线程控制机制进行控制。
4.健壮性: 因为同一进程的所以线程共享此线程的资源,因此当一个线程发生崩溃时,此进程也会发生崩溃。 但是各个进程之间的资源是独立的,因此当一个进程崩溃时,不会影响其他进程。因此进程比线程健壮。
线程执行开销小,但不利于资源的管理与保护。
进程的执行开销大,但可以进行资源的管理与保护。进程可以跨机前移。
进程间通信方式
(1)管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信。
(2)命名管道(named pipe):命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。
(3)信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。Linux中可以使用kill -12 进程号,像当前进程发送信号,但前提是发送信号的进程要注册该信号。
example:
OperateSignal operateSignalHandler = new OperateSignal();
Signal sig = new Signal("USR2");
Signal.handle(sig, operateSignalHandler);
(4)消息(Message)队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺限。
(5)共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
(6)内存映射(mapped memory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它。
Java 中有类 MappedByteBuffer实现内存映射
(7)信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
(8)套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。
信号量是如何实现的
semaphore是一种基于计数的信号量,他可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号会被阻塞,semaphore可以用来构建一些对象池,资源池之类的,比如数据库连接池
JAVA中的线程
线程实现的四种方式
1.继承Thread类
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("重写run中的方法");
}
}
MyThread myThread = new MyThread();
myThread.start();
2.实现Runnable接口实现runnable的类需要作为参数传递给新建的Thread类
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("runnbale");
}
}
MyRunnable myRunnable = new MyRunnable();
Thread th = new Thread(myRunnable);
th.start();
3.实现Callable如果想要线程有返回值则必须要实现Callable接口,然后使用Feature类来接受,调用get()可以获取返回结构
public class MyCallable implements Callable {
@Override
public Object call() throws Exception {
return new String("你好");
}
}
ExecutorService executorService = Executors.newFixedThreadPool(10);
List<Future> lists = new ArrayList<>();
for (int i =0;i<10; i++) {
Callable callable = new MyCallable();
Future f = executorService.submit(callable);
lists.add(f);
}
executorService.shutdown();
for (Future list:lists) {
list.get().toString();
}
4.使用线程池来创建
结束线程的三种方式
1.正常运行完毕线程结束
2.线程在使用的时候触发了异常
3.使用stop()函数直接结束
stop()函数可能引发的问题就是stop函数结束的时候线程资源可能会不能正常是释放,造成死锁的现象,让整个进程崩溃
打断线程的四种方式
- 正常结束
- 使用退出标识
- 使用interrput()函数
- 抛出InterrputException异常捕获后调用break结束循环,然后正常结束run()方法
- 和isInterrput一起使用,当interrput()函数调用的时候isInterrput会变成true,用它来当做结束标识符
- stop()可能造成死锁,不推荐
四种线程池
4.1.3.1. newCachedThreadPool
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。
4.1.3.2. newFixedThreadPool
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
4.1.3.3. newScheduledThreadPool
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
scheduledThreadPool.schedule(newRunnable(){
@Override
public void run() {
System.out.println("延迟三秒");
}
}, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){
@Override
public void run() {
System.out.println("延迟 1 秒后每三秒执行一次");
}
},1,3,TimeUnit.SECONDS);
4.1.3.4. newSingleThreadExecutor
Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!
线程池的工作流程
拒绝策略
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK 内置的拒绝策略如下:
- AbortPolicy : 直接抛出异常,阻止系统正常运行。
- CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
- DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
- DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口。
Java 线程池工作过程
- 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
- 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。 - 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小
线程的生命周期
-
新建
- 使用new关键字的时候
-
准备
- start()的时候
-
运行
- 被分配cpu的时候
-
阻塞
- 发生阻塞的情况
- wait()
- jvm放入等待队列
- 等待获取锁
- jvm放入锁队列
- sleep(),join(),IO读写,变成阻塞状态
- 发生阻塞的情况
-
死亡
- 发生死亡的情况
- Run(),call()正常直行完成
- stop()直接结束,但是可能会造成死锁非常不建议
- 线程抛出异常。
- 发生死亡的情况
锁
锁的种类
乐观锁
一种类乐观机制,认为访问的线程多读少写,并且不会出现并发的情况。使用cas实现,当时写入的时候一般会先读取这个数据的版本号,如果版本号之后确认版本号是否和自己读取到的一直,然后获取锁,如果一致则提交不一致就重复读-比较-写的操作
悲观锁
synchronized是悲观锁的实现,认为访问线程写多读少,并且都会发生并发操作,每次读写的时候就都会上锁
自旋锁
如果获取了锁的线程运行时间非常的短就让线程们等一等,然后在获取锁,不需要转化为阻塞状态
1.当cpu的消耗,比线程从核线程转化为用户线程消耗小的时候,可以资源开支
2.如果自旋时间过长,占用cpu太多,可能会导致其他线程没有办法使用cpu,并且大量浪费cpu资源
可重入锁
可重用锁就是当一个线程的外层函数获取了锁,之后他递归的内层函数也是有锁的代码的。
公平锁与非公平锁
公平锁就是先进入队列的先获取到cpu
不同平锁就是不需要排队,直接尝试获取,获取不到到队列尾部等待。
非公平锁比公平锁快5-1倍,Synchronized,ReentrantLock都是非公平锁
读写锁
- 读锁
- 可以同时读,不能同时写
- 写锁
- 不能同时读,不能同时写
为了提高java性能,更方便的控制在读的地方使用读锁,写的地方使用写锁
读读不互斥,读写互斥,写写互斥
共享锁和独占锁
共享锁:读锁
独占锁:悲观锁
锁的状态
重量锁
依赖于操作系统的锁,称之为重锁,因为他们开销太大
轻量锁
适用于线程交替执行同步块的情况,如果存在统一是时间访问同一锁的情况,会导致轻量锁膨胀
偏向锁
在一个线程获取锁之后,消除这个线程重入的开销,只在单线程中有效
无状态锁
分段锁
分段锁是一种思想,为了帮助锁最好的提高系统的稳定性和性能
减少锁持有的时间
只用在有现成安全的要求的程序上加锁
减少锁的粒度
减小锁要锁定的区间
锁分离
读写锁
锁粗话
对一个锁不停的请求,锁的创建和销毁需要耗费大量资源,所以选择的处理锁
锁消除
编译器期间看到代码中发现不可能被共享的对象就删掉他
AtomicInteger
原子操作
把一系列的代码看做一个原子,原内部的所有操作要么都执行要么都不执行。会有回滚的操作
单核cpu在原子类,操作的时候每次只让一个线程使用这个操作
多核心cpu锁定这个内存地址,然后不让其他线程访问。
Synchronized
ReentrantLock
面试题
控制线程执行顺序
ExecutorService executorService = Executors.newSingleThreadExecutor();
利用并发包里的Excutors的newSingleThreadExecutor产生一个单线程的线程池,而这个线程池的底层原理就是一个先进先出(FIFO)的队列。代码中executor.submit依次添加了123线程,按照FIFO的特性,执行顺序也就是123的执行结果,从而保证了执行顺序。
Join()作用:让主线程等待子线程运行结束后才能继续运行。
这段代码里面的意思是这样的:
程序在main线程中调用thread1线程的join方法,则main线程放弃cpu控制权,并返回thread1线程继续执行直到线程thread1执行完毕
所以结果是thread1线程执行完后,才到主线程执行,相当于在main线程中同步thread1线程,thread1执行完了,main线程才有执行的机会
synchronized底层原理和代码
底层是使用一个mino(记不太清了,反正是一个信号量对象来实现的),当一个对象获取锁的时候他会给这个信号量加一,当新号量为0的时候才可以继续竞争。
网络编程
网络的七层网络模型
1.物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由 1、0 转化为电流强弱行传输,到达目的地后在转化为1、0,也就是我们常说的模数转换与数模转换)。这一层的数据叫做比特。
2.数据链路层:主要将从物理层接收的数据进行 MAC 地址网卡的地址)的封装与解封装。常把这一层的数据叫做帧。在这一层工作的设备是交换机,数据通过交换机来传输。
3.网络层:主要将从下层接收到的数据进行 IP 址(例 192.168.0.1)的封装与解封装。在这一层工作的设备是路由器,常把这一层的数据叫做数据包。
4.传输层:定义了一些传输数据的协议和端口号(WWW 端口 80 等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与 TCP 特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如 QQ 聊天数据就是通过这
种方式传输的)。 主要是将从下层接收的数据进行分段进行传输,到达目的地址后在进行重组。常常把这一层数据叫做段。
5.会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或或者接受会话请求(设备之间需要互相认识可以是 IP 也可以是 MAC 或者是主机名)
6.表示层:主要是进行对接收的数据进行解释、加密与解密、压缩与解压缩等(也就是把计算机能够识别的东西转换成人能够能识别的东西(如图片、声音等))
7.应用层 主要是一些终端的应用,比如说FTP(各种文件下载),WEB(IE浏览),QQ之类的(你就把它理解成我们在电脑屏幕上可以看到的东西.就 是终端应用)。
TCP网络的四层网络模型
网络访问层 访问层(Network Access Layer)
网络访问层(Network Access Layer)在 TCP/IP 参考模型中并没有详细描述,只是指出主机必须使用某种协议与网络相连。
网络层 网络层(Internet Layer)
网络层(Internet Layer)是整个体系结构的关键部分,其功能是使主机可以把分组发往任何网络,并使分组独立地传向目标。这些分组可能经由不同的网络,到达的顺序和发送的顺序也可能不同。高层如果需要顺序收发,那么就必须自行处理对分组的排序。互联网层使用因特网协议(IP,Internet Protocol)。
传输层 传输层(Tramsport Layer-TCP/UDP)
传输层(Tramsport Layer)使源端和目的端机器上的对等实体可以进行会话。在这一层定义了两个端到端的协议:传输控制协议(TCP,Transmission Control Protocol)和用户数据报协议(UDP,User Datagram Protocol)。TCP 是面向连接的协议,它提供可靠的报文传输和对上层应用的连接服务。为此,除了基本的数据传输外,它还有可靠性保证、流量控制、多路复用、优先权和安全性控制等功能。UDP 是面向无连接的不可靠传输的协议,主要用于不需要 TCP 的排序和流量控制等功能的应用程序。
应用层 应用层(Application Layer)
应用层(Application Layer)包含所有的高层协议,包括:虚拟终端协议(TELNET,TELecommunications NETwork)、文件传输协议(FTP,File Transfer Protocol)、电子邮件传输协议(SMTP,Simple Mail Transfer Protocol)、域名服务(DNS,Domain Name13/04/2018 Page 161 of 283Service)、网上新闻传输协议(NNTP,Net News Transfer Protocol)和超文本传送协议(HTTP,HyperText Transfer Protocol)等。
五层网络模型
物理层主要作用是定义物理设备如何传输数据,机器的硬件,网卡端口,网线等。
数据链路层在通信的实体间建立数据链路连接,比如最基础的数据传输数据流,可以自己选择二进制或者ASCII码形式等。
网络层为数据在结点之间传输创建逻辑链路,比如输入百度,网络层会为我们找到百度的网址,如何寻找到的过程就是网络层要做的事。
传输层:向用户提供可靠的端到端(end-to-end)服务;传输层向高层屏蔽了下层数据通信的细节(比如一个post请求,如何分片如何发送使服务端很好接收到,这个规则由传输层实现,应用层的HTTP不用关心这些,但是适当理解对HTTP更好地使用是很有帮助的)。
应用层:为应用软件提供了很多服务,帮我们实现了HTTP协议,我们只要按照规则去使用HTTP协议;它构建于TCP协议之上;屏蔽了网络传输相关细节。
网络套接字和本地套接字的区别
本地套接字:套接字用于同一台计算机上运行的进程之间的通信。他的执行效率更高,仅仅是复制数据,并不执行协议处理,不需要添加和删除网络报头,不需要计算校验和等,只支持流式和数据报两种接口,服务是可靠的,不会丢失报文也不会出错,
网络套接字:进行tcp连接,需要连接的时候需要经过三次握手,断开的时候需要经过四次挥手。
TCP详解
TCP协议全称: 传输控制协议,,对数据的传输进行一定的控制。
TCP首部:至少有20byte,包括
-
16位源端口号/16位目的端口号: 表示数据从哪个进程来, 到哪个进程去.
-
32位序号:SEQ 表示当前请求的序号
-
32为确认号:ACK一般是确认响应的请求序号加一
-
4位首部长度: 表示该tcp报头有多少个4字节(32个bit)
-
6位保留: 顾名思义, 先保留着, 以防万一
-
6位标志位
URG: 标识紧急指针是否有效
ACK: 标识确认序号是否有效
PSH: 用来提示接收端应用程序立刻将数据从tcp缓冲区读走
RST: 要求重新建立连接. 我们把含有RST标识的报文称为复位报文段
SYN: 请求建立连接. 我们把含有SYN标识的报文称为同步报文段
FIN: 通知对端, 本端即将关闭. 我们把含有FIN标识的报文称为结束报文段 -
16位窗口大小:
-
16位检验和: 由发送端填充, 检验形式有CRC校验等. 如果接收端校验不通过, 则认为数据有问题. 此处的校验和不光包含TCP首部, 也包含TCP数据部分.
-
16位紧急指针: 用来标识哪部分数据是紧急数据.
-
选项和数据
TCP的三次握手和四次挥手
三次握手
四次挥手
关于time_wait状态的理解
TIME_WAIT状态之所以存在,是为了保证网络的可靠性
有以下原因:
1.为实现TCP全双工连接的可靠释放(缓冲的作用)
当服务器先关闭连接,如果不在一定时间内维护一个这样的TIME_WAIT状态,那么当被动关闭的一方的FIN到达时,服务器的TCP传输层会用RST包响应对方,这样被对方认为是有错误发生,事实上这只是正常的关闭连接工程,并没有异常
2.为使过期的数据包在网络因过期而消失(过期的作用)
在这条连接上,客户端发送了数据给服务器,但是在服务器没有收到数据的时候服务器就断开了连接
现在数据到了,服务器无法识别这是新连接还是上一条连接要传输的数据,一个处理不当就会导致诡异的情况发生
下面讲讲大量的TIME_WAIT产生需要的条件:
1.高并发:请求量很多
2.服务器主动关闭连接:那么下面的所有的客户端都开始触发TIME_WAIT
如果服务器不主动关闭连接,那么TIME_WAIT就是客户端的事情了
问题1:如果服务器端确实存在大量的TIME_WAIT,那么会导致什么问题呢?
问题2: 首先先明确TIME_WAIT状态占用的到底是什么?
被占用的是一个五元组(协议,本地IP,本地端口,远程IP,远程端口)
对于Web服务器,协议是TCP,本地ip也只有一个,端口一般是80或者433或8080(固定的),只剩下远程IP和远程端口可用了,如果远程IP相同的话,就只有远程端口可用了,远程端口只有几万个,所以当同一客户端向服务器建立了大量连接的话,可用的五元组会耗尽导致问题
现在我们知道了大量的TIME_WAIT会占用大量的五元组
那么五元组什么时候会耗尽呢?
当客户端通过应用层的负载均衡代理到服务器导致进入服务器的ip地址只有几个的话,可能会导致五元组耗尽!
产生大量TIME_WAIT状态的解决办法:
解决方法1:服务器不主动关闭连接,那么这个问题就是客户端应该解决的了~(TIME_WAIT将不会产生)
解决方法2:增加客户端IP(一般客户端IP少都是通过应用层的负载均衡到达服务器的)(五元组将不会耗尽)
解决方法3:设置允许地址重用,这样每次bind的时候,如果五元组正在使用,bind就会把五元组抢过来(不安全)
TCP机制
确认应答
每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你要从哪里开始发.
比如, 客户端向服务器发送了1005字节的数据, 服务器返回给客户端的确认序号是1003, 那么说明服务器只收到了1-1002的数据.
1003, 1004, 1005都没收到.
此时客户端就会从1003开始重发.
超时重传
主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B
如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发
但是主机A没收到确认应答也可能是ACK丢失了。服务器返回的数据丢包
这种情况下, 主机B会收到很多重复数据.
那么TCP协议需要识别出哪些包是重复的, 并且把重复的丢弃.
这时候利用前面提到的序列号, 就可以很容易做到去重.
滑动窗口
一个概念: 窗口
窗口大小指的是无需等待确认应答就可以继续发送数据的最大值.
上图的窗口大小就是4000个字节 (四个段).
发送前四个段的时候, 不需要等待任何ACK, 直接发送
收到第一个ACK确认应答后, 窗口向后移动, 继续发送第五六七八段的数据…
因为这个窗口不断向后滑动, 所以叫做滑动窗口.
操作系统内核为了维护这个滑动窗口, 需要开辟发送缓冲区来记录当前还有哪些数据没有应答
只有ACK确认应答过的数据, 才能从缓冲区删掉.
如果该出现丢包的情况:
此时分两种情况讨论:
1, 数据包已经收到, 但确认应答ACK丢了.
这种情况下, 部分ACK丢失并无大碍, 因为还可以通过后续的ACK来确认对方已经收到了哪些数据包.
2, 数据包丢失
当某一段报文丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 “我想要的是 1001”
如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送
这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了
因为2001 - 7000接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中.
流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被填满, 这个时候如果发送端继续发送, 就会造成丢包, 进而引起丢包重传等一系列连锁反应.
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度.
这个机制就叫做 流量控制(Flow Control)
接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段,
通过ACK通知发送端;
窗口大小越大, 说明网络的吞吐量越高;
接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
发送端接受到这个窗口大小的通知之后, 就会减慢自己的发送速度;
如果接收端缓冲区满了, 就会将窗口置为0;
这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 让接收端把窗口大小再告诉发送端.
那么接收端如何把窗口大小告诉发送端呢?
我们的TCP首部中, 有一个16位窗口大小字段, 就存放了窗口大小的信息;
16位数字最大表示65536, 那么TCP窗口最大就是65536字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是窗口字段的值左移 M 位(左移一位相当于乘以2).
拥塞控制
虽然TCP有了滑动窗口这个大杀器, 能够高效可靠地发送大量数据.
但是如果在刚开始就发送大量的数据, 仍然可能引发一些问题.
因为网络上有很多计算机, 可能当前的网络状态已经比较拥堵.
在不清楚当前网络状态的情况下, 贸然发送大量数据, 很有可能雪上加霜.
因此, TCP引入 慢启动
机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态以后, 再决定按照多大的速度传输数据.
在此引入一个概念 拥塞窗口
- 发送开始的时候, 定义拥塞窗口大小为1;
- 每次收到一个ACK应答, 拥塞窗口加1;
- 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口
像上面这样的拥塞窗口增长速度, 是指数级别的.
“慢启动” 只是指初使时慢, 但是增长速度非常快.
为了不增长得那么快, 此处引入一个名词叫做慢启动的阈值
, 当拥塞窗口的大小超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长.
- 当TCP开始启动的时候, 慢启动阈值等于窗口最大值
- 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1
少量的丢包, 我们仅仅是触发超时重传;
大量的丢包, 我们就认为是网络拥塞;
当TCP通信开始后, 网络吞吐量会逐渐上升;
随着网络发生拥堵, 吞吐量会立刻下降.
拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.
延迟应答
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小.
假设接收端缓冲区为1M. 一次收到了500K的数据;
如果立刻应答, 返回的窗口大小就是500K;
但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了; 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
如果接收端稍微等一会儿再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M
窗口越大, 网络吞吐量就越大, 传输效率就越高.
TCP的目标是在保证网络不拥堵的情况下尽量提高传输效率;
那么所有的数据包都可以延迟应答么?
肯定也不是
有两个限制
- 数量限制: 每隔N个包就应答一次
- 时间限制: 超过最大延迟时间就应答一次
具体的数量N和最大延迟时间, 依操作系统不同也有差异
一般 N
取2, 最大延迟时间
取200ms
捎带应答
在延迟应答的基础上, 我们发现, 很多情况下
客户端和服务器在应用层也是 “一发一收” 的
意味着客户端给服务器说了 “How are you”
服务器也会给客户端回一个 “Fine, thank you”
那么这个时候ACK就可以搭顺风车, 和服务器回应的 “Fine, thank you” 一起发送给客户端
面向字节流
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区
和一个 接收缓冲区
;
调用write
时, 数据会先写入发送缓冲区中;
如果发送的字节数太大, 会被拆分成多个TCP的数据包发出;
如果发送的字节数太小, 就会先在缓冲区里等待, 等到缓冲区大小差不多了, 或者到了其他合适的时机再发送出去;
接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
然后应用程序可以调用read
从接收缓冲区拿数据;
另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区,
那么对于这一个连接, 既可以读数据, 也可以写数据, 这个概念叫做 全双工
由于缓冲区的存在, 所以TCP程序的读和写不需要一一匹配
例如:
- 写100个字节的数据, 可以调用一次
write
写100个字节, 也可以调用100次write
, 每次写一个字节; - 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次
read
100个字节, 也可以一次read
一个字节, 重复100次;
粘包问题
首先要明确, 粘包问题中的 “包”, 是指应用层的数据包
.
在TCP的协议头中, 没有如同UDP一样的 “报文长度” 字段
但是有一个序号字段.
站在传输层的角度, TCP是一个一个报文传过来的. 按照序号排好序放在缓冲区中.
站在应用层的角度, 看到的只是一串连续的字节数据.
那么应用程序看到了这一连串的字节数据, 就不知道从哪个部分开始到哪个部分是一个完整的应用层数据包.
此时数据之间就没有了边界, 就产生了粘包问题
那么如何避免粘包问题呢?
归根结底就是一句话, 明确两个包之间的边界
对于定长的包
- 保证每次都按固定大小读取即可
例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可对于变长的包
- 可以在数据包的头部, 约定一个数据包总长度的字段, 从而就知道了包的结束位置
还可以在包和包之间使用明确的分隔符来作为边界(应用层协议, 是程序员自己来定的, 只要保证分隔符不和正文冲突即可)
UDP是否存在粘包问题
对于UDP, 如果还没有向上层交付数据, UDP的报文长度仍然存在.
同时, UDP是一个一个把数据交付给应用层的, 就有很明确的数据边界.
站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收.
不会出现收到 “半个” 的情况.
TCP和UDP的区别
1、 TCP面向连接 (如打电话要先拨号建立连接); UDP是无连接 的,即发送数据之前不需要建立连接
2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。
3、UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。
4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP对系统资源要求较多,UDP对系统资源要求较少。
面试问题总结:
请谈一下,你知道的http请求,并说明应答码502和504的区别
502:作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。
504:作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器(URI标识出的服务器,例如HTTP、FTP、LDAP)或者辅助服务器(例如DNS)收到响应。
请说明一下http和https的区别
考察点:http协议
参考回答;
https协议要申请证书到ca,需要一定经济成本;2) http是明文传输,https是加密的安全传输;3) 连接的端口不一样,http是80,https是443;4)http连接很简单,没有状态;https是ssl加密的传输,身份认证的网络协议,相对http传输比较安全。
请讲一下浏览器从接收到一个URL,到最后展示出页面,经历了哪些过程。
考察点:http协议
参考回答:
1.DNS解析 2.TCP连接 3.发送HTTP请求 4.服务器处理请求并返回HTTP报文 5.浏览器解析渲染页面、
DNS解析:
如用客户端浏览器请求这个页面: http://localhost.com:8080/index.htm 从中分解出协议名、主机名、
端口、对象路径等部分
请你说明一下,SSL四次握手的过程
考察:HTTP加密协议
参考回答:
1、 客户端发出请求
首先,客户端(通常是浏览器)先向服务器发出加密通信的请求,这被叫做ClientHello请求。
2、服务器回应
服务器收到客户端请求后,向客户端发出回应,这叫做SeverHello。
3、客户端回应
客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。
4、服务器的最后回应
服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的"会话密钥"。然后,向客户端最后发送下面信息。
(1)编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
(2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。
至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的HTTP协议,只不过用"会话密钥"加密内容。
请你讲讲http1.1和1.0的区别
考察点:http
参考回答:
主要区别主要体现在:
缓存处理,在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
带宽优化及网络连接的使用,HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
错误通知的管理,在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
Host头处理,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)
。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。
长连接,HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。
请求头
connection:keep-alive
content-type
host
allow-accrossing
authorization
cookie
GET和POST两种基本请求方法有什么区别
GET和POST是什么?HTTP协议中的两种发送请求的方法。
HTTP是什么?HTTP是基于TCP/IP的关于数据如何在万维网中如何通信的协议。
通俗的说法:
- GET请求只能进行url编码,而POST支持多种编码方式。
- GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
- GET请求在URL中传送的参数是有长度限制的,而POST么有。
- 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
- GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
- GET参数通过URL传递,POST放在Request body中。
加分:
简单的说:GET产生一个TCP数据包;POST产生两个TCP数据包。
长的说:对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
八大排序算法
冒泡排序
思路:比较前后两个数据,如果前面的数据比后面的数据大就让前后的数据交换位置,如果后面的数据比较大,就把后面的数据设置为要移动的对象。
public static int[] reput(int[] array){
int temp;
int[] arr=array;
for(int i = 0;i<arr.length;i++){
for (int j = 1; j < arr.length - i; j++) {
if( arr[j-1]>arr[j]){
temp = arr[j-1];
arr[j-1]=arr[j];
arr[j]=temp;
}
}
}
return arr;
}
选择排序
思路:查找所有数据中最下打的数据并把它放到队列的最左端。(最左端还是最右端看自己想按着从小到大排序还是从大到小排序)
public static int[] xuanze(int[] array){
int[] arr =array;
int single;
for(int i=0;i<arr.length;i++){
single = i;
for(int j=i;j<arr.length;j++){
if(arr[single]>arr[j]){
single = j;
}
}
int temp = arr[single];
arr[single] = arr[i];
arr[i] = temp;
}
return arr;
}
插入排序
思路:标记一个没有顺序的第一个元素,然后把他和前面有序的数组进行比较,找到可以插入的位置,让后让插入位置后面和插入位置上的数据全部后移一位,再把刚才的值赋值给该位置。
public class ChaRuSort {
/**
* 时间复杂度为 O(n*n)
* 空间复杂度为 O(1)
*
* 稳定排序
*/
public static void main(String[] args) {
int[] arr = {1234,123415,616,6,6,1461,7417,51,818,58,245,6164,2471754,735735};
sort(arr);
for (int a:arr) {
System.out.print(a+" ");
}
}
public static void sort(int[] arr){
for (int i = 0; i <arr.length-1 ; i++) {
for (int j = i+1; j > 0 ; j--) {
if(arr[j-1]>arr[j]){
int temp = arr[j-1];
arr[j-1] = arr[j];
arr[j] = temp;
}
}
}
}
}
希尔排序
思路:在插入排序的基础上设定一个步长,每次从大步长的插入排序逐渐减小步长,直到步长为一。
public class XiErSort {
/**
* 时间复杂度为 O(nlogn)
* 空间复杂度为 O(1)
* 不稳定
*/
public static void main(String[] args) {
int[] arr = {1234,123415,616,6,6,121,4654,56465,46,4,54654564,4,6548789,4561,6,165,165,45,1461,7417,51,818,58,245,6164,2471754,735735};
sort(arr);
for (int a:arr) {
System.out.print(a+" ");
}
}
public static void sort(int[] arr){
int d = arr.length;
while(d>1){
d = d/2;
for (int i = d; i < arr.length; i=i+d) {
for (int j = i; j > d-1 ; j=j-d) {
if(arr[j-d] > arr[j]){
int temp = arr[j-d];
arr[j-d] = arr[j];
arr[j] = temp;
}
}
}
}
}
}
归并排序
思路:将所有的数据拆分,拆分成最小单元,然后进行合并,合并过程中按照顺序合并,分别对左右两边的数据进行合并,最终合并成一个大数组。
/**
* 时间复杂度为 O(nlogn)
* 空间复杂度为 O(logn)
*
* 不稳定算法
*/
public class GuiBingSort {
public static void main(String[] args) {
int[] arr = {9879,21,54,54654,498,79,87,8979,797,987,789};
sort(arr);
for (int i = 0; i <arr.length ; i++) {
System.out.print(arr[i]+" ");
}
System.out.println();
}
public static void sort(int[] arr){
int[] temp = new int[arr.length];
sort(arr,0,arr.length-1,temp);
}
public static void sort(int[] arr,int left,int right,int[] temp){
if(left < right){
int mid = (left+right)/2;
sort(arr,left,mid,temp);
sort(arr,mid+1,right,temp);
merge(arr,left,mid,right,temp);
}
}
public static void merge(int[] arr,int left,int mid,int right,int[] temp){
int l = left;
int r = mid+1;
int t =0;
while (l<=mid && r<=right){
if(arr[l]<=arr[r]){
temp[t++] = arr[l++];
}else {
temp[t++] = arr[r++];
}
}
while(l<=mid){
temp[t++] = arr[l++];
}
while(r<=right){
temp[t++] = arr[r++];
}
for (int i = 0; i < t; i++) {
arr[left+i] = temp[i];
}
}
}
快速排序
思路:在数组中选择一个基数,把数组中大于基数的数放到数组右边,小于基数的数放到左边,然后对左右两边在分别运行这种算法,直到都只有一个子集为止。
/**
* 快速排序算法:
* 平均时间复杂度为 O(nlogn)
* 平均空间复杂度为 O(logn)
*
* 算法不稳定
*
* 最坏的情况下时间复杂度为 O(n*n)
*/
public class QuickSort {
/**
* 快排的思路就是先找到一个基础知识,然后分别从左右两边进行寻找左边寻找大于基础值的,右边寻找小于基础值的,找到后交换。
* @param arr
* @param star
* @param end
*/
public static void quickSort(int[] arr,int star ,int end){
if(star > end){
return;
}
int low = star;
int height = end;
//找到对应的基准点取low就好
int base = arr[low];
while(low < height){
//从右边开始查找对应的第一个的可以交换的数据
while(base < arr[height] && low < height){
height--;
}
if(low < height){
arr[low] = arr[height];
low++;
}
while(base >= arr[low] && low<height){
low++;
}
if(low <height){
arr[height] = arr[low];
height--;
}
}
arr[low] = base;
quickSort(arr,star,low-1);
quickSort(arr,low+1,height);
}
public static void main(String[] args) {
int[] arr = {1456,48,4,87,9879,7897,789,2454,84,4,446,454,645,21,21,35,4,534,54,354,3543544};
quickSort(arr,0,arr.length-1);
for (int i = 0; i <arr.length ; i++) {
System.out.print(arr[i]+" ");
}
System.out.println();
}
}
堆排序
思路:首先构建一个堆(我构建的是大底堆),然后把堆顶的元素放到堆的最后一个位置。然后把最后一个位置之前的元素重新构建成一个大底堆,以此类推,直到最后一个元素。
/**
* 大佬的排序算法
* 时间复杂度O(nlog2n)
* 空间复杂度O(1)
* 稳定性 不稳定
*/
public void HeapAdjust(int[] array, int parent, int length) {
int temp = array[parent]; // temp保存当前父节点
int child = 2 * parent + 1; // 先获得左孩子
while (child < length) {
// 如果有右孩子结点,并且右孩子结点的值大于左孩子结点,则选取右孩子结点
if (child + 1 < length && array[child] < array[child + 1]) {
child++;
}
// 如果父结点的值已经大于孩子结点的值,则直接结束
if (temp >= array[child])
break;
// 把孩子结点的值赋给父结点
array[parent] = array[child];
// 选取孩子结点的左孩子结点,继续向下筛选
parent = child;
child = 2 * child + 1;
}
array[parent] = temp;
}
基数排序
思路:创建10个桶,然后分别按照每个位上的数组放入对应的桶中,排序次数是最大数的位数。
/**
* 时间复杂度为O(n*k) k表示最高位的个数
* 空间复杂度为O(n*k)
* 稳定性算法
*
* 最好的情况是O(n*k)
* 最坏的情况是O(n*k)
*/
public class JiShuSort {
public static void main(String[] args) {
// for (int i = 356 ; i > 0 ; i=i/10) {
// System.out.println(i);
// }
int[] arr = {9879,21,54,54654,498,79,87,8979,797,987,789};
search(arr);
for (int i = 0; i <arr.length ; i++) {
System.out.print(arr[i]+" ");
}
System.out.println();
}
public static int getMax(int[] arr){
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if(max < arr[i]){
max=arr[i];
}
}
return max;
}
public static void search(int[] arr){
int max = getMax(arr);
int count =0;
for (int i = max ; i > 0 ; i=i/10) {
count++;
JiShu(arr,count);
}
}
public static void JiShu(int[] arr,int count){
int coun = (int) Math.pow(10,count-1);
ArrayList<ArrayList<Integer>> arrayList = new ArrayList<>();
//创建10个队列
for (int i = 0; i < 10; i++) {
arrayList.add(new ArrayList<>());
}
//根据个位数把所有的对象放入对应的桶中
for (int i = 0; i < arr.length; i++) {
arrayList.get((arr[i]/coun)%10).add(arr[i]);
}
//把桶中的数据取出来按顺序放回数组中
int index=0;
for (ArrayList<Integer> array :arrayList) {
for (Integer i:array) {
arr[index++] = i;
}
}
}
}
桶排序
思路:桶排序是分治思想,把所以的数据分散到多个桶中,然后在桶中对他们进行排序,然后从桶中把全部的数据再取出来。
/**
*
* 时间复杂度:O(n+k),k为 n*logn-n*logm
*
* 空间复杂度:O(n+m), m是桶的数量
*
*
* 桶排序是一种稳定排序算法。
*/
public class TongSort {
public static void main(String[] args) {
int[] arr = {1,21,5,4,49,78,797,89,79,7,87,64,984,4545,987,56540,87,6,54,64,464,6546,867,41,648,765,6,4,678};
sort(arr);
for (int i:
arr) {
System.out.print(i+ " ");
}
}
public static void sort(int[] array) {
//最大最小值
int max = array[0];
int min = array[0];
int length = array.length/4;
for(int i=1; i<array.length; i++) {
if(array[i] > max) {
max = array[i];
} else if(array[i] < min) {
min = array[i];
}
}
//最大值和最小值的差
int diff = max - min;
//桶列表
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<>();
for(int i = 0; i < length; i++){
bucketList.add(new ArrayList<>());
}
//每个桶的存数区间
float section = (float) diff / (float) (length - 1) ;
//数据入桶
for(int i = 0; i < array.length; i++){
//当前数除以区间得出存放桶的位置 减1后得出桶的下标
int num = (int) (array[i] / section) - 1;
if(num < 0){
num = 0;
}
bucketList.get(num).add(array[i]);
}
//桶内排序
for(int i = 0; i < bucketList.size(); i++){
//jdk的排序速度当然信得过
Collections.sort(bucketList.get(i));
}
//写入原数组
int index = 0;
for(ArrayList<Integer> arrayList : bucketList){
for(int value : arrayList){
array[index] = value;
index++;
}
}
}
}