Java面试题收录

JavaSE

1.HashMap、HashTable与ConcurrentHashMap(hash冲突,扩容机制,1.7与1.8的区别,锁)

Hash的定义:

Hash,一般翻译做“散列”,也有直接音译为“哈希”的。它是将一个任意长度的二进制值通过一个映射关系转换成一个固定长度的二进制值,任意长度的二进制值和固定长度的二进制值存在一一对应关系(就是key–value的关系)。

Hash表(取数据的时间复杂度为1):

又叫散列表,通过一个 key 映射到表中的一个位置,来找到与这个key对应的唯一映射的Value,加快查询的速度,这个映射函数叫做Hash函数,存放记录的数组叫做散列表。

构造Hash函数:

HashMap的内部实现是使用了除留余数法

除留余数法(最常用的构造散列函数方法):

f(key) = key mod p (p≤m),m为散列表长。根据经验,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最大质数,可以更好的减小冲突。
若表的长度为16(最接近16的素数为15,key为传入的值通过特定的算法获取到的值,value为要存取的值,index 为在Hash表中的坐标)

hash冲突:
哈希(Hash)冲突:当关键字集合很大时,关键字值不同的元素可能会映像到哈希表的同一地址上,即K1!=K2,但f(K1)=f(K2),这种现象称为hash冲突。散列表要解决的一个问题就是散列值的冲突问题,通常有4种解决冲突的方法:
  (1)、链表法(链地址法):将相同hash值的对象组织成一个链表放在hash值对应的槽位,其中一个对象会记录下一个对象的地址;
  (2)、开放地址法:通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位;
  (3)、再Hash法:同时构造多个不同的哈希函数,当一个出现冲突时用另一个,直至没有冲突发生。这种方法不易产生聚集,但增加了计算时间。
  (4)、建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
  HashMap和Hashtable Hash冲突解决的方法采用的都是链表法,链表是单向链表。
  
总结:
1、Hashmap总体比Hashtable高效,建议使用Hashmap,考虑线程安全的话,可以使用ConcurrentHashMap;
2、JDK8 中哈希冲突过多,链表会转红黑树,时间复杂度是O(logn),不会是O(n),提高效率;
3、hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的。

扩容机制-1.7
HashMap/HashTable初始容量大小和每次扩充容量大小的不同:HashTable默认的初始大小为11,之后每次扩充为原来的2n+1。HashMap默认的初始化大小为16,之后每次扩充为原来的2倍。如果在创建时给定了初始化大小,那么HashTable会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小。

ConcurrentHashMap的锁分离技术
Java5中的ConcurrentHashMap,线程安全,设计巧妙,用桶粒度的锁,避免了put和get中对整个map的锁定,尤其在get中,只对一个HashEntry做锁定操作,性能提升是显而易见的。

concurrentLevel:并发级别,决定segment的个数,最大为1<<16
换句话说,segment的个数代表了并发量
在这里插入图片描述

1.7和1.8的区别
JDK1.8之后的改进:

1.链表改成了红黑树,当链表中的结点达到一个阀值TREEIFY_THRESHOLD(=8)时,会将链表转换为红黑树,查询效率提从原来的O(n),提高为O(logn)
2.将每个segment的分段锁ReentrantLock改为CAS+Synchronized
3.头插法改为尾插法

乐观锁与悲观锁
独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

volatile的问题
与锁相比,volatile变量是一和更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换和线程调度等操作,但是volatile变量也存在一些局限:不能用于构建原子的复合操作,因此当一个变量依赖旧值时就不能使用volatile变量。
volatile只能保证变量对各个线程的可见性,但不能保证原子性。

Java中的原子操作( atomic operations)
原子操作指的是在一步之内就完成而且不能被中断。原子操作在多线程环境中是线程安全的,无需考虑同步的问题。在java中,下列操作是原子操作:
1 、all assignments of primitive types except for long and double(除了long和double类型以外的所有的基本类型的赋值)
2 、all assignments of references
3 、all operations of java.concurrent.Atomic* classes
4、 all assignments to volatile longs and doubles
为什么long型赋值不是原子操作呢?例如:

long foo = 65465498L;

实时上java会分两步写入这个long变量,先写32位,再写后32位。这样就线程不安全了。如果改成下面的就线程安全了:

private volatile long foo;

CAS无锁算法
CAS, CPU指令,在大多数处理器架构,包括IA32、Space中采用的都是CAS指令,CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。CAS无锁算法的C实现如下:

int compare_and_swap
 (int* reg, int oldval, int newval)
{
  ATOMIC();
  int old_reg_val = *reg;
  if (old_reg_val == oldval) 
     *reg = newval;
  END_ATOMIC();
  return old_reg_val;
}

JVM对CAS的支持:AtomicInt, AtomicLong.incrementAndGet()
在JDK1.5之前,如果不编写明确的代码就无法执行CAS操作,在JDK1.5中引入了底层的支持,在int、long和对象的引用等类型上都公开了CAS的操作,并且JVM把它们编译为底层硬件提供的最有效的方法,在运行CAS的平台上,运行时把它们编译为相应的机器指令,如果处理器/CPU不支持CAS指令,那么JVM将使用自旋锁。因此,值得注意的是,CAS解决方案与平台/编译器紧密相关(比如x86架构下其对应的汇编指令是lock cmpxchg,如果想要64Bit的交换,则应使用lock cmpxchg8b。在.NET中我们可以使用Interlocked.CompareExchange函数)

