java面试知识点(难)

1.HashMap底层原理

HashMap内部数据结构是由数组+链表,红黑树实现。

JDK1.7时,hashMap是由数组+链表实现,在1.8后,在JDK1.7的基础上针对增加了红黑树来进行优化。即当链表超过8时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。

HashMap初始化时:new HashMap() 不传值时,默认大小是16,负载因子是0.75,如果传入初始大小k,初始化大小为大于k的2的整数次方,如传入10,大小为16。

HashMap的哈希函数设计:hash函数是先通过key的hashcode,是32位的int值,然后通过hashcode的高16位和低16位进行异或操作得到。

这样设计的原因有:

  1. 尽可能降低hash碰撞,越分散越好
  2. 算法要尽可能高效,因为这是高频操作,因此采用位运算

HashMap的数据插入原理

  1. 判断数组是否为空,为空进行初始化
  2. 不为空,计算k的hash值,通过(n-1)& hash计算应当存放在数组中下标index;
  3. 查看table[index]是否存在数据,没有数据就构造一个Node结点存放在table[index]中
  4. 存在数据,说明发送了hash冲突(存在两个结点key的hash值一样),继续判断key是否相等,相等,用新的value值替换原数据
  5. 如果不相等,判断当前结点类型是不是树形结点,如是,创造树形结点插入红黑树
  6. 如果不是树形结点,创建普通的Node加入链表中,判断链表长度是否大于8,大于则转换为红黑树
  7. 插入完成之后判断当前节点数是否大于阈值,如果大于则扩充为原数组的两倍

2.ArrayList,ArrayList和Vector的区别,LinkedList和ArrayList的区别

ArrayList和Vector的联系和区别

相同点:
1底层都是用数组实现;
2.功能相同,实现增删改查等操作的方法相似;
3.长度可变的数组结构;
不同的:
1.Vector是早起JDK版本提供,ArrayList是新版本替代Vector的;
2.Vector的方法是同步的,线程安全,ArrayList非线程安全,但性能比Vector好;
3.默认初始化容量都是10,Vector扩容默认会翻倍,可指定扩容的大小,ArrayList只增加50%。

ArrayList和LinkedList的联系和区别

LinkedList插入删除快,ArrayList查询快

1.ArrayList是基于数组实现的,添加元素时,存在扩容问题,扩容时需要复制数组,消耗性能;
2.LinkedList是基于链表实现的,只要将元素添加到链表最后一个元素的下一个即可。

3.String,StringBuffer,StringBuilder的区别 扩展:String不可变有什么好处?

共同
都是final类,不允许被继承.

区别

  1. String:final修饰,String类的方法都是返回new String,即对String对象的任何改变都不影响到原对象,对字符串的修改操作都会生成新的对象;
  2. StringBuffer:对字符串的操作的方法都加了synchronized,保证线程安全;
  3. StringBuilder:不保证线程安全,在方法体内需要进行字符串的修改操作,可以new StringBuilder对象,调用StringBuilder对象的append,replace,delete等方法修改字符串。

运行速度,或者说是执行速度:StringBuilder > StringBuffer > String
String最慢的原因:String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。

线程安全:StringBuilder是线程不安全的,而StringBuffer是线程安全的。如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。

String不可变好处
因为在程序编写的过程中,会大量地用到String常量,如果每次声明一个String引用都要新建一个String对象,那么会造成空间的极大浪费。于是,在java的堆中开辟了一块存储空间String pool,用于存储String常量对象。当有多个String引用指向同样的String字符串时,实际上是指向的是同一个Sting pool中的对象,而不需要额外的创建对象。

其他好处:

  1. 以String作为HashMap的key,String的不可变保证了hash值的不可变。
  2. String作为网络连接的参数,它的不可变性提供了安全性。
  3. String不可变,所以线程安全。

4.迭代器是做什么的?迭代器的fail-fast机制了解吗?主要为了解决什么问题?

