- 内存管理:垃圾回收机制解释与编译执行.java -> .class,.class 文件存储的是字节码,在执行时需要 JVM 逐行将字节码解释为机器上可以运行的机器码,在引入 JIT(即时编译器)之后,JVM 会对部分热点代码直接编译为机器码,存储在本地内存,以提高运行效率面向对象语言Java 的虚拟机 JVM 是 Java 宣传的 “一次编译,到处运行” 的关键所在日常开发所需要的 JDK 包括 JRE 与 编译工具,JRE 是 Java 程序运行的环境,包括 JVM 与 基础类库JVM:
- 内存管理:垃圾回收机制、垃圾收集器类加载:类加载过程、双亲委派机制诊断工具:jmap、jconsole、jstack
- 集合 – List、Map、SetJUC – AQS、线程池网络 – IO
Java 是一种面向对象的语言,它有两个显著的特性,第一个特性是,Java 是解释与编译共存的,我们写好的代码经由 javac 编译之后生成 .class 字节码文件,在执行时,JVM 需要逐行将字节码解释为本地机器可以识别的机器码,对于提供 JIT(即时编译器)的虚拟机来说,即时编译器会将部分热点代码直接编译为机器码,存储在本地内存,这部分代码在需要时可以直接调用,而不需要再解释了,提高了运行效率。class 文件的存在以及 JVM 解释执行的特点正是 Java “一次编译,到处运行” 的根本。
第二个特性是,Java 具有自动内存管理机制,不需要为每一个 new 操作去写配对的 delete/free 代码,Java 通过垃圾收集器回收分配内存,大部分情况下不需要操心内存的分配与回收,Java 有适应不同场景的垃圾收集器,也有不同种类的垃圾回收机制。
综合来看的话,以上两个特点都离不开 JVM 的支持。我们日常开发所需要的 JDK 包括 JRE 与 编译工具,JRE 是 Java 程序的运行环境,包含着 JVM 与 基础类库,JVM 除了有内存管理的功能,还管理着我们类加载的过程,以及所使用的双亲委派模型等,它还包含着一些诊断工具,比如说 jmap、jconsole、jstack等。
2. Exception 和 Error 有什么区别?- Exception 和 Error 都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类。Exception 是程序正常运行中,可以预料的意外情况,并且应该被捕获,进行相应处理;Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序本身处于非正常的、不可恢复的状态Exception 又分为 可检查异常 和 不检查异常。可检查异常是在编译期就可以确定的问题,比如说 ClassNotFoundException;不检查异常也叫运行时异常,这类异常大部分属于逻辑问题,如数组越界、空指针异常
Error:StackOverflowError、OutOfMemoryError
Exception:IOException、ClassNotFoundException(反射,Class.forName() 时会出现)
RuntimeException:NullPointerException、ClassCastException
2.2 追问:throw、thorws 有什么区别?throw
- throw 是语句抛出一个具体的异常类型,定义在方法体内,当程序出现某种逻辑错误时由程序员主动抛出某种特定类型的异常throw 只有在确定会发生哪种异常了才可以使用,执行 throw 则一定抛出了某种异常
throws
- throws 是在方法参数列表后,后面可以跟着多个异常名throws 表示向该方法的调用者抛出异常,由调用者来处理异常throws 表示出现异常的一种可能性,并不一定会发生这些异常
- 第一,尽量不要捕获类似 Exception 这样的异常,而是应该捕获特定具体的异常第二,不要生吞异常,也就是说,不能基于假设这段代码可能不会发生,或者感觉忽略异常是无所谓的
final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 Java 类代表不可以被继承扩展,final 修饰的方法代表不可以被重写,final 修饰的变量也是不可以修改的
finally 是 Java 保证重点代码一定要被执行的一种机制。我们可以使用 try-cache-finally 块来进行关闭 JDBC 的连接、保证 unlock 锁等动作
finalize 的设计目的是保证对象在被垃圾收集前完成特定资源的回收。现在已经不推荐使用了
4. 强引用、软引用、弱引用、虚引用有什么区别?不同的引用类型,主要体现的是 对象不同的可达性状态和对垃圾收集的影响
强引用
通过 new 关键字创建的对象所关联的引用就是强引用,当 JVM 内存空间不足,JVM 宁愿抛出 OutOfMemoryError ,也不愿回收具有强引用的存活对象
软引用
是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。软引用通常用来实现内存敏感的缓存
弱引用
弱引用的声明周期比软引用短,在垃圾收集线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存是否充足,都会回收它的内存
虚引用
对于虚引用,我们不能通过它访问对象,虚引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中
- String 是不可变类,被声明成 final class,其内部的所有属性,都被声明为 final 的,也正是由于它的不可变性,类似于拼接、裁剪字符串都会产生新的字符串对象StringBuffer 和 StringBuilder 就是为了解决会产生大量中间字符串对象而出现的,他俩都继承自 AbstractStringBuilder,区别在于StringBuffer 是线程安全的,StringBuffer 内部通过将各种修改数据的方法都加上了 synchronized 关键字来实现线程安全;StringBuilder 没有线程安全的部分,有效减小了开销,比较常用。
反射机制是 Java 语言提供的一种基础功能,赋予程序在运行时 自省 的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。
动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的,比如用来包装 RPC 调用、面向切面的编程(AOP)。
实现动态代理的方式很多,比如 JDK 自身提供的动态代理,就是主要利用了上面提供的反射机制。还有其他的实现方式,ASM、cglib 等
7. int 和 Integer 有什么区别?int 是我们常说的整型数字,是 Java 的 8 个基本数据类型之一。
Integer 是 int 对应的包装类,它有一个 int 类型的字段存储数据,并且提供了基本操作,比如数学运算、int 和 字符串之间转换等,在 Java 5 中,引入了自动装箱和自动拆箱功能,Java 可以根据上下文,自动进行转换,极大简化了相关编程。Java 5 中也新增了静态工厂方法 valueOf ,在调用它的时候会利用一个缓存机制,带来了明显的性能改进,这个值默认缓存是 -128 ~ 127 之间。
8. 对比 Vector、ArrayList、linkedList 有何区别?这三者都实现了 List 接口,都是有序集合
基本定义
Vector 是 Java 早期提供的线程安全的集合类,Vector 内部使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据
ArrayList 是应用绞广泛的动态数组实现,本身并不是线程安全的,所有性能好很多。ArrayList 与 Vector 在扩容时的逻辑有所区别,Vector 是提高一倍,ArrayList 则是增加 50%
linkedList 是 Java 提供的双向链表,所以并不需要调整容量,也不是线程安全的。
适用场景
Vector 和 ArrayList 作为动态数组,其内部元素以数组形式顺序存储,所以非常适合随机访问的场景。但是在进行插入和删除元素时,性能会相对较差
而 linkedList 进行节点插入、删除要高效的多,但是随机访问性能要比动态数组慢
Hashtable、HashMap、TreeMap 都是最常见的一些 Map 实现,是以键值对的形式存储和操作数据的容器类型。
Hashtable 是早期 Java 类库的一个哈希表实现,本身是同步的,不支持 null 键和值,现在已经很少被推荐使用了。
HashMap 是应用更加广泛的哈希表实现,HashMap 不是同步的,支持 null 键和值。通常情况下,HashMap 进行 put 和 get 操作时,可以达到常数时间的性能。
TreeMap 则是基于红黑树的一种提供顺序访问的 Map ,和 HashMap 不同,它的 get 、put 、remove 之类的操作都是 O(long(n)) 的。
9.1 追问:HashMap 的底层源码 9.2 追问:HashMap 的扩容逻辑 9.3 hash 函数内部实现 9.4 容量、负载因子,为什么一定要是 2 的次幂 9.5 hashcode 和 equals 10. 如何保证集合是安全的?ConcurrentHashMap 如何实现高效地线程安全? 11. Java 提供了哪些 IO 方式?NIO 如何实现多路复用? 12. 文件有集中拷贝方式?哪一种最高效? 13. 谈谈接口和抽象类有什么区别?[深入理解Java的接口和抽象类]
接口和抽象类是 Java 面向对象设计的两个基础机制
语法层面上的区别
- 抽象类可以提供成员方法的实现细节,而接口只能存在public abstract方法;抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;接口中不能包含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;一个类只能继承一个抽象类,而一个类却可以实现多个接口;
设计层面的区别
抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。
比如说飞机和鸟,飞机和鸟都会飞。在设计的时候,可以将飞机设计为一个类Airplane,鸟设计为一个类Bird,但是不能将飞行这个特性也设计为类,因为它只是一个行为特性,并不是对一类事物的描述。因此可以将飞行设计为一个接口Fly,包含方法fly(),然后Airplane和Bird再分别根据自己的需求实现Fly接口。然后至于什么战斗机、民用飞机等直接继承Airplane即可,鸟类也是这样。
设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。
- 公平与非公平代码灵活性功能:ReentrantLock 支持更灵活的条件变量、支持带超时时间的尝试获取锁可重入实现原理:synchronized 利用 monitorenter 和 monitorexit 实现了同步的语义;ReentrantLock 依赖 AQS 实现了锁机制使用:synchronized 可以用来修饰方法,也可以使用在特定代码块上,出代码块后会自动释放锁,ReentrantLock 需要调用 lock() 方法,并且在释放的时候需要明确调用 unlock() 方法。ReentrantLock 相对来说更加灵活性能:早期版本 synchronized 性能相对较差,目前来看差不多
synchronized 是早期 Java 仅有的同步手段,ReentrantLock 是 Java 1.5 之后提供的锁实现,synchronized 可以用来修饰方法、也可以用在特定代码块上,出代码块后会自动释放锁,而 ReentrantLock 需要调用 lock() 方法,并且在释放锁的时候需要明确调用 unlock() 方法。synchronized 是可重入的非公平锁,ReentrantLock 也是可重入的,它同时支持公平与非公平的实现。在功能方面,ReentrantLock 提供了更多的功能,它支持不同类型的条件变量、支持带超时时间的尝试获取锁,能够实现很多 synchronized 无法做到的细节控制。在实现原理方面上,synchronized 利用 monitorenter 和 monitorexit 实现了同步的语义,ReentrantLock 依赖 AQS 实现了锁机制。对于性能方面,早期版本的 synchronized 性能相对较差,目前都差不多
15.3 追问:什么是线程安全?线程安全是一个多线程环境下正确性的概念,也就是保证多线程环境下 共享的、可修改的 状态的正确性,这里的状态反映在程序中其实可以看作是数据 15.4 追问:synchronized 、ReentrantLock 底层实现;理解锁膨胀、降级;理解偏向锁、自旋锁、轻量级锁、重量级锁等概念 16. synchronized 底层如何实现?什么是锁的升级、降级? 16.1 联想点
synchronized 底层:monitorenter 、monitorexit对象头、MarkWord、Monitor 16.2 总结
synchronized 代码块是由一对 monitorenter / monitorexit 指令实现的,Monitor 对象是同步的基本实现单元。
在现代的 JDK 中, JVM 提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏向锁、轻量级锁和重量级锁。
所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
当没有竞争出现时,默认会使用偏向锁。JVM 会利用 CAS 操作,在对象头的 Mark Word 部分设置线程 ID ,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象声明周期中最多会被一个线程锁定,使用偏向锁可以降低无竞争开销。
如果有另外的线程试图锁定某个已经被偏向过的对象,JVM 就需要撤销(revoke)偏向锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。
16.3 知识扩展:其他一些特别的锁类型 基于 Lock 接口的锁降级确实也是会发生的,当 JVM 进入安全点的时候,会检查是否有闲置的 Montor,然后试图进行降级。
ReentrantLockReentrantReadWriteLock.ReadLockReentrantReadWriteLock.WriteLock ReadWriteLock
多个读操作不互斥读写互斥、写写互斥
在运行过程中,如果读锁试图锁定时,写锁是被某个线程持有,读锁将无法获得,而只好等待对方操作结束,这样就可以自动保证不会读取到有争议的数据。
StampedLock在提供类似读写锁的同时,还支持优化读模式。优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先试着读,然后通过 validate 方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁。
17. 一个线程两次调用 start() 方法会出现什么情况?Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常,多次调用 start() 被认为是编程错误
17.1 扩展 17.1.1 线程声明周期的不同状态线程状态被明确定义在其公共内部枚举类型 java.lang.Thread.State 中,分别是:
新建(NEW),表示线程被创建出来还没真正启动的状态就绪(RUNNABLE),表示该线程已经在 JVM 中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它 CPU 片段,在就绪队列里面排队。阻塞(BLOCKED),阻塞表示线程在等待 Monitor lock。等待(WAITING),表示正在等待其他线程采取某些操作。计时等待(TIMED_WAIT),其进入条件和等待状态类似,但是调用的是存在超时条件的方法终止(TERMINATED),不管是意外退出还是正常执行结束,线程已经完成使命,终止运行。
17.1.2 线程到底是什么以及 Java 底层实现方式从操作系统的角度,可以简单认为,线程是系统调度的最小单元,一个进程可以包含多个线程,作为任务的真正运作者,有自己的栈、寄存器、本地存储等,但是会和进程内其他线程共享文件描述符、虚拟地址空间等。
在现在的线程具体实现中,Java 线程是一对一映射到操作系统内核线程的
18. 什么情况下 Java 程序会产生死锁?如何定位、修复?死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待之中,没有任何个体可以继续前进。死锁不仅仅是在线程之间会发生,存在资源独占的进程之间同样也可能出现死锁。通常来说,我们大多数是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的锁,而永久处于阻塞的状态。
定位死锁最常见的方式就是利用 jstack 等工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁。
18.1 产生死锁的条件死锁的产生必须具备以下四个条件:
互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求使用该资源,则请求者只能等待,直至占有资源的线程释放该资源请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源环路等待条件:指在发生死锁时,必然存在一个线程一资源的环形链,即线程集合 {T0, T1, T2, … , Tn} 中的 T0 正在等待一个 T1 占用的资源,T1 正在等待 T2 占用的资源,…Tn 正在等待已被 T0 占用的资源。 18.2 如何在编程中尽量预防死锁?
- 如果可能的话,尽量避免使用多个锁,并且只有需要时才持有锁如果必须使用多个锁,尽量设计好锁的获取顺序使用带超时的方法,为程序带来更多可控性,类似 Object.wait(timed_wait) 获取 CountDownLatch.await(timed_wait),指定超时时间,无法得到锁时准备退出逻辑;ReentrantLock 的 tryLock() 方法,尝试获取锁,如果执行时对象恰好没有被独占,则直接获取锁。
提供了比 synchronized 更加高级的各种同步结构,包括 CountDownLatch、CyclicBarrier、Semaphore 等,可以实现更加丰富的多线程操作各种线程安全的容器,比如最常见的 ConcurrentHashMap、有序的 ConcurrentSkipListMap,或者同步类似快照机制,实现线程安全的动态数组 CopyonWriteArrayList 等各种并发队列实现,比如 ArrayBlockingQueue、SynchronousQueue 等强大的 Executor 框架,可以创建不同类型的线程池,调度任务运行等 19.1 扩展:CountDownLatch、CyclicBarrier、Semaphore 的机制 20. 并发包中的 ConcurrentlinkedQueue 和 linkedBlockingQueue 有什么区别? 21. Java 并发类库提供的线程池有哪几种?分别有什么特点?
Executors 目前提供了 5 种不同的线程池创建配置:
newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源,其内部使用 SynchronousQueue 作为工作队列。newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定数目的 nThreads。newSingleThreadExecutor():它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务都是被顺序执行的,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。newScheduledThreadPool(int corePoolSize) 和 newSingleThreadScheduledExecutor():创建的是个 ScheduledExecutorService ,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。newWorkStealingPool(int parallelism):Java 8 才加入这个创建方法,其内部会构建 ForkJoinPool ,利用 Work-Stealing 算法并行地处理任务,不保证处理顺序。
21.1 追问:线程池参数具体说一下 21.2 追问:线程池大小的选择策略
如果我们的任务主要是进行计算,那么就意味着 CPU 的处理能力是最稀缺的资源,如果线程太多,反倒可能导致大量的上下文切换开销。所以,在这种情况下,通常建议按照 CPU 核的数目 N 或者 N + 1
如果是需要较多等待的任务,例如 I/O 操作较多,推荐的计算方法是
线程数 = CPU核数 * 目标CPU利用率 * (1 + 平均等待时间/平均工作时间)22. AtomicInteger 底层实现原理是什么?如何在自己的产品代码中应用 CAS 操作?
AtomicInteger 是对 int 类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于 CAS 技术
所谓 CAS ,代表的含义是一系列操作的集合,先获取当前数值,进行一些运算,利用 CAS 指令试图进行更新。如果当前数值未变,代表没有其他线程进行并发修改,则更新成功。否则,可能出现不同的选择,要么进行重试,要么就返回一个成功或者失败的结果。
AtomicInteger 依赖于 Unsafe 提供的一些底层能力,进行底层操作;以 volatile 的 value 字段,记录数值,以保证可见性。它提供的有例如 getAndIncrement() 之类的一些原子更新操作。
22.1 扩展:AbstractQueuedSynchronizer(AQS)AQS 是 Java 并发包中,实现各种同步结构和部分其他组成单元的基础。
AQS 的设计初衷:从原理上讲,一种同步结构往往是可以利用其他的结构实现的,但是对某种同步结构的倾向,会导致复杂、晦涩的实现逻辑,所以,设计师选择了将基础的同步相关操作抽象在 AbstractQueuedSynchronizer 中,利用 AQS 为我们构建同步结构提供了范本
AQS 内部数据和方法,可以简单拆分为:
一个 volatile 的整数成员表示状态,同时提供了 setState() 和 getState() 方法
private volatile int state;
一个先入先出(FIFO)的等待队列,以实现多线程间的竞争与等待,这是 AQS 机制的核心之一
各种基于 CAS 的基础操作方法,以及各种期望具体同步结构去实现的 acquire/release 方法
以 ReentrantLock 为例,它内部通过扩展了 AQS 实现了 Sync 类型,以 AQS 的 state 来反映锁的持有情况。
23. 请介绍类加载过程,什么是双亲委派模型?一般来说,我们把 Java 的类加载过程分为三个主要步骤:加载、链接、初始化
首先是加载阶段,它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。
加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。
第二阶段是链接,这是核心的步骤,简单说是把原始的类定义信息平滑地转入 JVM 运行的过程中。这一步可以细分为三个步骤:
验证:这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。准备:创建类或接口中的静态变量,并初始化静态变量的初始值。但这里的 “初始化” 和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的 JVM 指令。解析:这一步会将常量池中的符号引用替换为直接引用。
最后是初始化阶段,这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
双亲委派模型,简单说就是当类加载器试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载 Java 类型。
24. 有哪些方法可以在运行时动态生成一个 Java 类? 25. 谈谈 JVM 内存区域的划分,哪些区域可能发生 OutOfMemoryError ?有些区域是以线程为单位,而有的区域则是整个 JVM 进程唯一的
程序计数器(线程私有)虚拟机栈(线程私有)本地方法栈(线程私有)堆(线程共享)方法区(线程共享)
除了程序计数器,其他区域都有可能会因为可能的空间不足发生 OutOfMemoryError
堆内存不足是最常见的 OOM 原因之一,可能是内存泄漏,也可能是内存溢出对于虚拟机栈和本地方法栈,如果我们写一段程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。类似这种情况,JVM 实际会抛出 StackOverFlowError; 当然,如果 JVM 试图去扩展栈空间的时候失败,则会抛出 OOM直接内存不足,也会导致OOM对于老版本的 ,因为永久代的大小是有限的,并且 JVM 对永久代垃圾回收(如常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代也会出现 OutOfMemoryError 26. 如何监控和诊断 JVM 堆内和堆外内存使用?
图形化工具:JConsole、VisualVMjstat、jmap 等等 27. Java 常见的垃圾收集器有哪些?
Serial GC:最古老的垃圾收集器,其收集工作是 单线程 的,并且在进行垃圾收集的过程中,会进入 STW 状态。当然其单线程设计也意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是 Clint 模式下 JVM 的默认选项
从年代的角度,通常将其老年代实现单独称作 Serial Old,它采用了 标记 - 整理 算法,区别于新生代的复制算法。
ParNew GC:新生代 GC,它实际是 Serial GC 的多线程版本,最常见的应用场景是配合老年代的 CMS GC 工作
CMS GC:基于标记 - 清除 算法,设计的目标是尽量减少停顿时间,这一点对于 Web 等反应敏感的应用非常重要,一直到今天,仍然有很多系统使用 CMS GC 。但是,CMS 采用的标记 - 清除算法,存在着内存碎片化问题,所以难以避免在长时间运行等情况下发生 full GC ,导致恶劣的停顿。
Parallel GC:在早期 JDK 8 等版本中,它是 server 模式 JVM 的默认 GC 选择,也被称作是吞吐量优先的 GC。它的算法和 Serial GC 比较相似,尽管实现要复杂的多,其特点是新生代和老年代 GC 都是并行进行的,在常见的服务器环境中更加高效。
G1 GC:是一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项。
G1 GC 仍然存在着年代的概念,但是其内存结构是类似棋盘的一个个 region。Region 之间是复制算法,但整体上实际可看作是 标记 - 整理 算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1 的优势更加明显。
27.1 垃圾回收算法 27.2 垃圾回收的过程 27.3 垃圾收集器如何清楚地知道哪些对象实例可以被回收 28. 谈谈你的 GC 调优思路? 29. Java 内存模型中的 happen-before 是什么?Happen-before 关系,是 Java 内存模型中保证多线程操作可见性的机制,它的具体表现形式,包括但远不止 synchronized、volatile、lock 操作顺序等方面,比如
对于 volatile 变量,对它的写操作,保证 happen-befor 在随后对该变量的读取操作对于一个锁的解锁操作,保证 happen-before 加锁操作对象构建完成,保证 happen-before 于 finalize 的开始动作
这些 happen-before 关系是存在着传递性的,如果满足 a happen-before b 和 b happen-before c,那么 a happen-before 也成立
happen-before 不是简单说前后,因为它不仅仅是对执行时间的保证,也包括对内存读、写操作顺序的保证。
知道哪些对象实例可以被回收
Happen-before 关系,是 Java 内存模型中保证多线程操作可见性的机制,它的具体表现形式,包括但远不止 synchronized、volatile、lock 操作顺序等方面,比如
对于 volatile 变量,对它的写操作,保证 happen-befor 在随后对该变量的读取操作对于一个锁的解锁操作,保证 happen-before 加锁操作对象构建完成,保证 happen-before 于 finalize 的开始动作
这些 happen-before 关系是存在着传递性的,如果满足 a happen-before b 和 b happen-before c,那么 a happen-before 也成立
happen-before 不是简单说前后,因为它不仅仅是对执行时间的保证,也包括对内存读、写操作顺序的保证。