在原子类变量中,如java.util.concurrent.atomic中的AtomicXXX,都使用了这些底层的JVM支持为数字类型的引用类型提供一种高效的CAS操作,而在java.util.concurrent中的大多数类在实现时都直接或间接的使用了这些原子变量类。

转载自:https://blog.csdn.net/bincavin/article/details/54671179

2.面向对象特点

一. 封装
   封装的思想就是将对象的属性和性为封装起来,不让客户了解其内部细节。增加代码的安全性。封装就是对属性和方法进行私有化
二. 继承
  优点:提高代码的复用性,减少错误的产生。
  继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。
三.多态
多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调
用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性可以解释为:当 A 系统访问 B 系统提供的服务时,B 系统有多种提供服务的方式,但一切对 A 系统来说都是透明的。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:

  1. 方法重写(子类继承父类并重写父类中已有的或抽象的方法);
  2. 对象造型(用父类型引用引用子类型对2. 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。

3.泛型

  1. Java中的泛型是什么 ? 使用泛型的好处是什么?
    泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
      那些拥有Java1.4或更早版本的开发背景的人都知道,在集合中存储对象并在使用前进行类型转换是多么的不方便。泛型防止了那种情况的发生。它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。
  2. Java的泛型是如何工作的 ? 什么是类型擦除 ?
      这是一道更好的泛型面试题。泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List在运行时仅用一个List来表示。这样做的目的,是确保能和Java 5之前的版本开发二进制类库进行兼容。你无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。根据你对这个泛型问题的回答情况,你会得到一些后续提问,比如为什么泛型是由类型擦除来实现的或者给你展示一些会导致编译器出错的错误泛型代码。请阅读我的Java中泛型是如何工作的来了解更多信息。
  3. 什么是泛型中的限定通配符和非限定通配符 ?
      这是另一个非常流行的Java泛型面试题。限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界,另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面<?>表示了非限定通配符,因为<?>可以用任意类型来替代。更多信息请参阅我的文章泛型中限定通配符和非限定通配符之间的区别。
  4. List<? extends T>和List <? super T>之间有什么区别 ?
      这和上一个面试题有联系,有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符。这两个List的声明都是限定通配符的例子,List<? extends T>可以接受任何继承自T的类型的List,而List<? super T>可以接受任何T的父类构成的List。例如List<? extends Number>可以接受List或List。在本段出现的连接中可以找到更多信息。
  5. 如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?
      编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用T, E or K,V等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。最简单的情况下,一个泛型方法可能会像这样:
  public V put(K key, V value) {
	    return cache.put(key, value);
    }
  1. Java中如何使用泛型编写带有参数的类?
      这是上一道面试题的延伸。面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,而且要使用JDK中采用的标准占位符。
  2. 编写一段泛型程序来实现LRU缓存?
      对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。当然,如果你已经编写了一个可运行的JUnit测试,你也可以随意编写你自己的实现代码。
  3. 你可以把List传递给一个接受List参数的方法吗?
      对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来String是一种Object,所以List应当可以用在需要List的地方,但是事实并非如此。真这样做的话会导致编译错误。如果你再深一步考虑,你会发现Java这样做是有意义的,因为List可以存储任何类型的对象包括String, Integer等等,而List却只能用来存储Strings。
 List<Object> objectList;
 List<String> stringList;
 objectList = stringList; //compilation error incompatible types
  1. Array中可以用泛型吗?
      这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。
  2. 如何阻止Java中的类型未检查的警告?
      如果你把泛型和原始类型混合起来使用,例如下列代码,Java 5的javac编译器会产生类型未检查的警告,例如
List<String> rawList = new ArrayList()

转载自:https://www.toutiao.com/a6614744216033559054/

4.String StringBuffer StringBuilder

(1)、可变不可变
String:字符串常量,在修改时不会改变自身;若修改,等于重新生成新的字符串对象。
StringBuffer:在修改时会改变对象自身,每次操作都是对 StringBuffer 对象本身进行修改,不是生成新的对象;使用场景:对字符串经常改变情况下,主要方法:append(),insert()等。
(2)、线程是否安全
String:对象定义后不可变,线程安全。
StringBuffer:是线程安全的(对调用方法加入同步锁),执行效率较慢,适用于多线程下操作字符串缓冲区大量数据。
StringBuilder:是线程不安全的,适用于单线程下操作字符串缓冲区大量数据。
(3)、共同点
StringBuilder 与 StringBuffer 有公共父类 AbstractStringBuilder(抽象类)。
StringBuilder、StringBuffer 的方法都会调用 AbstractStringBuilder 中的公共方法,如 super.append(…)。只是 StringBuffer 会在方法上加 synchronized 关键字,进行同步。最后,如果程序不是多线程的,那么使用StringBuilder 效率高于 StringBuffer。

5.Overload 与Override

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求。

方法重载的规则:
1.方法名一致,参数列表中参数的顺序,类型,个数不同。
2.重载与方法的返回值无关,存在于父类和子类,同类中。
3.可以抛出不同的异常,可以有不同修饰符。

方法重写的规则:
1.参数列表必须完全与被重写方法的一致,返回类型必须完全与被重写方法的返回类型一致。
2.构造方法不能被重写,声明为 final 的方法不能被重写,声明为 static 的方法不能被重写,但是能够被再次声明。
3.访问权限不能比父类中被重写的方法的访问权限更低。
4.重写的方法能够抛出任何非强制异常(UncheckedException,也叫非运行时异常),无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。

6.抽象类和接口(abstract 与interface)

不同:
抽象类:
1.抽象类中可以定义构造器
2.抽象类中的成员全都是 public 的
3.有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法
4.一个类只能继承一个抽象类
接口:
1.接口中不能定义构造器
2.接口中的成员可以是 private、默认、protected、public
3.接口中定义的成员变量实际上都是常量
4.一个类可以实现多个接口
Java8的新特性:接口可以存在默认方法(default 修饰)和静态方法
相同:
1.不能够实例化
2.可以将抽象类和接口类型作为引用类型
3.一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类

7.final finally finalize

1)final:用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,被其修饰的类不可继承。
2)finally:异常处理语句结构的一部分,表示总是执行。
3)finalize:Object 类的一个方法,在垃圾回收器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。该方法更像是一个对象生命周期的临终方法,当该方法被系统调用则代表该对象即将“死亡”,但是需要注意的是,我们主动行为上去调用该方法并不会导致该对象“死亡”,这是一个被动的方法(其实就是回调方法),不需要我们调用。