迭代器Iterator是可以遍历集合的对象,为各种容器提供了公共的操作接口,隔离对容器的遍历操作和底层实现,从而解耦;

fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件

5.ConcurrentHashMap的底层数据结构

ConcurrentHashMap相比HashMap而言,是多线程安全的,其底层数据与HashMap的数据结构相同;

ConcurrentHashMap和Hashtable主要区别就是围绕着锁的粒度以及如何锁,可以简单理解成把一个大的HashTable分解成多个,形成了锁分离。
ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。

1.7中采用Segment+HashEntry的方式实现
1.8中放弃了Segment分段锁的设计,使用的是Node+CAS+Synchronized来保证线程安全性

应用场景
当有一个大数组时需要在多个线程共享时就可以考虑是否把它给分层多个节点了,避免大锁。并可以考虑通过hash算法进行一些模块定位。
其实不止用于线程,当设计数据表的事务时(事务某种意义上也是同步机制的体现),可以把一个表看成一个需要同步的数组,如果操作的表数据太多时就可以考虑事务分离了(这也是为什么要避免大表的出现),比如把数据进行字段拆分,水平分表等.

6.synchronized和lock分别在什么情况下使用,使用的理由

synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。

lock:需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。

用途区别

synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面2种需求的时候。

  1. 某个线程在等待一个锁的控制权的这段时间需要中断
  2. 需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
  3. 具有公平锁功能,每个到来的线程都将排队等候

7.java中各种锁的实现和使用场景

在并发编程中,经常遇到多个线程访问同一个共享变量。当同时对共享变量进行写操作时,就会产生数据不一致的情况。
为了解决这个问题,在JDK1.5前,使用synchronized关键字,拿到Java对象的锁,保护锁定的代码块。JVM保证同一时刻只有一个线程可以拿到这个Java对象的锁,执行对应的代码块。在JDK1.5后,引入了并发工具 包java.util.concurrent.locks.Lock,
让锁的功能更丰富。

常见的锁

  1. synchronized 关键字锁定代码库 ;
  2. 可重入锁 java.util.concurrent.lock.ReentrantLock;
  3. 可重复读写锁 java.util.concurrent.lock.ReentrantReadWriteLock;

Java中不同维度的锁分类:
可重入锁:指在同一个线程在外层方法获取锁的时候,进入内层方法会自动获取锁,JDK中基本都是可重入锁,避免死锁的发生;

公平锁/非公平锁:

  1. 公平锁:指多个线程按照申请锁的顺序来获取锁;
  2. 非公平锁:指多个线程获取锁的顺序不是按照申请锁的顺序,有可能后申请的线程先后的锁;

独享锁/共享锁:

  1. 独享锁:指锁一次只能被一个线程所持有;
  2. 共享锁:指锁可被多个线程所持有;

悲观锁/乐观锁:

  1. 悲观锁:一律会对代码块进行加锁;
  2. 乐观锁:默认不会进行并发修改,通常采用CAS算法不断尝试更新(悲观锁适合写操作较多的场景,乐观锁适合读操作较多的场景);

粗粒度锁/细粒度锁:

  1. 粗粒度锁:就是把执行的代码块都锁定;
  2. 细粒度锁:就是锁住尽可能小的代码块;

偏向锁/轻量级锁/重量级锁:

  1. JDK1.5后新增锁的升级机制,提升性能;
  2. 通过synchronized加锁后,一段同步代码一直被同一个线程所访问,那么该线程获取的就是偏向锁;
  3. 偏向锁被一个其他线程所访问时,Java对象的偏向锁就会升级为轻量锁;
  4. 再有其他线程会以自旋的形式尝试获取锁,不会阻塞,自旋一定次数仍然未获取到锁,就会膨胀为重量级锁;

自旋锁:就是尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处就是减少线程上下文切换的消耗,缺点是循环占有,浪费CPU资源。

8.面向对象的七个基本原则,能说出分别的目的吗?

1.单一职责原则(Single Responsibility Principle)
每一个类应该专注于做一件事情。

