一、java基础
1.面向对象的四大特性及其含义
封装:对细节实现隐藏,只像外界提供简单的访问接口,内部的实现方式并不会对外界暴露,封装实现代码的模块化,也方便代码复用。实现代码的高内聚低耦合。
继承:子类继承父类,拥有父类的方法和属性,继承是java代码复用的重要手段,也是多态的实现方式。但是继承破坏了代码的封装性。高效java中提到慎用集成,除非一个类是专门为继承设计的(抽象类,一些基类),否则应该考虑用对象组合代替继承。
抽象:找到事物的共性,忽略细节的不同,把具体事物或者行为抽象成类或接口,只关注属性和行为是什么,不关注具体细节,细节由具体对象或实现接口的类去实现。
多态:多态通过继承显现,包括继承类和实现接口,通过重写父类方法实现,多态是对同一方法的不同实现方式。多态是代码解耦,代码可拓展性的重要手段。
2.String、StringBuffer和StringBuilder的区别?
String是用final修饰的,不可变,通过String常量池实现对相同String的复用,频繁做字符串改变时效率低下,因为要不断开辟新的内存创建新的String对象;StringBuffer和StringBuilder可以代替String做频繁的字符串修改,StringBuffer线程安全,效率低。StringBuilder效率高,非线程安全。
3.String a="“和String a=new String(”")的的关系和异同?
都是一个字符串变量指向一个字符串内存地址,前者会先检查String常量池,如果存在则服用,后者直接创建一个新的内存,将地址复制给a
4.Object的equal()和==的区别?
equal比较的是对象的具体内容(比较hashcode值);==比较的是对象的内存地址,判断是否指向同一个对象。
5.装箱、拆箱什么含义?
java为每一种基本数据类型都提供了对应的包装类型,装箱就是自动将基本数据类型转换成对应的包装类型,拆箱就是将包装类型转换成基本数据类型 Integer i = 10; //装箱 int n = i; //拆箱
6.int和Integer的区别?
int是基本数据类型,Integer是int的包装类,Integer和其他对象一样,在堆中分配内存,栈中保存对象的引用,而int的值直接保存在栈中,int的默认值是0,integer的默认值是null.
7.遇见过哪些运行时异常?异常处理机制知道哪些?
java.lang.NullPointerException ,java.lang.ClassNotFoundException ,java.lang.ArrayIndexOutOfBoundsException,java.lang.NoSuchMethodError ,java.io.IOException 等
处理异常的机制:
抛出异常:在执行一个方法时,如果发生异常,则这个方法生成代表该异常的一个对象,停止执行当前路径,并把异常对象交给JRE.
捕获异常:JRE得到该异常后,寻找相应的代码来处理该异常。JRE在方法的调用栈中查找,从生成异常的方法开始回溯,直到找到相应的异常处理代码为止。
使用“try/catch”捕获异常、使用“throws”声明异常
1)一个try语句必须带有至少一个catch语句块或一个finally语句块,当异常处理的代码执行结束以后,不会回到try语句去执行尚未执行的代码。
2)每个try语句块可以伴随一个或多个catch语句,用于处理可能产生的不同类型的异常对象。
3)不管是否发生了异常,即使try和catch块中存在return语句,finally都必须要执行
4)finally语句块只有一种情况是不会执行的,那就是在执行finally之前遇到了System.exit(0)结束程序运行
throw:
只能用于方法体内。
后面跟异常对象。
只能跟一个异常对象
throws:
用在方法的声明处。参数列表的后面方法体的前面。
后面跟异常的类型。
可以跟多个异常类型。使用 逗号 分隔。
8.什么是反射,有什么作用和应用?
反射机制就是在程序运行的过程中允许对程序本身进行操作和自我检查,可以获取类的所有成员属性和方法,类的对象。还可以在运行过程中动态创建类的实例,通过实例调用类的方法。
反射机制的原理:Java程序在运行时,Java运行时系统一直对所有的对象进行所谓的运行时类型标识。这项信息纪录了每个对象所属的类。虚拟机通常使用运行时类型信息选准正确方法去执行,用来保存这些类型信息的类是Class类。也就是说,ClassLoader找到了需要调用的类时(java为了调控内存的调用消耗,类的加载都在需要时再进行,很抠但是很有效),就会加载它,然后根据.class文件内记载的类信息来产生一个与该类相联系的独一无二的Class对象。该Class对象记载了该类的字段,方法等等信息。以后jvm要产生该类的实例,就是根据内存中存在的该Class类所记载的信息(Class对象应该和我所了解的其他类一样会在堆内存内产生、消亡)来进行。而java中的Class类对象是可以人工自然性的(也就是说开放的)得到的(虽然你无法像其他类一样运用构造器来得到它的实例,因为Class对象都是jvm产生的。不过话说回来,客户产生的话也是无意义的),而且,更伟大的是,基于这个基础,java实现了反射机制。https://blog.csdn.net/ghuilee/article/details/45821537
个人理解:所谓的反射,其实就是通过java.lang.Class对象获得想要的类的信息,其关键是Class对象,从这个角度讲,它与其他 类的方法调用没有任何区别,虚拟机在使用一个类时,会先判断该类是否被加载,如果没有则加载(双亲委托机制查找和加载),这样每一个类都会有一个Class类型的实例对象(即使程序中一个类有多个实例也只会对应一个Class类型的对象),这个Class类型的对象中保存着该java类的所有信息,所有的方法对应的是一个Method类型的属性,类中的属性,则对应Feild类型的属性。所有进行反射的操作,其实都是在对这个Class类型的对象进行操作,与操作其他java对象并无本质区别。
9.什么是内部类?有什么作用?静态内部类和非静态内部类的区别?
将一个类定义在类的内部或者方法体内部,这样的类叫做内部类。
内部类可以分为四种:成员内部类,局部内部类,静态内部类和匿名内部类
非静态内部类依赖他的外部类存在,创建内部类要先创建他的外部类。内部类可以使用外部类的所有属性和方法,默认持有外部类的引用。 staitic内部类是内部类中一个比较特殊的情况,java文档中这样描述静态内部类:一旦内部类使用了static,那么这个内部类就自动升级为顶级类。也就是说,静态内部类除了写法不一样之外,他拥有一切外部类的特征。
10.final、finally、finalize()分别表示什么含义?
final用于修饰属性,方法体或者类,修饰属性时表示该属性不可改变,如String类。修饰方法表示方法不可被重写。修饰类表示该类不能被继承。finally通常与try-catch共同使用,finally中的代码块会保证得到执行,可用于做一些资源回收操作。finalize()是object类的生命周期方法,在对象被回收时调用,finalize()方法中一般用于释放非java资源(如打开的文件资源、数据库连接等),但是开发中应该避免使用该方法,因为finalize()方法的调用时机是不可确定的,从一个对象引用不可达到该对象被回收的时间是不固定的,也代表 这finalize()的方法不会得到及时的调用,正确的做法是提供显式的close方法供客户端手动调用释放资源。利用finalize()方法最多只会被调用一次的特性,我们可以实现延长对象的生命周期。
11.重写和重载的区别?
重写是对继承的父类或者接口的方法的重新定义,方法名,返回值和参数都与父类中的相同。重载是指同一个类中,定义方法名相同但是参数不同的方法
12.抽象类和接口的异同?
抽象类可以有具体的方法实现,接口只能有方法声明,不能有具体的实现。抽象类是通过继承使用,java中单继承,使用有所限制,接口是通过实现接口使用,可以多实现。一般如果需要做一些公共的实现使用抽象类,否则使用接口
13.为什么匿名内部类中使用局部变量要用final修饰?
用final修饰实际上就是为了保护数据的一致性。这里所说的数据一致性,对引用变量来说是引用地址的一致性,对基本类型来说就是值的一致性。
说明:对匿名内部类的字节码文件反编译得到的代码中可以看到,匿名内部类之所以可以访问局部变量,是因为在底层将这个局部变量的值传入到了匿名内部类中,并且以匿名内部类的成员变量的形式存在,这个值的传递过程是通过匿名内部类的构造器完成的。那么就很好理解了,同样的数据有两份存在,当匿名内部类中使用备份的数据时,无法知晓外部变量的改变,这就可能造成不可预期的运行结果,所以必须给外部变量加上final关键字,保证两份数据的统一
14.对java泛型的理解
泛型就是参数化类型,使得代码可以适应多种类型,他的主要目的是指定容器要持有什么类型的对象。java的泛型是停留在预编译阶段的,虚拟机在对待泛型数据时,依然把它看成object类型的,在使用这些元素时,jvm会自动帮助开发者完成类型的转换。泛型Java1.5的特性,泛型可以用在类,接口和方法中,分别称为泛型类,泛型接口,泛型方法。
泛型的优点:保证了类型的安全,可以指定容器中存放的是特定的对象,如果不是,则编译器会报错提醒;
消除了类的强制转换;提高了代码的通用性,也提高了性能。
规则和限制
1、泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。
2、同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
3、泛型的类型参数可以有多个。
4、泛型的参数类型可以使用extends语句,例如<T extends superclass>。习惯上称为“有界类型”。
5、泛型的参数类型还可以是通配符类型。例如Class<?> classType = Class.forName(java.lang.String);
泛型详细使用讲解https://blog.csdn.net/thunderous/article/details/5342134
Q:谈谈泛型通配符的理解和使用场景
https://www.jianshu.com/p/d468eae37a10
15.四种引用类型
强应用:只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了
软应用:SoftReference<>,软应用对象会在内存不足时通过GC回收,通常在做缓存时时使用,可以保证不会占用系统可贵内存资源。
弱引用:WeakReference<>,弱引用会在GC时被立即回收,不管是否内存不足。
虚引用:虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
引用队列
引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。
与软引用、弱引用不同,虚引用必须和引用队列一起使用。
集合
Q:Java集合框架中有哪些类?都有什么特点
java集合框架由两大接口衍生而来:Collection接口和Map接口,Collection下主要分为List和set集合。
List集合:List接口继承自Collection,用于定义以列表形式存储的集合,List接口为集合中的每个对象分配了一个索引,标记该对象在List中的位置,并且可以通过Index定位到指定的位置找到对象。
List接口常用实现类:
ArrayList:ArrayList基于数组来实现集合的功能,其内部维护了一个可变长度的数组对象,集合内所有对象存储于这个数组中,并实现该数组长度的动态伸缩,ArrayList使用数组拷贝来实现指定位置的插入和删除
LinkedList:LinkedList基于链表来实现集合的功能,其实现了静态类Node,集合中的每个对象都由一个Node保存,每个Node都拥有到自己的前一个和后一个Node的引用
ArrayList vs LinkedList
ArrayList的随机访问更高,基于数组实现的ArrayList可直接定位到目标对象,而LinkedList需要从头Node或尾Node开始向后/向前遍历若干次才能定位到目标对象
LinkedList在头/尾节点执行插入/删除操作的效率比ArrayList要高
由于ArrayList每次扩容的容量是当前的1.5倍,所以LinkedList所占的内存空间要更小一些
二者的遍历效率接近,但需要注意,遍历LinkedList时应用iterator方式,不要用get(int)方式,否则效率会很低
Vector
Vector和ArrayList很像,都是基于数组实现的集合,它和ArrayList的主要区别在于
Vector是线程安全的,而ArrayList不是
由于Vector中的方法基本都是synchronized的,其性能低于ArrayList
Vector可以定义数组长度扩容的因子,ArrayList不能
CopyOnWriteArrayList
与 Vector一样,CopyOnWriteArrayList也可以认为是ArrayList的线程安全版,不同之处在于 CopyOnWriteArrayList在写操作时会先复制出一个副本,在新副本上执行写操作,然后再修改引用。这种机制让 CopyOnWriteArrayList可以对读操作不加锁,这就使CopyOnWriteArrayList的读效率远高于Vector。 CopyOnWriteArrayList的理念比较类似读写分离,适合读多写少的多线程场景。但要注意,CopyOnWriteArrayList只能保证数据的最终一致性,并不能保证数据的实时一致性,如果一个写操作正在进行中且并未完成,此时的读操作无法保证能读到这个写操作的结果。
Map集合:Map将key和value封装至一个叫做Entry的对象中,Map中存储的元素实际是Entry。只有在keySet()和values()方法被调用时,Map才会将keySet和values对象实例化。
每一个Map根据其自身特点,都有不同的Entry实现,以对应Map的内部类形式出现。
Set类集合:Set 接口继承Collection,用于存储不含重复元素的集合。几乎所有的Set实现都是基于同类型Map的,简单地说,Set是阉割版的Map。每一个Set内都有一个同类型的Map实例(CopyOnWriteArraySet除外,它内置的是CopyOnWriteArrayList实例),Set把元素作为key存储在自己的Map实例中,value则是一个空的Object。Set的常用实现也包括 HashSet、TreeSet、ConcurrentSkipListSet等,原理和对应的Map实现完全一致,此处不再赘述。
Queue/Deque类集合
Queue和Deque接口继承Collection接口,实现FIFO(先进先出)的集合。二者的区别在于,Queue只能在队尾入队,队头出队,而Deque接口则在队头和队尾都可以执行出/入队操作
Collection接口
Collection接口定义了一个包含一批对象的集合。接口的主要方法包括:
size() - 集合内的对象数量
add(E)/addAll(Collection) - 向集合内添加单个/批量对象
remove(Object)/removeAll(Collection) - 从集合内删除单个/批量对象
contains(Object)/containsAll(Collection) - 判断集合中是否存在某个/某些对象
toArray() - 返回包含集合内所有对象的数组
Map接口
Map接口在Collection的基础上,为其中的每个对象指定了一个key,并使用Entry保存每个key-value对,以实现通过key快速定位到对象(value)。Map接口的主要方法包括:
size() - 集合内的对象数量
put(K,V)/putAll(Map) - 向Map内添加单个/批量对象
get(K) - 返回Key对应的对象
remove(K) - 删除Key对应的对象
keySet() - 返回包含Map中所有key的Set
values() - 返回包含Map中所有value的Collection
entrySet() - 返回包含Map中所有key-value对的EntrySet
containsKey(K)/containsValue(V) - 判断Map中是否存在指定key/value
等
Q:集合、数组、泛型集合的关系,并比较
数组与集合的区别:
一:数组声明了它容纳的元素的类型,而集合不声明。这是由于集合以object形式来存储它们的元素。
二:一个数组实例具有固定的大小,不能伸缩。集合则可根据需要动态改变大小。
三:数组是一种可读/可写数据结构没有办法创建一个只读数组。然而可以使用集合提供的ReadOnly方 只读方式来使用集合。该方法将返回一个集合的只读版本。
泛型与集合的区别
泛型听起来很高深的一个词,但实际上它的作用很简单,就是提高c#程序的性能。
比如在计算机中经常用到一些数据结构,如队列,链表等,而其中的元素以前一般这么定义:object a=new object();
这样就带来一个严重的问题,用object来表示元素没有逻辑问题,但每次拆箱、封箱就占用了大量的计算机资源,导致程序性能低下,而这部分内容恰恰一般都是程序的核心部分,如果使用object,那么程序的表现就比较糟糕。
而使用泛型则很好的解决这个问题,本质就是在编译阶段就告诉编译器,数据结构中元素的种类,既然编译器知道了元素的种类,自然就避免了拆箱、封箱的操作,从而显著提高c#程序的性能。
比如List<string>就直接使用string对象作为List的元素,而避免使用object对象带来的封箱、拆箱操作,从而提高程序性能。
Q:ArrayList和LinkList的区别?
ArrayList是基于数组实现的,LinkList是基于链表实现的。
ArrayList的随机访问更高,基于数组实现的ArrayList可直接定位到目标对象,而LinkedList需要从头Node或尾Node开始向后/向前遍历若干次才能定位到目标对象
LinkedList在头/尾节点执行插入/删除操作的效率比ArrayList要高
由于ArrayList每次扩容的容量是当前的1.5倍,所以LinkedList所占的内存空间要更小一些
二者的遍历效率接近,但需要注意,遍历LinkedList时应用iterator方式,不要用get(int)方式,否则效率会很低
Q:ArrayList和Vector的区别?
二者都是基于数组实现的,主要区别在于
Vector是线程安全的,而ArrayList不是
由于Vector中的方法基本都是synchronized的,其性能低于ArrayList
Vector可以定义数组长度扩容的因子,ArrayList不能
Q:HashSet和TreeSet的区别?
HashSet:
不能保证元素的排列顺序,顺序有可能发生变化
集合元素可以是null,但只能放入一个null
HashSet底层是采用HashMap实现的
HashSet底层是哈希表实现的
当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置,如果相等,那就不能插入,如果不等,才会调用equals()方法,如果equals结果为true,说明已经存在,就不能再插入,如果为false,可以插入。
简单的说,HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值相等。
注意,如果要把一个对象放入HashSet中,重写该对象对应类的equals方法,也应该重写其hashCode()方法。其规则是如果两个对象通过equals方法比较返回true时,其hashCode也应该相同。另外,对象中用作equals比较标准的属性,都应该用来计算 hashCode的值。
Treeset
Treeset中的数据是排好序的,不允许放入null值。
TreeSet是通过TreeMap实现的,只不过Set用的只是Map的key。
TreeSet的底层实现是采用二叉树(红-黑树)的数据结构。
TreeSet的底层实现是采用红-黑树的数据结构,采用这种结构可以从Set中获取有序的序列,但是前提条件是:元素必须实现Comparable接口,该接口中只用一个方法,就是compareTo()方法。当往Set中插入一个新的元素的时候,首先会遍历Set中已经存在的元素,并调用compareTo()方法,根据返回的结果,决定插入位置。进而也就保证了元素的顺序。
TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序 和定制排序,其中自然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象。
TreeSet判断两个对象不相等的方式是两个对象通过equals方法返回false,或者通过CompareTo方法比较没有返回0
自然排序
自然排序使用要排序元素的CompareTo(Object obj)方法来比较元素之间大小关系,然后将元素按照升序排列。
Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现了该接口的对象就可以比较大小。
obj1.compareTo(obj2)方法如果返回0,则说明被比较的两个对象相等,如果返回一个正数,则表明obj1大于obj2,如果是 负数,则表明obj1小于obj2。
如果我们将两个对象的equals方法总是返回true,则这两个对象的compareTo方法返回应该返回0
定制排序
自然排序是根据集合元素的大小,以升序排列,如果要定制排序,应该使用Comparator接口,实现 int compare(T o1,T o2)方法。
Q:HashMap和Hashtable的区别?
HashMap
HashMap是基于哈希表实现的,其结构是数组加链表(jdk1.8后采用链表+红黑树),每一个元素是一个key-value对,其内部通过单链表/红黑书解决冲突问题,容量不足(超过了阀值)时,数组会扩容。
HashMap是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap。
HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆。
Q:HashMap在put、get元素的过程?
put过程:通过key的哈希值,做特定的位移运算后与数组长度求模(做位移运算的目的是让高位也能参与运算,影响散列结果,否则,一个很大的数对一个很小的数求模,高位时没有意义的),可以得到一个对应数组下标,每一个数组下标对应这一个链表或者红黑数,如果该下标对应有数据,则判断插入的数据的key是否存在,如果存在,会覆盖原来的节点,否则将数据插入链表尾部。jdk1.8后,当数组长度大于64,且链表长度大于8,则链表会转为红黑树,目的很明了,就是为了保证查找速度。红黑数是一个查找二叉树,节点非红即黑,不仅查找方便,在插入或删除数据后,如果树的结果被打破,通过节点的左旋转或右旋转可以快速回复新的平衡。需要注意的是,hashMap可以指定初始数组的长度,但是真实分配的数组长度不一定会等于开发者传入的数,而是比这个数大的最接近2的n次幂的数,也就是说,hashMap的数据长度是2的n次幂,这样做的好处是扩容的时候可以快速实现新的hash映射,hashMap的扩容因子是0.75,也就是数据长度到达数组长度的0.75就会引发扩容,为了尽可能避免扩容,要实现预测数据量,给一个合理的初始大小。
列:如果一个hashMap要存放30个数据,初始化的数组大小为30,是否会引起扩容?
答案是会,原因很简单,30个数据达到了扩容因子0.75的上限会引发扩容。那如果是20是数据数组大小为20呢?此时不会引发扩容,因为虽然声明数组大小为20,但是实际分配的时候,hashMap的数组大小总是2的n次幂,所以,实际分配的数据长度是32,而32*0.75=24;所以存储20个数据不会引发扩容
Q:如何解决Hash冲突?
HashMap中使用链表/红黑树保存hash冲突的数据,当数据量达到扩容因子后,为了保证查询效率,HashMap会扩容
Q:如何保证HashMap线程安全?什么原理?
HashTable:HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
ConcurrentHashMap的锁分段技术(jdk1.7) HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
Q:HashMap是有序的吗?如何实现有序?
从HashMap的实现原理可知,HashMap是无序的。可以使用LinkedHashMap实现有序的HashMap功能,LinkedHashMap结构和HashMap很像,内部维护了一个有序链表,使用LinkedHashMap时,可以指定它的排序方式,按数据插入顺序排序,或者按访问时间排序,基于后者,java使用LinkedHashMap实现了LURCache缓存。
Q:HashMap是如何扩容的?如何避免扩容?
参考上述HashMap描述
Q:hashcode()的作用,与equal()有什么区别?
Hashcode用于Hash散列。
JVM
Q:JVM内存是如何划分的?
根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。
关于开发中程序员管理的内存主要是指堆内存,所以,这里只对对内存进行详细讲解
所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。
堆被划分为新生代和老年代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace和ToSpace组成,结构图如下所示:
新生代:新建的对象都是用新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor中,
新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例。
旧生代(老年代):用于存放新生代中经过多次垃圾回收仍然存活的对象。
Q:谈谈垃圾回收机制?为什么引用计数器判定对象是否回收不可行?知道哪些垃圾回收算法?
JVM分别对新生代和旧生代采用不同的垃圾回收机制
新生代的GC:
新生代通常存活时间较短,因此基于复制算法来进行回收,所谓复制算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中.
对应于新生代:就是在Eden和其中一个Survivor,复制到另一个之间Survivor空间中,然后清理掉原来就是在Eden和其中一个Survivor中的对象。
新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。
当连续分配对象时,对象会逐渐从eden到 survivor,最后到老年代。
用javavisualVM来查看,能明显观察到新生代满了后,会把对象转移到旧生代,然后清空继续装载,当旧生代也满了后,就会报outofmemory的异常。
在执行机制上JVM提供了串行GC(SerialGC)、并行回收GC(ParallelScavenge)和并行GC(ParNew)
1)串行GC
在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,
可以通过-XX:+UseSerialGC来强制指定
2)并行回收GC
在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,
可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数
3)并行GC
与旧生代的并发GC配合使用。
老年代的GC:
旧生代与新生代不同,对象存活的时间比较长,比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。在执行机制上JVM提供了串行 GC(SerialMSC)、并行GC(parallelMSC)和并发GC(CMS),具体算法细节还有待进一步深入研究。
以上各种GC机制是需要组合使用的
https://www.cnblogs.com/alsf/p/9017447.html
问:新的对象的内存是在新生代分配的,那如果一个大内存的对象要分配内存,此时新生代通过垃圾回收后仍然无法分配足够大的内存,怎么办,会直接报oom吗?
答案自然是不会,否则jvm的内存管理岂不是有一个大的弊端吗,实际上,在新对象内存分配上,老年代对新生代存在一个所谓的内存担保机制,即当一个新的对象所需内存无法在新生代分配时,由于内存担保机制的存在,这个对象会被直接放入老年代,只有老年代的内存不足,GC之后仍然无法分配内存,才会报oom
Q:Java中引用有几种类型?在Android中常用于什么情景?
1.强引用
Java中默认声明的就是强引用,比如:
Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收obj = null; //手动置null
只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了
2.软引用
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。
3.弱引用
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。
4.虚引用
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
5.引用队列(ReferenceQueue)
引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。
与软引用、弱引用不同,虚引用必须和引用队列一起使用。
Q:类加载的全过程是怎样的?什么是双亲委派模型?
一般来说,Java类加载过程分为三个主要步骤:加载、链接、初始化。接下来,对这三个方面简要介绍一下:
1、加载
是Java将字节码数据从不同的数据源中读取到JVM中的,并且映射为JVM可以识别的数据结构(Class对象),这里的数据源,可以是jar,可以是class文件,甚至是网络数据源;如果输入数据不是ClassFile的结构,就会抛出异常ClassFormatError。
加载阶段是用户参与的,可以自定义类加载器,实现自己的类加载过程。
2、链接
这是核心的步骤,简单来说就是把原始的类定义信息平滑的转入JVM运行的过程中,这里分为三步:
验证,这是JVM安全的保障,JVM需要检验字节码的数据结构是否满足要求,不然就抛出VerifyError,这样就防止了恶意信息或者不合规的信息危害JVM的运行,验证阶段有可能触发更多的class加载。
准备,创建类或接口的静态变量,并且初始化静态变量的初始值。但是这里的“初始化”和下面的显示初始化是有区别的,侧重点在于分配所需要的内存空间不会去执行更进一步的JVM指令。
解析,在这一步将常量池中的符号引用替换为直接引用。
3、初始化
这一步真正执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义的静态初始化块中的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优于当前类型的逻辑。
双亲委派模型
简单的说就是当类加载器(Class-Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器。使用委派模型的目的是避免重复加载Java类型(注意这里说的父加载器并不是指继承的父类BaseDexClassLoader)。
对于android来说,谷歌系统提供的SDK类是通过bootClassLoader加载的,App内部的类通过DexClassLoader加载,而第三方的dex文件通常通过PathClassLoader通过指定目录加载,需要注意的是,DexClassLoader和PathClassLoader其实没有区别,都可以用于加载第三方的dex文件的类(网上很多博客说DexClassLoader不能用于加载指定目录的dex文件的类,其实是错的,他们的使用唯一的区别是构造函数,DexClassLoader的构造函数多了一个optimizedDirectory参数,输出dex优化后的odex文件,可以为null,pathClassLoader也会有默认的优化目录)
更多双亲委托机制参考https://www.jianshu.com/p/c17da9acc068
Q:工作内存和主内存的关系?在Java内存模型有哪些可以保证并发过程的原子性、可见性和有序性的措施?
Q:JVM、Dalvik、ART的区别?
简单来说JVM是虚拟机,执行的是字节码文件,执行指令基于栈。Dalvik是android虚拟机,执行的是dex文件,执行指令基于寄存器,执行效率要高与jvm,ART是android5.0推出的。ART代表AndroidRuntime,Dalvik是依靠一个Just-In-Time(JIT)编译器去解释字节码,运行时编译后的应用代码都需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运行。
ART则完全改变了这种做法,在应用安装的时候就预编译字节码到机器语言,这一机制叫Ahead-Of-Time(AOT)预编译。在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。(这也是在5.0后apk安装速度变慢的原因)
https://www.cnblogs.com/qitian1/p/6461541.html
Q:Java中堆和栈的区别?
最主要的区别就是栈内存用来存储局部变量和方法调用。
而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。
独有还是共享
栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
而堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
异常错误
如果栈内存没有可用的空间存储方法调用和局部变量,JVM会抛出java.lang.StackOverFlowError。
而如果是堆内存没有可用的空间存储生成的对象,JVM会抛出java.lang.OutOfMemoryError。
空间大小
栈的内存要远远小于堆内存,如果你使用递归的话,那么你的栈很快就会充满。如果递归没有及时跳出,很可能发生StackOverFlowError问题。
你可以通过-Xss选项设置栈内存的大小。-Xms选项可以设置堆的开始时的大小,-Xmx选项可以设置堆的最大值。
这就是Java中堆和栈的区别。理解好这个问题的话,可以对你解决开发中的问题,分析堆内存和栈内存使用,甚至性能调优都有帮助。
Q:静态代理和动态代理的区别,使用场景