8.JAVA8 新特性

一、接口的默认方法
a: 默认方法: 允许在已有的接口中添加新方法,而同时又保持了与旧版本代码的兼容性。默认方法与抽象方法不同之处在于抽象方法必须要求实现,但是默认方法则没有这个要求。相反,每个接口都必须提供一个所谓的默认实现,这样所有的接口实现者都默认继承它(如果有必要的话,可以覆盖这个默认实现)
b: 静态方法: 允许添加静态方法,接口的静态方法属于接口类本身,不被继承,也需要提供方法的实现
c: 默认方法和接口方法的方法
静态方法: 接口的类名.方法名
默认方法: 实现类的引用.方法名。默认方法可以被实现类覆盖

二、函数式接口
Java 8 引入的一个核心概念是函数式接口。如果一个接口定义个唯一一个抽象方法,那么这个接口就成为函数式接口。比如,java.lang.Runnable就是一个函数式接口,因为它只顶一个一个抽象方法:

三、Lambda 表达式
一个函数式接口非常有价值的属性就是他们能够用lambdas来实例化。这里有一些lambdas的例子:
左边是指定类型的逗号分割的输入列表,右边是带有return的代码块:

(int x, int y) -> { return x + y; }
(x, y) -> x + y

四、方法引用
方法引用使得开发者可以直接引用现存的方法、Java类的构造方法或者实例对象。方法引用和Lambda表达式配合使用,使得java类的构造方法看起来紧凑而简洁,没有很多复杂的模板代码。

西门的例子中,Car类是不同方法引用的例子,可以帮助读者区分四种类型的方法引用。

public static class Car {
    public static Car create( final Supplier< Car > supplier ) {
        return supplier.get();
    }              
 
    public static void collide( final Car car ) {
        System.out.println( "Collided " + car.toString() );
    }
 
    public void follow( final Car another ) {
        System.out.println( "Following the " + another.toString() );
    }
 
    public void repair() {   
        System.out.println( "Repaired " + this.toString() );
    }
}

五、重复注解
自从Java 5中引入注解以来,这个特性开始变得非常流行,并在各个框架和项目中被广泛使用。不过,注解有一个很大的限制是:在同一个地方不能多次使用同一个注解。Java 8打破了这个限制,引入了重复注解的概念,允许在同一个地方多次使用同一个注解。

在Java 8中使用@Repeatable注解定义重复注解,实际上,这并不是语言层面的改进,而是编译器做的一个trick,底层的技术仍然相同。

六、拓宽注解的应用场景
Java 8拓宽了注解的应用场景。现在,注解几乎可以使用在任何元素上:局部变量、接口类型、超类和接口实现类,甚至可以用在函数的异常定义上。

七、Java官方库的新特性
Java 8增加了很多新的工具类(date/time类),并扩展了现存的工具类,以支持现代的并发编程、函数式编程等。
1.Optional
Java应用中最常见的bug就是空值异常。在Java 8之前,Google Guava引入了Optionals类来解决NullPointerException,从而避免源码被各种null检查污染,以便开发者写出更加整洁的代码。Java 8也将Optional加入了官方库。
2.Streams
新增的Stream API(java.util.stream)将生成环境的函数式编程引入了Java库中。这是目前为止最大的一次对Java库的完善,以便开发者能够写出更加有效、更加简洁和紧凑的代码。
3.Date/Time API(JSR 310)
Java 8引入了新的Date-Time API(JSR 310)来改进时间、日期的处理。时间和日期的管理一直是最令Java开发者痛苦的问题。java.util.Date和后来的java.util.Calendar一直没有解决这个问题(甚至令开发者更加迷茫)。

因为上面这些原因,诞生了第三方库Joda-Time,可以替代Java的时间管理API。Java 8中新的时间和日期管理API深受Joda-Time影响,并吸收了很多Joda-Time的精华。新的java.time包包含了所有关于日期、时间、时区、Instant(跟日期类似但是精确到纳秒)、duration(持续时间)和时钟操作的类。新设计的API认真考虑了这些类的不变性(从java.util.Calendar吸取的教训),如果某个实例需要修改,则返回一个新的对象。

4.Nashorn JavaScript引擎
ava 8提供了新的Nashorn JavaScript引擎,使得我们可以在JVM上开发和运行JS应用。Nashorn JavaScript引擎是javax.script.ScriptEngine的另一个实现版本,这类Script引擎遵循相同的规则,允许Java和JavaScript交互使用,例子代码如下:

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName( "JavaScript" );
 
System.out.println( engine.getClass().getName() );
System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );

这个代码的输出结果如下:

jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2