2.里氏替换原则(Liskov Substitution Principle)
超类存在的地方,子类是可以替换的。

3.依赖倒置原则(Dependence Inversion Principle)
实现尽量依赖抽象,不依赖具体实现。

4.接口隔离原则(Interface Segregation Principle)
应当为客户端提供尽可能小的单独的接口,而不是提供大的总的接口。

5.迪米特法则(Law Of Demeter)
又叫最少知识原则,一个软件实体应当尽可能少的与其他实体发生相互作用。

6.开闭原则(Open Close Principle)
面向扩展开放,面向修改关闭。

7.组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP)(优先使用组合而不是继承原则)
尽量使用合成/聚合达到复用,尽量少用继承。原则: 一个类中有另一个类的对象。

9.java中的一些类的设计是如何体现设计模式的

10.什么是CAS算法?CAS底层做了什么?CAS可能产生什么问题?

CAS的定义

一个线程失败或挂起并不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法。而CAS就是一种非阻塞算法实现,也是一种乐观锁技术,它能在不使用锁的情况下实现多线程安全,所以CAS也是一种无锁算法。

CAS (Compare And Swap)比较并交换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。CAS具体包括三个参数:当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。CAS 有效地说明了“ 我认为位置 V 应该包含值 A,如果真的包含A值,则将 B 放到这个位置,否则,不要更改该位置,只告诉我这个位置现在的值(A)即可。 ”整个比较并交换的操作是原子操作。

CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。

  1. 循环时间长、开销很大。
    当某一方法比如:getAndAddInt执行时,如果CAS失败,会一直进行尝试。如果CAS长时间尝试但是一直不成功,可能会给CPU带来很大的开销。
  2. 只能保证一个共享变量的原子操作。
    当操作1个共享变量时,我们可以使用循环CAS的方式来保证原子操作,但是操作多个共享变量时,循环CAS就无法保证操作的原子性,这个时候就需要用锁来保证原子性。
  3. 存在ABA问题

11.知道AQS框架吗?说一下

AQS(AbstractQueueSynchronizer类)是一个用来构建锁和同步器的框架,各种Lock包中的锁,甚至早起的FutureTask,都是基于AQS构建的。

1.AQS在背部定义了一个volatile int state变量,表示同步状态:当线程调用Lock方法时,如果state=0,说明没有任何线程占用共享资源锁,可以获得锁并将状态state改为1,;如果state=1,说明有线程正在使用共享变量,其他线程必须加入同步队列并等待。

2.AQS通过Node内部类构成一个双向链表结构的同步队列,来完成线程获取锁的排队工作,当有线程获取锁失败后,就会被添加到队列末尾。

3.AQS通过内部类ConditionObject构建等待队列(可有多个),当Condition调用wait()后,线程将会加入等待队列中,Condition调用signal()后,线程会从等待队列中转移到同步队列中进行锁竞争。

4.AQS和Condition各自维护不同的队列,在使用Lock和Condition的时候,其实就是两个队列在互相移动。

12.如果一个线程要在其他几个线程运行完之后运行,有什么办法?

CountDownLatch

软件包
java.util.concurrent

适用情景
主线程等待多个工作线程结束

主要方法

  1. CountDownLatch(int count) (主线程调用)
  2. 初始化计数
  3. CountDownLatch.await (主线程调用)
  4. 阻塞,直到等待计数为0解除阻塞
  5. CountDownLatch.countDown
  6. 计数减一(工作线程调用)

等待结束

各线程之间不再互相影响,可以继续做自己的事情。不再执行下一个目标工作。

13.多线程在运行过程中抛出异常怎么捕获?

使用工厂模式创建线程池,将异常捕获类与线程池联系起来

Future或回调函数
在java中要捕捉多线程产生的异常,需要自定义异常处理器,并设定到对应的线程工厂中(即第一步和第二步)

14.如果想把两个线程的结果拿到进行下一步操作怎么做

见12

