袁艺

个人站

欢迎来到我的个人站~


美团

一面

概述:一面来说还是比较简单的了,面试官也比较和蔼,问了一些简单的java基础,问了一下项目。

int和integer的区别

int是基本数据类型,存储到方法区里面,占用了4个字节,初始值为0 integer是引用数据类型,存储在堆里面,初始值为null,对于-128到127之间的数,会进行缓存。关于integer的内存,有两种推论,一种把堆分为两部分:一部分为句柄池,另一部分为对象池。每个实例引用都是一个指向句柄池的本地指针,句柄池由指向对象池和类数据的两个指针组成;对象池中只存实例数据。这种表示法的优点是 gc时候,清理完垃圾对象后,还要整理碎片,就需要把非垃圾对象拷贝到一个新的区域,此时由于拷贝,新对象的地址已经改变,但并不需要指向对象的引用值做改变,只用改变句柄池中指向对象池的引用地址即可;缺点是每次访问对象都需要经过两次指针跳转,效率较低。第二种是对象指针指向一组数据,这组数据本身包含有实例数据和指向类数据的指针。优缺点跟第一种方案相反。通过以上分析可得,Integer在内存中有一个指向方法区里边类信息的指针,这个指针占用4bytes;另外Integer中实例变量只有一个int类型的字段,所以为32位,4bytes。在不考虑lock、wait set、gc相关信息占用的时候,如果是第一种方案,有4bytes的指向对象池的指针,一共是34=12bytes;如果是第二种实现方案,则是24-8bytes的指针。我们怎么能确定jvm用的是第一种方案还是第二种方案呢?lock、wait set、gc相关信息在仅仅创建,累加,销毁的时候是否的确不存在呢?从VisualVM生成的heap dump文件分析,每个Integer占用了3*4bytes:一般来说JVM的Integer占用8bytes

//对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false
Integer i1 = 127;//java在编译的时候,被翻译成-> Integer i5 = Integer.valueOf(127);
Integer i2 = 127;//在缓存中取得127所以为true
System.out.println(i1 == i2);//true
Integer i3 = 128;
Integer i4 = 128;//没有缓存了,只能new
System.out.println(i3 == i4);//false
 
//JDK源码的valueOf函数式这样的:
public static Integer valueOf(int i) {
      assert IntegerCache.high >= 127;
      if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
}

jvm的垃圾收集器

Serial 串行垃圾回收器

单线程新生代收集器,只会用一条线程完成收集工作在Client模式下的虚拟机可以选择,新生代:复制算法。老年代:标记—整理

ParNew

Serial的多线程版本,收集过程以及算法与Serival一致,可与CMS老年代收集器配合

Serial Old

Serial 的老年代版本,采用 标记—整理

Parallel Scavenge 并行垃圾回收器

新生代收集器,多线程,主要关注吞吐量,适合在后台运算不需要太多交互的任务,采用复制算法

Parallel Old

Parallel Scavenge的老年代版本,采用多线程标记—整理

CMS 并发标记扫描垃圾回收器

老年代收集器,关注停顿时间,采用标记—清除。4个过程,初始标记,并发标记,重新标记,并发清除。收集过程与用户线程并发执行,缺点:并发收集占用CPU资源,降低吞吐率。浮动垃圾,程序不断运行会产生新的垃圾。JDK1.5老年代占用达到68%会触发,JDK1.6老年代占用达到92%会触发,触发的阈值可以设置,设置不当会导致FuLL GC 降低性能。会出现内存碎片,可设置参数,多少次不压缩的Full GC之后,进行一次压缩的,默认0,每次都会压缩。

G1 G1垃圾回收器

特点:并行与并发,分代收集,空间整合,可预测的停顿将Java堆分为多个大小相等的独立区域,跟踪每个区域里垃圾的价值,维护一个优先列表,优 先回收价值最大的。区域空间对象的引用使用Remembered Set记录,如果引用的对象处于不同的区域,通过Card Table把引用信息记录到被引用对象的Remembered Set中。过程:初始标记,并发标记,最终标记,筛选回收