5.Base64
对Base64编码的支持已经被加入到Java 8官方库中,这样不需要使用第三方库就可以进行Base64编码,例子代码如下:

package com.javacodegeeks.java8.base64;
 
import java.nio.charset.StandardCharsets;
import java.util.Base64;
 
public class Base64s {
    public static void main(String[] args) {
        final String text = "Base64 finally in Java 8!";
 
        final String encoded = Base64
            .getEncoder()
            .encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
        System.out.println( encoded );
 
        final String decoded = new String( 
            Base64.getDecoder().decode( encoded ),
            StandardCharsets.UTF_8 );
        System.out.println( decoded );
    }
}

这个例子的输出结果如下:

QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally in Java 8!

新的Base64API也支持URL和MINE的编码解码。
(Base64.getUrlEncoder() / Base64.getUrlDecoder(), Base64.getMimeEncoder() / Base64.getMimeDecoder())。

八、JVM的新特性
使用Metaspace(JEP 122)代替持久代(PermGen space)。在JVM参数方面,使用-XX:MetaSpaceSize和-XX:MaxMetaspaceSize代替原来的-XX:PermSize和-XX:MaxPermSize

转载自:https://blog.csdn.net/u014470581/article/details/54944384

JVM

1.类加载机制

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们开始的顺序如下图所示:
在这里插入图片描述
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

这里简要说明下Java中的绑定:绑定指的是把一个方法的调用与方法所在的类(方法主体)关联起来,对java来说,绑定分为静态绑定和动态绑定:

静态绑定:即前期绑定。在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。针对java,简单的可以理解为程序编译期的绑定。java当中的方法只有final,static,private和构造方法是前期绑定的。
动态绑定:即晚期绑定,也叫运行时绑定。在运行时根据具体对象的类型进行绑定。在java中,几乎所有的方法都是后期绑定的。

加载
加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:
1、通过一个类的全限定名来获取其定义的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
注意,这里第1条中的二进制字节流并不只是单纯地从Class文件中获取,比如它还可以从Jar包中获取、从网络中获取(最典型的应用便是Applet)、由其他文件生成(JSP应用)等。

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。
说到加载,不得不提到类加载器,下面就具体讲述下类加载器。

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类的加载阶段。对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性,也就是说,即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,那这两个类就必定不相等。这里的“相等”包括了代表类的Class对象的equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用instanceof关键字对对象所属关系的判定结果。

站在Java虚拟机的角度来讲,只存在两种不同的类加载器:

  • 启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分。
  • 所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。

站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

  • 启动类加载器:Bootstrap ClassLoader,跟上面相同。它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
  • 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点

1)在执行非置信代码之前,自动验证数字签名。

2)动态地创建符合用户特定需要的定制化构建类。

3)从特定的场所取得java class,例如数据库中和网络中。

事实上当使用Applet的时候,就用到了特定的ClassLoader,因为这时需要从网络上加载java class,并且要检查相关的安全信息,应用服务器也大都使用了自定义的ClassLoader技术。

这几种类加载器的层次关系如下图所示:
在这里插入图片描述
这种层次关系称为类加载器的双亲委派模型。我们把每一层上面的类加载器叫做当前层类加载器的父加载器,当然,它们之间的父子关系并不是通过继承关系来实现的,而是使用组合关系来复用父加载器中的代码。该模型在JDK1.2期间被引入并广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者们推荐给开发者的一种类的加载器实现方式。

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

使用双亲委派模型来组织类加载器之间的关系,有一个很明显的好处,就是Java类随着它的类加载器(说白了,就是它所在的目录)一起具备了一种带有优先级的层次关系,这对于保证Java程序的稳定运作很重要。例如,类java.lang.Object类存放在JDK\jre\lib下的rt.jar之中,因此无论是哪个类加载器要加载此类,最终都会委派给启动类加载器进行加载,这边保证了Object类在程序中的各种类加载器中都是同一个类。

验证
验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证

  • 文件格式的验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
  • 元数据验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。
  • 字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
  • 符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。

准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

假设一个类变量的定义为:

public static int value = 3;

那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。

下表列出了Java中所有基本数据类型以及reference类型的默认零值:
在这里插入图片描述
这里还需要注意如下几点:

  • 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
  • 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
  • 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
  • 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

3、如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

假设上面的类变量value被定义为:

 public static final int value = 3;

编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中。

解析

解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。前面说解析阶段可能开始于初始化之前,也可能在初始化之后开始,虚拟机会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),还是等到一个符号引用将要被使用前才去解析它(初始化之后)。

对同一个符号引用进行多次解析请求时很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动作重复进行。
解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四种常量类型。

1、类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。
2、字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束,查找流程如下图所示:
在这里插入图片描述
从下面一段代码的执行结果中很容易看出来字段解析的搜索顺序:

class Super{
	public static int m = 11;
	static{
		System.out.println("执行了super类静态语句块");
	}
}
 
 
class Father extends Super{
	public static int m = 33;
	static{
		System.out.println("执行了父类静态语句块");
	}
}
 
class Child extends Father{
	static{
		System.out.println("执行了子类静态语句块");
	}
}
 
public class StaticTest{
	public static void main(String[] args){
		System.out.println(Child.m);
	}
}

执行结果如下:
执行了super类静态语句块
执行了父类静态语句块
33

如果注释掉Father类中对m定义的那一行,则输出结果如下:
执行了super类静态语句块
11

