目录
- 、JVM
- 1、说一下JVM的主要组成部分及其作用
- 2、什么是JVM内存结构(谈谈对运行时数据区的理解)
- 3、堆和栈的区别是什么
- 4、堆中存什么?栈中存什么?
- 5、为什么不把基本类型放堆中呢?
- 6、为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗
- 7、什么情况下会发生栈内存溢出
- 7、判断垃圾可以回收的算法有哪些(如何判断一个对象是否存活)
- 8、强引用、软引用、弱引用、虚引用是什么,有什么区别?(谈谈堆Java中引用的了解)
- 9、被引用的对象就一定能存活吗
- 10、垃圾回收是从哪里开始的呢
- 11、被标记为垃圾的对象一定会被回收吗
- 12、谈谈对内存泄露的理解
- 13、内存泄漏的根本原因是什么
- 14、尽量避免内存泄漏的方法
- 15、Java中垃圾回收算法有哪些
- 16、为什么要采用分代收集算法
- 17、什么是浮动垃圾
- 18、什么是内存碎片?如何解决
- 19、常见的垃圾收集器
- 20、详细说一下CMS的回收过程,CMS的问题是什么
- 21、谈谈你对G1收集器的理解
- 22、JVM中一次完整的GC是什么样子的
- 23、Minor GC 和 Full GC有什么不同呢?
- 24、谈谈你对内存分配的理解?大对象怎么分配?
- 25、介绍下空间分配担保原则
- 26、Java中有哪些类加载器
- 27、说说类加载器双亲委派模型
- 28、列举一些你知道的打破双亲委派机制的例子,为什么要打破
- 29、JVM中哪些是线程共享区
- 30、一个对象加载到JVM,再到被GC清除,都经历了什么过程
- 31、怎么确定一个对象到底是不是垃圾
- 32、JVM中有哪些垃圾回收算法
- 33、什么是STW
- 34、说说对线程安全的理解
- 35、对守护线程的理解
- 33、什么是STW
- 34、说说对线程安全的理解
- 35、对守护线程的理解
各组件的作用:首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
jvm将虚拟机分为5大区域,程序计数器、虚拟机栈、本地方法栈、java堆、方法区:
堆和栈(虚拟机栈)是完全不同的两块内存区域,一个是线程独享的,一个是线程共享的。。二者之间最大的区别就是存储的内容不同:堆中主要存放对象实例。栈(局部变量表)中主要存放各种基本数据类型、对象的引用。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。在 Java 中一个线程就会相应有一个线程栈与之对应,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享的。
栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而堆只负责存储对象信息。
申请方式
申请后系统的响应
申请效率的比较
堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。
因为基本数据类型占用的空间一般是1~8个字节,需要空间比较少,而且因为是基本类型,所以不会出现动态增长的情况,长度固定,因此栈中存储就够了。如果把它存在堆中是没有什么意义的。
基本类型和对象的引用都是存放在栈中,而且都是几个字节的一个数,因此在程序运行时,它们的处理方式是统一的。
但是基本类型、对象引用和对象本身就有所区别了,因为一个是栈中的数据一个是堆中的数据。
垃圾收集器在对堆区和方法区进行回收前,首先要确定这些区域的对象哪些可以被回收,哪些暂时还不能回收,这就要用到判断对象是否存活的算法。
在 Java 语言中,可作为 GC Roots 的对象包括下面几种:
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在Java语言中,将引用又分为强引用、软引用、弱引用、虚引用 4 种,这四种引用强度依次逐渐减弱。
String s = new String("xiaolin")
不一定,看 Reference 类型,弱引用在 GC 时会被回收,软引用在内存不足的时候,即 OOM 前会被回收,但如果没有在 Reference Chain 中的对象就一定会被回收。
栈是真正进行程序执行地方,垃圾回收是从栈开始。以栈为引用起点,我们可以找到堆中的对象,又从这些对象找到对堆中其他对象的引用,这种引用逐步扩展,最终以 null 引用或者基本类型结束,这样就形成了一颗以 Java 栈中引用所对应的对象为根节点的一颗对象树。
如果栈中有多个引用,则最终会形成多颗对象树。在这些对象树上的对象,都是当前系统运行所需要的对象,不能被垃圾回收。而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收
即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。
第一次标记:如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记
第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。在 finalize() 方法中没有重新与引用链建立关联关系的,将被进行第二次标记。二次标记成功的对象将真的会被回收,如果对象在 finalize() 方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活
在 Java 中,内存泄漏就是存在一些不会再被使用却没有被回收的对象,这些对象有下面两个特点:
如果对象满足这两个条件,这些对象就可以判定为 Java 中的内存泄漏,这些对象不会被 GC 所回收,然而它却占用内存。
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是 Java 中内存泄漏的发生场景。
ava中有四种垃圾回收算法,分别是标记清除法、标记整理法、复制算法、分代收集算法:
标记清除算法
复制算法
标记-整理算法
分代收集算法
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率
在 Java 程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如 Http 请求中的 Session 对象、线程、Socket 连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String 对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,所以分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。
由于不同 Java 对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行内存整理,就会出现零散的内存碎片。
碎片最直接的问题就是会导致无法分配大块的内存空间,以及程序运行效率降低。所以,在上面提到的基本垃圾回收算法中,“复制”方式和“标记-整理”方式,都可以解决碎片的问题。
Serial GC | 串行 | 工作线程暂停,一个线程进行垃圾回收 | 新生代 | 复制算法 | |
---|---|---|---|---|---|
Serial Old GC | 串行 | 工作线程暂停,一个线程进行垃圾回收 | 老年代 | 标记-整算法 | |
ParNew GC | 并行 | 工作线程暂停,多个线程进行垃圾回收 | 新生代 | 复制算法 | Serial GC的多线程版 |
CMS GC | 并行 | 用户线程和垃圾回收线程同时执行 | 老年代 | 标记-清除算法 | 低暂停 |
Parallel GC | 并行 | 工作线程暂停,多个线程进行垃圾回收 | 新生代 | 复制算法 | 和ParNew相比,能动态调整内存分配情况(JDK8默认) |
Parallel Old GC | 并行 | 工作线程暂停,多个线程进行垃圾回收 | 老年代 | 标记-整理算法 | 替代串行的Serial Old GC |
G1 | 并行 | 用户线程和垃圾回收线程同时执行 | 整堆 | 分区算法 | JDK9默认 |
ZGC | 并行 | 用户线程和垃圾回收线程同时执行 | 整堆 | 分页算法 |
CMS(Concurrent Mark Sweep,并发标记清除) 收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。
CMS 回收过程分为以下四步:
CSM的问题:
并发回收导致CPU资源紧张:在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低程序总吞吐量。CMS默认启动的回收线程数是:(CPU核数 + 3)/ 4,当CPU核数不足四个时,CMS对用户程序的影响就可能变得很大。
无法清理浮动垃圾:在CMS的并发标记和并发清理阶段,用户线程还在继续运行,就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留到下一次垃圾收集时再清理掉。
并发失败:如果在并发标记、并发清理过程中,由于用户线程同时在执行,如果有新对象要进入老年代,但是空间又不够,此时就会利用 Serial Old 来做一次垃圾收集,就会做一次全局的 STW。
内存碎片问题:CMS是一款基于“标记-清除”算法实现的回收器,这意味着回收结束时会有内存碎片产生。
G1 可谓博采众家之长,力求到达一种完美。它吸取了增量收集优点,把整个堆划分为一个一个等大小的区域(region)。内存的回收和划分都以region为单位。同时,它也吸取了 CMS 的特点,把这个垃圾回收过程分为几个阶段,分散一个垃圾回收过程。而且,G1 也认同分代垃圾回收的思想,认为不同对象的生命周期不同,可以采取不同收集方式,因此,它也支持分代的垃圾回收。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old ),新生代默认占总空间的 1/3,老年代默认占 2/3。 新生代有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是8:1:1
再描述它们之间转化流程:
对象优先在Eden分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
大对象直接进入老年代,大对象就是需要大量连续内存空间的对象(比如:字符串、数组),为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率
老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和老年代
Minor GC:只收集新生代的GC。
Full GC: 收集整个堆,包括 新生代,老年代
Full GC触发条件:
老年代空间不够分配新的内存
调用System.gc时,系统建议执行Full GC,但是不必然执行
由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
如果YougGC时新生代有大量对象存活下来,而 survivor 区放不下了,这时必须转移到老年代中,但这时发现老年代也放不下这些对象了,那怎么处理呢?其实JVM有一个老年代空间分配担保机制来保证对象能够进入老年代。
在执行每次 YoungGC 之前,JVM会先检查老年代最大可用连续空间是否大于新生代所有对象的总大小。因为在极端情况下,可能新生代 YoungGC 后,所有对象都存活下来了,而 survivor 区又放不下,那可能所有对象都要进入老年代了。这个时候如果老年代的可用连续空间是大于新生代所有对象的总大小的,那就可以放心进行 YoungGC。
在允许担保失败并尝试进行YoungGC后,可能会出现三种情况:
类加载器是指:通过一个类的全限定性类名获取该类的二进制字节流叫做类加载器;类加载器分为以下四种:
当一个类加载器收到一个类加载的请求,他首先不会尝试自己去加载,而是将这个请求委派给父类加载器去加载,只有父类加载器在自己的搜索范围类查找不到给类时,子加载器才会尝试自己去加载该类。
所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进行加载,如果没加载到才由自己进行加载。
怎么打破双亲委派模型?
答案:自定义类加载器,继承ClassLoader类,重写loadClass方法和findClass方法。
webapp
中的 class
和 lib
,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况,而对于许多应用,需要有共享的lib以便不浪费资源jvm
一样的安全性问题。使用单独的 classloader
去装载 tomcat
自身的类库,以免其他恶意或无意的破坏堆区和方法区是所有线程共享的,栈、本地方法栈、程序计数器是每个线程独有的。
STW: Stop-The-World,暂停整个世界,是在垃圾回收算法执行过程当中,需要将JVM内存冻结的一种状态。在STW状态下,JAVA的所有线程都是停止执行的-GC线程除外,native方法可以执行,但是,不能与JVM交互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点。
线程安全指的是,我们写的某段代码,在多个线程同时执行这段代码时,不会产生混乱,依然能够得到正常的结果,比如i++,i初始化值为0,那么两个线程来同时执行这行代码,如果代码是线程安全的,那么最终的结果应该就是一个线程的结果为1,一个线程的结果为2,如果出现了两个线程的结果都为1,则表示这段代码是线程不安全的。
所以线程安全,主要指的是一段代码在多个线程同时执行的情况下,能否得到正确的结果。
,然后将边界以外的所有内存直接清除。
STW: Stop-The-World,暂停整个世界,是在垃圾回收算法执行过程当中,需要将JVM内存冻结的一种状态。在STW状态下,JAVA的所有线程都是停止执行的-GC线程除外,native方法可以执行,但是,不能与JVM交互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点。
线程安全指的是,我们写的某段代码,在多个线程同时执行这段代码时,不会产生混乱,依然能够得到正常的结果,比如i++,i初始化值为0,那么两个线程来同时执行这行代码,如果代码是线程安全的,那么最终的结果应该就是一个线程的结果为1,一个线程的结果为2,如果出现了两个线程的结果都为1,则表示这段代码是线程不安全的。
所以线程安全,主要指的是一段代码在多个线程同时执行的情况下,能否得到正确的结果。
线程分为用户线程和守护线程,用户线程就是普通线程,守护线程就是JVM的后台线程,比如垃圾回收线程就是一个守护线程,一直在运行,守护线程会在其他普通线程都停止运行之后才会自动关闭。我们可以通过设置thread.setDaemon(true)来把一个线程设置为守护线程。