面试题
- java基础
- java语言特点
- java的执行机制
- 命名规范
- JDK 和 JRE 有什么区别?
- 什么是跨平台性?原理是什么
- 什么是字节码?采用字节码的最大好处是什么
- java中的编译器和解释器:
- 一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制?
- 程序文件名必须与公共外部类的名称完全一致(包括大小写)(对)
- Java语言采用何种编码方案?有何特点?
- 语句:char foo='中',是否正确?(假设源文件以GB2312编码存储,并且以javac – encoding GB2312命令编译)
- /* *..................** /中可以嵌套//注释,也能嵌套/*..........*/注释(错误)。
- JDK常用的包
- 一个十进制的数在内存中是怎么存的?
- jdk1.8新特性
- 说说 Lamda 表达式的优缺点。
- == 和 equals 的区别是什么?
- Java 中应该使用什么数据类型来代表价格?
- float f=3.4;是否正确
- 3*0.1 == 0.3 将会返回什么?true 还是 false?
- 为啥有时会出现 4.0-3.6=0.40000001 这种现象
- short s1 = 1; s1 = s1 + 1;有什么错? short s1 =1; s1 +=1;有什么错?
- a = a + b 与 a += b 的区别
- 下面程序运行结果
- java 中有可能出现 i + 1 < i 的情况吗?为什么
- ⽤最有效率的⽅法算出2乘以8
- 下面代码输出结果是?(C)
- 下列代码的输出结果是
- break 和 continue,return 的区别?
- 下面的方法,当输入为2的时候返回值是多少
- java支持的基本数据类型有那些,什么是自动拆箱
- java 基本类型与引用类型的区别
- 包装类的作用:
- int和integer的区别
- 两个new生成的Integer变量的对比
- Integer变量和int变量的对比
- 非new生成的Integer变量和new Integer()生成变量的对比
- 两个非new生成的Integer对象的对比
- 如下两个题目输出结果相同吗?各是什么
- 什么是值传递和引用传递?
- &与&&的区别
- switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String上
- 数组有没有 length() 方法?String 有没有 length() 方法
- 怎么将 byte 转换为 String?
- 我们能将 int 强制转换为 byte 类型的变量吗?如果该值大于 byte 类型的范围,将会出现什么现象?
- 下列哪些语句关于内存回收的说明是正确的? (B )
- 关于访问权限说法正确 的是 ? ( B)
- 数组
- java语言中的数组元素下标总是从0开始,下标可以是整数或整型表达式。(对)
- java语言的下面几种数组复制方法中,哪个效率最高?(B)
- 面向对象
- 什么是面向对象,面向对象理解
- java创建对象的四种方法
- 构造方法的特点
- 构造器Constructor是否可被override
- 封装
- 继承
- 继承特点
- 继承带来的好处与弊端
- 继承中变量的访问特点
- 继承中构造方法的访问特点
- 继承中成员方法的访问特点
- 继承的注意事项
- java为什么是单继承
- 如果一个接口Glass有个方法setColor(),有个类BlueGlass实现接口Glass,则在类BlueGlass中正确的是? (C )
- 多态
- 多态中的成员访问特点
- 多态的好处和弊端
- 根据下面这个程序的内容,判断哪些描述是正确的:( )
- final 在 java 中有什么作用
- final、finally、finalize的区别?
- static的访问特点
- 静态变量的特点与区别
- 抽象类(abstract class)和接口(interface)有什么异同
- 什么时候用接口什么时候用抽象类
- 抽象选择题
- 接⼝是否可继承接⼝? 抽象类是否可实现(implements)接⼝? 抽象类是否可继承具体类? 抽象类中是否可以有静态的main⽅法?
- 抽象类方法的访问权限默认都是public。( 错 )
- 说说overload和override的区别
- Java 中是否可以覆盖(override)一个 private 或者是 static 的方法?
- 为什么函数不能根据返回类型来区分重载?
- 构造器(constructor)是否可被重写(override)?
- 说出下面运行结果
- 代码块
- java静态变量、代码块、和静态方法的执行顺序是什么?
- 说出下面运行结果
- 说出下面运行结果
- 运行代码,输出的结果是()
- 常用类
- math类
- Math.round(11.5) 等于多少?Math.round(-11.5)等于多少
- String类
- String 是基本数据类型吗?
- 字符串如何转基本数据类型
- 基本数据类型如何转字符串?
- String不变性理解
- 什么是不可变对象?好处是什么?
- 能否创建一个包含可变对象的不可变对象?
- java 中操作字符串都有哪些类?它们之间有什么区别?
- String为什么要设计成不可变的?
- 什么是字符串常量池?
- String 转出 int 型,判断能不能转?如何转
- String s = "Hello";s = s + " world!";这两行代码执行后,原始的 String 对象中的内容到底变了没有?
- 请说出下面程序的输出
- 下面程序的运行结果是
- 下面程序运行结果
- 集合
- ArrayList的源码分析:
- jdk 7情况下
- jdk 8中ArrayList的变化
- ArrayList 和 Vector,LinkedList 的区别
- 说⼀下ArrayList和LinkedList区别
- ArrayList和Vector的区别
- 数组(Array)和列表(ArrayList)有什么区别?什么时候应该使⽤Array⽽不是ArrayList?
- Iterator和ListIterator的区别是什么?
- java集合的快速失败机制"fail-fast"
- Object 若不重写 hashCode()的话,hashCode()如何计算出来的?
- 为什么重写 equals 方法必须重写 hashcode 方法 ?
- hashCode()与equals()
- hashset底层原理
- 在List内去除重复数字值,要求尽量简单
- 说出下面结果
- 说一下 map 的分类和常见的情况
- HashMap索引计算方法
- HashMap的长度为什么必须为2^n
- 为什么HashMap把hashcode做一个改进运算
- HashMap添加的对象为什么要重写equals和hashcode
- HashMapkey 的设计要求
- 说⼀下HashMap的Put⽅法
- HashMap什么时候链表会转化成为红黑树
- HashMap的树化与退化
- HashMap JDK7和JDK8的不同
- HashMap装填因子,负载因子,加载因子为什么是0.75
- HashMap和Hashtable的区别
- map底层为什么用红黑树来实现
- HashSet底层原理
- 下面程序的运行结果
- 在使用 HashMap 的时候,用 String 做 key 有什么好处?
- Comparable和Comparator接⼝是⼲什么的?列出它们的区别。
- Comparable
- Comparator
- Java集合类框架的最佳实践有哪些
- Collection和Collections的区别
- 如果一个list初始化为{5,3,1},执行以下代码后,其结果为()?
- IO流
- 字节字符区别
- char 型变量中能不能存储一个中文汉字,为什么?
- 序列化 ID作用
- 多线程
- 线程和进程有什么区别?
- 同步代码块能发生CPU的切换吗 (能)
- 并行和并发
- 创建线程的方式有哪些
- run()和start()区别
- 以下哪个事件会导致线程销毁?(D)
- sleep方法和wait方法的区别
- 说⼀下ThreadLocal
- java8中,下面哪个类用到了解决哈希冲突的开放定址法(C)
- 下列程序的运行结果
- 当编译并运行下面程序时会发生什么结果( D)
- 下列代码执行结果为()
- Executor,ExecutorService和Executors的区别
- 在java中守护线程和用户线程的区别
- 介绍一下 Syncronized 锁,如果用这个关键字修饰一个静态方法,锁住了什么?如果修饰成员方法,锁住了什么?
- 什么是多线程中的上下文切换
- 什么是死锁?死锁的危害
- 死锁产生的4个必要条件
- 锁有了解嘛,说一下 Synchronized 和 lock
- 异常
- final、finally、finalize 的区别?
- getCustomerInfo()方法如下,try中可以捕获三种类型的异常,如果在该方法运行中产生了一个IOException,将会输出什么结果()
- Throw 和 throws 的区别
- AccessViolationException异常触发后,下列程序的输出结果为( A )
- 以下代码执行的结果显示是多少()?
- 给定以下JAVA代码,这段代码运行后输出的结果是()
- try括号里有return语句, finally执行顺序
- 反射
- 什么是反射
- 反射是否破坏了封装性
- JVM
- 常用命令
- JVM中可以运行多种语言吗
- 说几个与JVM内存相关的核心参数
- 常用的jvm监控工具
- 谈谈Java中不同的引用类型
- 什么是内存泄露,什么是内存溢出
- Java运行时一个类是什么时候被加载的?
- JVM一个类的加载过程?
- 继承时父子类的初始化顺序是怎样的?
- ClassLoader如何加载class
- JVM三层类加载器之间的关系是继承吗?
- JVM类加载的双亲委派模型
- JDK为什么要设计双亲委派模型,有什么好处
- 可以打破JVM双亲委派模型吗?如何打破JVM双亲委派模型?
- 如何自定义自己的类加载器?
- ClassLoader中的loadClass()、findClass()、defineClass()区别?
- JVM有哪些内存区域
- JVM运行时数据区 程序计数器 的特点及作用
- JVM运行时数据区 虚拟机栈的特点及作用
- JVM运行时数据区 本地方法栈的特点及作用
- JVM运行时数据区 Java堆的特点及作用
- JVM运行时数据区 元空间的特点及作用
- JVM在创建对象是采用了哪些并发安全机制
- JVM中对象如何在堆内存分配
- 本地线程分配缓冲
- JVM堆内存中的对象布局
- 什么是对象头?对象头里面有哪些东西
- 实例对象是怎样存储的
- 如何判断对象是否存活
- 引用计数法
- 根搜索算法
- JVM中哪些内存区域会发生内存溢出(OOM)
- 为什么不要使用Finalize方法
- JVM堆内存分代模型
- 什么是老年代空间分配担保机制
- 什么情况下对象会进入老年代
- 堆为什么要分成年轻代和老年代
- JVM堆的年轻代为什么要有两个Survivor区
- 复制算法
- 流程:
- 标记清除算法
- 标记-整理
- 为什么扩容新生代可以提高GC的效率
- 请介绍一下JVM垃圾收集器
- Serial收集器
- ParNew收集器
- Parallel Scavenge收集器
- Serial Old收集器
- Parallel Old收集器
- 简述一下CMS垃圾回收器,他有哪些问题
- G1收集器
- G1垃圾收集器的基本原理
- G1垃圾收集器如何做到可预测的停顿时间
- G1垃圾收集器是否还有年代的划分
- G1垃圾收集器中的大对象
- G1垃圾收集器内存大小如何设置
- G1垃圾收集器新生代还有Eden和Survivor吗
- G1垃圾收集器的新生代垃圾回收
- G1垃圾收集器的老年代垃圾回收
- G1垃圾收集器的混合垃圾回收?
- G1回收失败时的Full GC
- 什么时候使用G1垃圾收集器
- ZGC收集器
- 线上环境的JVM都设置多大?
- 线上Java服务器内存飙升怎么回事
- 线上Java项目CPU飙到100%怎么排查?
- 以下哪个区域不属于新生代?(C)
- java程序内存泄露的最直接表现是( C)
- 数据库
- 你知道哪些非关系型数据库的类型呢?
- mysql
- 数据库三大范式是什么
- char和varchar的区别
- 数据库的隔离级别有哪些?各自的含义是什么
- 事务管理(ACID)
- 原子性(Atomicity)
- 原子性
- 一致性(Consistency)
- 持久性(Durability)
- 事务的四个特性以及对应的子系统:
- InnoDB引擎与MyISAM引擎的区别 ?
- mysql 内连接和外连接的区别
- 左外连接和右外连接的区别
- 索引失效
- 怎么优化SQL查询语句
- 索引设计原则
- 索引语法
- 视图
- 视图作用
- 视图语法
- MySQL数据库cpu飙升的话,要怎么处理呢?
- select for update有什么含义,会锁表还是锁行还是其他。
- 你们数据库是否支持emoji表情存储,如果不支持,如何操作?
- union all和union的区别
- javaweb
- get请求和post请求的区别
- 转发和重定向
- Cookie和Session的区别
- Servlet什么时候被创建?
- 下列有关Servlet的生命周期,说法不正确的是?(A)
- spring
- srping是什么?
- spring优点
- Spring 中的设计模式
- 什么是IOC、DI 及其两者的优点 、 有哪几种注入方式
- ioc的实现机制是什么
- AOP的理解
- Spring 中的设计模式
- SpringMVC
- SpringMVC 的工作原理
- SpringMVC 常用注解都有哪些?
- 如何开启注解处理器和适配器
- Spring Boot
- Spring Boot有哪些优点
- SpringBoot中如何解决跨域问题
- Spring Cloud
- 什么是Spring Cloud
- Spring Boot和Spring Cloud的关系是什么
- 单体应用
- 什么是微服务
- dubbo和springcloud
- 微服务一定要用SpringCloud 吗
- 分布式和微服务是什么?二者的区别又是什么?
- 分布式是否属于微服务?
- 服务注册和服务发现是什么意思,springcloud是如何实现的
- ribbon和nginx负载均衡的区别
- redis
- redis 中的数据类型有哪些
- redis的应用场景
- 为什么说redis能够快速执行
- redis和mamcached的区别
- redis一个字符串类型的值能存储最大容量是多少?
- redis大key是怎么回事?
- 缓存雪崩
- 缓存穿透
- 缓存击穿
- Linux
- 统计一个文件中"java"出现的行数?
- 排查线上问题的命令
- 内存磁盘相关命令
- vim编辑器可以分为三种模式
- 在使用VI编辑器的时候,查找内容的方法有两种:
- 实现文件去重并排序:
- 压缩相关命令
java基础
java语言特点
-
面向对象(贴近人类思维模式,模拟现实世界,解决现实问题)
-
简单性(自动内存管理机制,不易造成内存溢出,简化流程处理,语义清晰)
-
健壮性(Java语言的强类型机制、异常处理、垃圾的自动收集等)
-
跨平台(操作系统,服务器,数据库)
-
支持网络编程并且很方便(Java语言诞生本身就是为简化网络编程设计的)
-
支持多线程(多线程机制使应用程序在同一时间并行执行多项任)
-
安全性好
java的执行机制
先编译,在解释
将源文件编译成字节码文件(平台中立文件.class),再将字节码文件进行解释执行
命名规范
包名小写,类名大驼峰,方法名小驼峰
JDK 和 JRE 有什么区别?
JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境。
包含了jre+类库+开发工具包(编译器+调试工具)JRE:Java Runtime Environment 的简称,java 运行环境,为 java 的运行提供了所需环境。
包含jvm和解释器,完整的java运行环境JVM虚拟机
使用软件在不同的操作系统中,模拟相同的环境具体来说 JDK 其实包含了 JRE,同时还包含了编译 java 源码的编译器 javac,还包含了很多 java 程序调试和分
析的工具。简单来说:如果你需要运行 java 程序,只需安装 JRE 就可以了,如果你需要编写 java 程序,需要安装JDK
说明:
三者关系: JDK > JRE > JVM
什么是跨平台性?原理是什么
所谓跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行。实现原理:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序
什么是字节码?采用字节码的最大好处是什么
字节码:Java源代码经过虚拟机编译器编译后产生的文件(即扩展为.class的文件),它不面向任
何特定的处理器,只面向虚拟机。
采用字节码的好处:
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留
了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定
的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行
java中的编译器和解释器:
Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机器。这台虚拟的机
器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能
够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚
拟机理解的代码叫做字节码(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟
机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变
成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻
译成特定机器上的机器码,然后在特定的机器上运行,这就是上面提到的Java的特点的编译与解释
并存的解释。
Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机
器可执行的二进制机器码---->程序运行
一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制?
可以。但最多只有一个类名声明为public,与文件名相同
程序文件名必须与公共外部类的名称完全一致(包括大小写)(对)
Java语言采用何种编码方案?有何特点?
Java语言采用Unicode编码标准,Unicode(标准码),它为每个字符制订了一个唯一的数值,因此在任何的语言,
平台,程序都可以放心的使用
语句:char foo=‘中’,是否正确?(假设源文件以GB2312编码存储,并且以javac – encoding GB2312命令编译)
正确
采用GB2312或GBK编码方式时,一个中文字符占2个字节;而采用UTF-8编码方式时,一个中文字符会占3个字节,在java中char类型,是16位,2个字节
/* …* /中可以嵌套//注释,也能嵌套/…/注释(错误)。
JDK常用的包
java.lang: 这个是系统的基础类,比如String、Math、Integer、System和Thread, 提供常用功能。
java.io: 这里面是所有输入输出有关的类,比如文件操作等
java.net: 这里面是与网络有关的类,比如URL,URLConnection等。
java.util : 这个是系统辅助类,特别是集合类Collection,List,Map等。
java.sql: 这个是数据库操作的类,Connection, Statememt,ResultSet等
一个十进制的数在内存中是怎么存的?
补码的形式
jdk1.8新特性
- Lambda表达式 -Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
- 函数式接口
- Stream API
- 接口中的默认方法和静态方法
- 新时间日期API
- 在jdk1.8中对hashMap等map集合的数据结构优化。hashMap数据结构的优化原来的hashMap采用的数据结构是哈希表(数组+链表),在1.8之后,在数组+链表+红黑树来实现hashmap
- Jdk1.8没有永久区,取而代之的是MetaSpace元空间,用的是物理内存
说说 Lamda 表达式的优缺点。
优点:
-
简洁。
-
非常容易并行计算。
-
可能代表未来的编程趋势。
缺点: -
不容易调试。
-
若其他程序员没有学过 lambda 表达式,代码
不容易让其他语言的程序员看懂
== 和 equals 的区别是什么?
==:它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。
基本类型:比较的是值是否相同;
引用类型:比较的是引用(内存地址)是否相同;equals 默认情况下是引用比较,只是很多类重写了 equals 方法,比如 String等把它变成了值比较
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
1.类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象
2.类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回true (即,认为这两个对象相等)。
Java 中应该使用什么数据类型来代表价格?
java中使用BigDecimal来表示价格是比较好的
float f=3.4;是否正确
不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成 floatf =3.4F
3*0.1 == 0.3 将会返回什么?true 还是 false?
false,因为有些浮点数不能完全精确的表示出来
System.out.println(3 * 0.1);//0.30000000000000004System.out.println(4 * 0.1);//0.4System.out.println(13 * 0.1);//1.3System.out.println(3 * 0.1 == 0.3);//falseSystem.out.println(13 * 0.1 == 1.3);//trueSystem.out.println(9 * 0.1 == 0.9);//trueSystem.out.println(3 * 0.1 / 3);//0.10000000000000002
为啥有时会出现 4.0-3.6=0.40000001 这种现象
2 进制的小数无法精确的表达 10 进制小数,计算机在计算 10 进制小
数的过程中要先转换为 2 进制进行计算,这个过程中出现了误差。
short s1 = 1; s1 = s1 + 1;有什么错? short s1 =1; s1 +=1;有什么错?
1.对于 short s1=1;s1=s1+1 来说,在 s1+1 运算时会自动提升表达式的类型为 int,那么将 int 赋予给 short 类型的变量 s1 会出现类型转换错误。,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。
2.对于 short s1=1;s1+=1 来说 +=是 java 语言规定的运算符,可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1 + 1);其中有隐含的强制类型转换
a = a + b 与 a += b 的区别
两者写法上主要在于是否能进行数据类型自动转换,事实上就是类型与精度上的差异,当两个操作数 数据类型一致时两种形式的运算结果没有差别 但数据类型不同时 此时两种形式就有区别了,a=a+b有可能因为类型不匹配报错,a+=b不会出现问题,因为它会自动强制类型转换
下面程序运行结果
public static void main(String[] args) {int i = 1;i = i++;int j = i++;int k = i + ++i * i++;System.out.println("i=" + i);System.out.println("j=" + j);System.out.println("k=" + k);}
i=4
j=1
k=11
分析:i=i++时 因为是i++,i确实后面变成2了,但是前面表达式的值为1,又赋值给i所以i又从2变成了1,给j赋值i++先赋值再++,所以j的值是1,i此时变成了2,给k赋值时 该表达式相当于 2+3*3 并且i又进行了加1 所以 i最后的值是4,k的值是11
java 中有可能出现 i + 1 < i 的情况吗?为什么
这个和java中的数值表示有关系,带符号的数都有最大值,到了最大值之后就变成负数了,可以看看java中负数的表示方法。原理讲了,下面给个例子:
int i = Integer.MAX_VALUE;
int j = i+1;
System.out.println(j<i);
溢出了,所以加1之后可能小于原来
⽤最有效率的⽅法算出2乘以8
2 << 3
因为将⼀个数左移n位,就相当于乘以了2的n次⽅,那么,⼀个数乘以8只要将其左移3位即
可,⽽位运算cpu直接⽀持的,效率最⾼,所以,2乘以8最效率的⽅法是2 << 3。
下面代码输出结果是?©
int i = 5;
int j = 10;
System.out.println(i + ~j);
A.Compilation error because”~”doesn’t operate on integers
B.-5
C.-6
D.15
~代表按位取反
负数的补码 = 原码取反 + 1,即:
-n = ~n + 1 → ~n = -n -1
j:10 = - 10 - 1 = -11
i + j = 5 + -11 = -6
下列代码的输出结果是
class Foo {final int i;int j;public void doSomething() {System.out.println(++j + i);}
}
不能执行,因为编译有错
final作为对象成员存在时,必须初始化;但是,如果不初始化,也可以在类的构造函数中初始
因为java允许将数据成员声明为final,却不赋初值。但是,blank finals必须在使用之前初始化,且必须在构造函数中初始化
break 和 continue,return 的区别?
break 和 continue 都是用来控制循环的语句。
break 用于完全结束一个循环,跳出循环体执行循环后面的语句。
continue 用于跳过本次循环,执行下次循环。return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)
下面的方法,当输入为2的时候返回值是多少
public static int getValue(int i) {int result = 0;switch (i) {case 1:result = result + i;case 2:result = result + i * 2;case 3:result = result + i * 3;}return result;}
10
case2 result=4 没有break,走入case3 result=4+6
java支持的基本数据类型有那些,什么是自动拆箱
Java支持的数据类型包括两种:基本数据类型、引用类型1)基本数据类型有8种:
byte、short、int、long、float、double、boolean、char
2)引用类型:
如String、包装类等类,数组,接口等
自动装箱是 Java 编译器在基本数据类型和对应的对象包装类型之间做的一个转化。比如:把 int 转化成 Integer,double 转化成 Double,等等。反之就是自动拆箱。
java 基本类型与引用类型的区别
基本类型保存原始值,引用类型保存的是引用值(引用值就是指对象在堆中所处的位置/地址)
自动装箱就是将基本数据类型自动转换为对应的包装类,即int转化为Integer,自动拆箱。就是将包装类自动转换为
基本数据类型所以自动装箱和拆箱就是基本类型和引用类型之间的转换
包装类的作用:
1.一个实现基本类型之间的转换
2.是便于函数传值
3.是在一些地方要用到Object的时候方便将基本数据类型装换,例如集合中不能存放基本类型数据,如果要存放数字,应该通过包装类将基本类型数据包装起来,从而间接处理基本类型数据
int和integer的区别
1.Integer是int的包装类,int则是java的一种基本数据类型
2、Integer变量必须实例化后才能使用,而int变量不需要
3、Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
4、Integer的默认值是null,int的默认值是0
两个new生成的Integer变量的对比
由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。
Integer i = new Integer(10000);
Integer j = new Integer(10000);
System.out.print(i == j); //false
Integer变量和int变量的对比
Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)
int a = 10000;Integer b = new Integer(10000);Integer c=10000;System.out.println(a == b); // trueSystem.out.println(a == c); // true
非new生成的Integer变量和new Integer()生成变量的对比
非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)
Integer b = new Integer(10000);Integer c=10000;System.out.println(b == c); // false
两个非new生成的Integer对象的对比
对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false
Integer i = 100;
Integer j = 100;
System.out.print(i == j); //trueInteger i = 128;
Integer j = 128;
System.out.print(i == j); //false
当值在 -128 ~ 127之间时,java会进行自动装箱,然后会对值进行缓存,如果下次再有相同的值,会直接在缓存中取出使用。缓存是通过Integer的内部类IntegerCache来完成的。当值超出此范围,会在堆中new出一个对象来存储。
java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了
在不是new出来的情况下在自动装箱时对于值从–128到127之间的值,它们被装箱为Integer对象后,会存在内存中被重用,始终只存在一个对
象而如果超过了从–128到127之间的值,被装箱后的Integer对象并不会被重用,即相当于每次装箱时都新建一个
Integer对象当包装对象的数值进行比较时,不能简单的使用==来进行判断,当该对象的数值在-128—127之间时可以进行比较,超
出范围时应该使用equals进行比较
public static void main(String[] args){Integer a=1;Integer b=1;System.out.println(a+"----"+b);System.out.println(a==b);System.out.println(a.equals(b));Integer c=200;Integer d=200;System.out.println(c+"----"+d);System.out.println(c==d);System.out.println(c.equals(d));}
结果:
如下两个题目输出结果相同吗?各是什么
Integer i = new Integer(1);Integer j = new Integer(1);System.out.println(i == j);Integer m = 1;Integer n = 1;System.out.println(m == n);//Integer x = 128;Integer y = 128;System.out.println(x == y);//
false
true
false
Object o1 = true ? new Integer(1) : new Double(2.0);System.out.println(o1);//1.0
三元运算符后面两个的类型需要一致,所有int会提升为double
什么是值传递和引用传递?
值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量. 引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本, 并不是原对象本身 。
所以对引用对象进行操作会同时改变原对象. 一般认为,java 内的传递都是值传递.
&与&&的区别
&是按位与
&&是逻辑与(短路运算)
二者都要求运算符左右两端的布尔值都是true整个表达式的值才是true,&&之所以称为短路运算是因为,如果&&左边的表达式的值是false,右边的表达式会被直接短路掉,不会进行运算
switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String上
Java5 以前 switch(expr)中,expr 只能是 byte、short、char、int。从 Java 5 开始,Java 中引入了枚举类型,
expr 也可以是 enum 类型。
从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的
数组有没有 length() 方法?String 有没有 length() 方法
数组没有 length()方法,而是有 length 的属性。String 有 length()方法。JavaScript 中,获得字符串的长度是
通过 length 属性得到的,这一点容易和 Java 混淆
怎么将 byte 转换为 String?
可以使用 String 接收 byte[] 参数的构造器来进行转换,需要注意的点是要使用 的正确的编码,否则会使用平台默认编码,这个编码可能跟原来的编码相同,也可能不同。
我们能将 int 强制转换为 byte 类型的变量吗?如果该值大于 byte 类型的范围,将会出现什么现象?
可以做强制转换,但是 Java 中 int 是 32 位的,而 byte 是 8 位的,byte 类型的范围是从 -128 到 127所以,如果强制转化是超出了范围,int 类型的高 24 位将会被丢弃,
下列哪些语句关于内存回收的说明是正确的? (B )
A.程序员必须创建一个线程来释放内存
B. 内存回收程序负责释放无用内存
C. 内存回收程序允许程序员直接释放内存
D.内存回收程序可以在指定的时间释放内存对象
A、JVM一旦启动,就会创建一个守护线程来监测是否需要有对象内存被释放。
C、无法直接释放。
D、不可以指定时间,System.gc(),只是提醒JVM可以进行一次Full GC,但是什么时候真正执行,还是不知道的。
关于访问权限说法正确 的是 ? ( B)
A.外部类前面可以修饰public,protected和private
B.成员内部类前面可以修饰public,protected和private
C.局部内部类前面可以修饰public,protected和private
D.以上说法都不正确
( 1 )对于外部类而言,它也可以使用访问控制符修饰,但外部类只能有两种访问控制级别: public 和默认。因为外部类没有处于任何类的内部,也就没有其所在类的内部、所在类的子类两个范围,因此 private 和 protected 访问控制符对外部类没有意义。
( 2 )内部类的上一级程序单元是外部类,它具有 4 个作用域:同一个类( private )、同一个包(protected )和任何位置( public )。
( 3 ) 因为局部成员的作用域是所在方法,其他程序单元永远不可能访问另一个方法中的局部变量,所以所有的局部成员都不能使用访问控制修饰符修饰
数组
下列说法正确的有( A)
A.数组是一种对象
B.数组属于一种原生类
C.int number=[]{31,23,33,43,35,63}
D.数组的大小可以任意改变
Java中的那些基本类型属于原生类,而数组是引用类型,不属于原生类,可以看成是一种对象。
而C中的数组声明和初始化的格式不对
数组的大小一旦指定,就不可以进行改变
java语言中的数组元素下标总是从0开始,下标可以是整数或整型表达式。(对)
java语言的下面几种数组复制方法中,哪个效率最高?(B)
A.for 循环逐一复制
B.System.arraycopy
C.Array.copyOf
D.使用clone方法
面向对象
什么是面向对象,面向对象理解
面向对象就是分工与协作,面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者,以及各自需
要做什么例如:洗衣机洗衣服
面向过程将会将任务拆解成一系列的步骤
1.打开洗衣机---》2.放衣服---》3.放洗衣粉---》4.清洗---》5.烘干面向对象会拆成人和洗衣机两个对象:
人:打开洗衣机,放衣服,放洗衣粉
洗衣机:清洗,烘干面向过程比较直接高效,面向对象易于复用维护和扩展
java创建对象的四种方法
- 使用new关键字来生成对象只是最常用的方式;
- 使用反射创建对象,调用java.lang.Class或者java.lang.reflect.Constructor类的new Instance()实例方法;
- 调用对象的clone()方法(要拷贝的对象需要实现Cloneable接口,并重写clone()方法);
- 使用反序列化方式,通过让类实现Serializable接口,然后使用new ObjectInputStream().readObject()来创建对象
构造方法的特点
构造方法的命名必须和类名完全相同
构造方法没有返回值
在创建新对象时,系统自动的调用该类的构造方法
构造方法可以重载, 不能重写
构造方法不能被static,final,abstract,synchronized,native等修饰符修饰
系统会为每个类默认添加一个无参构造,一旦为类编写构造方法,默认的构造方法将会被覆盖
构造器Constructor是否可被override
构造器Constructor不能被继承,因此不能重写Override,但可以被重载Overload。
封装
封装的意义,在于明确表示出允许外部使用的所有成员函数和数据项,内部细节对外部调用透明,外部调用无需修改或者关心内部实现
1.javabean的属性私有,提供getset对外访问,因为属性的赋值或者获取逻辑只能由javabean本身决定,而不能由外部胡乱修改
private string name;
public void setName(String name){this.name="物联网"+name;
}
//该name有自己的命名规则,明显不能由外部直接赋值
2.orm框架
操作数据库,我们不需要关心链接是如何建立的,sql是如何执行的,只需要引入mybatis,调用方法即可
继承
继承基类的方法,并做出自己的改变和扩展
子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的,把共性的抽到父类,达到代码复用
继承特点
继承鼓励类的重用
继承可以多层继承
一个类只能继承一个父类,java只支持单继承
父类中private,default修饰的不能被继承
构造方法不能被继承,只能调用
继承带来的好处与弊端
继承的好处:
继承可以让类与类之间产生关系,子父类关系,产生子父类后,子类则可以使用父类中非私有的成员。提高了代码的复用性(多个类相同的成员可以放到同一个类中) 提高了代码的维护性(如果方法的代码需要修改,修改一
处即可)继承弊端:
继承让类与类之间产生了关系,类的耦合性增强了,当父类发生变化时子类实现也不得不跟着变化,削 弱了子类的独
立性继承的应用场景: 使用继承,需要考虑类与类之间是否存在is..a的关系,不能盲目使用继承 is..a的关系:谁是谁
的一种,例如:老师和学生是人的一种,那人就是父类,学生和老师就是子类
继承中变量的访问特点
在子类方法中访问一个变量,采用的是就近原则。
1. 子类局部范围找
2. 子类成员范围找
3. 父类成员范围找
4. 如果都没有就报错(不考虑父亲的父亲…)
继承中构造方法的访问特点
注意:子类中所有的构造方法默认都会访问父类中无参的构造方法子类会继承父类中的数据,可能还会使用父类的数据。所以,子类初始化之前,一定要先完成父类数据的初始化,
原因在于,每一个子类构造方法的第一条语句默认都是:super()问题:如果父类中没有无参构造方法,只有带参构造方法,该怎么办呢?1. 通过使用super关键字去显示的调用父类的带参构造方法 2. 在父类中自己提供一个无参构造方法
继承中成员方法的访问特点
1.通过子类对象访问一个方法
3. 子类成员范围找
4. 父类成员范围找
5. 如果都没有就报错(不考虑父亲的父亲…)
继承的注意事项
Java中类只支持单继承,不支持多继承(接口支持多继承)错误范例:class A extends B, C { }Java中类支持多层继承
java为什么是单继承
java中为什么要单继承,多实现,总结如下:
若为多继承,那么当多个父类中有重复的属性或者方法时,子类的调用结果会含糊不清,因此用了单继承。这样也会使程序更具安全性
为什么是多实现呢?
通过实现接口拓展了类的功能,若实现的多个接口中有重复的方法也没关系,因为实现类中必须重写接口中的方法,所以调用时还是调用的实现类中重写的方法。
如果一个接口Glass有个方法setColor(),有个类BlueGlass实现接口Glass,则在类BlueGlass中正确的是? (C )
A,protected void setColor() { …}
B.void setColor() { …}
C.public void setColor() { …}
D.以上语句都可以用在类BlueGlass中
接口中属性为public static final。方法为public abstract。子类的权限不能比父类更低
多态
基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同
概念:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。简单的说:就是用父类的引用指向子类的对象
多态的三个条件:继承,方法重写,父类引用指向子类对象
父类类型 变量名=new 子类对象;
变量名.方法名();
缺点:无法调用子类特有的功能
多态中的成员访问特点
成员访问特点:成员变量编译看父类,运行看父类或者编译看左边,运行也看左边成员方法编译看父类,运行看子类或者编译看左边,运行看右边(因为方法有重写)
多态的好处和弊端
好处提高程序的扩展性。定义方法时候,使用父类型作为参数,在使用的时候,使用具体的子类型参与操作弊端不能使用子类的特有成员
根据下面这个程序的内容,判断哪些描述是正确的:( )
public class Test {public static void main(String args[]) {String s = "tommy";Object o = s;sayHello(o); //语句1sayHello(s); //语句2}public static void sayHello(String to) {System.out.println(String.format("Hello, %s", to));}public static void sayHello(Object to) {System.out.println(String.format("Welcome, %s", to));}
}
语句2输出为:Hello, tommy
语句1输出为:Welcome, tommy
如果是重载方法之间的选择,则是使用静态类型。
如果是父类与子类之间的重写方法的选择,则是使用动态类型。
如A a = new B(); 会使用类型B去查找重写的方法,使用类型A去查找重载的方法
final 在 java 中有什么作用
1.修饰变量时,定义时必须赋值,被修饰的变量不可变,一旦赋了储值就不能重新赋值
对于 基本类型 来说,不可改变指的是变量当中的数据不可改变,但是对于 引用类型 来说,不可改变的指的是变量当
中的地址值不可改变,数据是可以变的2.修饰方法 该方法不能被子类重写但是可以被重载3.修饰类,该类不能被继承,比如math,string类
对于类、方法来说,abstract关键字和final关键字不能同时使用,因为作用相互矛盾fianl关键字的作用 final代表最终的意思,可以修饰成员方法,
成员变量,类 final修饰类、方法、变量的效果 fianl
修饰类:该类不能被继承(不能有子类,但是可以有父类)
final修饰方法:该方法不能被重写
final修饰变量:表明该变量是一个常量,不能再次赋值
final、finally、finalize的区别?
final 用于修饰变量、方法和类。
- final 变量:被修饰的变量不可变,不可变分为
引用不可变
和对象不可变
,final 指的是引用不可变
,final 修饰的变量必须初始化,通常称被修饰的变量为常量
。 - final 方法:被修饰的方法不允许任何子类重写,子类可以使用该方法。
- final 类:被修饰的类不能被继承,所有方法不能被重写。
finally 作为异常处理的一部分,它只能在 try/catch
语句中,并且附带一个语句块表示这段语句最终一定被执行(无论是否抛出异常),经常被用在需要释放资源的情况下,System.exit (0)
可以阻断 finally 执行。
finalize 是在 java.lang.Object
里定义的方法,也就是说每一个对象都有这么个方法,这个方法在 gc
启动,该对象被回收的时候被调用。
一个对象的 finalize 方法只会被调用一次,finalize 被调用不一定会立即回收该对象,所以有可能调用 finalize 后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会再次调用 finalize 了,进而产生问题,因此不推荐使用 finalize 方法
static的访问特点
非静态的成员方法
能访问静态的成员变量
能访问非静态的成员变量
能访问静态的成员方法
能访问非静态的成员方法
静态的成员方法
能访问静态的成员变量
能访问静态的成员方法
静态成员方法只能访问静态成员
静态变量的特点与区别
静态变量全局唯一,为所有的对象共用,修改它的值,其他对象使用该变量时,值也会改变非静态变量,每个对象持有一份,是独立的,修改对象的值不会影响其他对象的该值。
抽象类(abstract class)和接口(interface)有什么异同
抽象类:
1.抽象类中可以定义构造器
2.可以有抽象方法和具体方法
3.抽象类中可以定义成员变量,抽象类中的成员可以是 private、默认、protected、public
4.有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法
5.抽象类中可以包含静态方法
6.一个类只能继承一个抽象类
接口:
1.接口中不能定义构造器
2.方法全部都是抽象方法
3.接口中的成员全都是 public 的
4.接口中定义的成员变量实际上都是常量
5.接口中不能有静态方法
6.一个类可以实现多个接口
相同:
1.不能够实例化
2.可以将抽象类和接口类型作为引用类型
3.一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类
什么时候用接口什么时候用抽象类
描述 概念的时候用抽象类,描述特征的时候用接口,例如狗类用抽象类,飞特征用接口
抽象选择题
下列哪一种叙述是正确的( D)
A.abstract修饰符可修饰字段、方法和类
B.抽象方法的body部分必须用一对大括号{ }包住
C.声明抽象方法,大括号可有可无
D.声明抽象方法不可写出大括号
A:abstract修饰方法和类
B、C:抽象方法没有方法体,有没有方法体看有没有大括号
接⼝是否可继承接⼝? 抽象类是否可实现(implements)接⼝? 抽象类是否可继承具体类? 抽象类中是否可以有静态的main⽅法?
接⼝可以继承接⼝。抽象类可以实现(implements)接⼝,抽象类可继承具体类。抽象类中可以有静态的main⽅法
只有记住抽象类与普通类的唯⼀区别就是不能创建实例对象和允许有abstract⽅法
抽象类方法的访问权限默认都是public。( 错 )
关于抽象类
JDK 1.8以前,抽象类的方法默认访问权限为protected
JDK 1.8时,抽象类的方法默认访问权限变为default
关于接口
JDK 1.8以前,接口中的方法必须是public的
JDK 1.8时,接口中的方法可以是public的,也可以是default的
JDK 1.9时,接口中的方法可以是private的
说说overload和override的区别
方法重载:在同一个类中,方法名相同,参数列表不同,和返回值无关
方法重写:在子父类中,子类有一个和父类方法名相同,参数列表相同,返回值类型也相同的方法。这个就叫方法的重写,要实现继承关系,子类方法修饰符要大于父类
参数列表不同可以是1.参数类型不同,2.参数个数不同,3.参数顺序不同
Java 中是否可以覆盖(override)一个 private 或者是 static 的方法?
Java 中 static 方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而 static 方法是编译时静态绑定的。static
方法跟类的任何实例都不相关,所以概念上不适用。java 中也不可以覆盖 private 的方法,因为 private 修饰的变量
和方法只能在当前类中使用,如果是其他的类继承当前类是不能访问到 private 变量或方法的,当然也不能覆盖
为什么函数不能根据返回类型来区分重载?
因为调用时不能指定类型信息,编译器不知道你要调用哪个函数
1.float max(int a, int b);
2.int max(int a, int b);
当调用 max(1, 2);时无法确定调用的是哪个,单从这一点上来说,仅返回值类型不同的重载是不应该允许的。
函数的返回值只是作为函数运行之后的一个“状态”,他是保持方法的调用者与被调用者进行通信的关键。并不能
作为某个方法的“标识”
构造器(constructor)是否可被重写(override)?
构造器不能被继承,因此不能被重写,但可以被重载。每一个类必须有自己的构造函数,负责构造自己这部分的构造。子类不会覆盖父类的构造函数,相反必须一开始调用父类的构造函数。
说出下面运行结果
char[] arr = new char[] { 'a', 'b', 'c' };System.out.println(arr);//int[] arr1 = new int[] { 1, 2, 3 };System.out.println(arr1);//double[] arr2 = new double[] { 1.1, 2.2, 3.3 };System.out.println(arr2);//
abc
[I@24d46ca6
[D@4517d9a3
代码块
java静态变量、代码块、和静态方法的执行顺序是什么?
基本上代码块分为三种:Static静态代码块、构造代码块、普通代码块
代码块执行顺序静态代码块——> 构造代码块 ——> 构造函数——> 普通代码块
继承中代码块执行顺序:父类静态块——>子类静态块——>父类代码块——>父类构造器——>子类代码块——>子类构造器
说出下面运行结果
class HelloA {public HelloA() {System.out.println("HelloA");}{ System.out.println("I'm A class"); }static { System.out.println("static A"); }}public class HelloB extends HelloA {public HelloB() {System.out.println("HelloB");}{ System.out.println("I'm B class"); }static { System.out.println("static B"); }public static void main(String[] args) { new HelloB(); }
}
static A
static B
I'm A class
HelloA
I'm B class
HelloB
父类–静态代码块
子类–静态代码块
父类–非静态代码块
父类–构造函数
子类–非静态代码块
子类–构造函数
说出下面运行结果
父类
package ms;public class Father {private int i=test();private static int j=method();//静态代码块static {System.out.print("(1)");}//构造方法Father(){System.out.print("(2)");}//成员代码块{System.out.print("(3)");}public int test(){System.out.print("(4)");return 1;}public static int method(){System.out.print("(5)");return 1;}
}
子类
package ms;public class Son extends Father{private int i=test();private static int j=method();//静态代码块static {System.out.print("(6)");}//构造方法Son (){System.out.print("(7)");}//成员代码块{System.out.print("(8)");}@Overridepublic int test(){System.out.print("(9)");return 1;}public static int method(){System.out.print("(10)");return 1;}public static void main(String[] args) {Son s1=new Son();System.out.println();Son s2=new Son();}
}
顺序为
(5)(1)(10)(6)(9)(3)(2)(9)(8)(7)
(9)(3)(2)(9)(8)(7)
解析:
首先得知道类的实例化顺序
1.父类静态变量
2.父类静态代码块
3.子类静态变量
4.子类静态代码块
5.父类非静态变量(父类成员变量)
6.父类成员态代码块
7.父类构造函数
8.子类非静态变量
9.子类成员代码块
10.子类构造函数
这一题除了要考虑执行顺序以外还要考虑方法的重写,test方法被子类重写了执行的子类的方法
method方法不被重写,因为final,static,private的方法都不可以被重写
运行代码,输出的结果是()
public class P {public static int abc = 123;static{System.out.println("P is init");}
}
public class S extends P {static{System.out.println("S is init");}
}
public class Test {public static void main(String[] args) {System.out.println(S.abc);}
}
A. P is init
123
B. S is init
P is init
123
C. P is init
S is init
123
D. S is init
123
不会初始化子类的几种
调用的是父类的static方法或者字段
调用的是父类的final方法或者字段
通过数组来引用
常用类
math类
Math.round(11.5) 等于多少?Math.round(-11.5)等于多少
Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5 然后进行下取整。
String类
String 是基本数据类型吗?
String 是引用类型,底层用 char 数组实现的
字符串如何转基本数据类型
调用基本数据类型对应的包装类中的方法 parseXXX(String)或 valueOf(String)即可返回相应基本类型
基本数据类型如何转字符串?
一种方法是将基本数据类型与空字符串(“”)连接(+)即可获得其所对应的字符串;另一种方法是调用 String
类中的 valueOf()方法返回相应字符串
String不变性理解
- String类是被final进行修饰的,不能被继承
- 在用+号拼接字符串时会创建新的字符串
- String s1=“abc”,"abc被放到常量池去了,String s2=“abc”,只会将引用指向原来那个常量
- String s1=new String(“abc”) 可能创建两个对象也可能创建一个对象,如果静态区有“abc”字符串常量的话,则仅仅在堆中创建一个对象,如果静态区中没有,则堆和静态区中都需要创建对象
- java使用+拼接字符串,最好使用StringBuilder的append(来实现)
什么是不可变对象?好处是什么?
不可变对象指对象一旦被创建,状态就不能再改变,任何修改都会创建一个新的对象,如 String、Integer及其它包装类.不可变对象最大的好处是线程安全.
能否创建一个包含可变对象的不可变对象?
当然可以,比如final Person[] persons = new Persion[]{}
. persons
是不可变对象的引用,但其数组中的Person实例却是可变的.这种情况下需要特别谨慎,不要共享可变对象的引用.这种情况下,如果数据需要变化时,就返回原对象的一个拷贝.
java 中操作字符串都有哪些类?它们之间有什么区别?
String、StringBuffer、StringBuilder。
都是final修饰的类,都不允许被继承String是不可变的,StringBuffer、StringBuilder是可变的String类利用了final修饰的char类型数组存储字符,源码如下:private final char value[];StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数
组保存字符串,这两种对象都是可变的。源码如下:char[] value;StringBuffer是线程安全的,StringBuilder不是线程安全的,但它们两个所有方法是相同的,StringBuffer在
StringBuilder的方法上添加了synchronized修饰,保证线程安全String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,在修改时不会改变自身;
若修改,等于重新生成新的字符串对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可
以在原有对象的基础上进行操作,在修改时会改变对象自身,每次操作都是对 对象本身进行修改,不是生成新的对象
所以在经常改变字符串内容的情况下最好不要使用 String。StringBuilder 与 StringBuffer 有公共父类 AbstractStringBuilder(抽象类)StringBuffer 和 StringBuilder
最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却
高于StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐 使用 StringBuffer
String为什么要设计成不可变的?
1.便于实现字符串池(String pool)
在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。
String a = "Hello world!";
String b = "Hello world!";
如果字符串是可变的,某一个字符串变量改变了其值,那么其指向的变量的值也会改变,String pool将不能够实现!
2.使多线程安全
在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。
3.避免安全问题
在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。
4.加快字符串处理速度
由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。
总体来说,String不可变的原因要包括 设计考虑,效率优化,以及安全性这三大方面。
什么是字符串常量池?
jvm为了提升性能和减少内存开销,避免字符的重复创建,其维护了一块特殊的内存空间,即字符串池,当需要使用字符串时,先去字符串池中查看该字符串是否已经存在,如果存在,则可以直接使用,如果不存在,初始化,并将该字符串放入字符串常量池中。
字符串常量池的位置也是随着jdk版本的不同而位置不同。在jdk6中,常量池的位置在永久代(方法区)中,此时常量池中存储的是对象。在jdk7中,常量池的位置在堆中,此时,常量池存储的就是引用了。在jdk8中,永久代(方法区)被元空间取代了。
String 转出 int 型,判断能不能转?如何转
可以转,得处理异常 Integer.parseInt(s) 主要为 NumberFormatException:当你输入为字母时,也就是内容不是数字时,如 abcd,
当你输入为空时 )当你输入超出int 上限时 Long.parseLong("123")转换为 long
String s = “Hello”;s = s + " world!";这两行代码执行后,原始的 String 对象中的内容到底变了没有?
没有。因为 String 被设计成不可变(immutable)类,所以它的所有对象都是不可变对象。在这段代码中,s 原先指
向一个 String 对象,内容是 “Hello”,然后我们对 s 进行了“+”操作,那么 s 所指向的那个对象是否发生了改变呢?
答案是没有。这时,s 不指向原来那个对象了,而指向了另一个 String 对象,内容为"Hello world!",原来那个对象还存在于内存之中,只是 s 这个引用变量不再指向它了。
请说出下面程序的输出
String s1 = "Programming";String s2 = new String("Programming");String s3 = "Program";String s4 = "ming";String s5 = "Program" + "ming";String s6 = s3 + s4;System.out.println(s1 == s2); //falseSystem.out.println(s1 == s5); //trueSystem.out.println(s1 == s6); //falseSystem.out.println(s1 == s6.intern()); //trueSystem.out.println(s2 == s2.intern()); //false
String 对象的 intern()方法会得到字符串对象在常量池中对应的版本的引用(如果常量池中有一个字符串与String 对象的 equals 结果是 true),如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用
下面程序的运行结果是
String str1 = "hello";
String str2 = "he" + new String("llo");
System.err.println(str1 == str2);//flase
String str1 = “hello”;这里的str1指的是方法区的字符串常量池中的“hello”,编译时期就知道的; String str2 = “he” + new String(“llo”);这里的str2必须在运行时才知道str2是什么,所以它是指向的是堆里定义的字符串“hello”,所以这两个引用是不一样的。
如果用str1.equal(str2),那么返回的是True;因为两个字符串的内容一样
下面程序运行结果
public class Example {String str=new String("good");char[] ch={'a','b','c'};public static void main(String[] args) {Example example = new Example();example.change(example.str,example.ch);System.out.print(example.str+"and");System.out.print(example.ch);}public void change(String str,char ch[]){str="test ok";ch[0]='g';}
}
goodandgbc
首先说下String确实是个不可变对象,这个不可变是JDK特有的,写JAVA的人特意针对的 但是这与本题无关,题目中的形参str只是原引用ex.str的一个引用副本,传的是一个副本地址值,这个值与ex.str地址值是不一样的,但是它们同时指向了堆中的对象new String(“good”),当你在函数中改变形参也就是地址的副本值也就是这句str=“test ok"只是将副本地址指向常量"test ok”,并没有改变原ex.str的指向方向,它还是指向对象new String(“good”)的 char数组与String一样传的也是地址的副本,但是关键是形参ch它没有新的指向 ch[0]只是ch在指向原对象时改变了对象的内部结构, 所以在ex.ch指向与它是同一个对象的情况下当然也会随之变化
public class test1 {public static void main(String[] args) {int i=1;String str="hello";Integer num=2;int[] arr={1,2,3,4,5};mydata mydata=new mydata();change(i,str,num,arr,mydata);System.out.println("i ="+i);System.out.println("str ="+str);System.out.println("num ="+num);System.out.println("arr ="+ Arrays.toString(arr));System.out.println("mydata.a ="+mydata.a);}public static void change(int j,String s,Integer n,int [] a,mydata m){j+=1;s+="world";n+=1;a[0]+=1;m.a+=1;}
}class mydata{int a=10;
}
i =1
str =hello
num =2
arr =[2, 2, 3, 4, 5]
mydata.a =11
包装类和string类都是新建了一个对象
集合
ArrayList的源码分析:
jdk 7情况下
ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementDatalist.add(123);//elementData[0] = new Integer(123);list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。
默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。
结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)
jdk 8中ArrayList的变化
ArrayList list = new ArrayList();//底层Object[] elementData初始化为{}.并没有创建长度为10的数组
list.add(123);//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData[0]
后续的添加和扩容操作与jdk 7 无异
小结:jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存
ArrayList 和 Vector,LinkedList 的区别
这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合,即存储在这两个集合中
的元素的位置都是有顺序的,相当于一种动态的数组,我们以后可以按位置索引号取出某个元素,并且其中的数据是允
许重复的,这是HashSet 之类的集合的最大不同处,HashSet 之类的集合不可以按索引号去检索其中的元素,也不允
许有重复的元素Vector 是线程安全的,也就是说是它的方法之间是线程同步的,而 ArrayList 是线程序不安全的,它的方法之间是
线程不同步的ArrayList 与 Vector 都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需要增加
ArrayList 与 Vector 的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每
次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。Vector 默认增长为原来两倍,ArrayList 与 Vector 都可以设置初始的空间大小,Vector 还可以设置增长的空间大小,而 ArrayList 没有提供
设置增长空间的方法总结:即 Vector 增长原来的一倍,ArrayList 增加原来的 0.5 倍。ArrayList 和 Vector 都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都
允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector 由
于使用了synchronized 方法(线程安全)。通常性能上较 ArrayList 差,而 LinkedList 使用双向链表实现存
储,按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快 。ArrayList 在查找时速度快,LinkedList 在插入与删除时更具优势
说⼀下ArrayList和LinkedList区别
- ⾸先,他们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的
- 由于底层数据结构不同,他们所适⽤的场景也不同,ArrayList更适合随机查找,LinkedList更适合删除和添加
- 二者都线程不安全,相对线程安全的Vector,执行效率高
- 另外ArrayList和LinkedList都实现了List接⼝,但是LinkedList还额外实现了Deque接⼝,所以LinkedList还可以当做队列来使⽤
ArrayList和Vector的区别
Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack。
数组(Array)和列表(ArrayList)有什么区别?什么时候应该使⽤Array⽽不是ArrayList?
Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。
Array⼤⼩是固定的,ArrayList的⼤⼩是动态变化的。
ArrayList处理固定⼤⼩的基本数据类型的时候,这种⽅式相对⽐较慢
Iterator和ListIterator的区别是什么?
Iterator可⽤来遍历Set和List集合,但是ListIterator只能⽤来遍历List。
Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
ListIterator实现了Iterator接⼝,并包含其他的功能,⽐如:增加元素,替换元素,获取前⼀个和后⼀个元素的索引,等等
java集合的快速失败机制"fail-fast"
- 是java集合一种错误检测机制,当多个线程对集合进行结构上的改变操作时,可能会产生fail-fast机制
- 例如假设两个线程,线程1通过iterator在遍历集合A中的元素,在某个线程改变了集合A的结构,(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出ConcurrentModificationException 异常
- 原因 迭代器在遍历时会直接访问集合中的内容,并且在遍历过程中使用一个modCount变量,集合在被遍历期间如果内容发生变化,就会改变modCount的值,每当迭代器使用HasNext()或者next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount,是的话就遍历返回,否则抛出异常,终止遍历。
Object 若不重写 hashCode()的话,hashCode()如何计算出来的?
Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法直接返回对象的 内存地址。
为什么重写 equals 方法必须重写 hashcode 方法 ?
判断的时候先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。如果只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果为true。
在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到不同的位置而造成对象的不能覆盖的问题。
hashCode()与equals()
- 如果两个对象相等,则hashcode一定也是相同的;
- 两个对象相等,对两个对象分别调用equals方法都返回true;
- 两个对象有相同的hashcode值,它们也不一定是相等的;
hashset底层原理
我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,
此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置),判断数组此位置上是否已经有元素:
7. 如果此位置上没有其他元素,则元素a添加成功。 —>情况1
8. 如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
1. 如果hash值不相同,则元素a添加成功。—>情况2
2. 如果hash值相同,进而需要调用元素a所在类的equals()方法:
equals()返回true,元素a添加失败
equals()返回false,则元素a添加成功。—>情况2
对于添加成功的情况2和情况3而言:元素a 与已经存在指定索引位置上数据以链表的方式存储。
jdk 7 :元素a放到数组中,指向原来的元素。
jdk 8 :原来的元素在数组中,指向元素a
总结:七上八下
HashSet底层:数组+链表的结构
在List内去除重复数字值,要求尽量简单
public static List duplicateList(List list) {HashSet set = new HashSet();set.addAll(list);return new ArrayList(set);}public static void main(String[] args) {List list = new ArrayList();list.add(new Integer(1));list.add(new Integer(2));list.add(new Integer(2));list.add(new Integer(4));list.add(new Integer(4));List list2 = duplicateList(list);for (Object integer : list2) {System.out.println(integer);}}
说出下面结果
其中Person类中重写了hashCode()和equal()方法
public static void main(String[] args) {HashSet set = new HashSet();Person p1 = new Person(1001,"AA");Person p2 = new Person(1002,"BB");set.add(p1);set.add(p2);p1.name = "CC";set.remove(p1);System.out.println(set);//[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}]set.add(new Person(1001,"CC"));System.out.println(set);//[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}]set.add(new Person(1001,"AA"));//[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}, Person{id=1001, name='AA'}]System.out.println(set);}
说一下 map 的分类和常见的情况
java 为数据结构中的映射定义了一个接口 java.util.Map;它有四个实现类,分别是 HashMap Hashtable LinkedHashMap 和 TreeMap
Map 主要用于存储健值对,根据键得到值,因此不允许键重复(重复了覆盖了),但允许值重复。
Hashmap
Hashmap 是一个最常用的 Map,它根据键的 HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。 HashMap 最多只允许一条记录的键为 Null;允许多条记录的值为 Null;HashMap 不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用 Collections 的
synchronizedMap 方法使 HashMap 具有同步的能力,或者使用ConcurrentHashMap。
Hashtable
Hashtable 与 HashMap 类似,它继承自 Dictionary 类,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写 Hashtable,因此也导致了 Hashtable在写入时会比较慢。
LinkedHashMap
LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历LinkedHashMap 时,先得到的记录肯定是先插入的.也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比 HashMap 慢,不过有种情况例外,当 HashMap 容量很大,实际数据较少时,遍历起来可能会比 LinkedHashMap 慢,因为 LinkedHashMap 的遍历速度只和实际数据有关,和容量无关,而 HashMap 的遍历速度和他的容量有关。
TreeMap
TreeMap 实现 SortMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。
一般情况下,我们用的最多的是 HashMap,在 Map 中插入、删除和定位元素,HashMap 是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么 TreeMap 会更好。如果需要输出的顺序和输入的相同,那么用 LinkedHashMap 可以实现,它还可以按读取顺序来排列.
HashMap索引计算方法
- 首先,计算对象的 hashCode()
- 再进行调用 HashMap 的 hash() 方法进行二次哈希
- 二次 hash() 是为了综合高位数据,让哈希分布更为均匀
- 最后 & (capacity – 1) 得到索引
HashMap的长度为什么必须为2^n
- h&(length-1)等效 h%length 操作,等效的前提是:length必须是2的整数倍,计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
- 防止哈希冲突,位置冲突
- 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
为什么HashMap把hashcode做一个改进运算
h=hashCode()
h>>>16 无符号右移16位
hash=h^(h>>>16) 计算hash
(n-1)&hash 计算下标
让高16位参与运算,计算出来的数组下标会更加分散
HashMap添加的对象为什么要重写equals和hashcode
一般对于存放到Set集合或者Map中键值对的元素,需要按需要重写hashCode与equals方法,以保证唯一性!
HashMapkey 的设计要求
- HashMap 的 key 可以为 null,但 Map 的其他实现则不然
- 作为 key 的对象,必须实现 hashCode 和 equals,并且 key 的内容不能修改(不可变,例如string)
- key 的 hashCode 应该有良好的散列性
说⼀下HashMap的Put⽅法
先说HashMap的Put⽅法的⼤体流程:
- 根据Key通过哈希算法与与运算得出数组下标
- 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是Node对象)并放⼊该位置
- 如果数组下标位置元素不为空,则要分情况讨论
a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对
象,并使⽤头插法添加到当前位置的链表中
b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node
i. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过程中会判断红⿊树中是否存在当前key,如果存在则更新value
ii. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插
⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链表中,插⼊到链表后,会看当前链表的节点个数,如果⼤于8,那么则会将该链表转成红⿊树
iii. 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要就
扩容,如果不需要就结束PUT⽅法
HashMap什么时候链表会转化成为红黑树
HashMap在元素比较少的时候,也只会有数组+链表的结构,当链表长度大于8,同时数组长度必须大于等于64(默认是16经过两次扩容),链表才会转变为红黑树,否则只会扩容
HashMap的树化与退化
树化意义
- 红黑树用来避免 DoS 攻击,防止链表超长时性能下降,树化应当是偶然情况,是保底策略
- hash 表的查找,更新的时间复杂度是 O ( 1 ) O(1) O(1),而红黑树的查找,更新的时间复杂度是 O ( l o g 2 n ) O(log_2n ) O(log2n),TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表
- hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小
树化规则
- 当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化
退化规则
- 情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表
- 情况2:remove 树节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表
HashMap JDK7和JDK8的不同
- JDK8中 HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组,当首次调用map.put()时,再创建长度为16的数组,JDK7是直接创建的
- JDK8数组为Node类型,在jdk7中称为Entry类型
JDK7 是大于等于阈值且没有空位时才扩容,而 JDK8是大于阈值就扩容 - 形成链表结构时,新添加的key-value对在链表的尾部(七上八下),JDK7是头插法,JDK8是尾插法
- 扩容后位置存储,JDK7全部进行重新计算,JDK8按照扩容后的规律计算=原位置或者原位置+旧容量
- 当数组指定索引位置的链表长度>8时,且map中的数组的长度>= 64时,此索引位置上的所有key-value对使用红黑树进行存储。
HashMap装填因子,负载因子,加载因子为什么是0.75
在空间占用与查询时间之间取得较好的权衡
装填因子设置为1:空间利用率得到了很大的满足,很容易碰撞,产生链表,导致查询效率低
装填因子设置为0.5: 碰撞的概率低,查询效率高,冲突减少了,但扩容就会更频繁,空间占用也更多,空间利用率低
HashMap和Hashtable的区别
- Hashtable 是线程安全的,HashMap 不是线程安全的。
- Hashtable 所有的元素操作都是 synchronized 修饰的,而 HashMap 并没有。
既然 Hashtable 是线程安全的,每个方法都要阻塞其他线程,所以 Hashtable 性能较差,HashMap 性能较好,使用更广。如果要线程安全又要保证性能,建议使用 JUC 包下的 ConcurrentHashMap - Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null(key只允许为一个null,value可以有多个null)。
- 两者继承的类不一样,Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap类。
- HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。
- HashMap扩容为原来的两倍,Hashtable扩容为原来两倍+1
map底层为什么用红黑树来实现
1、平衡二叉树(AVL树):
红黑树是在AVL树的基础上提出来的。
平衡二叉树又称为AVL树,是一种特殊的二叉排序树。其左右子树都是平衡二叉树,且左右子树高度之差的绝对值不超过1。
AVL树中所有结点为根的树的左右子树高度之差的绝对值不超过1。
将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF,那么平衡二叉树上的所有结点的平衡因子只可能是-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。
2、红黑树:
红黑树是一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树,相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,通常使用红黑树。
性质:
-
每个节点非红即黑
-
根节点是黑的;
-
每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的;
-
如果一个节点是红色的,则它的子节点必须是黑色的。
-
对于任意节点而言,其到叶子点树NULL指针的每条路径都包含相同数目的黑节点;
3、红黑树较AVL树的优点:
AVL 树是高度平衡的,频繁的插入和删除,会引起频繁的rebalance,导致效率下降;红黑树不是高度平衡的,算是一种折中,插入最多两次旋转,删除最多三次旋转。
所以红黑树在查找,插入删除的性能都是O(logn),且性能稳定
HashSet底层原理
HashSet底层就是利用HashMap来完成的,每次添加其实就是调用的HashMap的put方法只是value是个固定值罢了
private transient HashMap<E,object> map;
private static final Object PRESENT = new Object();
public HashSet() {map = new HashMap<>(); //HashSet底层就是利用HashMap来完成的}
public boolean add(E e) {return map.put(e, PRESENT)==null;}
下面程序的运行结果
public static void main(String[] args) {List list = new ArrayList();list.add(1);list.add(2);list.add(3);updateList(list);System.out.println(list);//}private static void updateList(List list) {list.remove(2);}
[1, 2]
解析:remove,一种是根据下标删除元素,一种是根据内容删除元素,当直接写list.remove(2),会直接当作下标删除,省去了自动装箱的过程如果想要删除的是元素2,而不是下标2那么就要如下,
public static void main(String[] args) {List list = new ArrayList();list.add(1);list.add(2);list.add(3);updateList(list);System.out.println(list);//}private static void updateList(List list) {list.remove(new Integer(2));}
[1, 3]
在使用 HashMap 的时候,用 String 做 key 有什么好处?
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
Comparable和Comparator接⼝是⼲什么的?列出它们的区别。
Comparable
Comparable可以认为是一个内比较器,实现了Comparable接口的类有一个特点,就是这些类是可以和自己比较的,至于具体和另一个实现了Comparable接口的类如何比较,则依赖compareTo方法的实现,compareTo方法也被称为自然比较方法。如果开发者add进入一个Collection的对象想要Collections的sort方法帮你自动进行排序的话,那么这个对象必须实现Comparable接口。compareTo方法的返回值是int,有三种情况:
1、比较者大于被比较者(也就是compareTo方法里面的对象),那么返回正整数
2、比较者等于被比较者,那么返回0
3、比较者小于被比较者,那么返回负整数
Comparator
Comparator可以认为是是一个外比较器,个人认为有两种情况可以使用实现Comparator接口的方式:
1、一个对象不支持自己和自己比较(没有实现Comparable接口),但是又想对两个对象进行比较
2、一个对象实现了Comparable接口,但是开发者认为compareTo方法中的比较方式并不是自己想要的那种比较方式
Comparator接口里面有一个compare方法,方法有两个参数T o1和T o2,是泛型的表示方式,分别表示待比较的两个对象,方法返回值和Comparable接口一样是int,有三种情况:
1、o1大于o2,返回正整数
2、o1等于o2,返回0
3、o1小于o3,返回负整数
1、如果实现类没有实现Comparable接口,又想对两个类进行比较(或者实现类实现了Comparable接口,但是对compareTo方法内的比较算法不满意),那么可以实现Comparator接口,自定义一个比较器,写比较算法
2、实现Comparable接口的方式比实现Comparator接口的耦合性 要强一些,如果要修改比较算法,要修改Comparable接口的实现类,而实现Comparator的类是在外部进行比较的,不需要对实现类有任何修 改。从这个角度说,其实有些不太好,尤其在我们将实现类的.class文件打成一个.jar文件提供给开发者使用的时候。实际上实现Comparator 接口的方式后面会写到就是一种典型的策略模
Java集合类框架的最佳实践有哪些
- 假如元素的⼤⼩是固 定的,⽽且能事先知道,我们就应该⽤Array⽽不是ArrayList。
- 有些集合类允许指定初始容量。因此,如果我们能估计出存储的元素的数⽬,我们可以设
置 初始容量来避免重新计算hash值或者是扩容。 - 为了类型安全,可读性和健壮性的原因总是要使⽤泛型。同时,使⽤泛型还可以避免运⾏
时的ClassCastException。 - 使⽤JDK提供的不变类(immutable class)作为Map的键可以避免为我们⾃⼰的类实现hashCode()和equals()⽅法。
- 编程的时候接⼝优于实现。
- 底层的集合实际上是空的情况下,返回⻓度是0的集合或者是数组,不要返回null
Collection和Collections的区别
collection是集合类的上级接⼝,继承与它的接⼝主要是set和list。
collections类是针对集合类的⼀个帮助类.它提供⼀系列的静态⽅法对各种集合的搜索,排序,线程安全化等操作
如果一个list初始化为{5,3,1},执行以下代码后,其结果为()?
nums.add(6);
nums.add(0,4);
nums.remove(1);
list{5,3,1}
nums.add(6); //往后边加一个6,{5,3,1,6}
nums.add(0,4);//往下标为0的数加一个4,{4,5,3,1,6}
nums.remove(1); // 移除下标为1 的元素,{4,3,1,6}
IO流
字节字符区别
字节是存储容量的基本单位,字符是数字,字母,汉子以及其他语言的各种符号。
1 字节=8 个二进制单位:一个字符由一个字节或多个字节的二进制单位组成
char 型变量中能不能存储一个中文汉字,为什么?
在C语言中,char类型占1一个字节,而汉子占2个字节,所以不能存储。在Java中,char类型占2个字节,而且Java默认采用Unicode编码,一个Unicode码是16位,所以一个Unicode码占两个字
节,Java中无论汉子还是英文字母都是用Unicode编码来表示的。所以,在Java中,char类型变量可以存储一个中文汉字
序列化 ID作用
它决定着是否能够成功反序列化,简单来说,java的序列化机制是通过在运行时判断类serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常
多线程
线程和进程有什么区别?
进程是运行中的程序,线程是进程的内部的一个执行序列
一个程序至少有一个进程,一个进程至少有一个线程.
进程是资源分配最小单位,线程是系统调度的最小单位
进程间不会相互影响,一个线程挂掉将导致整个进程挂掉
同一线程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
进程间切换代价大,线程间切换代价小
同步代码块能发生CPU的切换吗 (能)
并行和并发
并发指的是单核cpu在执行多个任务时,由于任务之间的切换时间非常短,就造成了一种看似同时执行在执行的任务,但是实际上单核CPU无法在同一个时间处理多个任务
并行指的是多核CPU同时处理多个任务,每个CPU核心对应一个任务,这个才是真正意义上的同时处理
并行一定是并发,但是并发并不是并行
创建线程的方式有哪些
run()和start()区别
run( ):只是调⽤普通run⽅法
start( ):启动了线程,由Jvm调⽤run⽅法
调用 start() 方法是用来启动线程的,轮到该线程执行时,会自动调用 run();直接调用 run() 方法,无法达到启动多线程的目的,相当于主线程线性执行 Thread 对象的 run() 方法。
一个线程的 start() 方法只能调用一次,多次调用会抛出 java.lang.IllegalThreadStateException 异常;run() 方法没有限制
以下哪个事件会导致线程销毁?(D)
A. 调用方法sleep()
B. 调用方法wait()
C. start()方法的执行结束
D.run()方法的执行结束
sleep方法和wait方法的区别
- sleep方法是thread类下的方法,wait方法是object的方法
- sleep方法不会释放锁,wait方法会释放锁
- sleep方法不依赖与同步器synchronized,但是wait需要依赖synchronized关键字
- sleep不需要被唤醒,休眠后退出阻塞,wait方法需要通过notify来唤醒
说⼀下ThreadLocal
- ThreadLocal是Java中所提供的线程本地存储机制,可以利⽤该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意⽅法中获取缓存的数据
- ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal
对象)中都存在⼀个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓
存的值 - 如果在线程池中使⽤ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使⽤完之后,应该
要把设置的key,value,也就是Entry对象进⾏回收,但线程池中的线程不会回收,⽽线程对象是通过强引⽤指向ThreadLocalMap,ThreadLocalMap也是通过强引⽤指向Entry对象,线程不被回收,Entry对象也就不会被回收,从⽽出现内存泄漏,解决办法是,在使⽤了ThreadLocal对象之后,⼿动调⽤ThreadLocal的remove⽅法,⼿动清除Entry对象 - ThreadLocal经典的应⽤场景就是连接管理(⼀个线程持有⼀个连接,该连接对象可以在不同的⽅法之间进⾏传递,线程之间不共享同⼀个连接)
java8中,下面哪个类用到了解决哈希冲突的开放定址法©
A.LinkedHashSet
B.HashMap
C.ThreadLocal
D.TreeMap
threadlocalmap使用开放定址法解决haah冲突,hashmap使用链地址法解决hash冲突
下列程序的运行结果
public static void main(String args[]) {Thread t = new Thread() {public void run() {pong();}};t.run();System.out.print("ping");}static void pong() {System.out.print("pong");}
pongping
这里需要注意Thread的start和run方法
用start方法才能真正启动线程,此时线程会处于就绪状态,一旦得到时间片,则会调用线程的run方法进入运行状态。
而run方法只是普通方法,如果直接调用run方法,程序只会按照顺序执行主线程这一个线程
当编译并运行下面程序时会发生什么结果( D)
public class Bground extends Thread{public static void main(String argv[]){Bground b = new Bground();b.run();}public void start(){for(int i=0;i<10;i++){System.out.println("Value of i = "+i);}}
}
A.编译错误,指明run方法没有定义
B.运行错误,只鞥呢run方法没有定义
C.编译通过并输出0到9
D.编译通过,但无输出
对于线程而言,start是让线程从new变成runnable。run方法才是执行体的入口。
但是在Thread中,run方法是个空方法,没有具体实现。
Bground继承了Thread,但是没有重写run方法,那么调用run方法肯定是无输出
下列代码执行结果为()
public static void main(String args[])throws InterruptedException{Thread t=new Thread(new Runnable() {public void run() {try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.print("2");}});t.start();t.join();System.out.print("1");}
A.21
B.12
C.可能为12,也可能为21
D.以上答案都不对
因为子线程的休眠时间太长,因此主线程很有可能在子线程之前结束也就是输出结果是12,但是子线程用了join函数,因此主线程必须等待子线程执行完毕才结束因此输出结果只能是21
Executor,ExecutorService和Executors的区别
- Executor 接口对象能执行我们的线程任务;
- Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
- ExecutorService 接口继承了Executor接口并进行了扩展,提供了更多的方法,我们能够获得任务执行的状态并且可以获取任务的返回值
在java中守护线程和用户线程的区别
java中的线程分为两种守护线程(Daemon)和用户线程(user)
守护线程通过调用Thread.setDaemon(true)设置
一般程序使用的都是用户线程
守护线程我们一般用不上,比如垃圾回收线程就是守护线程(Daemon)
使用守护线程的注意点
- Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常
- 守护线程是为其他线程提供服务的,如果全部的用户线程已结束,守护线程没有可服务的线程,JVM关闭
介绍一下 Syncronized 锁,如果用这个关键字修饰一个静态方法,锁住了什么?如果修饰成员方法,锁住了什么?
synchronized 修饰静态方法以及同步代码块的 synchronized (类.class)用法锁的是类,线程想要执行对应同步代码,需要获得类锁。
synchronized 修饰成员方法,线程获取的是当前调用该方法的对象实例的对象锁。
什么是多线程中的上下文切换
CPU时间片:CPU时间片是CPU分配给每个线程执行的时间段,一般为几十毫秒
上下文切换:当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一次上下文切换。
上下文:在这种切入切出的过程中,操作系统需要保存和恢复响应的进度信息,这个进度信息就是上下文
什么是死锁?死锁的危害
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种互相等待的僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:
也可以用形象的理解在一个狭窄的道路上(只能由一辆车过去),两辆车都不倒退,退一步,就一直卡在哪里都想前进。
危害
- 死锁会使进程得不到正确的结果
- 死锁会使资源的利用率降低
- 死锁还会导致产生新的死锁
死锁产生的4个必要条件
产生死锁的必要条件:
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
锁有了解嘛,说一下 Synchronized 和 lock
synchronized 是 Java 的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证
在同一时刻最多只有一个线程执行该段代码。JDK1.5 以后引入了自旋锁、锁粗化、轻量级锁,
偏向锁来有优化关键字的性能。
-
层面 synchronized属于jvm层面,是java的关键字,是内置特性;而Lock属于api层面,是java5后产生的一个接口
-
释放锁 synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
-
用法 不一样。synchronized既可以加在方法上,也可以加载特定的代码块上,括号中表示需要锁的对象。而Lock需要显示地指定起始位置和终止位置。synchronzied是托管给jvm执行的,Lock锁定是通过代码实现的。
-
性能 上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
-
异常 synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
-
锁公平 synchronized是非公平锁;ReentrantLock可以是公平锁,也可以是非公平锁,默认是非公平锁,通过构造方法传入boolean值来确定。
异常
final、finally、finalize 的区别?
- final:用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,被其修饰的类不可继承
- finally:异常处理语句结构的一部分,表示一定会执行
- finalize:Object 类的一个方法,在垃圾回收器执行的时候会调用被回收对象的此方法,可以覆盖此方法
提供垃圾收集时的其他资源回收,例如关闭文件等。该方法更像是一个对象生命周期的临终方法,当该方法
被系统调用则代表该对象即将“死亡”,但是需要注意的是,我们主动行为上去调用该方法并不会导致该对
象“死亡”,这是一个被动的方法(其实就是回调方法),不需要我们调用
getCustomerInfo()方法如下,try中可以捕获三种类型的异常,如果在该方法运行中产生了一个IOException,将会输出什么结果()
public void getCustomerInfo() {try {// do something that may cause an Exception} catch (java.io.FileNotFoundException ex) {System.out.print("FileNotFoundException!");} catch (java.io.IOException ex) {System.out.print("IOException!");} catch (java.lang.Exception ex) {System.out.print("Exception!");}}
A IOException!
B IOException!Exception!
C FileNotFoundException!IOException!
D FileNotFoundException!IOException!Exception!
正确答案是 A考察多个catch语句块的执行顺序。当用多个catch语句时,catch语句块在次序上有先后之分。从最前面的catch语句
块依次先后进行异常类型匹配,这样如果父异常在子异常类之前,那么首先匹配的将是父异常类,子异常类将不会获
得匹配的机会,也即子异常类型所在的catch语句块将是不可到达的语句。所以,一般将父类异常类即Exception老大
放在catch语句块的最后一个
Throw 和 throws 的区别
- throws 用在函数上,后面跟的是异常类,可以跟多个;而 throw 用在函数内,后面跟的是异常对象。
- throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,
执行 throw 则一定抛出了某种异常对象 - 两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异
常,真正的处理异常由函数的上层调用处理
AccessViolationException异常触发后,下列程序的输出结果为( A )
static void Main(string[] args)
{ try { throw new AccessViolationException(); Console.WriteLine("error1"); } catch (Exception e) { Console.WriteLine("error2"); } Console.WriteLine("error3");
}
A. error2 error3
B. error3
C.error2
D.error1
try里面如果抛出的异常被catch捕获,依旧可以执行trycatch外面的语句,并且抛出异常后面的代码不会执行,直接执行catch里面,如果有finally{}代码无论有没有出现异常都会执行
以下代码执行的结果显示是多少()?
public class Demo{public static void main(String[] args){System.out.print(getNumber(0));System.out.print(getNumber(1));System.out.print(getNumber(2));System.out.print(getNumber(4));}public static int getNumber(int num){try{int result = 2 / num;return result;}catch (Exception exception){return 0;}finally{if(num == 0){return -1;}if(num == 1){return 1;}}}
}
A. 0110
B.-1110
C.0211
D.-1211
finally一定会在return之前执行,但是如果finally使用了return或者throw语句,将会使trycatch中的return或者throw失效
给定以下JAVA代码,这段代码运行后输出的结果是()
public class Test
{ public static int aMethod(int i)throws Exception{try{return i/10;}catch (Exception ex){throw new Exception("exception in a aMethod");}finally{System.out.printf("finally");}
} public static void main(String[] args){try{aMethod(0);}catch (Exception ex){System.out.printf("exception in main");}System.out.printf("finished");}
}
finallyfinished
1、先进入main函数,进入try块调用aMethod(0)方法;
2、执行aMethod()方法的try块,i/10可以正确执行,故并未抛出异常,catch块不执行,而需要执行finally(该块任何时候都要执行),故打印finally;
3、回到main函数,由于aMethod()调用成功,因此main函数的catch块同样不执行,顺序执行finally块,打印finished
因此,最终的输出结果就是:finally finished
try括号里有return语句, finally执行顺序
return前执行
反射
什么是反射
Java中的反射机制是指在运行状态中,对于任意一个类,能够动态获取这个类中的属性和方法;对于任意一个对象,都能够任意调用它的属性和方法。这种动态获取类的信息以及动态调用对象方法的功能称为Java的反射机制。总结就是:反射可以实现运行时知道任意一个类的属性和方法。
我们.java文件在编译后会变成.class文件,这就像是个镜面,本身是.java,在镜中是.class,他们其实是一样的;那么同理,我们看到镜子的反射是.class,就能通过反编译,了解到.java文件的本来面目。即为反射。
反射是否破坏了封装性
封装性是指对外隐藏对象的属性和实现细节,仅对外提供公共的访问方式。反射是通过对象找到类,既然找到类了,那么我们就可以得到这个类的成员结构了,例如这个类的属性和方法,即使是private的也能得到,你想,现在这个类我都得到了,那么这个类中的所以东西我肯定是都得到了,我现在只是得到了这个类的成员,并没有说是在外部访问这个类的private的东西。这并没有破坏面向对象的封装性
打个比喻,核武器不是用来炸的,而是用来吓人的,但是当对象封装不合理的的时候,迫不得已还是要用的,就想上面所说,如果封装性不好的话,就可以用反射的来直接访问私有变量,比如类的私有变量没有相应的set方法,就可以暴力一下
JVM
常用命令
1、jps:查看本机java进程信息。
2、jstack:打印线程的栈信息,制作线程dump文件。
3、jmap:打印内存映射,制作堆dump文件
4、jstat:性能监控工具
5、jhat:内存分析工具
6、jconsole:简易的可视化控制台
7、ClassLoader如何加载classjvisualvm:功能强大的控制台
JVM中可以运行多种语言吗
jvm不仅可以跨平台,还可以跨语言,只要遵循字节码规范,JVM只识别字节码,所以JVM其实是跟语言是解耦的,没有直接关联,像Scala、Groovy等语言都可以在JVM上运行
说几个与JVM内存相关的核心参数
-Xms Java堆内存的大小;
-Xmx Java堆内存的最大大小;
-Xmn Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小;
-XX:MetaspaceSize 元空间大小;
-XX:MaxMetaspaceSize 元空间最大大小;
-Xss 每个线程的栈内存大小;
-XX:SurvivorRatio=8 设置eden区 和survivor 区大小的比例,默认是8:1:1;
-XX:MaxTenuringThreshold=5 年龄阈值;
-XX:+UseConcMarkSweepGC 指定CMS垃圾收集器;
-XX:+UseG1GC 指定使用G1垃圾回收器
–查看默认的堆大小及默认的垃圾收集器
java -XX:+PrintCommandLineFlags -version
常用的jvm监控工具
- 阿里巴巴开源的jvm诊断工具Arthas(阿尔萨斯)
- jdk自带的jvisualvm
- jdk自带的jconsole
- jdk命令行监控工具:
- jinfo 显示JVM配置信息
- jps 查看当前java进程
- jstat显示虚拟机运行时各方面数据
- jmap 内存使用情况监控,生成堆转存文件
- jstack生成jvm线程快照信息
谈谈Java中不同的引用类型
Java里有不同的引用类型,分别是强引用、软引用、弱引用 和 虚引用;
强引用:Object object = new Object();
软引用:SoftReference 内存充足时不回收,内存不足时则回收;
弱引用:WeakReference 不管内存是否充足,只要GC一运行就会回收该引用对象;
虚引用:PhantomReference这个其实暂时忽略也行,因为很少用,它形同虚设,就像没有引用一样,其作用就是该引用对象被GC回收时候触发一个系统通知,或者触发进一步的处理;
什么是内存泄露,什么是内存溢出
- 内存溢出
内存溢出是指在申请内存时,没有足够的内存供其使用,抛出outofmemory错误, - 内存泄露
内存泄露,他是指程序运行后,没有释放所占用的内存空间,一次内存泄露可能不会有很大影响,但长时间的内存泄露,堆积到一定程度就会产生内存溢出。
单例对象,声明周期和应用程序一样长,如果单例对象持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会产生内存泄露。
一些资源未关闭也会导致内存泄露,比如数据库连接,网络连接socket和io流的连接,必须在finally中close,否则不能被回收
Java运行时一个类是什么时候被加载的?
虚拟机是按需加载,在需要用到该类的时候加载这个类
JVM一个类的加载过程?
1、加载(Loading)
2、验证(Verification)
3、准备(Preparation)
4、解析(Resolution)
5、初始化(Initialization)
6、使用(Using)
7、卸载(Unloading)
其中验证、准备、解析三个阶段统称为链接(Linking)
继承时父子类的初始化顺序是怎样的?
父类–静态变量
父类–静态初始化块
子类–静态变量
子类–静态初始化块
父类–变量
父类–初始化块
父类–构造器
子类–变量
子类–初始化块
子类–构造器
ClassLoader如何加载class
JVM⾥有多个类加载,每个类加载可以负责加载特定位置的类,例如,bootstrap类加载负责加载jre/lib/rt.jar中的类, 我们平时⽤的jdk中的类都位于rt.jar中。extclassloader负责加载jar/lib/ext/*.jar中的类,appclassloader负责classpath指定的⽬录或jar中的类。除了bootstrap之外,其他的类加载器本身也都是java类,它们的⽗类是ClassLoader。
JVM三层类加载器之间的关系是继承吗?
不是,启动类加载器(Bootstrap ClassLoader):(根的类加载器)C++语言实现的,扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)是java编写的是继承于 ClassLoader
JVM类加载的双亲委派模型
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当上一层类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到这个类)时,下一层类加载器才会尝试自己去加载;直到所有加载器都找不到抛出异常
JDK为什么要设计双亲委派模型,有什么好处
1、确保安全,避免Java核心类库被修改;
2、避免重复加载;
3、保证类的唯一性;
可以打破JVM双亲委派模型吗?如何打破JVM双亲委派模型?
可以;
想要打破这种模型,那么就自定义一个类加载器,重写其中的loadClass方法,使其不进行双亲委派即可;
如何自定义自己的类加载器?
1、继承ClassLoader
2、覆盖findClass(String name)方法 或者 loadClass() 方法;
findClass(String name)方法 不会打破双亲委派;
loadClass() 方法 可以打破双亲委派;
ClassLoader中的loadClass()、findClass()、defineClass()区别?
1、当我们想要自定义一个类加载器的时候,并且想破坏双亲委派模型时,我们会重写loadClass()方法;
2、如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢?可以可以重写findClass方法(),findClass()方法是JDK1.2之后的ClassLoader新添加的一个方法,这个方法只抛出了一个异常,没有默认实现;
JDK1.2之后已不再提倡用户直接覆盖loadClass()方法,而是建议把自己的类加载逻辑实现到findClass()方法中;
所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass()中实现你自己的加载逻辑即可;
JVM有哪些内存区域
线程私有区可以分为:虚拟机栈、本地方法栈和程序计数器
线程共享区分化为方法区和堆
虚拟机栈:在JVM运行过程中存储当前线程运行方法所需要的数据、指令和返回地址
本地方法栈:他服务的对象是native方法,调用的是本地库,c/c++编写的
程序计数器:主要用来记录各个线程执行的字节码地址
方法区:JDK1.7及以前“永久代”,JDK1.8及以后“元空间”,存放类信息、常量池,方法数据、方法代码
堆:堆是JVM上最大的内存区域,几乎所有的对象都在堆中存储,jvm调优一般就是调堆
JVM运行时数据区 程序计数器 的特点及作用
1、程序计数器是一块较小的内存空间,几乎可以忽略;
2、是当前线程所执行的字节码的行号指示器;
3、Java多线程执行时,每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响;
4、该区域是“线程私有”的内存,每个线程独立存储;
5、该区域不存在OutOfMemoryError;
6、无GC回收
JVM运行时数据区 虚拟机栈的特点及作用
1、线程私有;
2、方法执行会创建栈帧,存储局部变量表等信息;
3、方法执行入虚拟机栈,方法执行完出虚拟机栈;(先进后出)
4、栈深度大于虚拟机所允许StackOverflowError;
5、栈需扩展而无法申请空间OutOfMemoryError(比较少见);hotspot虚拟机没有;
6、栈里面运行方法,存放方法的局部变量名,变量名所指向的值(常量值、对象值等)都存放到堆上的;
7、栈一般都不设置大小,栈所占的空间其实很小,可以通过-Xss1M进行设置,如果不设置默认为1M;
8、随线程而生,随线程而灭;
9、该区域不会有GC回收;
JVM运行时数据区 本地方法栈的特点及作用
1、与虚拟机栈基本类似;
2、区别在于本地方法栈为Native方法服务;
3、HotSpot虚拟机将虚拟机栈和本地方法栈合并;
4、有StackOverflowError和OutOfMemoryError(比较少见);
5、随线程而生,随线程而灭;
6、GC不会回收该区域;
JVM运行时数据区 Java堆的特点及作用
1、线程共享的一块区域;
2、虚拟机启动时创建;
3、虚拟机所管理的内存中最大的一块区域;
4、存放所有实例对象或数组;
5、GC垃圾收集器的主要管理区域;
6、可分为新生代、老年代;
7、新生代更细化可分为Eden、From Survivor、To Survivor,Eden:Survivor = 8:1:1
8、可通过-Xmx、-Xms调节堆大小;
9、无法再扩展java.lang.OutOfMemoryError: Java heap space
10、如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率;
JVM运行时数据区 元空间的特点及作用
1、在JDK1.8开始才出现元空间的概念,之前叫方法区/永久代;
2、元空间与Java堆类似,是线程共享的内存区域;
3、存储被加载的类信息、常量、静态变量、常量池、即时编译后的代码等数据;
4、元空间采用的是本地内存,本地内存有多少剩余空间,它就能扩展到多大空间,也可以设置元空间大小;
-XX:MetaspaceSize=20M -XX:MaxMetaspaceSize=20m
5、元空间很少有GC垃圾收集,一般该区域回收条件苛刻,能回收的信息比较少,所以GC很少来回收;
6、元空间内存不足时,将抛出OutOfMemoryError;
JVM在创建对象是采用了哪些并发安全机制
JVM创建对象的过程:类加载、检查加载、分配内存、内存空间初始化、设置、对象初始化
划分内存的方式:指针碰撞、空闲列表
解决并发安全:CAS+失败重试(使用乐观锁的机制,有一定的内存开销)、本地线程分配缓冲
JVM中对象如何在堆内存分配
1、指针碰撞(Bump The Pointer):内存规整的情况下;
2、空闲列表(Free List):内存不规整的情况下;
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定;
因此,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;
而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存;
3、本地线程分配缓冲(Thread Local Allocation Buffer,TLAB):对象创建在虚拟机中频繁发生,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况;
那么解决方案有两种:
(1)同步锁定,JVM是采用CAS配上失败重试的方式保证更新操作的原子性;
(2)线程隔离,把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定,虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定;
-XX:TLABSize=512k 设置大小;
本地线程分配缓冲
每个线程在java堆中预先分配一小块私有内存,也就是本地线程分配缓冲,这样没有线程都独立拥有一个buffer,如果需要分配内存,就在自己的buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率
JVM堆内存中的对象布局
在 HotSpot 虚拟机中,一个对象的存储结构分为3块区域:对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding);
对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit,官方称为 ‘Mark Word’;
第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例,另外,如果是Java数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以;
实例数据(Instance Data):程序代码中所定义的各种成员变量类型的字段内容(包含父类继承下来的和子类中定义的);
对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍,HotSpot虚拟机,任何对象的大小都是8字节的整数倍;
什么是对象头?对象头里面有哪些东西
对象填充必须是8字节的整数倍,如果对象头+实例数据没有达到8字节的整数倍数据,就要用对象填充到8字节的整数倍
好处:这样方便字节对齐 提高程序运行效率 ,而且对于压缩指针也更加的方便
实例对象是怎样存储的
实例对象存放在堆区,对实例的引用存在线程栈上,而实例的元数据存在方法区或者元空间,如果实例对象没有发生线程逃逸行为,就会被存储在线程栈中
如何判断对象是否存活
引用计数法
原理:实际上是通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数+1,如果删除对该对象的引用,那么它的引用计数就-1,当该对象的引用计数为0时,那么该对象就会被回收。
GC的时候会将计数器为0的对象C给销毁.
引用计数法无法解决循环引用的问题
循环依赖问题:
A a = new A()
B b = new B()
a.x=b
b.x=a
a=null
b=null
很难判断 然后 怎么去标记为0 去回收
根搜索算法
根搜索算法。它的处理方式就是,设立若干种根对象,当任何一个根对象到某一个对象均不可达时,则认为这个对象是可以被回收的。
ObjectD和ObjectE是互相关联的,但是由于GC roots到这两个对象不可达,所以最终D和E还是会被当做GC的对象,上图若是采用引用计数法,则A-E五个对象都不会被回收。
说到GC roots(GC根),在JAVA语言中,可以当做GC roots的对象有以下几种:
1、虚拟机栈中的引用的对象。2、方法区中的类静态属性引用的对象。3、方法区中的常量引用的对象。4、本地方法栈中JNI的引用的对象。5、Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器; 6、所有被同步锁(synchronized关键字)持有的对象;
JVM中哪些内存区域会发生内存溢出(OOM)
- 栈溢出
- 堆溢出
- 方法区溢出
- 本机直接内存溢出
唯一不会发生OOM的就是程序计数器
会发生内存溢出的区域
- 不会出现内存溢出的区域 – 程序计数器
- 出现 OutOfMemoryError 的情况
- 堆内存耗尽 – 对象越来越多,又一直在使用,不能被垃圾回收
- 方法区内存耗尽 – 加载的类越来越多,很多框架都会在运行期间动态产生新的类
- 虚拟机栈累积 – 每个线程最多会占用 1 M 内存,线程个数越来越多,而又长时间运行不销毁时
- 出现 StackOverflowError 的区域
- JVM 虚拟机栈,原因有方法递归调用未正确结束、反序列化 json 时循环引用
为什么不要使用Finalize方法
一个对象要被回收,需要经过两次过程,一次是没有找到GCRoots的引用链,它将被第一次标记,随后进行一次筛选(如果对象覆盖了finalize),我们可以在finalize方法中去拯救(变为存活对象)
finalize方法执行的优先级很低(需要等待),往往调用了也可能不成功,可以采用线程休眠的方式让finalize方法生效
finalize方法只执行一次
JVM堆内存分代模型
JVM堆内存的分代模型:年轻代、老年代;
大部分对象朝生夕死,少数对象长期存活;
什么是老年代空间分配担保机制
新生代Minor GC后剩余存活对象太多,无法放入Survivor区中,此时就必须将这些存活对象直接转移到老年代去,如果此时老年代空间也不够怎么办?
1、执行任何一次Minor GC之前,JVM会先检查一下老年代可用内存空间,是否大于新生代所有对象的总大小,因为在极端情况下,可能新生代Minor GC之后,新生代所有对象都需要存活,那就会造成新生代所有对象全部要进入老年代;
2、如果老年代的可用内存大于新生代所有对象总大小,此时就可以放心大胆的对新生代发起一次Minor GC,因为Minor GC之后即使所有对象都存活,Survivor区放不下了,也可以转移到老年代去;
3、如果执行Minor GC之前,检测发现老年代的可用空间已经小于新生代的全部对象总大小,那么就会进行下一个判断,判断老年代的可用空间大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小,如果判断发现老年代的内存大小,大于之前每一次Minor GC后进入老年代的对象的平均大小,那么就是说可以冒险尝试一下Minor GC,但是此时真的可能有风险,那就是Minor GC过后,剩余的存活对象的大小,大于Survivor空间的大小,也大于老年代可用空间的大小,老年代都放不下这些存活对象了,此时就会触发一次“Full GC”;
所以老年代空间分配担保机制的目的?也是为了避免频繁进行Full GC;
4、如果Full GC之后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致“OOM”内存溢出 ;
什么情况下对象会进入老年代
1、躲过15次GC之后进入老年代,可通过JVM参数
“-XX:MaxTenuringThreshold”来设置年龄,默认为15岁;
2、动态对象年龄判断;
3、老年代空间担保机制;
4、大对象直接进入老年代;
堆为什么要分成年轻代和老年代
因为年轻代和老年代不同的特点,需要采用不同的垃圾回收算法;
年轻代的对象,它的特点是创建之后很快就会被回收,所以需要用一种垃圾回收算法;
老年代的对象,它的特点是需要长期存活,所以需要另外一种垃圾回收算法
所以需要分成两个区域来放不同的对象;
1、绝大多数对象都是朝生夕灭的;
如果一个区域中大多数对象都是朝生夕灭,那么把它们集中放在一起,每次回收时只关注如何保留少量存活对象,而不是去标记那些大量将要被回收的对象,就能以较低的代价回收到大量的空间;
2、熬过越多次垃圾收集的对象就越难以回收;
如果是需要长期存活的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用;
JVM堆的年轻代为什么要有两个Survivor区
1、如果没有Survivor区会怎么样?
此时每触发一次Minor GC,就会把Eden区的对象复制到老年代,这样当老年代满了之后会触发Major Gc/Full GC(通常伴随着MinorGC),比较耗时,所以必须有Survivor区;
2、如果只有1个Survivor区会怎么样?
刚刚创建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中存活的对象就会被移动到Survivor区,下一次Eden满了的时候,此时进行Minor GC,Eden和Survivor各有一些存活对象,因为只有一个Survivor,所以Eden区第二次GC发现的存活对象也是放入唯一的一个Survivor区域中,但此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化问题,并且由于不连续的空间会导致再分配大对象的时候,由于没有连续的空间来分配,会导致提前垃圾回收;
如果将Survivor中的所有存活对象进行整理消除碎片,然后将所有的存活对象放入其中,这样做会降低效率;
如果把两个区域中的所有存活对象都复制转移到一个完全独立的空间中,也就是第二块Survivor中,这样就可以留出一块完全空着的Eden和Survivor了,下次GC的时候再重复这个流程,所以我们便要有两个Survivor区;
复制算法
GC 复制算法是利用 From 空间进行分配的。当 From 空间被完全占满时,GC 会将存活对象全部复制到 To 空间,并且年龄加一。当复制完成后,该算法会把 From 空间和 To 空间互换,GC 也就结束了。From 空间和 To 空间大小必须一致。这是为了保证能把 From 空间中的所有活动对象都收纳到 To 空间里
-
不适用于存活对象较多的场合,如老年代(复制算法适合做新生代的GC)
-
幸存区from和幸存区to中谁空谁是to,我们会将to中的数据复制到from中保持to中数据为空;
-
from和to区实际上为逻辑上的概念,保证to区一直空;
-
默认对象经过15次GC后还没有被销毁就会进入养老区
流程:
将Eden区进行GC存活对象放入空的to区,将from区存活的放到空的to区
此时from区为空变成了to区,to区有数据变为from区
经过15次GCfrom区还存活的对象会被移动到养老区
好处:没有内存碎片,效率高
坏处:浪费内存空间,多了一半to空间永远是空的。
复制算法最佳使用场景:对象存活度较低的时候 -> 新生区 (如果存活度较高,则from区空间全部被占满导致会将全部内容复制到to区)
标记清除算法
标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象;然后,在清除阶段,清除所有未被标记的对象。
它的做法是当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被成为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
需要两次扫描,第一次扫描标记存活对象,第二次扫描清除没有被标记的对象
优点:不需要额外的空间,实现简单
缺点:两次扫描严重浪费时间,并且还会产生内存碎片,(内存碎片会导致明明有空间,但是无法存储大对象)
标记-整理
标记整理算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记;但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端;之后,清理边界外所有的空间。
优点 不会产生内存碎片
缺点 效率低
为什么扩容新生代可以提高GC的效率
请介绍一下JVM垃圾收集器
如上图,一共有7种作用于不同分代的垃圾收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用,垃圾收集器所处区域表示它是属于新生代收集器还是老年代收集器;
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1
垃圾收集器的最前沿成果:ZGC 和 Shenandoah
Serial收集器
新生代收集器,最早的收集器,单线程的,收集时需暂停用户线程的工作,所以有卡顿现象,效率不高,但Serial收集器简单,不会有线程切换的开销
参数: -XX:+UseSerialGC
java -XX:+PrintFlagsFinal -version 打印jvm默认的参数值;
ParNew收集器
它是新生代收集器,就是Serial收集器的多线程版本,大部分基本一样,单CPU下,ParNew还需要切换线程,可能还不如Serial;
Serial和ParNew收集器可以配合CMS收集器,前者收集新生代,后者CMS收集老年代,
“-XX:+UseConcMarkSweepGC”:指定使用CMS后,会默认使用ParNew作为新生代垃圾收集器;
“-XX:+UseParNewGC”:强制指定使用ParNew;
“-XX:ParallelGCThreads=2”:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
Parallel Scavenge收集器
简称Parallel,它是新生代收集器,基于复制算法,并行的多线程收集器(与ParNew收集器类似),侧重于达到一个可控的吞吐量,而且这个垃圾收集器是jdk8 jvm默认的新生代的垃圾收集器;
它提供一个参数设置吞吐量:
-XX:MaxGCPauseMillis 该参数设置大于0的毫秒数,每次GC的时间将尽量保持不超过设置的值,但是这个值也不是设置得越小就越好,GC暂停时间越短,那么GC的次数会变得更频繁;
参数:-XX:+UseParallelGC 指定使用Parallel Scavenge垃圾收集器
java -XX:+PrintCommandLineFlags -version 打印jvm默认初始堆和最大堆大小以及垃圾收集器
java -XX:+PrintFlagsFinal -version 打印jvm所有的默认的参数值;
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:d:/dev/gc.log
Parallel Scavenge垃圾收集器中的Ergonomics负责自动的调节gc暂停时间和吞吐量之间的平衡,自动优化虚拟机的性能;
Serial Old收集器
它是Serial收集器的老年代版本,同Serial一样,单线程,采用标记-整理算法,Serial Old收集器也可以作为CMS 收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用;
Parallel Old收集器
是Parallel Scavenge的老年代版本,多线程,标记整理算法,它是在jdk1.6开始才提供;
在注重吞吐量和CPU资源的情况下, Parallel Scavenge新生代+ Parallel Old老年代是一个很好的搭配;
参数:-XX:+UseParallelOldGC 指定使用Parallel Old收集器;
简述一下CMS垃圾回收器,他有哪些问题
采用多线程和标记清除算法,在初始标记和重新标记会发生stw
- 初始标记: 暂停所有的其他线程(STW),并记录gc roots直接能引⽤的对象。在这个过程中,虽然会触发STW机制,但是时间会非常的短
- 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较⻓但是不需要STW,可以与垃圾收集线程⼀起并发运⾏。这个过程中,⽤户线程和GC线程并发,可能会有导致已经标记过的对象状态发⽣改变。
- 重新标记:为了修正并发标记期间因为⽤户程序继续运⾏⽽导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓,远远⽐并发标记阶段时间短。主要⽤到三⾊标记⾥的算法做重新标记。
- 并发清理:开启⽤户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为⿊⾊不做任何处理。
- 并发重置:重置本次GC过程中的标记数据。
CMS需要注意的问题
- 消耗cpu资源多
- 无法处理浮动的垃圾,并发清除的时候,用户线程也在运行,还是会产生垃圾,这就是浮动垃圾,浮动垃圾只有在执行下一次垃圾回收的时候才会被真正回收掉
- 会产生大量的空间碎片CMS是基于标记-清除算法的,只会将标记为为存活的对象删除,并不会移动对象整理内存空间,会造成内存碎片(会发生垃圾回收器退化为serialOld 单线程的垃圾回收标记清除算法)
G1收集器
G1全称Garbage First,G1垃圾回收器可以同时回收新生代和老年代,不需要两个垃圾回收器配合起来使用;
G1垃圾收集器的基本原理
G1是一款可以让我们设置垃圾回收的预期停顿时间的垃圾收集器,设置参数是-XX:MaxGCPauseMillis,默认值是200ms;
G1垃圾收集器是尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象,这就是G1垃圾收集器的核心原理;
G1垃圾收集器如何做到可预测的停顿时间
G1垃圾收集器独特的设计有关,它最大的特点就是把Java整个堆内存拆分为多个大小相等的Region
G1它会追踪每个Region的回收价值,即它会计算每个Region里的对象有多少是垃圾,如果对这个Region进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾
G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为垃圾回收的最小单元,即每次可以选择一部分Region进行收集,避免在整个Java堆中进行全区域的垃圾收集,让G1收集器去跟踪各个Region里面的垃圾的“回收价值”,然后根据用户设定的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),然后在后台维护一个优先级列表,优先处理回收价值大的那些Region,这也是“Garbage First”名字的由来,这种使用Region划分堆内存空间,基于回收价值的回收方式,保证了G1收集器在有限的时间内尽可能收集更多的垃圾;
G1垃圾收集器是否还有年代的划分
G1也有新生代和老年代的概念,但只不过是逻辑上的概念,也就是说一个Region此时是属于新生代的Eden空间,过一会儿可能就属于老年代空间,也就是一个Region在运行过程中动态地扮演着新生代的Eden空间、Survivor空间,或者老年代空间,每个Region并不是固定属于某一个空间,另外新生代、老年代也不一定是连续空间,可能是分开的;
G1垃圾收集器中的大对象
Region中有一类特殊的Humongous 区域,专门用来存储大对象;
G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象,每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待;
G1垃圾收集器内存大小如何设置
每个region = 1m ~32m,最多有2048个region;
通过参数“-XX:G1HeapRegionSize”指定每个Region是多少兆
默认新生代对堆内存的占比是5%,可以通过“-XX:G1NewSizePercent”来设置新生代初始占比,一般默认值即可,因为在系统运行中,JVM会不停的给新生代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”设置,并且一旦Region进行了垃圾回收,此时新生代的Region数量就会减少,这些都是动态的;
新生代 :老年代 = 60% :40%
G1垃圾收集器新生代还有Eden和Survivor吗
G1垃圾收集器依然有新生代、老年代的概念,新生代里依然有Eden和Survivor的划分,只不过随着对象不停的在新生代里分配,属于新生代的Region会不断增加,Eden和Survivor对应的Region也会不断增加;
5% – 60%
G1垃圾收集器的新生代垃圾回收
G1的新生代也有Eden和Survivor,其触发垃圾回收的机制也是类似的,随着不停在新生代Eden对应的Region中放对象,JVM就会不停的给新生代加入更多的Region,直到新生代占据堆大小的最大比例60%;
假设堆 4G,最大2048个region,每个region为2M,新生代最大60%=2.4G;
一旦新生代达到了设定的占据堆内存的最大大小60%,按照上面的数据大概就是有1200个Region,里面的Eden可能占据了1000个Region,每个Survivor是100个Region,而且Eden区满了,此时触发新生代的GC,G1就会依然用复制算法来进行垃圾回收,进入一个“Stop the World”状态,然后把Eden对应的Region中的存活对象复制到S0对应的Region中,接着回收掉Eden对应的Region中的垃圾对象;
但这个过程与之前是有区别的,因为G1是可以设定目标GC停顿时间的,也就是G1执行GC的时候最多可以让系统停顿多长时间,可以通过“-XX:MaxGCPauseMills”参数来设定,默认值是200ms,那么G1就会通过对每个Region追踪回收它需要多少时间,可以回收多少对象来选择回收一部分Region,保证GC停顿时间控制在指定范围内,尽可能多地回收对象;
G1垃圾收集器的老年代垃圾回收
1、初始标记,需要Stop the World,不过仅仅标记一下GC Roots直接能引用的对象,这个过程速度很快,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿;
2、并发标记,不需要Stop the World,这个阶段会从GC Roots开始追踪所有的存活对象,初始标记阶段仅仅只是标记GC Roots直接关联的对象,而在并发标记阶段,就会进行GC Roots追踪,从这个GC Root对象直接关联的对象开始往下追踪,追踪全部的存活对象,这个阶段是很耗时的,但可以和系统程序并发运行,所以对系统程序的影响不大;
3、重新标记(最终标记),需要Stop the World,用户程序停止运行,最终标记一下有哪些存活对象,有哪些是垃圾对象;
4、筛选回收,需要Stop the World,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间,这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的;
从整体上看,G1垃圾收集像是一种标记-整理算法,它不存在内存碎片问题,实际上它是一种复制算法,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的, 所以它并不是纯粹地追求低延迟,而是给它设定暂停目标,使其在延迟可控的情况下获得尽可能高的吞吐量;
G1垃圾收集器的混合垃圾回收?
混合垃圾收集即mixed gc,它不是一个old gc,除了回收整个young region,还会回收一部分的old region,是回收一部分老年代,而不是全部老年代,可以选择部分old region进行收集,从而可以对垃圾回收的耗时时间进行控制;
G1有一个参数,是“-XX:InitiatingHeapOccupancyPercent”,它的默认值是45%,即如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段;
G1回收失败时的Full GC
在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,把各个Region中存活的对象复制到其他空闲的Region中;
如果万一出现复制时没有空闲Region可以存放存活对象了,就会停止系统程序,然后采用单线程进行标记清除和压缩整理,空闲出来一批Region,这个过程很慢;
什么时候使用G1垃圾收集器
-XX:+UseG1GC
1、针对大内存、多处理器的机器推荐采用G1垃圾收集器,比如堆大小至少6G或以上;
2、超过50%的堆空间都被活动数据占用;
3、在要求低延迟的场景,也就是GC导致的程序暂停时间要比较少,0.5-1秒之间;
4、对象在堆中分配频率或者年代升级频率变化比较大,防止高并发下应用雪崩现象的场景;
ZGC收集器
-XX:+UseZGC
ZGC(Z Garbage Collector)是一款由Oracle公司研发的,以低延迟为首要目标的一款垃圾收集器,它是基于动态Region内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的收集器,在JDK 11新加入,还在实验阶段,主要特点是:回收TB级内存(最大4T),停顿时间不超过10ms;
线上环境的JVM都设置多大?
线上:4核8G机器;
1、栈1m,一个线程是1m,一个线上项目Tomcat可能有300个线程,300m;
2、堆:大概把机器的一半内存给堆,4G(新生代、老年代);
CMS: 1/3 、2/3 G1: 6:4
3、元空间:一般512M肯定够了;
此时JVM参数如下:
-Xms4096M -Xmx4096M -Xss1M -XX: MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -XX:+UseG1GC
线上Java服务器内存飙升怎么回事
jps -l # 查看java进程
jmap -histo pid
jmap -heap pid
jmap -dump:format=b,file=heap.hprof pid
线上Java项目CPU飙到100%怎么排查?
top
top -H -p pid #查看线程id
printf '%x’tid
jstack pid
以下哪个区域不属于新生代?(C)
A.eden区
B.from区
C.元数据区
D.to区
java程序内存泄露的最直接表现是( C)
A.频繁FullGc
B.jvm崩溃
C.程序抛内存溢出的Exception
D.java进程异常消失
数据库
你知道哪些非关系型数据库的类型呢?
- 键值型数据库:Redis
- 文档型数据库:MongoDB
- 搜索引擎数据库:ES、Solr
- 列式数据库:HBase
- 图形数据库:InfoGrid
mysql
数据库三大范式是什么
第一范式:每个列都不可以再拆分。
第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。
在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。
char和varchar的区别
区别一,定长和变长
char 表示定长,长度固定,varchar表示变长,即长度可变。char如果插入的长度小于定义长度
时,则用空格填充;varchar小于定义长度时,还是按实际长度存储,插入多长就存多长。因为其长度固定,char的存取速度还是要比varchar要快得多,方便程序的存储与查找;但是char
也为此付出的是空
间的代价,因为其长度固定,所以会占据多余的空间,可谓是以空间换取时间效率。varchar则刚好
相反,以时间换空间。区别之二,存储的容量不同
对 char 来说,最多能存放的字符个数 255,和编码无关。
而 varchar 呢,最多能存放 65532 个字符。varchar的最大有效长度由最大行大小和使用的字符
集确定。整体最大长度是 65,532字节。
数据库的隔离级别有哪些?各自的含义是什么
脏读:指当一个事务正在访问数据,并且对数据进行了修改,而这种数据还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据还没有提交那么另外一个事务读取到的这个数据我们称之为脏数据。依据脏数据所做的操作肯能是不正确的。
简单来说就是:脏读是指一个事务在处理数据的过程中,读取到另一个未提交事务的数据
不可重复读:指在一个事务内,多次读同一数据。在这个事务还没有执行结束,另外一个事务也访问该同一数据,那么在第一个事务中的两次读取数据之间,由于第二个事务的修改第一个事务两次读到的数据可能是不一样的,这样就发生了在一个事物内两次连续读到的数据是不一样的,这种情况被称为是不可重复读。
简单来说就是:不可重复读是指对于数据库中的某个数据,一个事务范围内的多次查询却返回了不同的结果,这是由于在查询过程中,数据被另外一个事务修改并提交了
幻象读:一个事务先后读取一个范围的记录,但两次读取的纪录数不同,我们称之为幻象读(两次执行同一条 select 语句会出现不同的结果,第二次读会增加一数据行,并没有说这两次执行是在同一个事务中)
不可重复读和脏读的区别是,脏读读取到的是一个未提交的数据,而不可重复读读取到的是前一个事务提交的数据。
而不可重复读在一些情况也并不影响数据的正确性,比如需要多次查询的数据也是要以最后一次查询到的数据为主
事务管理(ACID)
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性(Consistency)
事务前后数据的完整性必须保持一致。
隔离性(Isolation)
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
银行转账举例
原子性
针对同一个事务
这个过程包含两个步骤
A: 800 - 200 = 600
B: 200 + 200 = 400
原子性表示,这两个步骤一起成功,或者一起失败,不能只发生其中一个动作
一致性(Consistency)
针对一个事务操作前与操作后的状态一致
操作前A:800,B:200
操作后A:600,B:400
一致性表示事务完成后,符合逻辑运算
持久性(Durability)
表示事务结束后的数据不随着外界原因导致数据丢失
操作前A:800,B:200
操作后A:600,B:400
如果在操作前(事务还没有提交)服务器宕机或者断电,那么重启数据库以后,数据状态应该为
A:800,B:200
如果在操作后(事务已经提交)服务器宕机或者断电,那么重启数据库以后,数据状态应该为
A:600,B:400
事务的四个特性以及对应的子系统:
(1)原子性(A):安全性管理子系统;
(2)一致性(C):完整性管理子系统;
(3)隔离性(I):并发控制子系统;
(4)持久性(D):恢复管理子系统;
InnoDB引擎与MyISAM引擎的区别 ?
- InnoDB引擎, 支持事务, 而MyISAM不支持。
- InnoDB引擎, 支持行锁和表锁, 而MyISAM仅支持表锁, 不支持行锁。
- InnoDB引擎, 支持外键, 而MyISAM是不支持的。
mysql 内连接和外连接的区别
1.内连接,只会取两表关联条件匹配的到的数据
2.外连接,如果两表关联条件匹配不到的数据,也会取到关联条件列展示NULL
左外连接和右外连接的区别
左外连接,查询结果以左表为主,主表的数据会全部显示处理,从表根据连接条件没有匹配,查询结果从表数据都会以NULL展示
右外连接,查询结果以右表为主,主表的数据会全部显示处理,从表根据连接条件没有匹配,查询结果从表数据都会以NULL展示
索引失效
- 满足最左前缀法则,最左前缀法则指的是查询从索引的最左列开始,
并且不跳过索引中的列。如果跳跃某一列,索引将会部分失效(后面的字段索引失效)。 - 联合索引中,出现范围查询(>,<),范围查询右侧的列索引失效。尽量使用>=,<=
- 不要在索引列上进行运算操作, 索引将失效
- 字符串类型字段使用时,不加引号,索引将失效,如果字符串不加单引号,对于查询结果,没什么影响,但是数据库存在隐式类型转换,索引将失效。
- 如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效。
- 当or连接的条件,左右两侧字段都有索引时,索引才会生效
怎么优化SQL查询语句
-
SELECT子句中避免使用*号,尽量使用具体字段 ,尽量全部大写SQL
-
不要超过5个以上的表连接。
-
对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引
-
in 和 not in 也要慎用,否则会导致全表扫描
-
应尽量避免在 where 子句中对字段进行 is null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,使用 IS NOT NULL
-
where 子句中使用 or 来连接条件,要符合最左前缀法则,也会导致引擎放弃使用索引而进行全表扫描
-
在where后面少使用函数或者算数运算
-
对于需要索引很长的字符串,可以建立前缀索引
-
一个表的索引最好不要超过5个,多了会影响插入修改
索引设计原则
- 针对于数据量较大,且查询比较频繁的表建立索引。
- 针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引。
- 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。
- 如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引。
- 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率。
- 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率。
- 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询。
索引语法
# 普通索引
create index 索引名称 on 表名(列名);# 唯一索引
create UNIQUE index 索引名称 on 表名(列名);# 创建复合索引
create index 索引名称 on 表名(列1,列2,列3);# 查看索引
show index from 表名;#删除索引
DROP INDEX 索引名 ON 表名
视图
视图(View)是一种虚拟存在的表。视图中的数据并不在数据库中实际存在,行和列数据来自定义视图的查询中使用的表,并且是在使用视图时动态生成的。
通俗的讲,视图只保存了查询的SQL逻辑,不保存查询结果。所以我们在创建视图的时候,主要的工作就落在创建这条SQL查询语句上
视图作用
视图不仅可以简化用户对数据的理解,也可以简化他们的操作。那些被经常使用的查询可以被定义为视图,从而使得用户不必为以后的操作每次指定全部的条件。
数据库可以授权,但不能授权到数据库特定行和特定的列上。通过视图用户只能查询和修改他们所能见到的数据
视图可帮助用户屏蔽真实表结构变化带来的影响。
视图语法
# 创建视图
CREATE [OR REPLACE] VIEW 视图名称 AS SELECT语句# 查看创建视图语句
SHOW CREATE VIEW 视图名称;#查看视图数据
SELECT * FROM 视图名称# 删除视图
DROP VIEW [IF EXISTS] 视图名称
MySQL数据库cpu飙升的话,要怎么处理呢?
排查过程:
1、 使用top 命令观察,确定是MySQLd导致还是其他原因。
2、 如果是MySQLd导致的,show processlist,查看session情况,确定是不是有消耗资源的sql在运行。
3、 找出消耗高的 sql,看看执行计划是否准确, 索引是否缺失,数据量是否太大。
处理:
1、 kill 掉这些线程(同时观察 cpu 使用率是否下降),
2、 进行相应的调整(比如说加索引、改 sql、改内存参数)
3、 重新跑这些 SQL。
其他情况:
也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等
select for update有什么含义,会锁表还是锁行还是其他。
select查询语句是不会加锁的,但是select for update除了有查询的作用外,还会加锁呢,而且它是悲观锁哦。至于加了是行锁还是表锁,这就要看是不是用了索引/主键啦。
没用索引/主键的话就是表锁,否则就是是行锁。
你们数据库是否支持emoji表情存储,如果不支持,如何操作?
更换字符集utf8–>utf8mb4
union all和union的区别
Union:对两个结果集进行并集操作,不包括重复行(去重),同时进行默认规则的排序;
Union All:对两个结果集进行并集操作,包括重复行,不进行排序;
javaweb
get请求和post请求的区别
1,GET在浏览器回退是无害的,而POST会再次提交请求
2,GET请求只能进行url编码,而post支持多种编码形式
3,get产生一个tcp数据包,post产生两个tcp数据包
4,get请求参数会被完整保留在浏览器历史记录里,而post中的参数不会被保留
5,get请求在url中传递的参数是有长度限制的不超过4k,而post没有
6,对参数的数据类型,get只接受ASCII类型,而post没有限制
7,get比post更不安全,因为参数直接暴露在url上,所以不能用传递敏感信息
8,get参数通过url传递,post放在request body报文体中
9,在进行文件上传时只能使用post而不能是get
转发和重定向
转发是服务器行为
转发浏览器只做了一次访问请求
转发浏览器的地址栏不发生变化
转发两次跳转之间的传输信息不会丢失,所以可以通过request进行数据的传递
转发只能将请求转发给同一个Web应用的组件重定向是客户端行为
重定向是浏览器至少做了两次的访问请求
重定向浏览器地址改变
重定向两次跳转之间传输的信息会丢失(不能使用request范围)
重定向可以指向任何的资源,包括当前应用程序中的其他资源,同一个站点上的其他应用程序中的资源,其他站点资源
Cookie和Session的区别
- 存储位置不同
- cookie的数据信息存放在客户端浏览器上。
- session的数据信息存放在服务器上。
- 存储容量大小不同
- 单个cookie保存的数据<=4KB,一个站点最多保存20个Cookie。
- 对于session来说并没有上限,但出于对服务器端的性能考虑,session内不要存放过多的东西,并且设置session删除机制
- 存储方式不同
- cookie中只能保管ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据。
- session中能够存储任何类型的数据,包括且不限于string,integer,list,map等。
- 隐私策略不同
- cookie对客户端是可见的,别有用心的人可以分析存放在本地的cookie并进行cookie欺骗,所以它是不安全的。
- session存储在服务器上,对客户端是透明对,不存在敏感信息泄漏的风险。
- 跨域支持上不同
- cookie支持跨域名访问。
- session不支持跨域名访问。
Servlet什么时候被创建?
1.默认情况下,第一次被访问时,Servlet被创建2.可以配置执行Servlet的创建时机。
在<servlet>标签下配置
(1)第一次被访问时,创建<load-on-startup>的值为负数或不写
(2)在服务器启动时,创建<load-on-startup>的值为正整数
下列有关Servlet的生命周期,说法不正确的是?(A)
A.在创建自己的Servlet时候,应该在初始化方法init()方法中创建Servlet实例
B.在Servlet生命周期的服务阶段,执行service()方法,根据用户请求的方法,执行相应的doGet()或是doPost()方法
C.在销毁阶段,执行destroy()方法后会释放Servlet 占用的资源
D. destroy()方法仅执行一次,即在服务器停止且卸载Servlet时执行该方法
Servlet的生命周期分为5个阶段:加载、创建、初始化、处理客户请求、卸载。
(1)加载:容器通过类加载器使用servlet类对应的文件加载servlet
(2)创建:通过调用servlet构造函数创建一个servlet对象
(3)初始化:调用init方法初始化
(4)处理客户请求:每当有一个客户请求,容器会创建一个线程来处理客户请求
(5)卸载:调用destroy方法让servlet自己释放其占用的资源
创建Servlet的实例是由Servlet容器来完成的,且创建Servlet实例是在初始化方法init()之前
spring
srping是什么?
spring是一个轻量级开源的容器框架,通常spring指的是spring franework,spring也是一个生态,可以构建java应用所需的一切基础设施
spring是一个ioc和aop的容器框架
ioc 控制反转
aop 面向切面
容器:包含并管理应用对象的生命周期
spring是为了解决企业级应用开发的业务逻辑层和其他层对象和对象直接耦合问题
通过控制反转IOC达到解耦的目的
包含管理对象的配置和生命周期,这个意义上是一个容器
将简单的组件配置,组合成为复杂的应用
spring优点
spring属于低侵入式设计,代码的污染极低;
spring的DI机制将对象之间的依赖关系交由框架处理,减低组件的耦合性;
Spring提供了AOP技术,支持将一些通用任务,如安全、事务、日志、权限等进行集中式管理,减少重复代码从而提供更好的复用。
spring对于主流的应用框架提供了集成支持
缺点是 简化开发,如果想要深入底层去了解就非常困难,(上层使用越简单,顶层封装的就越复杂)
源码缺点,由于spring大而全,代码非常庞大,一百多万对深入学习源码带了了一定困难
Spring 中的设计模式
什么是IOC、DI 及其两者的优点 、 有哪几种注入方式
IOC:控制反转,把创建对象的控制权利由代码转移到spring容器由工厂统一推送。并由Spring根据配置文件去创建实例和管理各个实例之间的依赖关系,以前创建对象的主动权和时机都是由自己把控的,IOC让对象的创建不用去new了,可以由spring工厂自动生产,使用java的反射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法的。
通俗讲,UserService service=new UserService();//耦合度太高,维护不方便
引入ioc,就将创建对象的控制权交给spring容器,以前由程序员自己控制对象的创建,现在交给spring的ioc去创建,在运行时动态的去创建对象以及管理对象,要使用对象只需要通过依赖注入即可
,
DI:依赖注入,在程序运行期间,由外部容器动态地将依赖对象注入到组件中。简单定义就是当一个对象需要另一个对象时,可以把另一个对象注入到对象中去。依赖注入是实现IOC的方法,就是由IOC容器在运行期间,动态的将某种依赖关系注入到对象中
优点就是把应用的代码量降到最低,达到松散耦合度。集中管理对象,方便维护
注入的方式:
构造注入
Set注入
自动注入
ioc的实现机制是什么
工厂模式+反射
AOP的理解
aop将程序中的交叉业务逻辑(比如安全,日志,事务等)封装成一个切面,然后注入到目标对象(具体的业务逻辑)中去,aop可以对某个对象或某些对象的功能进行增强,比如对象中的某些方法进行增强,可以在某些方法之前额外做一些事情,在某个方法执行之后额外的做一些事情
Spring 中的设计模式
- 单例模式——spring 中两种代理方式,
单例模式——在 spring 的配置文件中设置 bean 默认为单例模式 - 模板方式模式——用来解决代码重复的问题
比如:RestTemplate、JdbcTemplate - 工厂模式——在工厂模式中
我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用同一个接口来指向新创建的对象。Spring 中使用 beanFactory 来创建对象的实例 - 代理模式
aop的实现,若目标对象实现了若干接口,spring 使用 jdk 的 java.lang.reflect.Proxy类代理。若目标兑现没有实现任何接口,spring 使用 CGLIB 库生成目标类的子类 - 观察者模式
spring的事件驱动模型使用的是 观察者模式 ,Spring中Observer模式常用的地方是listener的实现,监听 - 包装器设计模式
我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源 - 适配器模式
Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller
SpringMVC
SpringMVC 的工作原理
- 1.用户向服务器发送请求,请求被 springMVC 前端控制器 DispatchServlet 捕获
- 2.DispatcherServle 对请求 URL 进行解析,得到请求资源标识符(URL),去查找处理器(Handler)
- 3.然后根据该 URL 调用 HandlerMapping将请求映射到处理器 HandlerExcutionChain
- 4.DispatchServlet 根据获得 Handler 选择一个合适的 HandlerAdapter 适配器处理
- 5.处理器适配器去执行Handler
- 6… Handler 对数据处理完成以后将返回一个 ModelAndView()对象给 适配器
- 7.处理器适配器向前端控制器DisPatchServlet;返回ModelAndView
- 8.Handler 返回的 ModelAndView()只是一个逻辑视图并不是一个正式的视图,DispatcherSevlet 通过ViewResolver 试图解析器将逻辑视图转化为真正的视图 View;
- 9.DispatcherServlet 通过 model 解析出 ModelAndView()中的参数进行解析最终展现出完整的 view 并返回给客
- 10.前端控制器对视图进行渲染
- 11.前端控制器向用户响应结果
SpringMVC 常用注解都有哪些?
@RequestMapping
用于映射请求地址,类上跟方法上都可以使用此注解,当同时使用的时候,url匹配需要同时组合匹配。
@RequestParam
用于将指定的请求参数赋值给方法中的形参,当提交的请求参数中的key跟方法中的形参命名一致的话,则不需要使用此注解,会自动将请求中的参数封装到形参中。如果2者命名不一致的话,则需要通过RequestParam来表示映射关系
@ModelAttribute
最主要的作用是将数据添加到模型对象中,用于视图页面展示时使用
@SessionAttributes
可以将Model中的属性同步到session当中
RequestBody
用于接收请求体中的json串(如ajax请求的data参数)可在直接接收并封装到bean中。
ResponseBody
注解实现将 controller 方法返回对象转化为 json 响应给客户
@PathVariable
接收请求路径中占位符的值,实现rest风格的路径收参
如何开启注解处理器和适配器
我们在项目中一般会在 springmvc.xml 中通过开启 mvc:annotation-driven来实现注解处理器和适配器的开启
Spring Boot
一个快速构建spring工程的脚手架
Spring Boot有哪些优点
1. 减少开发时间,通过提供默认值快速开发
2. 使用javaConfig有利于避免使用XML,无代码生成,开箱即用
3. 避免大量的Maven导入和各种版本冲突
4. 不需要单独的服务器,SpringBOOT工程在部署时,采用的是jar包的方式,内部自动依赖Tomcat容器,提供了多环境配置
5. SpringBOOT中默认整合第三方框架时,只需要导入响应的starter依赖包,就会自动整合了
6. 微服务框架SpringClod需要建立在SpringBOOT基础上
SpringBoot中如何解决跨域问题
- 方法或者类上标注@CrossOrigin注解
- 添加 CORS 过滤器
@Configuration
public class CorsConfig {@Beanpublic CorsFilter corsFilter(){CorsConfiguration corsConfiguration = new CorsConfiguration();corsConfiguration.addAllowedOrigin("*");corsConfiguration.addAllowedHeader("*");corsConfiguration.addAllowedMethod("*");UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", corsConfiguration);return new CorsFilter(source);}}
- 实现 WebMvcConfigurer 接口,重写 addCorsMappings 方法
@Configuration
public class CorsConfiguration implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOriginPatterns("*").allowedMethods("GET","POST","PUT","DELETE","HEAD","OPTIONS").allowCredentials(true).maxAge(3600).allowedHeaders("*");}
}
Spring Cloud
什么是Spring Cloud
Spring Cloud是一系列框架的集合,它利用Spring Boot的开发便利性简化了分布式基础设施开发,如服务发现,服务注册,配置中心,负载均衡,服务熔断降级,消息总线,网关,是一套简单易懂,易部署和维护的分布式开发工具包
Spring Boot和Spring Cloud的关系是什么
Spring Boot是spring的一套快速配置脚手架,可以基于Spring Boot快速开发单个微服务,Spring Cloud是一个基于Spring Boot实现的开发工具,Spring Boot专注于快速方便继承单个微服务个体,Spring Cloud关注于全局服务的治理,Spring Cloud离不开Spring Boot
单体应用
- 优点
- 单体架构模式在项目初期很小的时候开发方便,测试方便,部署方便,运行良好
-
缺点
1.代码维护和扩展困难,耦合度高
2. 有可能因为某一个功能导致整个系统出问题
什么是微服务
微服务是一种架构,根据业务功能将项目才分为独立的服务进行开发。每个服务运行在自己的进程内,也就是可独立部署和升级,职责单一,各个微服务之间的关联通过暴露api来实现,甚至不同服务可以使用不同的语言去实现。
dubbo和springcloud
- Dubbo是SOA时代的产物,主要关注于服务的远程调用,请求负载分发,流量监控和熔断,定位于服务治理
- SpringCloud 诞生于微服务架构时代,考虑的是微服务治理,建立在SpringBoot优势之上,是一个技术生态
- Dubbo底层使用Netty这样的NIO框架,基于TCP协议传输,配合Hession系列化完成RPC通信
- SpringCloud 基于http协议+Rest接口调用方式通信,相对而言,http请求会有更大的报文,占用带宽也会更多,但rest相比于rpc更加灵活,服务提供方和调用方不存在代码级别的强依赖
微服务一定要用SpringCloud 吗
- 微服务只是一种项目的架构放放水和理念,任何技术都可以实现这种架构理念
- 微服务架构里面有很多问题需要解决比如:负载均衡,服务的注册与发现,服务的远程调用,服务熔断降级
- 如果我们自己从零开始实现微服务架构理念,那是完全没有必要的,SpringCloud 帮我们做了这些事情,SpringCloud 将解决这些问题的技术全部打包好了我们只需要开箱即用
分布式和微服务是什么?二者的区别又是什么?
微服务的特征:
- 单一职责的。一个微服务应该都是单一职责的,这才是“微”的体现,一个微服务解决一个业务问题(注意是一个业务问题而不是一个接口)。
- 面向服务的。将自己的业务能力封装并对外提供服务,这是继承SOA的核心思想,一个微服务本身也可能使用到其它微服务的能力。
简单来说微服务就是很小的服务,小到一个服务只对应一个单一的功能,只做一件事。这个服务可以单独部署运行,服务之间可以通过RPC来相互交互
分布式是否属于微服务?
答案是属于。微服务的意思也就是将模块拆分成一个个独立的服务单元通过接口来实现数据的交互,多个服务独立运行,每个服务占用独立进程。但是微服务不一定是分布式,因为微服务的应用不一定是分散在多个服务器上,他也可以是同一个服务器。这也是分布式和微服务的一个细微差别。
服务注册和服务发现是什么意思,springcloud是如何实现的
服务注册是让所有微服务将微服务的名称、IP、端口注册到注册中心,服务发现是让微服务可以拉取注册中心里的服务列表,方便结合feign进行远程调用,注册中心可以说是微服务架构中的通讯录,它记录了服务和服务地址的映射关系,在分布式架构中,服务会注册到这里,当服务需要调用其他服务时,就到这里找到服务地址进行调用
springcloud支持的多种注册中心Eureka、Consul、Zookeeper、以及阿里巴巴推出Nacos。这些注册中心在本质上都是用来管理服务的注册和发现以及服务状态的检查的。
这一过程是springcloud自动实现 只需要在main方法添加@EnableDiscoveryClient 同一个服务修改端口就可以启动多个实例
调用方法:传递服务名称通过注册中心获取所有的可用实例 通过负载均衡策略调用(ribbon和feign)对应的服务
ribbon和nginx负载均衡的区别
Nginx服务器端负载均衡:
nginx是客户端所有请求统一交给nginx,由nginx进行实现负载均衡请求转发,属于服务器端负载均衡。
既请求有nginx服务器端进行转发。
Ribbon客户端负载均衡:
Ribbon是从注册中心服务器端上获取服务注册信息列表,缓存到本地,让后在本地实现轮训负载均衡策略。即在客户端实现负载均衡。
redis
redis 中的数据类型有哪些
Redis 有 5 种基础数据结构,分别为:string (字符串)、list (列表)、set (集合)、hash (哈希) 和 zset (有序集合)。
Redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n), 当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。
Redis 的哈希相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同 Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。
Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值NULL。 当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。
zset 类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。
redis的应用场景
为什么说redis能够快速执行
- redis是基于内存的,所以读写效率非常高
- 采用单线程,避免了不必要的上下文切换和竞争条件
- 非阻塞IO - IO多路复用
redis和mamcached的区别
- 两者都是基于内存存储的,不同的是redis可以进行持久化操作,memcached掉电后会丢失
- redis支持多数据类型,memcache存放字符串
- Redis是单线程的;而,Memcached是多线程的
redis一个字符串类型的值能存储最大容量是多少?
512M
redis大key是怎么回事?
- string类型控制在10KB以内
- hash,list,set,zset元素个数不要超过5000
string类型可以使用命令 --bigkeys
redis-cli -h127.0.0.1 -p6379 --bigkeys
Rdbtools工具(python开发的第三方开源工具)
rdb dump.rdb -c memory --bytes 10240 -f redis.csv
主动删除大key
UNLINK key名称
被动删除大key
在配置文件中配置
lazyfree-lazy-expire on #过期惰性删除
lazyfree-lazy-eviction #超过最大内存惰性删除
lazyfree-lazy-server-del on #服务端被动惰性删除
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决办法:
- 给不同的Key的TTL添加随机值,避免大量key集体失效
- 利用Redis集群提高服务的可用性,将热点数据分布在不同的redis库中
- 跑定时任务在缓存失效前刷新缓存
- 给业务添加多级缓存
缓存穿透
用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力
解决办法:
- 对不存在的数据缓存null值,并设置一个较短过期时间
- 使用布隆过滤器进行拦截,使用一个足够大的bitmap,用于存储可能访问的key,不存在的key直接被过滤
- 对参数进行校验,不合法参数进行拦截
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击
解决办法:
- 使用互斥锁
- 设置逻辑过期
Linux
统计一个文件中"java"出现的行数?
// grep 要查找的单词 文件名 | wc -l
grep "java" abc.txt | wc -l
wc -l # 统计行数
wc -w # 统计单词数量
排查线上问题的命令
top 查看整个系统资源使用情况
free -m 查看内存使用情况
iostat 查看磁盘读写情况
netstat 查看网络连接情况
df -h 查看磁盘的使用情况
du -sh 查看文件大小情况
内存磁盘相关命令
du(disk usage) 命令功能说明:统计目录(或文件)所占磁盘空间的大小
df(disk file) 命令功能说明: 用于显示文件系统的磁盘使用情况
free 命令功能说明: 可以显示当前系统未使用的和已使用的内存数目,还可以显示被内核使用的内存缓冲区
vmstat 命令功能说明: 命令报告关于内核线程、虚拟内存、磁盘、陷阱和 CPU 活动的统计信息
vim编辑器可以分为三种模式
1.命令模式:控制屏幕光标的移动,进行文本的删除,复制等文字编辑工作,不使用【del】和【backspace】键,以及进入插入模式或者回到底行模式;
2.插入模式:只有在插入模式下,才可以输入文字,按【esc】可以回到命令模式,vim编辑器一打开是不可以输入的,因为刚打开时候处于命令模式;
3.底行模式:保存文件或者退出vim,也可以设置编辑环境和一些编译工作
在使用VI编辑器的时候,查找内容的方法有两种:
1、“/”:这个查找是自上而下
2、“?”:这个查找是自下而上
针对“/”:使用“n”查找下一个,使用“N”查找上一个
实现文件去重并排序:
sort demo.txt|uniq
压缩相关命令
tar是操作.tar的命令
gzip是压缩.gz压缩包的命令
compress:压缩.Z文件
uncompress:解压缩.Z文件