类加载机制
类加载的生命周期?
**类加载过程包含了加载,验证,准备,解析,初始化五个阶段。**在这五个阶段中,加载验证准备和初始化这四个阶段发生的顺序是一定的,而解析阶段不一定,他在某些情况下可以在初始化后开始。
- 加载:查找并加载类的二进制数据
- 连接:
2.1验证:确保被加载类的正确性(语义正确等)
2.2准备:为类的静态变量分配内存,并将其初始化为默认值
2.3解析:将类中的符号引用转为直接引用
3.初始化:为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要是对类的变量进行初始化
Class.forName()和ClassLoader.loadClass()区别?
class.forName
是将类的.,class文件加载到JVM,同时还会对类进行解释,执行类中的static块。(也可带参数控制是否执行static块)- 而
ClassLoader.loadClass
只是将.class文件加载到JVM中,不会执行static块,只有newInstance时才会执行static块
JVM有哪些类加载机制?
- 全盘负责: 当一个类加载器负责加载某个Class时,该Class所依赖的引用的其他Class也由该加载器加载,除非显示的指定使用另一个加载器加载
- 父类委托: 先让父类加载器试图加载该类,只有当父类加载器无法加载该类时,才会从自己的类路径加载该类
- 缓存机制: 缓存机制保证每个被加载过的Class都会被缓存,当程序中需要使用某个Class的时候,先从缓存中查找,只有缓存不存在,系统才会读取该类的.class文件加载为Class对象存入缓存区
- 双亲委派机制: 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是吧请求委托到父加载器去完成,依次向上,因此,所有的类加载请求最后都应该被传递到顶层的启动类加载器中,只有当父加载器在他的搜索范围中没有找到所需的类时,子加载器才会尝试自己去加载该类
双亲委派机制的过程
- APPClassLoader加载class时,不会直接加载该类,而是吧加载请求委派到父加载器ExtClassLoader
- ExtClassLoader加载class时,也不会直接加载该类,而是把加载请求委派到BootStrapClassLoader
- BootStrapClassLoader最终加载该类,若加载失败(如lib目录下为找到该class)会向下使用ExtClassLoader加载
- 若ExtClassLoader也加载失败,则会继续向下使用AppClassLoader
- 若AppClassLoader也加载失败,则证明类加载失败,抛出ClassNotFoundException异常
类加载器的层次
- 启动类加载器 Bootstrap ClassLoader: 负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
- 扩展类加载器 Extension ClassLoader: 负责加载lib\ext目录下或由系统变量指定的类库(javax),可被程序直接引用
- 应用程序类加载器: Application ClassLoader:它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
- 自定义类加载器: 因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
- 在执行非置信代码之前,自动验证数字签名。
- 动态地创建符合用户特定需要的定制化构建类。
- 从特定的场所取得java class,例如数据库中和网络中。
内存结构
说说JVM的内存整体结构?线程私有还是共享
- 线程私有:程序计数器、虚拟机栈、本地方法区
- 线程共享:堆、方法区, 堆外内存(Java7的永久代或JDK8的元空间、代码缓存)
[
](https://www.pdai.tech/md/interview/x-interview.html)
Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程一一对应的数据区域会随着线程开始和结束而创建和销毁。
什么是程序计数器(线程私有)?
程序计数器用来存储指向下一条指令的地址,即将要执行的代码,由执行引擎读取到下一条指令执行
为什么程序计数器是线程私有的?
每一个线程拥有自己独有的程序计数器,多线程在某一时间段只会执行其中某一个线程的方法,导致CPU会不停的做任务切换,就会导致线程经常中断恢复,若计数器共享的话,会互相影响。(某一线程的下一条指令已经写入程序计数器,此时中断其他线程就会覆盖掉这条指令)
什么是虚拟机栈(线程私有)?
虚拟机栈主管程序的运行,保存了方法的局部变量,部分结果,并参与方法的调用和返回,每个线程在创建时都会创建一个虚拟机栈,生命周期与线程一致,其内部保存的一个个栈帧,对应了一次次的java方法调用
特点?
- 栈是一种快速有效的分配内存的方式,访问速度仅次于程序计数器
- JVM对栈只有两个操作:每个方法执行(入栈),方法执行结束(出栈)
- 栈不存在垃圾回收问题,方法结束后栈帧会出栈
- 栈的大小直接决定了函数调用的深度
会出现的异常?
1,若使用固定大小虚拟机栈,当栈满后会抛出StackOverflowError栈溢出异常
2,若使用可动态扩展的虚拟机栈,并且在尝试扩展时无法申请到足够内存会抛出OOM异常
栈帧的内部结构?
- 局部变量表
- 操作数栈
- 动态连接:指向方法区的方法引用
- 方法返回地址:方法正常退出和异常退出的地址
- 附加信息
什么是本地方法栈(线程私有)?
- 本地方法接口
一个 Native Method 就是一个 Java 调用非 Java 代码的接口。我们知道的 Unsafe 类就有很多本地方法。
- 本地方法栈(Native Method Stack)
Java 虚拟机栈用于管理 Java 方法的调用,而本地方法栈用于管理本地方法的调用
什么是方法区(线程共享)
方法区只是JVM定义的一种规范,属于方法论,用于存储类信息,常量池,静态变量,编译后代码等数据。不同的厂商有不同的实现,而永久代(PermGen)是 Hotspot 虚拟机特有的概念, Java8 的时候又被元空间取代了,永久代和元空间都可以理解为方法区的落地实现。
栈,堆,方法区的交互关系?
栈区保存引用test1 , 指向的实际对象存储在堆区中,栈调用printName方法,堆区中对象去方法区中找到此方法的执行数据。
堆区?(线程共享)
堆区是JVM中内存最大的一块,被所有线程共享,堆区的唯一目的就是存放对象实例,几乎所有对象实例即数据都在这里分配内存。
堆区内存的划分?
为了进行高效的垃圾回收(分代的唯一目的就是优化GC性能)虚拟机在**逻辑上(注意不是物理上)**吧堆区内存分为三个区
- 新生带(年轻代):新对象和没达到一定年龄的对象都在新生代
- 老年代(养老区):被长时间使用的对象,老年代的内存空间应该要比年轻代更大
- 永久代:java8中改为元空间
JVM规定,堆可以物理不连续,逻辑连续即可。实现时,既可以是固定大小,也可以是可扩展的,主流虚拟机都是可扩展的,如果堆中没有完成实例分配,并且堆无法再扩展时,就会抛出 OutOfMemoryError 异常。
- 新生代()
新生代是大多数对象创建的地方(大对象除外,大对象会直接创建在老年代)
新生代也被细化为三个部分——伊甸园区和两个幸存者区,默认大小比例是8:1:1
- 大多数对象的创建都位于伊甸园区
- 当伊甸园区内存不足时,执行Minor GC,并将所有未被清除的对象移入幸存者空间
- Minor GC 检查幸存者对象,并将他们移入另一个幸存者空间,所以每次,都会有一个幸存者空间是空的
- 经过多次GC循环后仍然幸存下来的对象会被移入老年代,通常是设置年龄阈值判断是否放入老年代
- 老年代()
保存的是许多轮Minor GC后仍然存活的对象。通常垃圾收集是在老年代内存满时执行的,老年代垃圾收集称为Major GC,通常时间较长
大对象会直接进入老年代,这样做的目的是防止在伊甸园区和幸存者区直接方式大量拷贝
- 永久代
永久代在Java8后被改为了元空间移出了堆区
对象在堆中的生命周期
- 在Java8后的JVM内存模型中,堆被划分为新生代和老年代
- 新生代又被进一步分为伊甸园区和幸存者区
- 当创建一个对象后,对象优先被分配到伊甸园区
- 此时JVM会分配给对象一个对象年龄计数器
- 当伊甸园区空间不足时,新生代执行一次Minor GC
- 将存活的对象移入幸存者区,对象年龄+1
- 幸存者区中的对象同样也会经历Minor GC,存活下来年龄+1
- 若分配对象大小超过阈值,会直接进入老年代
什么是TLAB
- TLAB是从内存模型考虑而不是垃圾回收考虑,是将伊甸园区继续划分,为每一个线程分配一个私有的缓存区域,包含在伊甸园空间内
- 多线程分配内存时,TLAB可以避免一系列的安全问题,同时还能提高分配吞吐量,这种设计被称为快速分配策略
为什么要有TLAB
- 堆区的线程是共享的,任何线程都可以访问到堆区中的共享数据
- 由于对象在JVM中创建非常麻烦,所以在并发环境下从堆区中分配内存空间是不安全的(可能分配到其他线程已经划定的区域)
- 为了避免多线程操作同一地址,需要加锁等机制,影响分配速度
尽管不是所有的对象实例都能够在 TLAB 中成功分配内存,但 JVM 确实是将 TLAB 作为内存分配的首选。
一旦对象在 TLAB 空间分配内存失败时,JVM 就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在 Eden 空间中分配内存。
GC垃圾回收
如何判断一个对象是否可以回收
- 引用计数算法
给对象添加一个引用计数器,当对象被引用时计数器+1,引用失效-1。当计数器为0时判断为可被回收
但两个对象循环引用的情况下,就会导致计数器永远不为0,导致无法回收
正是因为循环引用的存在,JVM不使用引用计数器判断回收
- 可达性算法
通过GC Roots进行起点进行搜索,能够达到的对象都是存活的,不能到达的对象可被回收。
Java虚拟机使用该算法判断是否可回收。
在Java的GC Root中一般包含以下内容
- 虚拟机栈中的引用对象
- 本地方法栈中的引用对象
- 方法区中静态变量引用的对象
- 方法区中常量引用的变量
对象的四种引用类型
强引用
一个引用直接指向的对象为强引用对象,强引用对象不会被回收
Object obj = new Object();
软引用
软引用对象只有在内存不够的情况下会被回收
将对象用SoftReference类引用以创建软引用
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
弱引用
弱引用对象一定会被回收,也就是说,弱引用对象只能存活到下一次GC发生之前
将对象用WeakReference 类引用以创建弱引用
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
虚引用
虚引用完全不会对其的生命周期有任何影响,也无法通过一个虚引用来获取到对象
引入虚引用的唯一目的就是在这个对象被回收时收到一个系统通知
将对象用PhantomReference 引用来实现虚引用
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;
垃圾回收算法
- 标记-清除法
将还存活的对象进行标记,然后清除掉未被标记的对象
不足:
- 标记和清除的效率都不搞
- 会产生大量的内存碎片,导致大对象无法分配内存
- 标记-整理算法
让所有存活的对象都像一端移动,然后清除掉边界以外的内存空间
- 复制法
将内存划分为大小相等的两块,每次只使用其中的一块,当其中一块用完后,将存活的对象直接复制到另一块上,然后清除原块
主要不足是只使用了内存的一半。
Java虚拟机就是采用这种算法回收新生代,将新生代分为一块较大的伊甸园区和两块较小的幸存者区,每次使用伊甸园区和其中一块幸存者区,在回收时,将伊甸园区和其中一块幸存者区的存活对象复制到另一块幸存者区。
由于两区之间的大小为8:1:1,所以有可能出现幸存者区放不下所有存活对象,这时就需要依赖老年代进行分配担保,借用老年代的空间存放放不下的对象
- 分代收集机制
现在的商用虚拟机采用分代收集机制,根据对象的存活周期将内存划分为几块,不同块中采用不同的收集算法
Java中将堆分为新生代和老年代
- 新生代:复制算法
- 老年代:标记—清除 或者 标记—整理 算法
分代收集和分区收集的区别
- 分代收集
当前主流 VM 垃圾收集都采用”分代收集”(Generational Collection)算法, 这种算法会根据 对象存活周期的不同将内存划分为几块, 如 JVM 中的 新生代、老年代、永久代,这样就可以根据 各年代特点分别采用最适当的 GC 算法
- 分区收集
分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的 好处是可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是 整个堆), 从而减少一次 GC 所产生的停顿。
Minor GC,Major GC,Full GC
在JVM进行GC时,并非每次都是对整个堆进行GC,GC按照回收区域可以分为:
- 部分收集
- 整堆收集
[
](https://www.pdai.tech/md/interview/x-interview.html)
- 部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
- 老年代收集(Major GC/Old GC):只是老年代的垃圾收集
- 目前,只有 CMS GC 会有单独收集老年代的行为
- 很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
- 目前只有 G1 GC 会有这种行为
- 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾
说说JVM内存分配策略
- 对象优先在伊甸园区进行分配
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
- 大对象直接进入老年代
大对象是指需要连续存储空间的对象,例如长字符串或长数组等,大对象直接进入老年代可以避免伊甸园区和幸存者区之间的大量拷贝
- 长期存活对象进入老年代
通过新生代区对象年龄计数器,判断达到一定年龄的对象放入老年区
- 动态年龄判断
虚拟机并不是一定要对象到达年龄阈值才放入老年区,当幸存者区中相同年龄的所有对象大小大于幸存者区空间的一半,则年龄大于等于该年龄的对象可以直接进入老年代
- 空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
- 若担保失败并且JVM设置不允许担保失败就会进行一次Full GC
- 若允许担保失败。就继续检查老年代可用连续空间是否大于历次放入老年代对象的平均值,
- 若小于进行Full GC
- 若大于则尝试 Minor GC
什么情况下触发Full GC
Minor GC 触发条件简单,当Eden空间满即触发
而Full触发则有条件
- 调用System.gc()方法
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
- 老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
- 空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
垃圾回收器
- G1 收集器
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。