动态代理

动态代理分为两种:jdk动态代理是由java内部的反射机制来实现的;cglib动态代理底层则是借助asm来实现的。总的来说,反射机制在生成类的过程中比较高效,而asm在生成类之后的相关执行过程中比较高效。还有一点必须注意:jdk动态代理的应用前提,必须是目标类基于统一的接口。如果没有上述前提,jdk动态代理不能应用。由此可以看出,jdk动态代理有一定的局限性,cglib这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。。

IOC和DI

Ioc—Inversion of Control,即“控制反转”,不是什么技术,而是一种设计思想。在Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。
 谁控制谁,控制什么:传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对 象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。
  为何是反转,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。
  用图例说明一下,传统程序设计如图2-1,都是主动去创建相关对象然后再组合起来:

DI—Dependency Injection,即“依赖注入”:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。
理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:
  谁依赖于谁:当然是应用程序依赖于IoC容器;
  为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源;
  谁注入谁:很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;
  注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

index索引

数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。设置索引要付出代价的:一是增加了数据库的存储空间,二是在插入和修改数据时要花费较多的时间(因为索引也要随之变动)。

优点:

  • 第一,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
  • 第二,可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
  • 第三,可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
  • 第四,在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
  • 第五,通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

缺点:

  • 第一,创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
  • 第二,索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间, 如果要建立聚簇索引,那么需要的空间就会更大。
  • 第三,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据 的维护速度。

索引使用的场景:

  • 在经常需要搜索的列上,可以加快搜索的速度;
  • 在作为主键的列上,强制该列的唯一性和组织表中数据的排列结构;
  • 在经常用在连接的列上,这些列主要是一些外键,可以加快连接的速度;
  • 在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;
  • 在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序
  • 查询时间;在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。

不应该使用索引的场景:

  • 第一,对于那些在查询中很少使用或者参考的列不应该创建索引。这是因为,既然这些列很少 使 用到,因此有索引或者无索引,并不能提高查询速度。相反,由于增加了索引,反而降低系统的 维护速度和增大了空间需求。
  • 第二,对于那些只有很少数据值的列也不应该增加索引。这是因为,由于这些列的取值很少,例如人事表的性别列,在查询的结果中,结果集的数据行占了表中数据行的很大比例,即需要在 表中搜索的数据行的比例很大。增加索引,并不能明显加快检索速度。
  • 第三,对于那些定义为text, image和bit数据类型的列不应该增加索引。这是因为,这些列的数据量要么相当大,要么取值很少。
  • 第四,当修改性能远远大于检索性能时,不应该创建索引。这是因为,修改性能和检索性能是互相矛盾的。当增加索引时,会提高检索性能,但是会降低修改性能。当减少索引时,会提高修改性能,降低检索性能。因此,当修改性能远远大于检索性能时,不应该创建索引。

索引种类

唯一索引、主键索引和聚集索引。

B树和B+树

1)B树

B树中每个节点包含了键值和键值对于的数据对象存放地址指针,所以成功搜索一个对象可以不用到达树的叶节点。成功搜索包括节点内搜索和沿某一路径的搜索,成功搜索时间取决于关键码所在的层次以及节点内关键码的数量。在B树中查找给定关键字的方法是:首先把根结点取来,在根结点所包含的关键字K1,…,kj查找给定的关键字(可用顺序查找或二分查找法),若找到等于给定值的关键字,则查找成功;否则,一定可以确定要查的关键字在某个Ki或Ki+1之间,于是取Pi所指的下一层索引节点块继续查找,直到找到,或指针Pi为空时查找失败。

2)B+树