15.年轻代出现OOM怎么处理?老年代出现OOM怎么处理?方法区出现OOM怎么处理?本地方法栈出现OOM怎么处理?

在这里插入图片描述
老年代:2/3的堆空间
年轻代:1/3的堆空间
eden区:8/10 的年轻代
survivor0: 1/10 的年轻代
survivor1:1/10的年轻代

内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出。

JVM内存模型
按照JVM规范,JAVA虚拟机在运行时会管理以下的内存区域:

程序计数器:当前线程执行的字节码的行号指示器,线程私有;
JAVA虚拟机栈:Java方法执行的内存模型,每个Java方法的执行对应着一个栈帧的进栈和出栈的操作。

本地方法栈:类似“ JAVA虚拟机栈 ”,但是为native方法的运行提供内存环境。

JAVA堆:对象内存分配的地方,内存垃圾回收的主要区域,所有线程共享。可分为新生代,老生代。

方法区:用于存储已经被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Hotspot中的“永久代”。

运行时常量池:方法区的一部分,存储常量信息,如各种字面量、符号引用等。

直接内存:并不是JVM运行时数据区的一部分, 可直接访问的内存, 比如NIO会用到这部分。

OOM出现区域
按照JVM规范,除了程序计数器不会抛出OOM外,其他各个内存区域都可能会抛出OOM。

最常见的OOM情况有以下三种:

  1. java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
  2. java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区,此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。
  3. java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。

调优总结

  1. 年轻代大小选择:
    响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
    吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

  2. 老年代大小选择
    响应时间优先的应用:老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:1.并发垃圾收集信息 2.持久代并发收集次数 3.传统GC信息

  3. 花在年轻代和年老代回收上的时间比例
    减少年轻代和年老代花费的时间,一般会提高应用的效率。

  4. 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

  5. 较小堆引起的碎片问题
    因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行JVM参数配置。

16.ThreadLocal可以解决什么问题?具体的应用场景?

ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。

经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。

17.ReenTrantLock中的condition有什么作用?condition的await和signal和Object的wait和notify有什么区别?

18.volatile主要解决了什么问题?

Java中的volatile关键字是一个类型修饰符,在JDK1.5后,对其语义进行了加强;

1.保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了共享变量的值,共享变量修改后的值对其他线程立即可见;

2.通过禁止编译器、CPU指令重排序和部分happens-befor规则,解决有序性问题;
volatile可见性的实现:

  1. 在生成汇编代码指令时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令;
  2. Lock前缀的指令会引起CPU缓存写回内存;
  3. 一个CPU的缓存回写到内存会导致其他CPU缓存了该内存地址的数据无效;
  4. volatile变量通过缓存一致性协议保证每个线程获得最新值;
  5. 缓存一致性协议保证每个CPU通过嗅探在总线上传播的数据来检查自己缓存的值是不是修改;
  6. 当CPU发现自己缓存行对应的内存地址被修改,会将当前CPU的缓存行设置成无效状态,重新从内存中把数据读到CPU缓存;

19.类的加载过程?如何保证我的类被指定的类加载器加载?相同的类被不同的类加载器加载了,这两个类相同吗?

一个class进来进行loading加载到内存中,然后进行linking,linking中又分为verification(检查),preparartion(准备),resolution(处理)。接着进行initalizing初始化给静态变量赋初始值,判断为无用类进行GC回收。

linking模块进行描述
verification:用来检查加载进来的class是否符合解析的标准
preparartion:给class的静态变量赋默认值,一般基本类型为0,引用类型为null。
resolution:将class类中常量池中的地址符号转换成为直接的内存地址,及可访问的内存地址

通过自定义类加载器,可以保证类被指定的类加载器加载

为什么要自定义ClassLoader
因为系统的ClassLoader只会加载指定目录下的class文件,如果你想加载自己的class文件,那么就可以自定义一个ClassLoader.

