Java基本知识点

Java基本知识点

类的加载过程

以Person person = new Person()为例进行说明

  1. 因为new用到了Person.class,所以会先找到Person.class文件,并加载到内存中;
  2. 执行该类中的static代码块,如果有的话,给Person.class类进行初始化;
  3. 在堆内存中开辟空间分配内存地址;
  4. 在堆内存中建立对象的特有属性,并进行默认初始化;
  5. 对属性进行显示初始化;
  6. 对对象进行构造代码块初始化;
  7. 对对象进行与之对应的构造函数进行初始化;
  8. 将内存地址付给栈内存中的p变量

JVM相关知识,GC机制

JVM基本构成

alt

从上图可知,JVM主要包括四个部分:

  1. 类加载器(ClassLoader):在JVM启动时或者在类运行时将需要的class加载到JVM中。 (下图表示了从 java 源文件到 JVM 的整个过程,可配合理解。)alt

  2. 执行引擎:负责执行class文件中包含的字节码指令;

  3. 内存区(也叫运行时数据区):是在JVM运行的时候操作所分配的内存区。 运行时内存区主要可以划分为5个区域,如图:alt

  • 方法区(Method Area): 用于存储类结构信息的地方,包括常量池、静态变量、构造函数等。 虽然 JVM 规范把方法区描述为堆的一个逻辑部分, 但它却有个别名non-heap(非堆),所以大家不要搞混淆了。 方法区还包含一个运行时常量池。

  • java堆(Heap): 存储java实例或者对象的地方。这块是GC的主要区域。 从存储的内容我们可以很容易知道,方法区和堆是被所有java线程共享的。

  • java栈(Stack): java栈总是和线程关联在一起,每当创建一个线程时, JVM就会为这个线程创建一个对应的java栈。在这个java栈中又会包含多个栈帧, 每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。 每一个方法从调用直至执行完成的过程, 就对应一个栈帧在java栈中入栈到出栈的过程。所以java栈是线程私有的。

  • 程序计数器(PC Register): 用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换), 所以为了保证线程切换回来后,还能恢复到原先状态, 就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。

  • 本地方法栈(Native Method Stack): 和java栈的作用差不多,只不过是为JVM使用到的native方法服务的。

  1. 本地方法接口:主要是调用C或C++实现的本地方法及返回结果。

GC机制

垃圾收集器一般必须完成两件事:检测出垃圾; 回收垃圾。怎么检测出垃圾?一般有以下几种方法:

引用计数法

给一个对象添加引用计数器,每当有个地方引用它, 计数器就加1;引用失效就减1。 好了,问题来了,如果我有两个对象A和B,互相引用, 除此之外,没有其他任何对象引用它们, 实际上这两个对象已经无法访问,即是我们说的垃圾对象。 但是互相引用,计数不为0,导致无法回收,所以还有另一种方法:

可达性分析算法

以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。 这里的根集一般包括java栈中引用的对象、 方法区常良池中引用的对象、本地方法中引用的对象等。

