//代码块1
short s1 = 1; s1 = s1 + 1;
//代码块2
short s1 = 1; s1 += 1;
答:代码块1编译报错,错误原因是:不兼容的类型:从int转换到short可能会有损失…
代码块2正常编译和执行.
其实,s1 += 1相当于s1 = (short)(s1+1).
Integer a = 128, b = 128, c = 127, d = 127;
System.out.println(a == b);
System.out.println(c == d);
答案是:flase,true.
执行Integer a = 128,相当于执行: Integer a = Integer.valueOf(128),基本类型自动转换为包装类的过程称为自动装箱(autoboxing).
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
在Integer中引入了IntegerCache来缓存一定范围的值,IntegerCache默认情况下范围为:-128-127.
本题中的127命中了IntegerCache,所以c和d是相同对象,而128则没有命中,所以a和b是不同对象.
但是这个缓存范围是可以修改的,可能有些人不知道.可以通过JVM启动参数:-XX:AutoBoxCacheMax= < size>来修改上限值,如下图所示:
答:不是.java中的基本数据类型只有8个:byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(reference)。
基本数据类型:数据直接存储在栈上。
引用数据类型区别:数据存储在堆上,栈上只存储引用地址。
答:不行。String类使用final修饰,无法被继承。
String:String的值被创建后不能被修改,任何对String的修改都会引发新的String对象的生成。
StringBuffer:跟String类似,但是值可以被修改,使用synchronized来保证线程安全。
StringBuilder:StringBuffer的非线程安全版本,没有使用synchronized,具有更高的性能,推荐优先使用。
答:一个或两个。如果字符串常量池已经有了“xyz”,则是一个;否则,两个。
当字符串常量池没有“xyz“,此时会创建如下两个对象:
一个是字符串字面量”xyz“所对应的、驻留(intern)在一个全局共享的字符串常量池中的实例,此时该实例也是在堆中,字符串常量池只放引用。
另一个是通过new String()创建并初始化的,内容与”xyz“相同的实例,也是在堆中。
详细解析:从面试题“String s = new String(“xyz”)创建了几个字符串对象?”学习String类
==:运算符,用于比较基础类型变量和引用类型变量。
对于基础类型变量,比较变量保存的值是否相同,类型不一定要相同。
short s1 = 1;long l1 = 1;
System.out.println(s1 == l1);//true 类型不同,但是值相同
对于引用类型变量,比较的是两个对象的地址是否相同。
Integer a = new Integer(1);
Integer b = new Integer(1);
System.out.println(a == b);//false.通过new创建,在内存中指向两个不同的对象
equals:Object类中定义的方法,通常用于比较两个对象的地址是否相等。
equals在Object方法中用来比较地址,但是在实际的使用中,equals通常被重写用于比较两个对象的值是否相同。并且大部分的类都重写了equals方法。例如:Integer等
StringBuffer a = new StringBuffer("aaa");
StringBuffer b = new StringBuffer("aaa");
System.out.println(a.equals(b));//false 通过new创建,在内存中指向两个不同的对象
//StringBuffer中没有重写Object类中的equals,因此判断的是a和b的地址是否相同
Integer a = new Integer(1);
Integer b = new Integer(1);
System.out.println(a.equals(b));//true. 两个不同的对象,但是具有相同的值。
/**
* Integer中的equals的重写方法
*/
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
答:不对。hashCode()和equals()之间的关系如下:
当有a.equals(b) == true时,则 a.hashCode() == b.hashCode()必然成立。
反过来,当 a.hashCode() == b.hashCode()时,a.equals(b)不一定为true。
反射是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象的方法的功能称为反射机制。
数据分为基本数据类型和引用数据类型。基本数据类型:数据直接存储在栈中;引用数据类型:存储在栈中的是对象的引用地址,真实的对象数据存放在堆内存中。
浅拷贝:对于基础数据类型:直接复制数据值;对于引用数据类型:只是复制了对象的引用地址,新旧对象指向同一个内存地址,修改其中一个对象的值,另一个对象的值随之改变。
深拷贝:对于基础数据类型:直接复制数据值;对于引用数据类型:开辟新的内存空间,在新的内存空间里复制一个一模一样的对象,新老对象不共享内存,修改其中一个对象的值,不会影响另一个对象。
深拷贝相比于浅拷贝速度较慢并且花销较大。
并发:两个或多个事件在同一时间间隔发生。
并行:两个或者多个事件在同一时刻发生。
并行是真正意义上,同一时刻做多件事情,而并发在同一时刻只会做一件事情,只是可以将事件切碎,交替做多件事情。
例子:你吃饭吃到一半,电话来了,你停下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃法,这说明你支持并行。
Constructor不能被overide(重写),但是可以被overload(重载),所以你可以看到一个类中有多个构造函数的情况。
通过面试题”构造器是否可以被重写?“复习重写和重载
答:是值传递。Java编程语言只有值传递参数。当一个对象实例作为一个参数被传递到方法中时,参数的值就是该对象引用的一个副本。指向同一个对象,对象的内容可以在被调用的方法中改变,但对象的引用(不是引用的副本)是永远不会改变的。
值传递和引用传递
public class Demo {
/**
* 静态变量:又称类变量,static修饰
*/
public static String STATIC_VARIABLE = "静态变量";
/**
* 实例变量:又称成员变量,没有static修饰
*/
public String INSTANCE_VARIABLE = "实例变量";
}
成员变量存在于堆内存中。静态变量存在于方法区中。
成员变量与对象共存亡,随着对象的创建而存在,随着对象被回收而释放。
静态变量与类共存亡,随着类的加载而存在,随着类的消失而消失。
成员变量属于对象,所以也称为实例变量。静态变量属于类,所以也称为类变量。
成员变量只能被对象所调用。静态变量可以被对象调用,也可以被类名调用。
public class InitialTest {
public static void main(String[] args) {
A ab = new B();
ab = new B();
}
}
class A {
static { // 父类静态代码块
System.out.print("A");
}
public A() { // 父类构造器
System.out.print("a");
}
}
class B extends A {
static { // 子类静态代码块
System.out.print("B");
}
public B() { // 子类构造器
System.out.print("b");
}
}
执行结果:ABabab。两个考察点:
1)静态变量只会初始化(执行)一次。
2)当有父类时,完整的初始化顺序为:父类静态变量(静态代码块)->子类静态变量(静态代码块)->父类非静态变量(非静态代码块)->父类构造器->子类非静态变量(非静态代码块)->子类构造器。
初始化进阶题:一道有意思的“初始化”面试题
如果我们有两个方法如下,当我们调用:test(1)时,编译器无法确认要调用的是哪个。
// 方法1
int test(int a);
// 方法2
long test(int a);
方法的返回值只是作为方法运行之后的一个"状态",但是并不是所有调用都关注返回值,所以不能将返回值作为重载的唯一区分条件。
设计思想的区别:
接口是自上而下的抽象过程,接口规范了某些行为,是对某一行为的抽象。我需要这个行为,我就去实现某个接口,但是具体这个行为怎么实现,完全由自己决定。
抽象类是自下而上的抽象过程,抽象类提供了通用实现,是对某一类事务的抽象。我们在写实现类的时候,发现某些实现类具有几乎相同的实现,因此我们将这些相同的实现抽取出来成为抽象类,然后如果有一些差一点,则可以提供抽象方法来支持自定义实现。
形象的比喻:
普通类像亲爹,他有啥都是你的。
抽象类像叔伯,有一部分会给你,还能指导你做事的方法。
接口像干爹,可以给你指引方法,但是做成啥样还得你自己努力实现。
抽象类和接口的哲学设计思想
Error和Exception都是Throwable的子类,用于表示程序出现了不正常的情况。区别在于:
Error表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题,比如内存溢出,不可能指望程序能处理这样的情况。
Exception表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题,也就是说,他表示如果程序运行正常,从不会发生的情况。
修饰类:该类不能再派生出新的子类,不能作为父类被继承。因此,一个类不能同时被声明为abstract和final。
修饰方法:该方法不能被子类重写。
修饰变量:该变量必须在声明时给定初值,而在以后只能读取,不可修改。如果变量是对象,则指的是引用不可修改,但是对象的属性还是可以修改的。
public class FinalDemo {
// 不可再修改该变量的值
public static final int FINAL_VARIABLE = 0;
// 不可再修改该变量的引用,但是可以直接修改属性值
public static final User USER = new User();
public static void main(String[] args) {
// 输出:User(id=0, name=null, age=0)
System.out.println(USER);
// 直接修改属性值
USER.setName("test");
// 输出:User(id=0, name=test, age=0)
System.out.println(USER);
}
}
答:其实三个时完全不相关的东西,只是长得有点像。。
final如上题所示。
finally:finally是对Java异常处理机制的最佳补充,通常配合try,catch使用,用于存放那些无论是否出现异常都一定会执行的代码。在实际使用中,通常用于释放锁,数据库连接等资源,把资源释放方法放到finally中,可以答案降低程序出错的几率。
finalize:Object中的方法,在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。finalize()方法仅作为了解即可,在Java 9中该方法已经被标记为废弃,并添加新的java.lang.ref.Cleaner,提供了更灵活和有效的方法来释放资源,这也侧面说明了,这个方法的设计是失败的,因此更加不能去使用它。
public class TryDemo {
public static void main(String[] args) {
System.out.println(test());
}
public static int test() {
try {
return 1;
} catch (Exception e) {
return 2;
} finally {
System.out.print("3");
}
}
}
执行结果:31.
相信很多同学应该都做对了,try,catch,finally的基础用法,在return前会先执行finally语句块,所以是先输出finally里的3,再输出return的1。
public class TryDemo {
public static void main(String[] args) {
System.out.println(test1());
}
public static int test1() {
try {
return 2;
} finally {
return 3;
}
}
}
执行结果:3。
这题有点先将,但也不难,try 返回前先执行 finally,结果 finally 里不按套路出牌,直接 return 了,自然也就走不到 try 里面的 return 了。
finally 里面使用 return 仅存在于面试题中,实际开发中千万不要这么用。
public class TryDemo {
public static void main(String[] args) {
System.out.println(test1());
}
public static int test1() {
int i = 0;
try {
i = 2;
return i;
} finally {
i = 3;
}
}
}
执行结果:2
这边估计有不少同学会以为结果应该是3,因为我们知道在return前会执行finally,而i在finally中被修改为3了,那最终返回i不是应该为3吗?确实容易这么想,只是这么想还是自己太年轻。
这边的根本原因是,在执行finally之前,JVM会先将i的结果暂存起来,然后finally执行完毕后,会返回之前暂存的结果,而不是返回i,所以即使这边i已经被修改为3,最终返回的还是之前暂存起来的结果2。
这边其实根据字节码可以很容易看出来,在进行finally之前,JVM会使用iload,istore两个指令,将结果暂存,在最终返回时再通过iload,ireturn指令返回暂存的结果,有兴趣的同学可以自己编译查看。
接口默认方法:Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用default关键字即可。
Lambda表达式和函数式接口:Lambda表达式本质上是一段匿名内部类,也可以是一段可以传递的代码。Lambda允许把函数作为一个方法的参数(函数作为参数传递到方法中),使用Lambda表达式使代码更加简洁,但是也不要滥用,否则会有可读性等问题,《Effective Java》作者Json Bloch建议使用Lambda表达式最好不要超过3行。
Stream API: 用函数式编程方式在集合类上进行复杂操作的工具,配合Lambda表达式可以方便的对集合进行处理。Java 8中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找’过滤和映射数据等操作。使用Stream API对集合数据进行操作,就类似于使用SQL执行的数据库查询。也可以使用Stream API来并行执行操作。简而言之,Stream API提供了一种高效且易于使用的处理数据的方式。
方法引用:方法引用提供了非常有用的语法,可以直接引用已有的Java类或对象(实例)的方法或构造器。与Lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
日期时间API: Java 8 引入了新的日期事件API,改进了日期时间的管理。
Optional类:著名的NullPointerException 是引起系统失败最常见的原因。很久以前Google Guava项目引入了Optional作为解决空指针异常的一种方式,不赞成代码被null检查的代码污染,期望程序员写整洁的代码。受Google Guava的鼓励,Optinal现在是Java 8库的一部分。
新工具:新的编译工具,如:Nashorn引擎的jjs,类依赖分析器jdeps。
答:来源不同:sleep()来自Thread类,wait()来自Object类。
对于同步锁的影响不同:sleep()不会改变同步锁的行为,如果当前线程持有同步锁,那么sleep()是不会让线程释放同步锁的。wait()会释放同步锁,让其他线程进入synchronized代码块执行。
使用范围不同:sleep()可以在任何地方使用。wait()只能在同步控制方法或者同步控制块里面使用,否则会抛IllegalMonitorStateException。
恢复方式不同:两者会暂停当前线程,但是在恢复上不太一样。sleep()在时间到了之后会重新恢复;wait()则需要其他线程调用同一对象的notify()才能重新恢复。
答:线程执行sleep()方法后进入超时等待(TIMED_WAITING)状态,而执行yield()方法后进入就绪(READY)状态。
sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会。
答:用于等待当前线程终止。如果一个线程A执行了threadB.join()语句,其含义是:当前线程A等待threadB线程终止之后才从threadB.join()返回继续往下执行自己的代码。
通常来说,可以认为有三种方式:1)继承Thread类;2)实现Runnable接口;3)实现Callable接口。
其中,Thread其实也是实现了Runnable接口。Runnale和Callable的主要区别在于是否有返回值。
run():普通的方法调用,在主线程中执行,不会新建一个线程来执行。
start():新启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到CPU时间片,就开始执行run()方法。
一个线程可以处在以下状态之一:
NEW:新建但是尚未启动的线程处于此状态,没有调用start()方法。
RUNNABLE:包含就绪(READY)和运行中(RUNNING)两种状态。线程调用start()方法会进入就绪(READY)状态,等待获取CPU时间片。如果成功获取到CPU时间片,选择会进入运行中(RUNNING)状态。
BLOCKED:线程在进入同步方法、同不快(sunchronized)时被阻塞,等待同步锁的线程处于此状态。
WAITING:无限期等待另一个线程执行特定操作的线程处于此状态,需要被显示的唤醒,否则会一直等待下去。例如对于Object.wait(),需要等待另一个线程执行Object.notify()或Object.notifyAll();对于Thread.join(),则需要等待指定的线程终止。
TIME_WAITING:在指定的时间内等待另一个线程执行某项操作的线程处于此状态。跟WAITING类似,区别在于该状态有超时时间参数,在超时时间到了后会自动唤醒,避免了无限期的等待。
TREMINATED:执行完毕已经退出的线程处于此状态。
线程在给定的时间点只能处于一种状态。这些状态是虚拟机状态,不反映任何操作系统线程状态。
1)Lock是一个接口;synchronized是Java中的关键字,synchronized是内置的语言实现;
2)Lock在发生异常时,如果没有主动通过unLock()去释放锁,很可能会造成死锁现象,因此使用Lock时需要在finally块中释放锁;synchronized不需要手动获取锁和释放锁,在发生异常时,会自动释放锁,因此不会导致死锁现象发生;
3)Lock的使用更加灵活,可以有响应中断,有超时时间等;而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,直到获取到锁;
4)在性能上,随着近些年synchronized的不断优化,Lock和synchronized在性能上已经没有很明显的差距了,所以性能不应该称为我们选择两者的主要原因。官方推荐尽量使用synchronizd,除非synchronized无法满足需求时,则可以使用Lock。
1.作用于非静态方法,锁住的是对象实例(this),每一个对象实例有一个锁。
public synchronized void method() {}
2.作用于静态方法,锁住的是类的Class对象,因为Class的相关数据存储在永久代元空间,元空间是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程。
public static synchronized void method() {}
3.作用于Lock.class,锁住的是Lock的Class对象,也是全局只有一个。
synchronized (Lock.class) {}
4.作用于this,锁住的是对象实例,每一个对象实例有一个锁。
synchronized(this){}
5.作用于静态成员变量,锁住的时该静态成员变量对象,由于是静态变量,因此全局只有一个。
public static Object monitor = new Object(); synchronized (monitor) {}
死锁的四个必要条件:
1)互斥条件:进程对所分配到的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
2)请求和保持条件:进程已经获取了至少一个资源,但又对其他资源发出请求,而该资源已被其他进程占有,此时该进程的请求被阻塞,但又对自己获得的资源保持不放。
3)不可剥夺条件:进程已获得的资源在未使用完毕之前,不可被其他进程强行剥夺,只能由自己释放。
4)环路等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{P1,P2…,Pn},其中Pi等待的资源被P(i+1)占有(i=0,1,…,n-1),Pn等待的资源被P0占有,如下图所示。
预防死锁的方式就是打破四个必要条件中的任意一个即可。
1)打破互斥条件:在系统里取消互斥。若资源不被一个进程独占使用,那么死锁是肯定不会发生的。但一般来说在所列的四个条件中,”互斥“条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏”互斥“条件。
2)打破请求和保持条件:1.采用资源预先分配策略,即进程运行申请全部资源,满足则运行,不然就等待。2.每个进程提出新的资源申请前,必须先释放它先前锁占有的资源。
3)打破不可剥夺条件:当进程占有某些资源后又进一步申请其他资源而无法满足,则该进程必须释放它原来占有的资源。
4)打破环路等待条件:实现资源有序分配策略,将系统的所有资源统一编号,所有进程智能被按序号底层的形式申请资源。
如果我们在方法中直接new一个线程来处理,当这个方法被调用频繁时就会创建很多线程,不仅会消耗系统资源,还会降低系统的稳定行。
如果我们合理的使用线程池,则可以避免把系统搞崩的窘境。总的来说,使用线程池可以带来以下几个好处:
threadFactory(线程工厂):用于创建工作线程的工厂。
corePoolSize(核心线程数):当线程池运行的线程少于corePoolSize时,将创建一个新线程来处理请求,即使其他工作线程处于空闲状态。
workQueue(队列):用于保留任务并移交给工作线程的阻塞队列。
maximumPoolSize(最大线程数):线程池允许开启的最大线程数。
handler(拒绝策略):往线程池添加任务时,将在下面两种情况触发拒绝策略:1)线程池运行状态不是RUNNING;2)线程池已经达到最大线程数,并且阻塞队列已满时。
keepAliveTime(保持存活时间):如果线程池当前线程数超过corePoolSize,则多余的线程空闲时间超过keepAliveTime时会被禁止。
AbortPolicy:中止策略。默认的拒绝策略,直接抛出RejectedEcecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。
DiscardPolicy:抛弃策略。什么都不做,直接抛弃被拒绝的任务。
DiscardOldestPolicy:抛弃最老策略。抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。如果阻塞队列时一个优先队列,那么”抛弃最旧的“策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用。
CallerRunsPolicy:调用者运行策略。在调用者线程中执行该任务。该策略实现了一种调节机制,该策略不会抛弃任务,也不会抛出异常,而是将任务回退到调用者(调用线程池执行任务的主线程),由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有时间来处理正在执行的任务。
List(对付顺序的好帮手):List接口存储一组不唯一(可以有多个元素引用相同的对象)、有序的对象。
Set(注重独一无二的性质):不允许重复的集合,不会有多个元素引用相同的对象。
Map(用key来搜索的专业户):使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任意对象。
ArrayList底层基于动态数组实现,LinkedList底层基于链表实现。
对于按index索引数据(get/set方法):ArrayList通过index直接定位到数组对应位置的节点,而LinkedList需要从头节点或尾节点开始遍历,直到寻找到目标节点,因此在效率上ArrayList优于LinkedList。
对于随机插入和删除:ArrayList需要移动目标节点后面的节点(使用System.arraycopy方法移动节点),而LinkedList只需修改目标节点前后节点的next或prev属性即可,因此在效率上LinkedList由于ArrayList。
对于顺序插入和删除:由于ArrayList不需要移动节点,因此在效率上比LinkedList更好。这也是为什么在实际使用中ArrayList更多,因为大部分情况下我们的使用都是顺序插入。
Vector和ArrayList几乎一致,唯一的区别是Vector在方法上使用了synchronized来保证线程安全,因此在性能上ArrayList具有更好的表现。
有类似关系的还有:StringBuilder和StringBuffer、HashMap和Hashtable。
我们现在用的都是JDK 1.8,底层是由”数组+链表+红黑树“组成,如下图,而在JDK1.8之前是由”数据+链表“组成。
主要是为了提升在hash冲突严重时(链表过长)的查找性能,使用链表的查找性能是O(n),而使用红黑树是O(logn)。
对于插入,默认情况下是使用链表节点。当同一个索引位置的节点在新增后超过8个(阈值8):如果此时数组长度大于等于64,则会触发链表节点转红黑树节点(treeifyBin);而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。
对于溢出,当同一个索引位置的节点在移除后达到6个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点(untreeify)。
默认初始容量是16。HashMap的容量必须是2的n次方,HashMap会根据我们传入的容量计算一个大于等于该容量的最小的2的n次方,例如传9,容量为16。
HashMap允许key和value为null,Hashtable不允许。
HashMap的默认初始容量为16,Hashtable为11.
HashMap的扩容为原来的2倍,Hashtable的扩容为原来的2倍加1。
HashMap是非线程安全的,Hashtable是线程安全的。
HashMap的hash值重新计算过,Hashtable直接使用hashCode。
HashMap去掉了Hashtable中的contains方法。
HashMap继承子AbstractMap类,Hashtable继承子Dictionary类。
程序计数器:线程私有。一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空。
Java虚拟机栈:线程私有。它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调至至执行完成的过程,就对应这一个栈正在虚拟机栈中入栈到出栈的过程。
本地方法栈:线程私有。本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
Java堆:线程共享。堆大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域。在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
方法区:与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(构造方法、接口定义)、常量、静态变量、即使编译器编译后的代码(字节码)等数据。方法区是JVM规范中定义的一个概念,具体放在哪里,不同的实现可以放在不同的地方。
运行时常量池:运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
String str = new String("hello");
上面的语句中变量str放在栈上,用new创建出来的字符串对象放在堆上,而”hello“这个字面量是放在堆中。
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器区完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
启动类加载器(Bootstrap ClassLoader):
这个类加载器负责将存放在
拓展类加载器(Extension ClassLoader):
这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载
应用程序类加载器(Application ClassLoader):
这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
自定义类加载器:
用户自定义的类加载器。
类加载的过程包括:加载、验证、准备、解析、初始化,其中验证、准备、解析统称为连接。
加载:通过一个类的全限定名来获取定义此类的二进制字节流,在内存中生成一个代表这个类的java.lang.Class对象。
验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备:为静态变量分配内存并设置静态变量初始值,这里所说的初始值“通常情况下”是数据类型的零值。
解析:将常量池内的符号引用替换为直接引用。
初始化:到了初始化阶段,才真正开始执行类中定义的Java初始化程序代码。主要是静态变量赋值动作和静态语句块(static{})中的语句。
在什么时候?
在触发GC的时候,具体如下,这里只说常见的Young GC和Full GC。
触发Young GC:当新生代中的Eden区没有足够空间进行分配时会触发Young GC。
触发Full GC:
对什么
对哪些JVM认为已经“死掉”的随性。即从GC Root开始搜索,搜索不到的,并且经过一次筛选标记没有复活的对象。
做了什么
对这些JVM认为已经“死掉”的对象进行垃圾收集,新生代使用复制算法,老年代使用标记-清除和标记-整理算法。
在Java语言中,可作为GCRoots的对象包括下面几种:
标记-清除算法
首先标记出所有需要回收的对象,在标记完成后同一回收所有被标记的对象。它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象是,无法找到走狗的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法
为了解决效率问题,一种称为:“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过得内存空间一次清理掉。这样是的每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩为了原来的一半,未免太高了一些。
标记-整理算法
复制手机算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点,有人提出了另外一种“标记-整理”(mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一段移动,然后直接清理掉端边界以外的内存。
分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。
一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集是都发现有大批对象死去,只有少量寻获,那就选用复制算法,只需要付出少量存活对象的赋值成本就可以完成收集。
在老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用标记-清理或者标记=-整理算法来进行回收。