这里我们便可以分析如下:static变量发生在静态解析阶段,也即是初始化之前,此时已经将字段的符号引用转化为了内存引用,也便将它与对应的类关联在了一起,由于在子类中没有查找到与m相匹配的字段,那么m便不会与子类关联在一起,因此并不会触发子类的初始化。
最后需要注意:理论上是按照上述顺序进行搜索解析,但在实际应用中,虚拟机的编译器实现可能要比上述规范要求的更严格一些。如果有一个同名字段同时出现在该类的接口和父类中,或同时在自己或父类的接口中出现,编译器可能会拒绝编译。如果对上面的代码做些修改,将Super改为接口,并将Child类继承Father类且实现Super接口,那么在编译时会报出如下错误:
StaticTest.java:24: 对 m 的引用不明确,Father 中的 变量 m 和 Super 中的 变量 m都匹配
System.out.println(Child.m);
^
1 错误

3、类方法解析:对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。

4、接口方法解析:与类方法解析步骤类似,知识接口不会有父类,因此,只递归向上搜索父接口就行了。

初始化

初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器< clinit >()方法的过程。

总结
整个类加载过程中,除了在加载阶段用户应用程序可以自定义类加载器参与之外,其余所有的动作完全由虚拟机主导和控制。到了初始化才开始执行类中定义的Java程序代码(亦及字节码),但这里的执行代码只是个开端,它仅限于< clinit >()方法。类加载过程中主要是将Class文件(准确地讲,应该是类的二进制字节流)加载到虚拟机内存中,真正执行字节码的操作,在加载完成后才真正开始。

原文:https://blog.csdn.net/ns_code/article/details/17881581

2.Java虚拟机(JVM)

1、JVM是什么
JVM是Java Virtual Mechine的缩写。它是一种基于计算设备的规范,是一台虚拟机,即虚构的计算机。

JVM屏蔽了具体操作系统平台的信息(显然,就像是我们在电脑上开了个虚拟机一样),当然,JVM执行字节码时实际上还是要解释成具体操作平台的机器指令的。

通过JVM,Java实现了平台无关性,Java语言在不同平台运行时不需要重新编译,只需要在该平台上部署JVM就可以了。因而能实现一次编译多处运行。(就像是你的虚拟机也可以在任何安了VMWare的系统上运行)

2、JRE和JDK
JRE:Java Runtime Environment,也就是JVM的运行平台,联系平时用的虚拟机,大概可以理解成JRE=虚拟机平台+虚拟机本体(JVM)。类似于你电脑上的VMWare+适用于VMWare的Ubuntu虚拟机。这样我们也就明白了JVM到底是个什么。

JDK:Java Develop Kit,Java的开发工具包,JDK本体也是Java程序,因此运行依赖于JRE,由于需要保持JDK的独立性与完整性,JDK的安装目录下通常也附有JRE。目前Oracle提供的Windows下的JDK安装工具会同时安装一个正常的JRE和隶属于JDK目录下的JRE。

3、JVM结构
JVM主要包括:程序计数器(Program Counter),Java堆(Heap),Java虚拟机栈(Stack),本地方法栈(Native Stack),方法区(Method Area)
在这里插入图片描述
3.1. 程序计数器(PC, Program Counter)

是一个寄存器,可以看作是代码行号指示器,类似于实际计算机里的PC,用于指示,跳转下一条需要执行的命令。Java的基础操作以及异常处理等都十分依赖PC。

JVM多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。在一个确定的时刻,一个处理器(或者说多核处理器的一个内核)只会执行一条线程中的命令。因此,为了正常的切换线程,每个线程都会有一个独立的PC,各线程的PC不会互相影响。这个私有的PC所占的这块内存即是线程的“私有内存”。

如果线程在执行的是Java方法,那么PC记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的不是Java方法即Native方法,那么PC的值为undefined。

PC的内存区域是唯一的没有规定任何OutOfMemoryError的Java虚拟机规范中的区域。

3.2. Java虚拟机栈(Stack,Java Virtual Mechine Stacks)

同PC一样(从工作流程图里我们可以看到,实际上,PC也是存在于JVM Stack上的),也是线程私有的,生命周期与线程相同。虚拟机栈描述Java方法执行的内存模型,每个方法被执行时都会创建一个栈帧(Stack Frame),栈帧会利用局部变量数组存储局部变量(Local Variables),操作栈(Operand Stack),方法出口(Return Value),动态连接(Current Class Constant Pool Reference)等信息。

局部变量数组存储了编译可知的八个基本类型(int, boolean, char, short, byte, long, float, double),对象引用(根据不同的虚拟机实现可能是引用地址的指针或者一个handle),returnAddress类型。64位的long和double会占用两个Slot,其余类型会占用一个Slot。在编译期间,局部变量所需的空间就会完成分配,动态运行期间不会改变所需的空间。

操作栈在执行字节码指令时会被用到,这种方式类似于原生的CPU寄存器,大部分JVM把时间花费在操作栈的花费上,操作栈和局部变量数组会频繁的交换数据。

动态连接控制着运行时常量池和栈帧的连接。所有方法和类的引用都会被当作符号的引用存在常量池中。符号引用是实际上并不指向物理内存地址的逻辑引用。JVM 可以选择符号引用解析的时机,一种是当类文件加载并校验通过后,这种解析方式被称为饥饿方式。另外一种是符号引用在第一次使用的时候被解析,这种解析方式称为惰性方式。无论如何 ,JVM 必须要在第一次使用符号引用时完成解析并抛出可能发生的解析错误。绑定是将对象域、方法、类的符号引用替换为直接引用的过程。绑定只会发生一次。一旦绑定,符号引用会被完全替换。如果一个类的符号引用还没有被解析,那么就会载入这个类。每个直接引用都被存储为相对于存储结构(与运行时变量或方法的位置相关联的)偏移量。