总之,JVM在做垃圾回收的时候,会检查堆中的所有对象是否会被这些根集对象引用, 不能够被引用的对象就会被垃圾收集器回收。 一般回收算法也有如下几种:

  1. 标记-清除(Mark-sweep)
  2. 复制(Copying
  3. 标记-整理(Mark-Compact)
  4. 分代收集算法

具体的解释可以参考本篇文章还不点我?

类的加载器,双亲机制,Android的类加载器

类的加载器

大家都知道,当我们写好一个 Java 程序之后,不是管是 CS 还是 BS应用, 都是由若干个 .class 文件组织而成的一个完整的 Java 应用程序, 当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能, 而这些功能都被封装在不同的 class 文件当中, 所以经常要从这个 class 文件中要调用另外一个 class 文件中的方法, 如果另外一个文件不存在的,则会引发系统异常。 而程序在启动的时候,并不会一次性加载程序所要用的所有 class 文件, 而是根据程序的需要, 通过 Java 的类加载机制(ClassLoader)来动态加载某个 class 文件到内存当中的, 从而只有 class 文件被载入到了内存之后,才能被其它 class 所引用。 所以 ClassLoader 就是用来动态加载 class 文件到内存当中用的。

双亲机制

原理介绍

ClassLoader 使用的是双亲委托模型来搜索类的, 每个 ClassLoader实例都有一个父类加载器的引用 (不是继承的关系,是一个包含的关系), 虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器, 但可以用作其它ClassLoader实例的的父类加载器。 当一个ClassLoader实例需要加载某个类时, 它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器, 这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载, 如果没加载到,则把任务转交给Extension ClassLoader试图加载, 如果也没加载到,则转交给App ClassLoader 进行加载, 如果它也没有加载得到的话,则返回给委托的发起者, 由它到指定的文件系统或网络等URL中加载该类。 如果它们都没有加载到这个类时,则抛出ClassNotFoundException 异常。 否则将这个找到的类生成一个类的定义,并将它加载到内存当中, 最后返回这个类在内存中的 Class实例对象。

为什么要使用双亲委托这种模型呢?

因为这样可以避免重复加载,当父亲已经加载了该类的时候, 就没有必要子ClassLoader再加载一次。 考虑到安全因素,我们试想一下,如果不使用这种委托模式, 那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型, 这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况, 因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载, 所以用户自定义的ClassLoader永远也无法加载一个自己写的 String, 除非你改变 JDK 中 ClassLoader 搜索类的默认算法。

但是 JVM 在搜索类的时候,又是如何判定两个 class 是相同的呢?

JVM 在判定两个 class 是否相同时,不仅要判断两个类名是否相同, 而且要判断是否由同一个类加载器实例加载的。 只有两者同时满足的情况下,JVM 才认为这两个 class 是相同的。 就算两个 class 是同一份 class 字节码, 如果被两个不同的 ClassLoader 实例所加载, JVM 也会认为它们是两个不同 class。 比如网络上的一个 Java 类org.classloader.simple.NetClassLoaderSimple, javac 编译之后生成字节码文件 NetClassLoaderSimple.class, ClassLoaderA 和ClassLoaderB 这两个类加载器并读取了 NetClassLoaderSimple.class文件, 并分别定义出了 java.lang.Class 实例来表示这个类, 对于 JVM 来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件, 如果试图将这个 Class 实例生成具体的对象进行转换时, 就会抛运行时异常 java.lang.ClassCaseException,提示这是两个不同的类型。

Android类加载器

对于 Android 而言,最终的 apk 文件包含的是 dex 类型的文件, dex文件是将 class 文件重新打包,打包的规则又不是简单地压缩, 而是完全对class 文件内部的各种函数表,变量表进行优化, 产生一个新的文件,即dex文件。 因此加载这种特殊的 Class 文件就需要特殊的类加载器DexClassLoader。

集合框架,list,map,set

list,map,set都有哪些具体的实现类,区别都是什么?

  • List,Set都是继承自 Collection 接口,Map 则不是;

  • List特点:元素有放入顺序,元素可重复;

    Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉, (注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的 HashCode决定的,其位置其实是固定的,加入Set 的Object必须定 义equals()方法;

    另外list支持for循环,也就是通过下标来遍历,也可以用迭代器, 但是 set只能用迭代,因为他无序,无法用下标来取得想要的值)。

  • Set和List对比:

    Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元 素位置改变。

    List:和数组类似,List可以动态增长,查找元素效率高,插入删除元 素效率低,因为会引起其他元素位置改变。

  • Map适合储存键值对的数据。

  • 线程安全集合类与非线程安全集合类

    LinkedList、ArrayList、HashSet是非线程安全的,Vector是线程安全的;

    HashMap是非线程安全的,HashTable是线程安全的;

    StringBuilder是非线程安全的,StringBuffer是线程安全的。

具体的使用介绍

1. ArrayList与LinkedList的区别和适用场景

Arraylist:

优点:ArrayList是实现了基于动态数组的数据结构,因为地址连续, 一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。

缺点:因为地址连续, ArrayList要移动数据, 所以插入和删除操作效率比较低。

LinkedList:

优点:LinkedList基于链表的数据结构,地址是任意的, 所以在开辟内存空间的时候不需要等一个连续的地址,对于新增和删除操作add和remove, LinedList比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景。

缺点:因为LinkedList要移动指针,所以查询操作性能比较低。

适用场景分析:

当需要对数据进行对此访问的情况下选用 ArrayList, 当需要对数据进行多次增加删除修改时采用 LinkedList。

2. ArrayList 与 Vector 的区别和适用场景

ArrayList有三个构造方法:

//构造一个具有指定初始容量的空列表
public ArrayList(int initialCapacity)
//构造一个初始容量为10的空列表
public ArrayList()
//构造一个包含指定 collection 的元素的列表
public ArrayList(Collection<? extends E> c)

Vector有四个构造方法:

//使用指定的初始容量和等于零的容量增量构造一个空向量。
public Vector()
//构造一个空向量,使其内部数据数组的大小,其标准容量增量为零。
public Vector(int initialCapacity)
//构造一个包含指定 collection 中的元素的向量
public Vector(Collection<? extends E> c)
//使用指定的初始容量和容量增量构造一个空的向量
public Vector(int initialCapacity,int capacityIncrement)

ArrayList 和 Vector 都是用数组实现的,主要有这么三个区别:

  1. Vector是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果。 而ArrayList不是,这个可以从源码中看出,Vector类中的方法很多有synchronized进行修饰, 这样就导致了Vector在效率上无法与ArrayList相比;
  2. 两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式是不同。
  3. Vector可以设置增长因子,而ArrayList不可以。
  4. Vector是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。

适用场景:

  1. Vector是线程同步的,所以它也是线程安全的, 而ArrayList是线程异步的,是不安全的。 如果不考虑到线程的安全因素,一般用ArrayList效率比较高。
  2. 如果集合中的元素的数目大于目前集合数组的长度时, 在集合中使用数据量比较大的数据,用Vector有一定的优势。

3. HashSet与Treeset的适用场景

  1. TreeSet 是二叉树(红黑树的树据结构)实现的, Treeset中的数据是自动排好序的,不允许放入null值。
  2. HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null, 但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束。
  3. HashSet要求放入的对象必须实现HashCode()方法, 放入的对象,是以hashcode码作为标识的,而具有相同内容的String对象, hashcode是一样,所以放入的内容不能重复。 但是同一个类的对象可以放入不同的实例。

适用场景分析:

HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。 为快速查找而设计的Set,我们通常都应该使用HashSet, 在我们需要排序的功能时,我们才使用TreeSet。

4. HashMap与TreeMap、HashTable的区别及适用场景

HashMap: 非线程安全

HashMap:基于哈希表(散列表)实现。 使用 HashMap 要求添加的键类明确定义了 hashCode() 和 equals() [可以重写 hashCode() 和equals()], 为了优化 HashMap 空间的使用,您可以调优初始容量和负载因子。 其中散列表的冲突处理主要分两种, 一种是开放定址法,另一种是链表法。 HashMap 的实现中采用的是链表法。

TreeMap:非线程安全基于红黑树实现。TreeMap 没有调优选项,因为该树总处于平衡状态。

适用场景分析:

HashMap和HashTable:HashMap去掉了HashTable的contains方法, 但是加上了containsValue()和containsKey()方法。 HashTable同步的,而HashMap是非同步的,效率上比HashTable要高。 HashMap允许空键值,而HashTable不允许。

HashMap:适用于Map中插入、删除和定位元素。

Treemap:适用于按自然顺序或自定义顺序遍历键(key)。

(ps:其实我们工作的过程中对集合的使用是很频繁的, 稍加注意并总结积累一下,在面试的时候应该会回答的很轻松)

concurrentHashmap原理,原子类

ConcurrentHashMap 作为一种线程安全且高效的哈希表的解决方案, 尤其其中的"分段锁"的方案,相比 HashTable 的全表锁在性能上的提升非常之大.

volatile原理

在《Java并发编程:核心理论》一文中, 我们已经提到过可见性、有序性及原子性问题。

通常情况下我们可以通过Synchronized关键字来解决这些个问题, 不过如果对Synchronized原理有了解的话, 应该知道Synchronized是一个比较重量级的操作, 对系统的性能有比较大的影响,所以,如果有其他解决方案, 我们通常都避免使用Synchronized来解决问题。

而volatile关键字就是Java中提供的另一种解决可见性和有序性问题的方案。 对于原子性,需要强调一点,也是大家容易误解的一点: 对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量, 但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。

参考文章

https://link.jianshu.com/?t=https://www.cnblogs.com/paddix/p/5428507.html

多线程的使用场景

使用多线程就一定效率高吗? 有时候使用多线程并不是为了提高效率, 而是使得CPU能够同时处理多个事件。

  1. 为了不阻塞主线程,启动其他线程来做好事的事情, 比如APP中耗时操作都不在UI中做.

  2. 实现更快的应用程序,即主线程专门监听用户请求, 子线程用来处理用户请求,以获得大的吞吐量. 感觉这种情况下,多线程的效率未必高。 这种情况下的多线程是为了不必等待,可以并行处理多条数据。 比如JavaWeb的就是主线程专门监听用户的HTTP请求, 然后启动子线程去处理用户的HTTP请求。

  3. 某种虽然优先级很低的服务, 但是却要不定时去做。比如Jvm的垃圾回收。

  4. 某种任务,虽然耗时,但是不耗CPU的操作时, 开启多个线程,效率会有显著提高。 比如读取文件,然后处理。 磁盘IO是个很耗费时间,但是不耗CPU计算的工作。 所以可以一个线程读取数据,一个线程处理数据。 肯定比一个线程读取数据,然后处理效率高。 因为两个线程的时候充分利用了CPU等待磁盘IO的空闲时间。

JAVA常量池

Interger 中的128(-128~127)

a. 当数值范围为-128~127时: 如果两个 new 出来 Integer 对象, 即使值相同,通过“==”比较结果为false, 但两个对象直接赋值,则通过“==”比较结果为“true, 这一点与String非常相似。

b. 当数值不在-128~127时,无论通过哪种方式, 即使两个对象的值相等,通过“==”比较, 其结果为false;

c. 当一个Integer对象直接与一个int基本数据类型通过“==”比较, 其结果与第一点相同;

d. Integer对象的hash值为数值本身;

为什么是-128-127?

在 Integer 类中有一个静态内部类 IntegerCache, 在 IntegerCache类中有一个 Integer 数组, 用以缓存当数值范围为-128~127时的Integer 对象。

简单介绍一下java中的泛型,泛型擦除以及相关的概念

泛型是 Java SE 1.5的新特性,泛型的本质是参数化类型, 也就是说所操作的数据类型被指定为一个参数。 这种参数类型可以用在类、接口和方法的创建中, 分别称为泛型类、泛型接口、泛型方法。 Java语言引入泛型的好处是安全简单。

在 Java SE 1.5 之前,没有泛型的情况的下, 通过对类型 Object 的引用来实现参数的“任意化”, “任意化”带来的缺点是要做显式的强制类型转换, 而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。 对于强制类型转换错误的情况,编译器可能不提示错误, 在运行的时候才出现异常,这是一个安全隐患。

泛型的好处是在编译的时候检查类型安全, 并且所有的强制转换都是自动和隐式的, 提高代码的重用率。

  1. 泛型的类型参数只能是类类型(包括自定义类), 不能是简单类型。
  2. 同一种泛型可以对应多个版本(因为参数类型是不确定的), 不同版本的泛型类实例是不兼容的。
  3. 泛型的类型参数可以有多个。
  4. 泛型的参数类型可以使用extends语句, 例如。习惯上称为“有界类型”。
  5. 泛型的参数类型还可以是通配符类型。 例如Class<?> classType = Class.forName("java.lang.String");

泛型擦除以及相关的概念

Java 中的泛型基本上都是在编译器这个层次来实现的。 在生成的 Java字节码中是不包含泛型中的类型信息的。 使用泛型的时候加上的类型参数, 会在编译器在编译的时候去掉。 这个过程就称为类型擦除。

类型擦除引起的问题及解决方法

  1. 先检查,在编译,以及检查编译的对象和引用传递的问题
  2. 自动类型转换
  3. 类型擦除与多态的冲突和解决方法
  4. 泛型类型变量不能是基本数据类型
  5. 运行时类型查询
  6. 异常中使用泛型的问题
  7. 数组(这个不属于类型擦除引起的问题)
  8. 类型擦除后的冲突
  9. 泛型在静态方法和静态类中的问题
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值