B+树非叶节点中存放的关键码并不指示数据对象的地址指针,非叶节点只是索引部分。所有的叶节点在同一层上,包含了全部关键码和相应数据对象的存放地址指针,且叶节点按关键码从小到大顺序链接。如果实际数据对象按加入的顺序存储而不是按关键码次数存储的话,叶节点的索引必须是稠密索引,若实际数据存储按关键码次序存放的话,叶节点索引时稀疏索引。B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶节点。所以 B+树有两种搜索方法:一种是按叶节点自己拉起的链表顺序搜索。一种是从根节点开始搜索,和B树类似,不过如果非叶节点的关键码等于给定值,搜索并不停止,而是继续沿右指针,一直查到叶节点上的关键码。所以无论搜索是否成功,都将走完树的所有层。B+ 树中,数据对象的插入和删除仅在叶节点上进行。 这两种处理索引的数据结构的不同之处:

  • a,B树中同一键值不会出现多次,并且它有可能出现在叶结点,也有可能出现在非叶结点中。而B+树的键一定会出现在叶结点中,并且有可能在非叶结点中也有可能重复出现,以维持B+树的平衡。
  • b,因为B树键位置不定,且在整个树结构中只出现一次,虽然可以节省存储空间,但使得在插入、删除操作复杂度明显增加。B+树相比来说是一种较好的折中。
  • c,B树的查询效率与键在树中的位置有关,最大时间复杂度与B+树相同(在叶结点的时候),最小时间复杂度为1(在根结点的时候)。而B+树的时候复杂度对某建成的树是固定的。

事务

1.事物的概念

  • 1)什么是事务 事务是对数据库操作的最基本单位。事务是一组操作,要不全成功,要不全不成功
  • 2)事务的特性 A:原子性(Atomicity)。事务是数据库的逻辑工作单位,事务中包括的诸操作要么全做,要么全不做。 C:一致性(Consistency) 事务执行的结果必须是使数据库从一个一致性 态变到另一个一致性状态。一致性与原子性是密切相关的。 I:隔离性(Isolation) 一个事务的执行不能被其他事务干扰。 D:持续性/永久性(Durability)一个事务一旦提交,它对数据库中数据的改 变就应该是永久性的。

  • 3)不考虑隔离性产生的问题

    脏读:脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据 不可重复读:不可重复读发生在一个事务执行相同的查询两次或两次以上,但是每次都得到 不同的数据时。这通常是因为另一个并发事务在两次查询期间进行了更新。 幻读:幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发 事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录。 幻读和不可重复读的区别: 不可重复读的重点是修改: 幻读的重点在于新增或者删除

  • 4)解决3)中的问题、

    设置隔离级别 ISOLATION_DEFAULT: 使用后端数据库默认的隔离级别 ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生 ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的

hashmap在高并发时期会出现的问题

Rehash/再散列扩展内部数组长度 在哈希表使用的过程中不断的对table容量进行调整,看一下put操作中的addEntry()方法:

void addEntry(int hash, K key, V value, int bucketIndex) {
   Entry<K,V> e = table[bucketIndex];
       table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
       if (size++ >= threshold)
           resize(2 * table.length);
   }

这里面resize的过程,就是再散列调整table大小的过程,默认是当前table容量的两倍。

void resize(int newCapacity) {
       Entry[] oldTable = table;
       int oldCapacity = oldTable.length;
       if (oldCapacity == MAXIMUM_CAPACITY) {
           threshold = Integer.MAX_VALUE;
           return;
       }
 
       Entry[] newTable = new Entry[newCapacity];
       //初始化一个大小为oldTable容量两倍的新数组newTable
       transfer(newTable);
       table = newTable;
       threshold = (int)(newCapacity * loadFactor);
   }

关键的一步操作是transfer(newTable),这个操作会把当前Entry[] table数组的全部元素转移到新的table中, 这个transfer的过程在并发环境下会发生错误,导致数组链表中的链表形成循环链表,在后面的get操作时e = e.next操作无限循环,Infinite Loop出现。 HashMap在多线程put后可能导致get无限循环 多线程put的时候可能导致元素丢失 那么如何使用线程安全的哈希表结构呢,这里列出了几条建议: 使用Hashtable 类,Hashtable 是线程安全的; 使用并发包下的java.util.concurrent.ConcurrentHashMap,ConcurrentHashMap实现了更高级的线程安全; 或者使用synchronizedMap() 同步方法包装 HashMap object,得到线程安全的Map,并在此Map上进行操作 </p>