对Java虚拟机栈这个区域,Java虚拟机规范规定了两种异常:

  • 线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverFlow异常。
  • 对于支持动态扩展的虚拟机,当扩展无法申请到足够的内存时会抛出OutOfMemoryError。

3.3. 本地方法栈(Native Stack)
本地方法栈如其名字,和Java Virtual Machine Stack其实极为类似,只是执行的是Native方法,为Native方法服务。在JVM规范中,没有对它的实现做具体规定。

3.4. Java 堆(Heap, Garbage Collection Heap)
Java堆是被所有线程共享的一块区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存(随着技术的发展,已不绝对)。

Java堆是垃圾收集器管理的主要区域,因而也被称为GC堆。收集器采用分代回收法,GC堆可以分为新生代(Yong Generation)和老生代(Old Generation)。新生代包括Eden Space和Survivor Space。但无论哪个区域,如何划分,存储的都是Java对象实例,进一步的划分是为了更好的回收内存或快速的分配内存。

根据Java虚拟机规范,堆所在的物理内存区间可以是不连续的,只要逻辑连续就可以。实现时既可以是固定大小,也可以是可扩展的。如果堆无法扩展时,就会抛出OutOfMemoryError。

3.5. 方法区(Method Area)
方法区和Java堆类似,也属于各线程共享的内存区域。用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码数据等。它属于非堆区(Non Heap),和Java堆区分开。对于存在永久代(Permanent)概念的虚拟机(HotSpot)而言,方法区存在于永久代。Java虚拟机规范对方法区的规定很宽松,甚至可以不实现GC。不过并非进入方法区的数据就会永久存在了,这块区域的内存回收主要为常量池的回收和类型的卸载。这个区域的回收处理不善也会导致严重的内存泄漏。

当方法区无法满足内存分配需求时也会抛出OutOfMemoryError。

注意:

  • jdk1,6常量池放在方法区,jdk1.7常量池放在堆内存,jdk1.8放在元空间里面,和堆相独立
  • 元空间并不在虚拟机中,而是使用本地内存

3.6 Metaspace(元空间)

其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了Java heap。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集

-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

总结
为什么从永久代切换到元空间?

1)字符串存在永久代中,容易出现性能问题和内存溢出。

2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

3)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

4、 JVM线程与原生线程的关系
JVM允许一个程序使用多个并发线程,Hotspot JVM中Java的线程与原生操作系统的线程是直接映射关系。即当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。run() 返回时,被处理未捕获异常,原生线程将确认由于它的结束是否要终止 JVM 进程(比如这个线程是最后一个非守护线程)。当线程结束时,会释放原生线程和 Java 线程的所有资源。

转载自:http://www.cnblogs.com/cielosun/p/6622983.html

3.垃圾回收器的分类

Java有四种类型的垃圾回收器

  • 串行垃圾回收器(Serial Garbage Collector)
  • 并行垃圾回收器(Parallel Garbage Collector)
  • 并发标记扫描垃圾回收器(CMS Garbage Collector)
  • G1垃圾回收器(G1 Garbage Collector)

1、串行垃圾回收器
串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。它最适合的是简单的命令行程序。

通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。

2、并行垃圾回收器
并行垃圾回收器也叫做 throughput collector 。它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,它也会冻结所有的应用程序线程当执行垃圾回收的时候

3、并发标记扫描垃圾回收器
并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。

当标记的引用对象在tenured区域;
在进行垃圾回收的时候,堆内存的数据被并发的改变。
相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。

通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。

4、G1垃圾回收器
G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域

通过JVM参数 –XX:+UseG1GC 使用G1垃圾回收器

Java 8 的新特性
在使用G1垃圾回收器的时候,通过 JVM参数 -XX:+UseStringDeduplication 。 我们可以通过删除重复的字符串,只保留一个char[]来优化堆内存。这个选择在Java 8 u 20被引入。
从JDK8开始,永久代(PermGen)的概念被废弃掉了,取而代之的是一个称为Metaspace的存储空间。Metaspace使用的是本地内存,而不是堆内存,也就是说在默认情况下Metaspace的大小只与本地内存大小有关。

我们给出了全部的四种Java垃圾回收器,需要根据应用场景,硬件性能和吞吐量需求来决定使用哪一种。

配置 描述
-XX:+UseSerialGC 串行垃圾回收器
-XX:+UseParallelGC 并行垃圾回收器
-XX:+UseConcMarkSweepGC 并发标记扫描垃圾回收器
-XX:ParallelCMSThreads= 并发标记扫描垃圾回收器 =为使用的线程数量
-XX:+UseG1GC G1垃圾回收器

GC的优化配置

配置 描述
-Xms 初始化堆内存大小
-Xmx 堆内存最大值
-Xmn 新生代大小
-XX:PermSize(java8 删除永久代,改为元空间 -XX:MetaspaceSize) 初始化永久代(元空间)大小
-XX:MaxPermSize(java8 删除永久代,改为元空间 -XX:MaxMetaspaceSize) 永久代(元空间)最大容量

使用JVM GC参数的例子

java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar

转载自:http://www.cnblogs.com/maokun/articles/7629037.html##1

4.垃圾回收算法

1、如何确定某个对象是垃圾

1.1. 引用计数法

在Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,则说明对象不太可能再被用到,那么这个对象就是可回收对象。这种方式即是引用计数法。这种方式的问题是无法解决循环引用的问题,举个例子:

public static void main(String[] args){
    Object object1=new Object();
    Object object2=new Object();
    object1.object=object2;
    object2.object=object1;
    object1=null;
    object2=null;
}

显然,在最后,object1和object2的内存块都不能再被访问到了,但他们的引用计数都不为0,这就会使他们永远不会被清除。

1.2. 可达性分析

为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

比较常见的将对象视为可回收对象的原因:

  • 显式地将对象的唯一强引用指向新的对象。
  • 显式地将对象的唯一强引用赋值为Null。
  • 局部引用所指向的对象(如,方法内对象)。

下述代码中,每次循环结束,object都会被视为可回收对象。

void fun() {
 
.....
    for(int i=0;i<10;i++) {
        Object obj = new Object();
        System.out.println(obj.getClass());
    }   
}
  • 只有弱引用与其关联的对象

2、典型的垃圾回收算法
在JVM规范中并没有明确GC的运作方式,各个厂商可以采用不同的方式去实现垃圾回收器。这里讨论几种常见的GC算法。

2.1 标记-清除算法(Mark-Sweep)
最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。如图:
在这里插入图片描述
从图中我们就可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。

2.2. 复制算法(Copying)
为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图:
在这里插入图片描述
这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying算法的效率会大大降低。

2.3. 标记-整理算法(Mark-Compact)
结合了以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。如图:
在这里插入图片描述
2.4. 分代收集算法(Generational Collection)
分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

目前大部分JVM的GC对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。
在这里插入图片描述
而老生代因为每次只回收少量对象,因而采用Mark-Compact算法。

另外,不要忘记处于方法区的永生代(Permanet Generation)。它用来存储class类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。

对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放对象的那一块),少数情况会直接分配到老生代。当新生代的Eden Space和From Space空间不足时就会发生一次GC,进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。如果To Space无法足够存储某个对象,则将这个对象存储到老生代。在进行GC后,使用的便是Eden Space和To Space了,如此反复循环。当对象在Survivor区躲过一次GC后,其年龄就会+1。默认情况下年龄到达15的对象会被移到老生代中。

转载自:http://www.cnblogs.com/dolphin0520/p/3783345.html

5 Java内存模型

参考:全面理解Java内存模型

多线程

1.创建线程的三种方式优缺点

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
一、继承Thread类创建线程类

二、实现Runnable接口创建线程类

三、使用Callable和Future创建线程

四、浅谈三种方式优劣势
通过继承Thread类或实现Runnable、Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此可以将实现Runnable接口和实现Callable接口归为一种方式。这种方式与继承Thread方式之间的主要差别如下。
1.采用实现Runnable、Callable接口的方式创建多线程的优缺点:
优势:
(1)线程类只是实现了Runnable接口与Callable接口,还可以继承其他类。
(2)在这种方式下,多个线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势:编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
2.采用继承Thread类的方法创建多线程的优缺点:
劣势:因为线程类已经继承了Thread类,所以不能再继承其他父类。
优势:编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。

五、总结
鉴于上面分析,因此一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。

详情请参考:创建线程的三种方式优缺点

Synchronized与ReentrantLock的比较

相似点:

这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,

进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。

1.区别:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,

则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

5)Lock可以提高多个线程进行读操作的效率。

总结:ReentrantLock相比synchronized,增加了一些高级的功能。

2.两者在锁的相关概念上区别:

1)可中断锁
顾名思义,就是可以相应中断的锁。

在Java中,synchronized就不是可中断锁,而Lock是可中断锁。如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,

可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

lockInterruptibly()的用法体现了Lock的可中断性。

2)公平锁

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁

(并不是绝对的,大体上是这种顺序),这种就是公平锁。

非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。ReentrantLock可以设置成公平锁。

3)读写锁

读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。

正因为有了读写锁,才使得多个线程之间的读操作可以并发进行,不需要同步,而写操作需要同步进行,提高了效率。

ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。

可以通过readLock()获取读锁,通过writeLock()获取写锁。

4)绑定多个条件

一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,

如果要和多余一个条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这么做,只需要多次调用new Condition()方法即可。

3.性能比较
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时ReentrantLock的性能要远远优于synchronized。

所以说,在具体使用时要根据适当情况选择。

在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,

这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的ReentrankLock对象,性能更高一些。到了JDK1.6,发生了变化,

对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。

官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

多线程同步有哪几种方法?

1、同步方法/同步代码块,使用synchronized关键字

2、使用特殊域变量(volatile)实现线程同步
volatile不是原子性的
它的原理是每次有线程要访问volatile修饰的变量时都是从内存中读取,而不是存缓存当中读取,因此每个线程访问到的变量值都是一样的。这样就保证了同步。

一般说来,volatile用在如下的几个地方:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。

3、使用ReentrantLock的重入锁实现线程同步

4、使用局部变量ThreadLocal实现线程同步

如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

线程通信的方式

①同步
这里讲的同步是指多个线程通过synchronized关键字这种方式来实现线程间的通信。

这种方式,本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。

②while轮询的方式

③wait/notify机制

详情请参考:Java线程间的通信方式详解

Java线程中wait和sleep方法的区别

1)第一个很重要的区别就是,wait方法必须正在同步环境下使用,比如synchronized方法或者同步代码块。如果你不在同步条件下使用,会抛出IllegalMonitorStateException异常。另外,sleep方法不需要再同步条件下调用,你可以任意正常的使用。

2)第二个区别是,wait方法用于和定义于Object类的,而sleep方法操作于当前线程,定义在java.lang.Thread类里面。

