文章目录
- 前言
- 一、新国都集团一面
- 1.java基本数据类型,char所占字节?
- 2.如果一个类中,声明一个私有属性,且没有构造方法,getter() 与 setter(),请问不在一个包下的类如何访问到此属性? 反射(reflection)
- 3.spring IOC,AOP(这个面试已经被问多次)
- 4.AOP的具体的实现,实现原理,访问顺序
- 5.使用IOC的优点?
- 6.tcp,udp的区别?为什么tcp的效率比udp低?
- 7.tcp几次握手,几次挥手?那只握两次会出现什么问题?
- 8.多线程线程一般是怎么创建的? (这个没问是怎么实现多线程,不是那3种!)
- 9.创建线程有个创建线程池方法,需要显式的插入两个参数,核心线程数与最大线程数,请问当前线程数与这两者的值有什么关联?
- 10.为什么不直接创建线程?
- 11.手写代码:手写任一类单例模式?
- 12.手写代码:linux命令,如何查看占用8080端口的线程信息?
- 13.什么时候进行数据库的水平分表,什么时候进行数据库的垂直分表?
- 14.简单说一下怎么让栈、堆内存溢出?
- 15.在不使用除号的情况下,除以8,怎么算?
- 16.tcp/udp之间有什么区别?他们属于ISO哪一层?HTTP协议属于哪一层?
- 17.get()和post()的区别(当时回答的是表象,可能面试官不太满意)
- 参考链接
- 二、苏小研一面
- 1.Integer a =100;Integer b=new Integer(100) 请问a==b返回?a.equals(b)又返回什么?Integer.valueof(a)==Integer.valueof(b)呢?
- 2.JVM基本结构以及内存模型
- 3.HashMap底层结构以及它在jdk1.7以及jdk1.8时刻的不同有哪些?
- 4.HashMap是线程安全的吗?线程安全的还有哪些?
- 5.HotSpotJVM,JIT,逃逸分析以及JVM的其他优化有了解过吗?
- 6.gc垃圾回收机制是怎样进行的?
- 7.几种常见的垃圾回收机制算法以及含义?
- 8.说明关键字final,finally,finalize有什么不同,调用System.gc()会发生什么?
- 9.说明多线程的sleep(),以及wait()有什么不同?
- 10.说说springboot的自动装配?
- 11.说说finalize()是在什么时候会被调用?如果我人为的调用类中的finalize(),会启动gc吗?
- 12.异常有哪些,列举几个常见的异常?
- 13.ConcurrentHashMap是怎么实现线程安全的?
- 14.说说同步与异步的区别?
- 15.Java源文件是如何执行的?
- 16.对象必定是存在于jvm堆上的吗?不一定
- 参考链接
- 三、苏小研二面
- 四、SNK笔试
- 五、建信金融笔试
前言
整理与记录JAVA通用且比较重要的笔面试题,以作将来学习参考。
一、新国都集团一面
1.java基本数据类型,char所占字节?
byte short int long
1 2 4 8
float double char boolean
4 8 2 1
2.如果一个类中,声明一个私有属性,且没有构造方法,getter() 与 setter(),请问不在一个包下的类如何访问到此属性? 反射(reflection)
反射(reflection):在运行状态中,对于java中的任意一个类都能够知道这个类的所有属性与方法,对于java中的任意一个对象,都能够调用它的任意方法和属性,这种动态获取信息以及动态调用对象方法的功能被称为java的反射机制。
-
获取Class类的对象:forName()、类名.Class、对象.getClass()
-
源代码阶段[forName()]–>Class对象阶段(内存)[类名.Class]–>Runtime阶段[对象.getClass()]
-
.java文件–编译–>Class文件–类加载器–>Class对象–创建对象–>new JavaBean()
-
详细请看 Java高级特性——反射
Demo验证:在单例模式包下TestThread.java访问HurrySington下的private属性num,输出打印num的值
HurrySington.class(单例类)
public class HurrySington {
//饿汉式单例:线程安全
private HurrySington(){
}
//类加载的时候直接初始化,在类加载时略微有些慢,获取对象时快。
private static HurrySington instance=new HurrySington();
private int num=101;
public static HurrySington getInstance(){
return instance;
}
}
java语句
//一般使用这个方法获取类名,但非静态属性不能实现
//String name = HurrySington.class.getName();
//非单例或者有公开构造方法,直接创建对象
//HurrySington name = new HurrySington();
//获取对象唯一实例
HurrySington name = HurrySington.getInstance();
//获取public或者非public字段
Field hurrySington = HurrySington.class.getDeclaredField("num");
hurrySington.setAccessible(true);
System.out.println("num="+hurrySington.get(name));
输出
num=101
3.spring IOC,AOP(这个面试已经被问多次)
一、IOC(Incersion of Control):控制反转,控制指对象对于内部成员的控制权,反转指的是将控制转给第三方容器或类去操作。分为两类:
①依赖注入(Dependency Injection,简称DI)
调用类对被调用类的依赖关系由第三方注入,以移除调用类对被调用类的引用。
②依赖查找(Dependency Lookup)
二、AOP(Aspect Oriented Programming):面向切面编程,AOP是能够让我们在不影响原有功能的前提下,为软件横向扩展功能。
在WEB项目开发中,通常都遵守三层原则:
包括控制层(Controller)->业务层(Service)->数据层(dao),
这个结构为纵向,那么它具体的某一层就是横向,
AOP就是作用于这某一个横向模块当中的所有方法。
AOP与OOP区别:
AOP是OOP的补充,当我们需要为多个对象引入一个公共行为,
比如日志,操作记录等,就需要在每个对象中引用公共行为,
这样程序就产生了大量的重复代码,使用AOP可以完美解决这个问题。
4.AOP的具体的实现,实现原理,访问顺序
- 切面:@aspect拦截器类,其中会定义切点以及通知 切点:具体拦截的某个业务点。
- 通知:切面当中的方法,声明通知方法在目标业务层的执行位置,通知类型如下:
- 前置通知:@Before 在目标业务方法执行之前执行
- 后置通知:@After 在目标业务方法执行之后执行 返回通知:@AfterReturning 在目标业务方法返回结果之后执行
- 异常通知:@AfterThrowing 在目标业务方法抛出异常之后
-
- 环绕通知:@Around 功能强大,可代替以上四种通知,还可以控制目标业务方法是否执行以及何时执行
项目 | 正常执行 | 异常执行 |
---|---|---|
spring 4(springboot 1中)访问顺序 | 环绕通知前->@before->环绕通知后->@after->@afterReturning | 环绕通知前->@before->@after->@afterThrowing |
spring 5(springboot 2中)访问顺序 | 环绕通知前->@before->@afterReturning->@after->环绕通知后 | 环绕通知前->@before->afterThrowing->after |
5.使用IOC的优点?
实现组件之间的解耦,提高程序的灵活性和可维护性。
6.tcp,udp的区别?为什么tcp的效率比udp低?
1,tcp面向字节流,udp面向报文
2,tcp面向连接,udp面向非连接(不需要事先连接)
3,tcp数据传输更加安全,udp数据传输比较不安全
tcp需要进行连接,udp不需要
7.tcp几次握手,几次挥手?那只握两次会出现什么问题?
三次握手,四次挥手。只握两次,在数据传输时,服务器端会等待客户端响应,这段时期会浪费服务器资源。
8.多线程线程一般是怎么创建的? (这个没问是怎么实现多线程,不是那3种!)
创建线程池。
9.创建线程有个创建线程池方法,需要显式的插入两个参数,核心线程数与最大线程数,请问当前线程数与这两者的值有什么关联?
第一步判断当前线程是否为空,为空则抛出空指针
第二步判断当前线程数是否小于核心线程数corePoolSize,当线程总数小于corePoolSize时会将任务通过addWorker()直接调度。
第三步会在workQueue.offer()处进入等待队列。如果进入等待队列失败(如有界队列达到上限,或者使用了SynchronousQueue)则会执行下一个的addWorker()将任务直接提交给线程池。
第四步如果当前线程数已经达到了maximumPoolSize则提交失败执行拒绝策略reject()。
10.为什么不直接创建线程?
线程的不断创建与销毁会极大损耗CPU资源,而线程池会提前创建好线程,等到需要的时候只需要调用线程池的线程,用完不销毁,放入线程池中,从而大大降低cpu资源损耗。
11.手写代码:手写任一类单例模式?
饿汉式:线程安全,类加载时直接初始化实例。
package 单例模式.sington;
//1.私有构造方法,防止外部通过构造方法直接获取实例
//2.声明一个指向自己的私有静态引用
//3.创建一个公开的静态获取唯一实例的方法
public class HurrySington {
//饿汉式单例:线程安全
private HurrySington(){
}
//类加载的时候直接初始化,在类加载时略微有些慢,获取对象时快。
private static HurrySington instance=new HurrySington();
public static HurrySington getInstance(){
return instance;
}
}
懒汉式:线程不安全,类加载时不初始化
public class LazySington {
//懒汉式,线程不安全,时间换空间
private LazySington(){
}
//类加载时不初始化,获取对象时才初始化,类加载快,获取对象时较慢
private static LazySington instance=null;
public static LazySington getInstance(){
if(instance==null){
LazySington instance = new LazySington();
}
return instance;
}
}
- 多线程下,但效率不高:为了保证在多线程环境下我们还是只能得到该类的一个实例,只需要在getInstanceB()方法加上同步关键字sychronized,就可以了。但每次调用getInstanceB()方法时都被synchronized关键字锁住了,会引起线程阻塞,影响程序的性能。
public static synchronized Singleton1 getInstance1() {
if (instance == null) {
instance = new Singleton1();
}
return instance;
}
- 双重检验锁:为了在多线程环境下,不影响程序的性能,不让线程每次调用getInstance2()方法时都加锁,而只是在实例未被创建时再加锁,在加锁处理里面还需要判断一次实例是否已存在,且将其指向自己的私有静态引用声明为volatile。
public static Singleton1 getInstance2() {
// 先判断实例是否存在,若不存在再对类对象进行加锁处理
if (instance == null) {
synchronized (Singleton1.class) {
if (instance == null) {
instance = new Singleton1();
}
}
}
return instance;
}
12.手写代码:linux命令,如何查看占用8080端口的线程信息?
详细查看: 查看Linux端口占用,并kill掉相关进程
13.什么时候进行数据库的水平分表,什么时候进行数据库的垂直分表?
水平分表:在mysql表中数据量太大,通过对id取余后分别存入另外的几张表中,表结构一致,数据内容拼接起来就是原先完整的数据表。
如:需要将table按id分别分到5个表中, id%5 ,0,1,2,3,4分别对应一个表,表结构完全一致,各个表中数据内容拼接起来就是原先的table.
垂直分表:在mysql表中对于一些大的字段我们不需要在一次查询中获取,“大表拆成小表”,我们可以将这些字段分别存放在不同的表中,需要哪些字段,分别在各自表中查询,表中结构不一致。
如:查询个人重要信息比如电话,邮箱,学历等时,我们往往不会太过关注地址,那么我们就可以将主键与地址存入另外一张表中,需要时根据主键查找对应地址。
14.简单说一下怎么让栈、堆内存溢出?
- 栈溢出(StackOverflowError)
栈是线程私有的,生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用类型(局部变量表所需的内存空间在编译期间完成分配。每个方法都对应一个栈帧。)
栈溢出就是方法执行是创建的栈帧超过了栈的深度。那么最有可能的就是方法递归调用产生这种结果。
public class Test {
private static int i=0;
public static void a(){
System.out.println(i++);
a();
}
public static void main(String[] args) {
int a=8;
System.out.println(a<<3);//64
System.out.println(a>>3);//1
Test test = new Test();
test.a();
}
}
}
异常信息:
Exception in thread "main" java.lang.StackOverflowError
我们需要使用参数 -Xss 去调整JVM栈的大小
- 堆溢出(OutOfMemoryError:java heap space)
heap space表示堆空间,堆中主要存储的是对象。如果不断的new对象则会导致堆中的空间溢出
public class Test {
private static int i = 0;
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
try {
while (true) {
list.add(i++);
list.add("TEST");
}
} catch (Throwable e) {
System.out.println(list.size());
e.printStackTrace();
}
}
}
异常信息:
java.lang.OutOfMemoryError: Java heap space
可以通过 -Xmx4096M 调整堆的总大小为4M
15.在不使用除号的情况下,除以8,怎么算?
移位操作,操作系统可以直接识别的操作,相比于/,//快
8>>3
int a=8;
System.out.println(a<<3);//64
System.out.println(a>>3);//1
16.tcp/udp之间有什么区别?他们属于ISO哪一层?HTTP协议属于哪一层?
(1)TCP是可靠传输,UDP是不可靠传输;
(2)TCP面向连接,UDP无连接;
(3)TCP传输数据有序,UDP不保证数据的有序性;
(4)TCP不保存数据边界,UDP保留数据边界;
(5)TCP传输速度相对UDP较慢;
(6)TCP有流量控制和拥塞控制,UDP没有;
(7)TCP是重量级协议,UDP是轻量级协议;
(8)TCP首部较长20字节,UDP首部较短8字节;
TCP/UDP属于传输层,HTTP属于应用层.
17.get()和post()的区别(当时回答的是表象,可能面试官不太满意)
GET和POST是什么?HTTP协议中的两种发送请求的方法。
HTTP是什么?HTTP是基于TCP/IP的关于数据如何在万维网中如何通信的协议。
HTTP的底层是TCP/IP,而GET和POST是HTTP的两个方法,所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。
GET和POST能做的事情是一样一样的。你要给GET加上request body,给POST带上url参数,技术上是完全行的通的。
区别:
-
表象:······
-
本质:GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。
参考链接
Java线程池 ThreadPoolExecutor(一)线程池的核心方法以及原理
二、苏小研一面
1.Integer a =100;Integer b=new Integer(100) 请问a==b返回?a.equals(b)又返回什么?Integer.valueof(a)==Integer.valueof(b)呢?
①==判断 不相等
解:不相等,
对==来说:int与int,int与Integer,只要值相等就为true,且new integer()与integer 不相等;
Integer与Integer,只有他们同时引用[-128,128)内的对象时才为true,其他情况均为false。
对equals()方法来说:要满足类型与内容同时相等才为true。
public static void main(String[] args) {
Integer i = new Integer(127);Integer j = new Integer(127);
System.out.println(j==i);//false重新new对象,必定不相等
Integer c=127;Integer d=127;
System.out.println(c==i);//false
System.out.println(c==d);//true使用Integer定义变量相等
int p=127;
System.out.println(p==i);//true
j=new Integer(128);int q=128;
System.out.println(q==j);//true
i=new Integer(128);
System.out.println(i==j);//false
c=128;d=128;
System.out.println(c==d);//false使用Integer定义变量不相等
c=-128;d=-128;
System.out.println(c==d);//true使用Integer定义变量相等,符合[-128,127]写入缓存
}
小总结(==)
int 与 new Integer ==会自动发生拆箱操作,值比较,true;
new Integer之间时无论值是否相等判断==,false;
Integer 与new Integer之间比较的是引用,无论值相不相等引用不同,false;
Integer 与 Integer之间在[-128,127]范围内会创建一个缓存比较时直接读取缓存中的值,
即在此范围内的两个值相等的对象相等,true
②.valueOf() 相等
但是我们需要注意 Integer.value 方法
public static void main(String[] args) {
// test1();
// test2();
System.out.println(Integer.valueOf("1000")==Integer.valueOf("1000"));//false
System.out.println(Integer.valueOf("128")==Integer.valueOf("128"));//false
System.out.println(Integer.valueOf("127")==Integer.valueOf("127"));//ture
System.out.println(Integer.valueOf("-128")==Integer.valueOf("-128"));//true
System.out.println(Integer.valueOf("-1000")==Integer.valueOf("-1000"));//false
System.out.println((Integer)128==(Integer)128);//false
System.out.println((Integer)127==(Integer)127);//true
System.out.println((Integer)(-128)==(Integer)(-128));//true
System.out.println((Integer)(-129)==(Integer)(-129));//false
}
源码
public static Integer valueOf(String s) throws NumberFormatException {
return Integer.valueOf(parseInt(s, 10));
}
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
小总结(valueOf):也就是说,在判断==时,只有Integer定义的变量与Integer.value() 返回的值会从IntegerCache中查找,即只要碰见这两种类型之一且在缓存[-128,127]上,则true
③equals判断
private static void test2() {
Integer i = new Integer(0);
Integer j = new Integer(0);
System.out.println(j.equals(i));//true
i=new Integer(128);
j=new Integer(128);
System.out.println(i.equals(j));//true
i=new Integer(127);
j=new Integer(127);
System.out.println(i.equals(j));//true
i=new Integer(-128);
j=new Integer(-128);
System.out.println(i.equals(j));//true
i=new Integer(-129);
j=new Integer(-129);
System.out.println(i.equals(j));//true
Integer c=127;i=new Integer(127);
Integer d=127;
System.out.println(c.equals(i));//true
System.out.println(c.equals(d));//true使用Integer定义变量相等
c=128;
d=128;
System.out.println(c.equals(d));//true
c=-128;
d=-128;
System.out.println(c.equals(d));//true
}
小总结(equals):String,Integer等重写了equals方法,使得它们同类型使用equals时只会比较他们的值是否相等
源码:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
public int intValue() {
return value;
}
ps: 也就是说,在integer或者String进行.equals()时,我们只需要比较它们的值是否相等
2.JVM基本结构以及内存模型
解:jvm
1.JVM 整体组成可分为以下四个部分:
类加载器(ClassLoader)
运行时数据区(Runtime Data Area)
执行引擎(Execution Engine)
本地库接口(Native Interface)
2.运行时数据区
程序计数器(Program Counter Register)
Java虚拟机栈(Java Virtual Machine Stacks)
本地方法栈(Native Method Stack)
Java堆(Java Heap)
方法区(Methed Area)
常见用法请看:JVM的组成
程序在执行之前先要把java代码转换成字节码(class文件)
jvm首先需要把字节码通过一定的方式
类加载器(ClassLoader) 把文件加载到内存中运行时数据区(Runtime Data Area)
而字节码文件是jvm的一套指令集规范,并不能直接交给底层操作系统去执行
因此需要特定的命令解析器 执行引擎(Execution Engine)
将字节码翻译成底层系统指令再交由CPU去执行
而这个过程中需要调用其他语言的接口 本地库接口(Native Interface)
来实现整个程序的功能。
虚拟机栈:主要存储基本数据类型变量与数组类型引用
堆:主要存储对象以及数组的元素
备注:
JDK>JRE>JVM
1.JDK中包含JRE,在JDK的安装目录下有一个名为jre的目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre。
JDK是整个JAVA的核心,包括了Java运行环境JRE(Java Runtime Envirnment)、一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API 包括rt.jar)。
2.JRE是运行基于Java语言编写的程序所不可缺少的运行环境。也是通过它,Java的开发者才得以将自己开发的程序发布到用户手中,让用户使用。
JRE中包含了Java virtual machine(JVM),runtime class libraries和Java application launcher,这些是运行Java程序的必要组件。
3.JVM(java virtual machine)就是我们常说的java虚拟机,它是整个java实现跨平台的最核心的部分,所有的java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行。也就是说class并不直接与机器的操作系统相对应,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。
只有JVM还不能成class的执行,因为在解释class的时候JVM需要调用解释所需要的类库lib,而jre包含lib类库。
3.HashMap底层结构以及它在jdk1.7以及jdk1.8时刻的不同有哪些?
1.不同点:
(1)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
(2)扩容后数据存储位置的计算方式也不一样:1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)
2.而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。在计算hash值的时候,JDK1.7用了9次扰动处理=4次位运算+5次异或,而JDK1.8只用了2次扰动处理=1次位运算+1次异或。
(3)JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率)
4.HashMap是线程安全的吗?线程安全的还有哪些?
不安全,ConcurrentHashMap,vector,hashtable,
5.HotSpotJVM,JIT,逃逸分析以及JVM的其他优化有了解过吗?
①hotSpot:java的一种虚拟机,主要包括一个解释器与两个编译器(client或server,二选一),默认其参数为解释编译混合型模式
C:\Users\Administrator>java -version
java version "12.0.2" 2019-07-16
Java(TM) SE Runtime Environment (build 12.0.2+10)
Java HotSpot(TM) 64-Bit Server VM (build 12.0.2+10, mixed mode, sharing)
②二八定律:百分之二十的代码占据了百分之八十的计算资源。
③JIT:just in time Compiler,即时编译器,hotSpot虚拟机通过JIT将常用的代码编译成本地机器码,提升代码执行效率。
JIT工作原理
当JIT编译启用时(默认是启用的) :javac将.java编译为.class,,JVM读入.class文件解释后,将其发给JIT编译器。JIT编译器将常用的字节码编译成本机机器代码。
当JIT编译关闭时:javac将.java编译为.class,JVM通过解释字节码将其翻译成相应的机器指令,逐条读入,逐条解释翻译。
非常显然,经过解释运行,其运行速度必定会比可运行的二进制字节码程序慢。为了提高运行速度,引入了JIT技术。
在执行时JIT会把翻译过的机器码保存起来,已备下次使用,因此从理论上来说,采用该JIT技术能够,能够接近曾经的纯编译技术。
④jvm优化技术:公共子表达式消除、数组索引范围检查消除、方法内联、逃逸分析等
- 与语言无关的优化:公共子表达式消除
- 与语言有关的优化:数组索引范围检查消除,隐式异常处理,自动装箱消除、安全点消除、消除反射等
- 最重要的优化技术:方法内联
- 最前沿的优化技术:逃逸分析
逃逸分析(Escape Analysis)
- 本质:算法 (一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法)
- 基本行为:分析对象的动态作用域。
- 含义:假如我们在一个方法内定义了一个对象,如果它被作为参数传递到其他地方,被本方法外的方法引用,这就就叫做方法逃逸。
有了逃逸分析,我们就可以判断出一个方法中的变量是否有可能被其他线程所访问或改变,JIT就可以据此进行一系列的优化,如标量替换、同步消除、栈上分配。
如果我们经过逃逸分析发现,某个对象并没有发生方法逃逸,那么它的生命周期则始于方法调用,卒于方法结束,那么此时它就是方法内的局部变量,局部变量我们采用栈上分配。
而堆内存是线程间共享的,如果将它分配到堆中,方法结束后,它将不在被任何对象所引用,还需要GC进行回收,很不划算,于是 JIT就会将其分配到方法的栈帧中,这就是栈上分配。
线程同步本身就是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么我们就会同步消除。
如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施就可以消除掉。
实际上在HotSpot中,栈上分配并不是直接在方法的栈帧中放入一个对象,它是通过标量替换的方式存储的,
标量替换:即将对象分解成组成对象的若干个成员变量,这些变量是无法再分解的更小的数据,叫做标量,然后用这些标量来代替之前的对象
通过标量替换,原本的一个对象,被替换成多个成员变量存储在栈中。而原本需要在堆上分配的内存,也就不再需要了,完全可以在本地方法栈中完成对成员变量的内存分配。
6.gc垃圾回收机制是怎样进行的?
Java中标记垃圾的算法主要有两种, 引用计数法和可达性分析算法。
①引用计数法
引用计数法就是给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的,可以当做垃圾收集。这种方法实现起来很简单而且优缺点都很明显。
优点 执行效率高,程序执行受影响较小
缺点 无法检测出循环引用的情况,导致内存泄露
②可达性分析算法
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
那么什么对象可以作为GCRoot?
-
虚拟机栈中的引用对象
-
方法区中的常量引用对象
-
方法区中的类静态属性引用对象
-
本地方法栈中的引用对象
-
活跃线程中的引用对象
那么不可达的对象是否是必死之局呢?答案也是否定的
重要!!!
在可达性分析法中不可达的对象,它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。 被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
7.几种常见的垃圾回收机制算法以及含义?
①标记-清除算法
阶段1: Mark-Sweep 标记清除阶段,先假设heap中所有对象都可以回收,然后找出不能回收的对象,给这些对象打上标记,最后heap中没有打标记的对象都是可以被回收的;
阶段2: Compact 压缩阶段,对象回收之后heap内存空间变得不连续,在heap中移动这些对象,使他们重新从heap基地址开始连续排列(节省内存资源)。
-
效率问题
-
空间问题(标记清除后会产生大量不连续的碎片)
②复制算法
新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。每次回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden区和Survivor区的比例为8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。当然,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖老年代进行分配担保(Handle Promotion)。
-
优点:解决碎片化问题,顺序分配内存简单高效
-
缺点:只适用于存活率低的场景,如果极端情况下如果对象面上的对象全部存活,就要浪费一半的存储空间。
③标记-整理算法
为了解决复制算法的缺陷,充分利用内存空间,提出了标记整理算法。与标记清理算法过程一样,只是不直接清理可回收对象,而是将所有存活对象移动到一端,之后清理边界之外的对象内存
④分代收集算法
根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保的(老年代),采用标记-清理算法或者标记-整理算法。
8.说明关键字final,finally,finalize有什么不同,调用System.gc()会发生什么?
final修饰类不可被继承;
final修饰方法不可被重写;
final修饰变量不可被更改且必须初始化。
finally 为try catch代码块无论发生什么都要执行时使用的关键字,用于代码中无论异常是否发生必须执行的语句
finalize :java允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。
子类覆盖finalize()方法以整理系统资源或者被执行其他清理工作。
finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
调用System.gc()会告知垃圾收集器打算进行垃圾收集,但垃圾收集器是否进行回收是不确定的
解决方法(不推荐使用):
System.gc();
Runtime.runFinalizationSync();
System.gc();
9.说明多线程的sleep(),以及wait()有什么不同?
sleep
1.sleep是Thread的一个静态(static)方法。使得Runnable实现的线程也可以使用sleep方法。而且避免了线程之间相互调用sleep()方法,引发死锁。
2.sleep()执行时需要赋予一个沉睡时间。在沉睡期间(阻塞线程期间),CPU会放弃这个线程,执行其他任务。当沉睡时间到了之后,该线程会自动苏醒,不过此时线程不会立刻被执行,而是要等CPU分配资源,和其他线程进行竞争。
3.此外如果这个线程之前获取了一个机锁,在沉睡期间,这个机锁不会释放。其他等待这个机锁的程序,必须等待这个线程醒来,且执行完后才能运行。
wait
1.wait()是Object类的一个方法。当调用wait()方法时,该线程会进入和该对象相关的等待池中,并释放它所拥有的机锁。
2.执行wait()后,必须使用notify()方法或notifyAll()方法或设置等待时间(wait(long time))唤醒在等待线程池中的线程。
3.wait()必须放在synchronized block中,否则会在运行时报“java.lang.IllegalMonitorStateException”异常
10.说说springboot的自动装配?
一:将主配置类(即@SpringBootApplication标注的类)的所在包及子包里面所有组件扫描加载到Spring容器。
二:springboot 以@SpringBootApplication 注解的类的主方法启动,然后按照指定,或者默认的路径进行扫描,取到路径下类上的注解 然后按照注解规定好的规则 进行处理
基本过程:
通过各种注解实现了类与类之间的依赖关系,容器在启动的时候Application.run,会调用EnableAutoConfigurationImportSelector.class的selectImports方法(其实是其父类的方法)
selectImports方法最终会调用SpringFactoriesLoader.loadFactoryNames方法来获取一个全面的常用BeanConfiguration列表
loadFactoryNames方法会读取FACTORIES_RESOURCE_LOCATION(也就是spring-boot-autoconfigure.jar 下面的spring.factories),获取到所有的Spring相关的Bean的全限定名ClassName,大概120多个
selectImports方法继续调用filter(configurations, autoConfigurationMetadata);这个时候会根据这些BeanConfiguration里面的条件,来一一筛选,最关键的是
@ConditionalOnClass,这个条件注解会去classpath下查找,jar包里面是否有这个条件依赖类,所以必须有了相应的jar包,才有这些依赖类,才会生成IOC环境需要的一些默认配置Bean
最后把符合条件的BeanConfiguration注入默认的EnableConfigurationPropertie类里面的属性值,并且注入到IOC环境当中
11.说说finalize()是在什么时候会被调用?如果我人为的调用类中的finalize(),会启动gc吗?
如上8
12.异常有哪些,列举几个常见的异常?
异常:java.lang.Throwable
子类
① java.lang.Error
Error:是程序中无法处理的错误,表示运行应用程序中出现了严重的错误。此类错误一般表示代码运行时JVM出现问题。
通常有Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。
比如说当jvm耗完可用内存时,将出现OutOfMemoryError。
此类错误发生时,JVM将终止线程。非代码性错误。
因此,当此类错误发生时,应用不应该去处理此类错误。
② java.lang.Exception
-
运行时异常(不受检异常):RuntimeException类极其子类表示JVM在运行期间可能出现的错误。编译器不会检查此类异常,并且不要求处理异常,比如用空值对象的引用(NullPointerException)、数组下标越界(ArrayIndexOutBoundException)。此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。
-
非运行时异常(受检异常):Exception中除RuntimeException极其子类之外的异常。编译器会检查此类异常,如果程序中出现此类异常,比如说IOException,必须对该异常进行处理,要么使用try-catch捕获,要么使用throws语句抛出,否则编译不通过。
Exception::程序本身可以捕获并且可以处理的异常。
13.ConcurrentHashMap是怎么实现线程安全的?
ConcurrentHashMap使用了锁分段(减小锁范围)、CAS(乐观锁,减小上下文切换开销,无阻塞)等方法
14.说说同步与异步的区别?
线程同步:是多个线程访问同一资源时,只有当前正在访问的线程访问结束之后,其他线程才能开始访问(被阻塞)。
线程异步:是多个线程在访问竞争资源时,可以在空闲等待时去访问其它资源(不被阻塞)。
15.Java源文件是如何执行的?
一次编译,到处运行
javac命令编译.java为.class,对于不同的操作系统有着不同的JVM,JVM会将.class翻译为可以被操作系统能识别的机器码文件,再交由cpu运行
1.使用编辑器或IDE(集成开发环境)编写Java源文件.即Simple.java
2.程序必须编译为字节码文件,javac(Java编译器)编译源文件为Simple.class文件.
3.不同的类文件可在不同平台/操作系统上由JVM(Java虚拟机)执行
4.JVM将字节码文件翻译为机器可以执行的机器码(0,1二进制)
16.对象必定是存在于jvm堆上的吗?不一定
见上5
不一定,随着JIT编译器的发展,在编译期间,如果JIT经过逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配(栈上分配,标量替换)。但是这也并不是绝对的(并不是全部在栈中分配)。
参考链接
Java编译(三) Java即时编译(JIT编译):运行时把Class文件字节码编译成本地机器码
三、苏小研二面
1.我们有时可以直接调用公开方法也可以使用反射,那么我们要在何时使用反射?
1)Java的反射机制在做基础框架的时候非常有用,有一句话这么说来着:反射机制是很多Java框架的基石。
2)当你做一个软件可以安装插件的功能,你连插件的类型名称都不知道,你怎么实例化这个对象呢?因为程序是支持插件的(第三方的),在开发的时候并不知道 。所以无法在代码中 New出来 ,但反射可以,通过反射,动态加载程序集,然后读出类,检查标记之后再实例化对象,就可以获得正确的类实例。
3)在编码阶段不知道那个类名,要在运行期从配置文件读取类名, 这时候就没有办法硬编码new ClassName(),而必须用到反射才能创建这个对象.反射的目的就是为了扩展未知的应用。比如你写了一个程序,这个程序定义了一些接口,只要实现了这些接口的dll都可以作为插件来插入到这个程序中。那么怎么实现呢?就可以通过反射来实现。就是把dll加载进内存,然后通过反射的方式来调用dll中的方法。很多工厂模式就是使用的反射。
2.使用反射的优缺点
1.优点:
使用反射,我们就可以在运行时获得类的各种内容,进行反编译,
对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,
这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。
2.缺点:
(1)反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;
(2)反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
3.使用JVM查看死锁的方式有哪些?
JConsole可视化查看死锁
Jstack查看死锁
4.其它则问了我一些有关于考研,加班的看法以及用三个词语自我评价实习总结一句话心得
参考链接
jvm 内存dump、gc查看、线程死锁,jmap、jstack、jstat
四、SNK笔试
1.类加载过程中,父子类静态代码,静态变量,构造方法,普通代码的加载顺序
总结:静态>普通>构造,父类>子类。
静态首先加载,满足第一条,
然后再普通,构造满足第二条;
测试代码如下:
Child子类
package program._02父子类各成分加载顺序;
//子类
public class Child extends Parent{
public Child() {
System.out.println("子类构造方法");
}
{
System.out.println("子类普通代码块");
}
static {
System.out.println("子类静态代码块");
}
}
Parent父类
//父类
public class Parent {
public Parent() {
System.out.println("父类构造方法");
}
{
System.out.println("父类普通代码块");
}
static {
System.out.println("父类静态代码块");
}
}
Test测试类:
- Child child = new Child();
Child child = new Child();
父类静态代码块
子类静态代码块
父类普通代码块
父类构造方法
子类普通代码块
子类构造方法
- Parent child1 = new Child();
Parent child1 = new Child();
父类静态代码块
子类静态代码块
父类普通代码块
父类构造方法
子类普通代码块
子类构造方法
2.接口可以多继承?
一个java类只可以继承一个java类,而可以实现多个接口,java使用接口来实现C++的多继承。
java接口可以extends多个接口,却不能implements任何接口。因而,Java中的接口是支持多继承的。
3.volatile保证线程安全?
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制
- 变量定义为 volatile 之后,将具备两种特性:
1.保证此变量对所有的线程的可见性
可见性:当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。
但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。
2. 禁止指令重排序优化
有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,
这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),
只有一个CPU访问内存时,并不需要内存屏障;
(指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
- volatile 性能:
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,
因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行
4.求数组连续子序列最大和
给定一个整数数组{5,-1,-12,4,-3,6,2}
求出它连续子序列的最大和 9
连续子序列{4,-3,6,9}
题目是求出来了,但是做完题发现以我这个思路求这个连续子序列很难求,这里有 请教!
代码如下:
import java.util.Scanner;
public class _03求数组连续子序列最大和 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
String[] s = sc.nextLine().split(",");
int[] arr=new int[s.length];
for (int i = 0; i < s.length; i++) {
arr[i]=Integer.valueOf(s[i]);
}
int sum;
int max=0;
int[] l = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
//5,-1,-12,4,-3,6,2
sum=arr[i];
for (int j = i+1; j < arr.length; j++) {
sum+=arr[j];
if(max<sum) {
max = sum;
}
}
}
System.out.println(max);
}
}
}
5.java垃圾回收算法,优缺点?
目前垃圾回收几种主流的回收算法.
1).Mark-Sweep(标记-清除)算法
这是最基础的算法,该算法就是标记出需要被回收的对象,等到需要执行GC操作时将标记的对象一并清除,实现垃圾回收。改方法简单,效率高,但是有个缺点就是会导致内存碎片。
2).Copying(复制)算法
Copying算法是将内存区域划分为两块相同大小的子区域,并且在其中的一块中执行对象分配,等到这一块的内存用完了,就将该区域还存活着的对象复制到另外一块内存区域上面,然后再把已使用的内存空间一次清理掉,这样就不会导致内存碎片的问题,但是有个明显的缺点,每次只能使用到一半的内存,对内存压力大。
3).Mark-Compact(标记-整理)算法
Mark-Compact算法是在Mark-Sweep算法的基础上进行了改进,算法标记跟Mark-Sweep一样,只是在标记完之后,将标记的对象向一端移动,然后清理掉边界以外的内存区域,这样就解决了内存碎片化的问题
3).Generational Collection(分代收集)算法
这是目前Jvm使用的垃圾回收算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。
分为老年代(Tenured Generation)和新生代(Young Generation)。老年代的内存区域的对象一般回收频率比较低,采用了Mark-Compact算法,
而新生代的内存区域由于每次需要回收大量对象,回收频率较高,所以将该区域又划分成了一个较大的Eden空间和两个较小的Suivivor空间,
每次使用Eden空间和一个Survivor空间的,
当需要回收垃圾时,
将Eden空间和该Survivor空间的存活对象复制到另外一块Survivor空间上,然后清理到Eden和刚才使用的Survivor空间,
实现垃圾回收机制.
6.深拷贝与浅拷贝
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。
7.string,stringBuffer,stringBuild各自有什么特点?
- 可变性,
- 线程安全性
String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间
StringBuffer是可变类和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量
StringBuilder可变类,速度更快,但是线程不安全,适用于单线程情况下可变字符串的使用
8.说明你知道的三种设计模式,并说出他们的使用场景
9.书写一个线程安全的单例模式(双重检验锁)
public class LazySington {
//懒汉式,线程不安全,时间换空间
private LazySington(){
}
//类加载时不初始化,获取对象时才初始化,类加载快,获取对象时较慢
private volatile static LazySington instance=null;
public static LazySington getInstance(){
if(instance==null){
synchronized (LazySington.class){
if(instance==null){
LazySington instance = new LazySington();
}
}
}
return instance;
}
}
10.创建线程方式,优缺点?
- 继承Thread类,重写run()
由于java支持单继承,所以此方法会限制类继承其他父类 - 实现runnable接口,重写run()
常用方法,java中支持接口多继承,继承一个类实现多个接口,扩展性强 - 实现callable接口,重写call()
有返回值且可以抛出异常,参数为Future对象,可用性更强
11.java容器,哪些是线程安全的?
同步容器类:使用了synchronized
1.Vector
2.HashTable
并发容器:
3.ConcurrentHashMap:分段
4.CopyOnWriteArrayList:写时复制,读写分离
5.CopyOnWriteArraySet:写时复制,读写分离
Queue:
6.ConcurrentLinkedQueue:是使用非阻塞的方式实现的基于链接节点的无界的线程安全队列,性能非常好。
(java.util.concurrent.BlockingQueue 接口代表了线程安全的队列。)
7.ArrayBlockingQueue:基于数组的有界阻塞队列
8.LinkedBlockingQueue:基于链表的有界阻塞队列,界限为Integer.MAX_VALUE
9.PriorityBlockingQueue:支持优先级的无界阻塞队列,即该阻塞队列中的元素可自动排序。默认情况下,元素采取自然升序排列
10.DelayQueue:一种延时获取元素的无界阻塞队列。
11.SynchronousQueue:不存储元素的阻塞队列。每个put操作必须等待一个take操作,否则不能继续添加元素。内部其实没有任何一个元素,容量是0
Deque:
(Deque接口定义了双向队列。双向队列允许在队列头和尾部进行入队出队操作。)
12.ArrayDeque:基于数组的双向非阻塞队列。
13.LinkedBlockingDeque:基于链表的双向阻塞队列。
Sorted容器:
14.ConcurrentSkipListMap:是TreeMap的线程安全版本
15.ConcurrentSkipListSet:是TreeSet的线程安全版本
12.非递归实现二叉树中序遍历
参考链接
五、建信金融笔试
1.一等奖找出,在奖品池子中有一等奖1份,二等奖2份,三等奖3份······,现在输入奖品的奖品值,找出其中的一等奖返回它的奖品值
思路:
一等奖1份,二等奖2份,三等奖3份······,那么一等奖则为1份;
想到新建一个长度为最大奖品值+1的数组;
通过对应索引上元素值的增加实现奖品对应份数的计算;
最后返回新数组元素值=1的索引值。
代码:
/*一等奖找出,在奖品池子中有一等奖一份,二等奖2份,三等奖3份······
现在输入奖品的奖品值,找出其中的一等奖*/
public int uniqueAward (int[] nums) {
// write code here
int max=0;
for (int i = 0; i < nums.length; i++) {
max=Integer.max(max,nums[i]);
}
int[] ints = new int[max + 1];
for (int i = 0; i < ints.length; i++) {
ints[i]=0;
}
for (int i = 0; i < nums.length; i++) {
int temp=nums[i];
ints[temp]++;
}
for (int i = 0; i < ints.length; i++) {
if(ints[i]==1){
return i;
}
}
return 0;
}
2.给一个按行升序排列的二维数组,整型值m,找出排序后第m小的数。
思路:
这一道题的思路很直接,容易想出来,但也很耗费内存,而且我也有些取巧,仅供各位参考,同时希望各位大佬不吝赐教!
代码:
public int kthSmallest (int[][] matrix, int m) {
// write code here
List<Integer> list = new ArrayList<>();
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
list.add(matrix[i][j]);
}
}
Collections.sort(list);//默认升序排列,第m小即为第m-1处元素
return list.get(m-1);
}
测试代码:
/*二维数组,排序后找出第m小的数*/
public int kthSmallest (int[][] matrix, int m) {
// write code here
for (int i = 0; i < matrix.length; i++) {
for (int i1 = 0; i1 < matrix[i].length; i1++) {
matrix[i][i1]= new Random().nextInt(999);
}
}
List<Integer> list = new ArrayList<>();
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
list.add(matrix[i][j]);
}
}
Collections.sort(list);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
//忘记了这个默认是升序,所以就测试了下。。。尴尬
System.out.print(iterator.next()+" ");
}
return list.get(m-1);
}