在一个数组中找到最长的递增序列

//时间复杂度为n
static int i=0,j=0;
	public static void main(String[] args) {
		int[] arr={1,2,3,4,2,3,4};
		System.out.println(cal_length(arr));
		
	}	
	public static int cal_length(int[] arr){
		int max=0;//记录最长的序列长度
		int p=0,q;//p指向序列的开始,q指向p的下一个元素,如果是递增的q++
		for(q=1;q<arr.length;++q){
			if (arr[q]<arr[q-1]) {//递增序列结束
				if (q-1-p>max) {//判断序列长度是否大于max
					i=p;
					j=q-1;
					max=j-i+1;
				}
				p=q;//将p重新赋值为q的位置
			}else if (q==arr.length-1) {//q走到数组结束的时候
				if (q-p+1>max) {
					i=p;
					j=q;
					max=j-i+1;
				}
			}
		}
		return max;
	}

二面

概述:二面的面试官主要是问了最近有学习什么知识,坑都是自己跳进去的。本人说了学习设计模式和JVM的相关知识,问了很多相关的内容,但是大部分时间面试官都是让自己在描述。

B+树和B树的区别

索引原理

数据库的搜索引擎

首先介绍一下Innodb引擎。
Innodb引擎提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统。它本身实际上是基于Mysql后台的完整的系统。Mysql运行的时候,Innodb会在内存中建立缓冲池,用于缓冲数据和索引。

接下来来说说MyIASM引擎。它是MySql的默认引擎,但不提供事务的支持,也不支持行级锁和外键。因此当执行Insert插入和Update更新语句时,即执行写操作的时候需要锁定这个表。所以会导致效率会降低。
我们在来说说这两种引擎的选择。其实上面已经提到了。这里我在补充了两点:

  • 1、大容量的数据集时趋向于选择Innodb。因为它支持事务处理和故障的恢复。Innodb可以利用数据日志来进行数据的恢复。主键的查询在Innodb也是比较快的。

  • 2、大批量的插入语句时(这里是INSERT语句)在MyIASM引擎中执行的比较的快,但是UPDATE语句在Innodb下执行的会比较的快,尤其是在并发量大的时候。

最后我们再来说说两种引擎所使用的索引的数据结构是什么?答案是都是B+树。
对于MyIASM引擎来说,B+树的数据结构中存储的内容实际上是实际数据的地址值。也就是说它的索引和实际数据是分开的,只不过使用索引指向了实际数据。这种索引的模式被称为非聚集索引。 而Innodb引擎的索引的数据结构也是B+树,只不过数据结构中存储的都是实际的数据,这种索引有被称为聚集索引。MyISAM 使用的是表锁, 而 InnoDB实现的是行锁。

mysql优化

优化SQL和索引

优化数据库对象
1)选择表合适存储引擎 2)优化表的数据类型,选择合适的数据类型 应用优化
1)使用连接池 2)减少对mysql的访问,使用mem缓存等 3)负载均衡,复制分流查询操作 分库分表
1)水平划分 如果某个表的数据太多,预期有上千条甚至上亿以上,我们可以化整为0:拆表。 这里就涉及到拆表的算法: 记录日志的表,也可以按周或者按月来拆。 记录用户信息的表,按用户id的hash算法来拆。 2)垂直拆分 如果表记录数并不多,可能也就2、3万条,但是字段却很长,表占用空间很大,检索表时需要 执行大量I/O,严重降低了性能。这个时候需要把大的字段拆分到另一个表,并且该表与原表是一对一的关系。

5.读写分离

读写分离,基本的原理是让主数据库处理事务性增、改、删操作(INSERT、UPDATE、DELETE),而从数据库处理SELECT查询操作。数据库复制被用来把事务性操作导致的变更同步到集群中的从数据库。