而且我们可以根据自己的需求,对class文件进行加密和解密。
如何自定义ClassLoader:

  1. 新建一个类继承自java.lang.ClassLoader,重写它的findClass方法。
  2. 将class字节码数组转换为Class类的实例
  3. 调用loadClass方法即可

相同的类被不同的类加载器加载了,这两个类不相同

双亲委派机制,其工作原理的是:
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改

20.jvm的内存模型和各种垃圾回收算法和垃圾回收器

标记-清除算法(Mark-Sweep)
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间

复制算法(Copying)
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题

标记-整理算法(Mark-compact)
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,(记住是完成标记之后,先不清理,先移动再清理回收对象),然后清理掉端边界以外的内存  
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题

分代收集算法 Generational Collection(分代收集)算法   
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前大部分垃圾收集器对于新生代都采取复制(Copying)算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间(一般为8:1:1),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。
年老代(Old Generation)的回收算法(回收主要以Mark-Compact为主)
持久代(Permanent Generation)(也就是方法区)的回收算法

21.jvm内存模型 扩展:各个jdk版本的内存模型有什么变化

按照JVM规范,JAVA虚拟机在运行时会管理以下的内存区域:

程序计数器:当前线程执行的字节码的行号指示器,线程私有

JAVA虚拟机栈:Java方法执行的内存模型,每个Java方法的执行对应着一个栈帧的进栈和出栈的操作。
本地方法栈:类似“ JAVA虚拟机栈 ”,但是为native方法的运行提供内存环境。

JAVA堆:对象内存分配的地方,内存垃圾回收的主要区域,所有线程共享。可分为新生代,老生代。

方法区:用于存储已经被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Hotspot中的“永久代”。

运行时常量池:方法区的一部分,存储常量信息,如各种字面量、符号引用等。

直接内存:并不是JVM运行时数据区的一部分, 可直接访问的内存, 比如NIO会用到这部分。

22.jvm的类加载机制,反射的原理以及反射的应用

原理:反射首先是能够获取到Java中的反射类的字节码,然后将字节码中的方法,变量,构造函数等映射成 相应的 Method、Filed、Constructor 等类。

应用:

  1. 取出类的modifiers,数据成员,方法,构造器,和超类;
  2. 找出某个接口里定义的常量和方法说明.;
  3. 取得和设定对象数据成员的值,如果数据成员名是运行时刻确定的也能做到.;
  4. 在运行时刻调用动态对象的方法.;

将类加载到本地程序集中,达到和调用本地的类效果是一样 ,另外利用了“依赖反转”的思想,让被调用者实现调用者提供的接口,这样就可以被转换为本程序集中的类,这样就能提升性能

23.java的四种引用类型?jvm如何判断这个对象可回收?finalize方法的作用?

引用类型可以说是整个Java开发的灵魂所在,如果没有合理的引用操作,那么就有可能产生垃圾问题,但是对于引用也需要有一些合理化的设计。在很多的时候并不是所有的对象都需要被我们一直使用,那么就需要对引用的问题做进一步的思考。从JDK1.2之后关于引用提出了四种方案:

  1. 强引用:当内存不足的时候,JVM宁可出现OutOfMemory错误停止,也需要进行保存,并且不会将此空间回收;
  2. 软引用:当内存不足的时候,进行对象的回收处理,往往用于高速缓存中;
  3. 弱引用:不管内存是否紧张,只要由垃圾产生了,那么立即回收;
  4. 幽灵引用:和没有引用是一样的。

如何判断这个对象可回收?
当一个对象地址没有变量去引用的时候,该对象就会成为垃圾对象,垃圾回收器在空闲时就会去对齐进行内存清理回收

怎么知道可回收?

GC Roots Analysis:主流用这个判断:
基本思路就是通过一系列名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。(即意味着直接回收)
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。

GC Root 引用点
虚拟机栈(栈帧中的本地变量表)中引用的对象;
方法区中类静态属性引用的对象;
方法区中常量引用的对象;
本地方法栈中JNI(即一般说的Native方法)引用的对象;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值