3)第三个区别是,调用wait()的时候方法会释放当前持有的锁,而sleep方法不会释放任何锁。

4)wait方法最好在循环里面调用,是为了处理错误的通告,比如说,即使线程唤醒了,等待状态仍然适用。(看不懂?大概是循环里面再判断一次线程是否真的醒来),然而sleep方法没这样的限制。最好别在循环里面调用sleep方法。

CyclicBarrier和CountDownLatch的区别?

CountDownLatch简介

CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

CountDownLatch和CyclicBarrier的区别

(01) CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待。

(02) CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier。

线程池

请参考:java常用的几种线程池比较

框架

SpringMVC

1、SpringMVC 的工作原理

1、 用户发送请求至前端控制器DispatcherServlet。
2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。
3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
4、 DispatcherServlet调用HandlerAdapter处理器适配器。
5、 HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
6、 Controller执行完成返回ModelAndView。
7、 HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
8、 DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
9、 ViewReslover解析后返回具体View。
10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
11、 DispatcherServlet响应用户。

在这里插入图片描述

2、 SpringMVC 常用注解都有哪些?

@requestMapping 用于请求 url 映射。
@RequestBody 注解实现接收 http 请求的 json 数据,将 json 数据转换为 java 对象。
@ResponseBody 注解实现将 controller 方法返回对象转化为 json 响应给客户。

3、如何开启注解处理器和适配器?

我们在项目中一般会在 springmvc.xml 中通过开启 < mvc:annotation-driven >来实现注解处
理器和适配器的开启。

4、如何解决 get 和 post 乱码问题?
解决 post 请求乱码:我们可以在 web.xml 里边配置一个 CharacterEncodingFilter 过滤器。 设置为 utf-8.
解决 get 请求的乱码:有两种方法。对于 get 请求中文参数出现乱码解决方法有两个:

  • 修改 tomcat 配置文件添加编码与工程编码一致。
  • 另外一种方法对参数进行重新编码 String userName = New
    String(Request.getParameter(“userName”).getBytes(“ISO8859-1”), “utf-8”);

Spring

1、 谈谈你对 Spring 的理解

Spring 是一个开源框架,为简化企业级应用开发而生。Spring 可以使简单的 JavaBean 实现以前只有 EJB 才能实现的功能。Spring 是一个 IOC 和 AOP 容器框架。
Spring 容器的主要核心是:

控制反转(IOC),传统的 java 开发模式中,当需要一个对象时,我们会自己使用 new 或者 getInstance 等直接或者间接调用构造方法创建一个对象。而在 spring 开发模式中,spring 容器使用了工厂模式为我们创建了所需要的对象,不需要我们自己创建了,直接调用 spring 提供的对象就可以了,这是控制反转的思想。

依赖注入(DI),spring 使用 javaBean 对象的 set 方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程,就是依赖注入的思想。

面向切面编程(AOP),在面向对象编程(oop)思想中,我们将事物纵向抽成一个个的对象。而在面向切面编程中,我们将一个个的对象某些类似的方面横向抽成一个切面,对这个切面进行一些如权限控制、事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想。AOP 底层是动态代理,如果是接口采用 JDK 动态代理,如果是类采用CGLIB 方式实现动态代理。

2、Spring 中的设计模式

a. 单例模式——spring 中两种代理方式,若目标对象实现了若干接口,spring 使用 jdk 的 java.lang.reflect.Proxy类代理。若目标兑现没有实现任何接口,spring 使用 CGLIB 库生成目标类的子类。
单例模式——在 spring 的配置文件中设置 bean 默认为单例模式。
b. 模板方式模式——用来解决代码重复的问题。
比如:RestTemplate、JmsTemplate、JpaTemplate
d. 前端控制器模式——spring 提供了前端控制器 DispatherServlet 来对请求进行分发。
e. 试图帮助(view helper)——spring 提供了一系列的 JSP 标签,高效宏来帮助将分散的代码整合在试图中。
f. 依赖注入——贯穿于 BeanFactory/ApplacationContext 接口的核心理念。
g. 工厂模式——在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用同一个接口来指向新创建的对象。Spring 中使用 beanFactory 来创建对象的实例。

3、Spring 的常用注解

Spring 在 2.5 版本以后开始支持注解的方式来配置依赖注入。可以用注解的方式来代替 xml 中 bean 的描述。注解注入将会被容器在 XML 注入之前被处理,所以后者会覆盖掉前者对于同一个属性的处理结果。
注解装配在 spring 中默认是关闭的。所以需要在 spring 的核心配置文件中配置一下才能使用基于注解的装配模式。配置方式如下:
< context:annotation-config />

常用的注解:
@Required:该注解应用于设值方法
@Autowired:该注解应用于有值设值方法、非设值方法、构造方法和变量。
@Qualifier:该注解和@Autowired 搭配使用,用于消除特定 bean 自动装配的歧义。

4、简单介绍一下 Spring bean 的生命周期

bean 定义:在配置文件里面用来进行定义。

bean 初始化:有两种方式初始化:
1.在配置文件中通过指定 init-method 属性来完成
2.实现 org.springframwork.beans.factory.InitializingBean 接口

bean 调用:有三种方式可以得到 bean 实例,并进行调用

bean 销毁:销毁有两种方式
1.使用配置文件指定的 destroy-method 属性
2.实现 org.springframwork.bean.factory.DisposeableBean 接口

发布了10 篇原创文章 · 获赞 61 · 访问量 5501
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览