6.动态代理的实现原理

详情

7.反射实现原理

在Java中的反射机制是指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法; 对于任意一个类,都能调用它的任意一个方法;这种动态获取信息以及动态调用对象的功能成为Java语言的反射机制。 Java的反射机制的实现要借助于4个类:class,Constructor,Field,Method;其中class代表的时类对 象,Constructor-类的构造器对象,Field-类的属性对象,Method-类的方法对象。通过这四个对象我们可以粗略的看到一个类的各个组 成部分。

8.泛型

9.进程之间的通信方式

管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机间的进程通信。

10.网络的七层模型

应用层:主要指应用程序; 表示层:用于数据格式交换、数据加解密、数据压缩和恢复; 会话层:用于建立、维持和断开网络会话; 传输层:负责端到端(进程到进程)之间数据的传输控制; 网络层:传送分组,主要负责主机到主机的数据传输以及分组的选路,实现路由选择、拥塞控制、网络互连的功能 数据链路层:数据在一段链路上的相邻结点间的传输。传输数据时,链路层将网络层的数据封装成帧(framing),从而实现以帧为传输单位,采用简单的差错控制方法,使有差错的物理线路变成无差错的数据链路。 物理层:利用物理传输介质为数据链路层提供物理连接,透明地传输比特流。

11.TCP和UDP的区别

UDP的主要特点
UDP是无连接的,即发送数据之前不需要建立连接
UDP使用尽最大努力交付,不保证可靠交付,同时也不使用拥塞控制。
UDP是面向报文的。UDP没有拥塞控制,很适合多媒体通信的要求。
UDP支持一对一、一对多、多对一和多对多的交互通信。
UDP的首部开销小,只有8个字节。
TCP 的主要特点:
TCP 是面向连接的运输层协议。
应用程序在使用TCP协议之前,首先要建立TCP连接。数据传输完毕后,要释放TCP连接。
每一条 TCP 连接只能有两个端点,TCP 连接只能是点对点的。因此TCP不能进行广播和多播,UDP可以。
TCP 提供可靠交付的服务。通过TCP连接传送的数据无差错、不丢失、不重复、并且按序到达。
TCP 提供全双工通信。
TCP是面向字节流的。
在一个TCP连接中传送的字节流中的每一个字节都按顺序编号

三面

概述:三面主要是问了平时怎么学习的,没问什么技术难点,最后问了一个数据结构的问题;已知一个二叉树的中序遍历和后序遍历的结果,请输出前序遍历的结果。

	 //全局变量存放后序序列
    //先写根,后写左子树,最后写右子树
    public static String res = "";
   
    //进行遍历输出
    //参数依此为中序序列,后序序列
    public static void cal_tree(String smid, String slast) {
        //boolean state = StringEquals(smid, slast);
       /* if (state == false)
            return;*/
        if (smid.length() == 0)
            return;
        //每次添加都是添加中序的字符,当中序字符串长度为1的时候,就返回
        if (smid.length() == 1) {
            res += smid;
            return;
        }
        //后序序列中最后一个就是根
        char root = slast.charAt(slast.length()-1);
        //获取字符在中序序列总的位置
        //mid代表的是索引
        int mid = smid.indexOf(root);
        //中序序列的左子树
        String c=smid.substring(0, mid);
        //中序序列的右子树
        String d = smid.substring(mid+1);
        //写入根
        res += root;
        //中序左子树,后序左子树
        cal_tree(c,slast.substring(0, c.length()));
        //中序右子树,后序右子树,注意这里后序的右子树要最大为slast.length()-1
        cal_tree(d,slast.substring(c.length(),slast.length()-1));
       
    }
    public static void main(String[] agrs) {
        String s1 = "ADEFGHMZ";
        String s2 = "AEFDHZMG";
        cal_tree(s1, s2);
        if (res.length() != s1.length())
        {
            System.out.println("wrong tree list!");
        }
        else {
            System.out.println(res);
        }
    }