Java 基础
1、Java 的类加载过程
jvm 将 .class 类文件信息加载到内存并解析成对应的 class 对象的过程,
注意:jvm 并不是一开始就把所有的类加载进内存中,只是在第一次遇到某个需要运行的类才会加载,并且只加载一次
主要分为三部分:1、加载,2、链接(1.验证,2.准备,3.解析),3、初始化
-
加载
类加载器包括BootClassLoader
、ExtClassLoader
、APPClassLoader
-
链接
- 验证:(验证 class 文件的字节流是否符合 jvm 规范)
- 准备:为类变量分配内存,并且进行赋初值
- 解析:将常量池里面的符号引用(变量名)替换成直接引用(内存地址)过程,在解析阶段,jvm 会把所有的类名、方法名、字段名、这些符号引用替换成具体的内存地址或者偏移量。
-
初始化
主要对类变量进行初始化,执行类构造器的过程,换句话说,只对 static 修试的变量或者语句进行初始化。
示例:Person person = new Person();
为例进行说明。
Java 编程思想中的类的初始化过程主要有以下几点:
- 找到class文件,将它加载到内存
- 在堆内存中分配内存地址
- 初始化
- 将堆内存地址指给栈内存中的 p 变量
2、String、StringBuilder、StringBuffer
StringBuffer 里面的很多方法添加了 synchronized 关键字,是可以表征线程安全的,所以多线程情况下使用它。
执行速度:
StringBuilder > StringBuffer > String
StringBuilder 牺牲了性能来换取速度的,这两个是可以直接在原对象上面进行修改,省去了创建新对象和回收老对象的过程,而 String 是字符串常量(final)修试,另外两个是字符串变量,常量对象一旦创建就不可以修改,变量是可以进行修改的,所以对于String字符串的操作包含下面三个步骤:
- 创建一个新对象,名字和原来的一样
- 在新对象上面进行修改
- 原对象被垃圾回收掉
3、JVM 内存结构
Java 对象实例化过程中,主要使用到虚拟机栈、Java 堆和方法区。Java 文件经过编译之后首先会被加载到 jvm 方法区中,jvm 方法区中很重的一个部分是运行时常量池,用以存储 class 文件类的版本、字段、方法、接口等描述信息和编译期间的常量和静态常量。
3.1 JVM 基本结构
类加载器 classLoader,在 JVM 启动时或者类运行时将需要的 .class 文件加载到内存中。执行引擎,负责执行 class 文件中包含的字节码指令。本地方法接口,主要是调用 C/C++ 实现的本地方法及返回结果。内存区域(运行时数据区),是在JVM运行的时候操作所分配的内存区,主要分为以下五个部分,如下图:
- 方法区:用于存储类结构信息的地方,包括常量池、静态变量、构造函数等。
- Java堆(heap):存储Java实例或者对象的地方。这块是gc的主要区域。
- Java栈(stack):Java 栈总是和线程关联的,每当创建一个线程时,JVM 就会为这个线程创建一个对应的 Java 栈。在这个 java 栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。所以 java 栈是线程私有的。
- 程序计数器:用于保存当前线程执行的内存地址,由于 JVM 是多线程执行的,所以为了保证线程切换回来后还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。
- 本地方法栈:和 Java 栈的作用差不多,只不过是为 JVM 使用到的 native 方法服务的。
3.2 JVM 源码分析
- JVM 源码分析
4、GC 机制
垃圾收集器一般完成两件事:
- 检测出垃圾;
- 回收垃圾;
4.1 Java 对象引用
通常,Java 对象的引用可以分为4类:强引用、软引用、弱引用和虚引用。
- 强引用:通常可以认为是通过new出来的对象,即使内存不足,GC进行垃圾收集的时候也不会主动回收。
Object obj = new Object();
- 软引用:在内存不足的时候,GC进行垃圾收集的时候会被GC回收。
Object obj = new Object();
SoftReference<Object> softReference = new SoftReference<>(obj);
- 弱引用:无论内存是否充足,GC进行垃圾收集的时候都会回收。
Object obj = new Object();
WeakReference<Object> weakReference = new WeakReference<>(obj);
- 虚引用:和弱引用类似,主要区别在于虚引用必须和引用队列一起使用。
Object obj = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj, referenceQueue);
- 引用队列:如果软引用和弱引用被GC回收,JVM就会把这个引用加到引用队列里,如果是虚引用,在回收前就会被加到引用队列里。
垃圾检测方法:
- 引用计数法:给每个对象添加引用计数器,每个地方引用它,计数器就+1,失效时-1。如果两个对象互相引用时,就导致无法回收。
- 可达性分析算法:以根集对象为起始点进行搜索,如果对象不可达的话就是垃圾对象。根集(Java栈中引用的对象、方法区中常量池中引用的对象、本地方法中引用的对象等。JVM在垃圾回收的时候,会检查堆中所有对象是否被这些根集对象引用,不能够被引用的对象就会被垃圾回收器回收。)
垃圾回收算法:
-
标记-清除
标记:首先标记所有需要回收的对象,在标记完成之后统计回收所有被标记的对象,它的标记过程即为上面的可达性分析算法。
清除:清除所有被标记的对象
缺点:效率不足,标记和清除效率都不高
空间问题,标记清除之后会产生大量不连续的内存碎片,导致大对象分配无法找到足够的空间,提前进行垃圾回收。 -
复制回收算法
将可用的内存按容量划分为大小相等的2块,每次只用一块,当这一块的内存用完了,就将存活的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉。
缺点:
将内存缩小了原本的一般,代价比较高
大部分对象是“朝生夕灭”的,所以不必按照1:1的比例划分。
现在商业虚拟机采用这种算法回收新生代,但不是按1:1的比例,而是将内存区域划分为eden 空间、from 空间、to 空间 3 个部分。
其中 from 空间和 to 空间可以视为用于复制的两块大小相同、地位相等,且可进行角色互换的空间块。from 和 to 空间也称为 survivor 空间,即幸存者空间,用于存放未被回收的对象。
在垃圾回收时,eden 空间中的存活对象会被复制到未使用的 survivor 空间中 (假设是 to),正在使用的 survivor 空间 (假设是 from) 中的年轻对象也会被复制到 to 空间中 (大对象,或者老年对象会直接进入老年带,如果 to 空间已满,则对象也会直接进入老年代)。此时,eden 空间和 from 空间中的剩余对象就是垃圾对象,可以直接清空,to 空间则存放此次回收后的存活对象。这种改进的复制算法既保证了空间的连续性,又避免了大量的内存空间浪费。 -
标记-整理
在老年代的对象大都是存活对象,复制算法在对象存活率教高的时候,效率就会变得比较低。根据老年代的特点,有人提出了“标记-压缩算法(Mark-Compact)”
标记过程与标记-清除的标记一样,但后续不是对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。
这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。 -
分带收集算法
根据对象存活的周期不同将内存划分为几块,一般是把Java堆分为老年代和新生代,这样根据各个年代的特点采用适当的收集算法。
新生代每次收集都有大量对象死去,只有少量存活,那就选用复制算法,复制的对象数较少就可完成收集。
老年代对象存活率高,使用标记-压缩算法,以提高垃圾回收效率。
5、类加载器
程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有 class 文件被载入到了内存之后,才能被其它class所引用。所以 ClassLoader 就是用来动态加载class文件到内存当中用的。
5.1 双亲委派原理
每个 ClassLoader 实例都有一个父类加载器的引用(不是继承关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但是可以用做其他 ClassLoader 实例的父类加载器。
当一个 ClassLoader 实例需要加载某个类时,它会试图在亲自搜索这个类之前先把这个任务委托给它的父类加载器,这个过程是由上而下依次检查的,首先由顶层的类加载器Bootstrap CLassLoader
进行加载,如果没有加载到,则把任务转交给Extension CLassLoader
视图加载,如果也没有找到,则转交给AppCLassLoader
进行加载,还是没有的话,则交给委托的发起者,由它到指定的文件系统或者网络等 URL 中进行加载类。还没有找到的话,则会抛出CLassNotFoundException
异常。否则将这个类生成一个类的定义,并将它加载到内存中,最后返回这个类在内存中的 Class 实例对象。
5.2 为什么使用双亲委托模型
JVM在判断两个class是否相同时,不仅要判断两个类名是否相同,还要判断是否是同一个类加载器加载的。
避免重复加载,父类已经加载了,则子CLassLoader没有必要再次加载。
考虑安全因素,假设自定义一个String类,除非改变JDK中CLassLoader的搜索类的默认算法,否则用户自定义的CLassLoader如法加载一个自己写的String类,因为String类在启动时就被引导类加载器 Bootstrap CLassLoader加载了。
6、集合
Java集合类主要由两个接口派生出:Collection 和 Map,这两个接口是Java集合的根接口。
Collection 接口是集合类的根接口,Java 中没有提供这个接口的直接的实现类。但是却让其被继承产生了两个接口,就是 Set 和List。Set中不能包含重复的元素。List是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式。
Map 是 Java.util 包中的另一个接口,它和 Collection 接口没有关系,是相互独立的,但是都属于集合类的一部分。Map 包含了 key-value 对。Map不能包含重复的key,但是可以包含相同的value。
6.1 区别
List,Set 都是继承自Collection接口,Map 则不是;
List 特点:元素有放入顺序,元素可重复; Set 特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法;
LinkedList、ArrayList、HashSet是非线程安全的,Vector是线程安全的;
HashMap是非线程安全的,HashTable是线程安全的;
6.2、List 和 Vector 比较
Vector 是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果。而 ArrayList 不是,这个可以从源码中看出,Vector 类中的方法很多有 synchronized 进行修饰,这样就导致了Vector在效率上无法与 ArrayList 相比;
两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式是不同。
Vector 可以设置增长因子,而 ArrayList 不可以。
Vector 是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。
6.3、HashSet如何保证不重复
HashSet 底层通过 HashMap 来实现的,在往 HashSet 中添加元素是
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
在 HashMap 中进行查找是否存在这个 key,value 始终是一样的,主要有以下几种情况:
- 如果 hash 码值不相同,说明是一个新元素,存;
- 如果 hash 码值相同,且 equles 判断相等,说明元素已经存在,不存;
- 如果 hash 码值相同,且 equles 判断不相等,说明元素不存在,存;
- 如果有元素和传入对象的 hash 值相等,那么,继续进行 equles() 判断,如果仍然相等,那么就认为传入元素已经存在,不再添加,结束,否则仍然添加;
6.4 HashSet 与 Treeset 的适用场景
- HashSet 是基于Hash算法实现的,其性能通常都优于 TreeSet。为快速查找而设计的 Set ,我们通常都应该使用 HashSet ,在我们需要排序的功能时,我们才使用 TreeSet 。
- TreeSet 是二叉树(红黑树的树据结构)实现的,Treeset 中的数据是自动排好序的,不允许放入 null 值
- HashSet 是哈希表实现的,HashSet 中的数据是无序的,可以放入 null,但只能放入一个 null,两者中的值都不能重复,就如数据库中唯一约束。
- HashSet 是基于 Hash 算法实现的,其性能通常都优于 TreeSet。为快速查找而设计的 Set,我们通常都应该使用 HashSet,在我们需要排序的功能时,我们才使用 TreeSet。
6.5 HashMap与TreeMap、HashTable的区别及适用场景
HashMap 非线程安全,基于哈希表(散列表)实现。使用HashMap要求添加的键类明确定义了 hashCode() 和 equals() [可以重写hashCode()和equals()],为了优化 HashMap 空间的使用,您可以调优初始容量和负载因子。其中散列表的冲突处理主要分两种,一种是开放定址法,另一种是链表法。HashMap的实现中采用的是链表法。
TreeMap :非线程安全基于红黑树实现,TreeMap 没有调优选项,因为该树总处于平衡状态
7、 常量池
7.1 Interger 中的128(-128~127)
当数值范围为-128~127时:如果两个new出来Integer对象,即使值相同,通过“ == ”比较结果为false,但两个对象直接赋值,则通过“ == ”比较结果为“true,这一点与String非常相似。当数值不在-128~127时,无论通过哪种方式,即使两个对象的值相等,通过“ == ”比较,其结果为false;当一个Integer对象直接与一个int基本数据类型通过“ == ”比较,其结果与第一点相同;Integer对象的hash值为数值本身;
@Override
public int hashCode() {
return Integer.hashCode(value);
}
7.2 为什么是 -128 ~ 127?
在 Integer 类中有一个静态内部类 IntegerCache,在 IntegerCache 类中有一个 Integer 数组,用以缓存当数值范围为 -128~127 时的 Integer 对象。
8、泛型
泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。 Java语言引入泛型的好处是安全简单。
泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
它提供了编译期的类型安全,确保你只能把正确类型的对象放入 集合中,避免了在运行时出现ClassCastException
。
使用Java的泛型时应注意以下几点:
- 泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。
- 同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
- 泛型的类型参数可以有多个。
- 泛型的参数类型可以使用extends语句,例如。习惯上称为“有界类型”。
- 泛型的参数类型还可以是通配符类型。例如
Class<?> classType = Class.forName("java.lang.String");
8.1 T泛型和通配符泛型
- ? 表示不确定的java类型。
- T 表示java类型。
- K V 分别代表java键值中的Key Value。
- E 代表Element。
8.2 泛型擦除
Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。
泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如 List在运行时仅用一个List来表示。这样做的目的,是确保能和Java 5之前的版本开发二进制类库进行兼容。你无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。
8.3 限定通配符
一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界,
另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界。
另一方面表 示了非限定通配符,因为可以用任意类型来替代。
例如List<? extends Number>可以接受List或List。
8.4 泛型面试题
你可以把List传递给一个接受List参数的方法吗?
对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来String是一种Object,所以
List应当可以用在需要List的地方,但是事实并非如此。真这样做的话会导致编译错误。如
果你再深一步考虑,你会发现Java这样做是有意义的,因为List可以存储任何类型的对象包括String,
Integer等等,而List却只能用来存储Strings。
Array中可以用泛型吗?
Array 事实上并不支持泛型,这也是为什么Joshua Bloch在Effective
Java 一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。
9、反射
9.1 概念
JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
9.2 作用
Java 反射机制主要提供了以下功能: 在运行时判断任意一个对象所属的类;在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的成员变量和方法;在运行时调用任意一个对象的方法;生成动态代理。
推荐阅读
- Android 面试之必问Java基础