JVM知识点总结
iwehdio的博客园:https://www.cnblogs.com/iwehdio/
- 说一下JVM的体系结构?
- 说一下运行时数据区?
- 说一下程序计数器(PC寄存器)?
- 使用程序计数器存储字节码指令地址有什么用呢?为什么使用程序计数器记录当前线程的执行地址呢?
- 程序计数器为么会被设定为线程私有?
- 为什么设计出了虚拟机栈?
- Java虚拟机栈是什么?
- 虚拟机栈的运行方式?
- 栈帧的内部结构是怎样的?
- 介绍一下局部变量表?
- 静态变量、实例变量和局部变量的初始化?
- 介绍一下操作数栈?
- 介绍一下动态链接?
- 说一下静态链接和动态链接?
- 什么是虚方法和非虚方法?
- 动态类型语言和静态类型语言的区别?
- 方法重写的本质是什么?
- 什么是本地方法?
- 介绍一下堆?
- 堆的细分内存结构?
- 介绍一下对象的内存分配过程?
- 堆空间的分代思想?
- 介绍一下堆内存分配策略?
- 介绍一下TLAB(Thread Local Allocation Buffer)?
- 堆是分配对象存储的唯一选择吗?
- 介绍一下方法区?
- 方法区的内部结构?
- 什么是运行时常量池?
- 字符串常量池为什么要调整(字符串常量从永久代放到堆)?
- 回收方法区中的类型数据,需要同时满足那些条件?
- 对象实例化的方式有哪些?
- 创建对象的步骤?
- 对象访问定位的方法?
- 介绍一下执行引擎?
- 什么是字节码?
- 什么是解释器(Interpreter),什么是JIT(即时)编译器?
- 为什么说Java是半编译半解释型语言?
- 为什么说Java是半编译半解释型语言?
- JIT编译器如何探测热点代码?
- String有哪些基本特性?
- String是怎样进行内存分配的?
- 字符串拼接的底层原理是什么?
- String的
intern()
方法怎么使用?
- 什么是垃圾(Garbage)呢?
- 为什么需要GC?
- 垃圾回收分为几个阶段?
- 如何判断对象存活?
- 在Java语言中,GC Roots包括哪几类元素?
- 对象的finalization机制是什么?
- 如何判定一个对象objA是否可回收?
- 常见的垃圾回收算法有哪些?
- 还有哪些垃圾回收算法的思想?
- System.gc()表示什么?
- 什么是内存溢出(OOM)?
- 什么是内存泄漏?
- 什么是Stop The World?
- 垃圾回收的并行与并发?
- 什么是安全点和安全区域?
- 什么是强引用?
- 什么是软引用?
- 什么是弱引用?
- 什么是虚引用?
- 垃圾回收器如何进行分类?
- 评估垃圾回收器的指标有哪些?
- 垃圾回收器分代划分?
- 垃圾回收器的组合关系是怎样的?
- 介绍一下Serial回收器?
- 介绍一下ParNew回收器?
- 介绍一下Parallel Scavenge回收器?
- 介绍一下CMS回收器?
- CMS是怎样进行垃圾回收的?
- 如何选择GC?
- 介绍一下G1回收器?
- G1是如何进行分区的?
- 什么是Remembered Set:记忆集RSet?
- G1是如何进行垃圾回收的?
- 什么是字节码文件?
- Class文件格式是怎样的?
- Class文件的结构是怎样的?
- 常量池中存储的是什么?
- 什么是字节码指令?
- 字节码指令的分类?
- 类模板文件的生命周期?
- 类、类的加载器、类的实例之间的引用关系是怎样的?
- 类加载器的作用?
- 类的显式加载和隐式加载?
- 类的唯一性是什么?
- 类加载机制的三个基本特征是什么?
- 类加载器的分类?
- 获取ClassLoader的途径?
- Class.forName()与ClassLoader.loadClass()的区别?
- 什么是双亲委派模型?
- 如何破坏双亲委派机制?
- 什么是沙箱安全机制?
- 为什么需要自定义类的加载器?
- Java9中对类加载器的修改?
1、概述
- 说一下JVM的体系结构?
- JVM主要分为四个部分,类装载子系统、运行时数据区、执行引擎、本地方法接口和本地方法库。
- 运行时数据区分为Java虚拟机栈、本地方法栈、程序计数器、方法区和堆。其中Java栈、本地方法栈、程序计数器是线程私有的,方法区和堆是线程共有的。
2、虚拟机栈
-
说一下运行时数据区?
- Java虚拟机在执行Java程序时。会把其所管理的内存划分为若干个不同的数据区域。这些数据区域各有各的用途,创建的销毁的时间也不同。有的是随着虚拟机进程的启动而一直存在,有些则是依赖用户线程的启动和结束而建立和销毁。
- 运行时数据区分为Java虚拟机栈、本地方法栈、程序计数器、方法区和堆。其中Java栈、本地方法栈、程序计数器是线程私有的,方法区和堆是线程共有的。
-
说一下程序计数器(PC寄存器)?
- 程序计数器是一块很小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
- 程序计数器用来存储指向下一条要执行的指令代码的指令地址。由执行引擎读取下一条指令。
- 每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefined)。
-
使用程序计数器存储字节码指令地址有什么用呢?为什么使用程序计数器记录当前线程的执行地址呢?
- 因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
- JVM的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令。
-
程序计数器为么会被设定为线程私有?
- 多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复。
- 为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
-
为什么设计出了虚拟机栈?
- 由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
- 优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
-
Java虚拟机栈是什么?
- Java虚拟机栈描述的是Java方法执行的内存模型。
- 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。每个方法被执行时就会同步创建一个栈帧。
- 是线程私有的,生命周期和线程一致。
- 作用是,主管Java程序的运行,它保存方法的局部变量(基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。
- 每一个方法被调用直至执行完毕的过程,就对应着一个栈帧从入栈到入栈的过程。
-
虚拟机栈的运行方式?
- JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈。
- 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(current Method),定义这个方法的类就是当前类(Current Class)。
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
- 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
- 如果当前方法是被前一个栈帧对应的方法调用的,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
- Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
-
栈帧的内部结构是怎样的?
- 局部变量表(Local variables)。
- 操作数栈(Operand stack)(或表达式栈)。
- 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)。
- 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)。
- 一些附加信息。
-
介绍一下局部变量表?
- 局部变量表也被称之为局部变量数组或本地变量表。
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
- 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
- 局部变量表,最基本的存储单元是Slot(变量槽)。32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
- 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
-
介绍一下变量槽?
- JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
- 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量中的每一个slot上。
- 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
- 栈帧中的局部变量表中的槽位是可以重用的。
-
静态变量、实例变量和局部变量的初始化?
- 静态变量有两次初始化的机会,都是在类加载过程中。第一次是在“准备阶段”,执行系统初始化,对静态变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。
- 实例变量随着对象的创建,会在堆空间中分配实例变量空间,并进行赋予默认值。
- 局部变量不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。
-
介绍一下操作数栈?
- 每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈,也可以称之为表达式栈(Expression stack)。
- 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
- 在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。
- 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配。
-
介绍一下动态链接?
- 虚拟机栈里的动态链接指的是,栈帧中持有一个指向运行时常量池中该栈帧所属方法的引用,是为了支持方法调用过程中的动态链接。
-
说一下静态链接和动态链接?
- 在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
- 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
- 动态链接:如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
- 对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding),(因为多态特性的存在)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
- 早期绑定:早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
- 晚期绑定:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
- 在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
-
什么是虚方法和非虚方法?
- 非虚方法:如果方法在编译期就确定了具体的调用版木,这个版本在运行时是不可变的。这样的方法称为非虚方法(不存在多态)。
- 包括:静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
- 其他方法称为虚方法。
-
动态类型语言和静态类型语言的区别?
- 动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。
- 静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息这是动态语言的一个重要特征。
-
方法重写的本质是什么?
- 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。
- 如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过则返回java.lang.IllegalAccessrror异常。
- 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
-
介绍一下方法返回地址?
- 一个方法的结束,有两种方式:
- 正常执行完成。
- 出现未处理的异常,非正常退出。
- 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。
- 方法正常退出时,调用者的程序计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
- 通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
- 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
- 一个方法的结束,有两种方式:
3、堆
- 什么是本地方法?
- 一个本地方法就是一个Java调用非Java代码的接口。该方法用native修饰,实现由非Java语言实现,比如C。
- 在定义一个本地方法时,并不提供实现体(有些像定义一个Java接口),因为其实现体是由非java语言在外面实现的。
- 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,提高效率,实现与底层操作系统交互。
- 介绍一下堆?
- 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
- 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。这是为了部分解决线程安全问题。
- 所有的对象实例以及数组都应当在运行时分配在堆上。
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。堆,是垃圾回收的重点区域。
- 堆的细分内存结构?
- 现代垃圾收集器大部分都基于分代收集理论设计,所以堆的结构也被设计为是分代的。
- Java 7及之前堆内存逻辑上分为三部分:新生代(Young Generation Space)+老年代(Tenure Generation Space)+永久代(Permanent Space)。
- Java 8及之后堆内存逻辑上分为三部分:新生代+老年代+元空间(Meta Space)。
- 实际上,永久代/元空间都是属于方法区的实现。
- 新生代又分为伊甸园(Eden)、幸存者0区(S0/from)和幸存者1区(S1/to)。
- 介绍一下对象的内存分配过程?
- 存储在JVM中的Java对象可以被划分为两类:
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速。
- 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
- new的对象先放伊甸园区。此区有大小限制。
- 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Young GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。
- 然后将伊甸园中的剩余对象移动到幸存者0区。再加载新的对象放到伊甸园区。
- 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。同时,伊甸园区中的剩余对象也放到幸存者1区。
- 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。每次被移动一次,年龄计数器age就会加1。
- 啥时候能去老年代呢?可以设置次数也就是年龄计数器的大小。默认是15次,与具体的垃圾回收器有关。
- 当老年代内存不足时,再次触发GC:Old GC,进行老年代的内存清理。
- 若老年代执行了Old GC之后发现依然无法进行对象的保存,就会产生OOM异常。
- 存储在JVM中的Java对象可以被划分为两类:
- 堆空间的分代思想?
- 不同对象的生命周期不同。70%-99%的对象是临时对象。
- 其实不分代完全可以,分代的唯一理由就是优化GC性能。
- 如果没有分代,那所有的对象都在一块。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。
- 而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
- 介绍一下堆内存分配策略?
- 优先分配到Eden。
- 大对象直接分配到老年代。尽量避免程序中出现过多的大对象。
- 长期存活的对象分配到老年代。
- 动态对象年龄判断。如果survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
- 介绍一下TLAB(Thread Local Allocation Buffer)?
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据。
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。
- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
- 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
- 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
- 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。
- 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
- 堆是分配对象存储的唯一选择吗?
- 如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
- 栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
- 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
- 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器(对于JVM就是栈)中。
- HotSpot中实现了标量替换,但未实现栈上分配。所以还是认为所有对象都是在堆上分配的。
4、方法区
-
介绍一下方法区?
- 方法区存储已经被虚拟机加载了对象类型信息、常量、静态变量、类是被那个加载器加载的和运行时常量池等。是各个线程共享的内存区域。
- 在jdk7及以前,习惯上把方法区,称为永久代。jdk8开始,使用元空间取代了永久代。元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。
-
方法区的内部结构?
- 类型信息:对每个加载的类型(类class,接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称。(全名=包名.类名)
- 这个类型直接父类的完整有效名。(对于interface或是java.lang.object,都没有父类)
- 这个类型的修饰符。(public,abstract,final的某个子集)
- 这个类型实现接口的一个有序列表。
- 域(Field,成员变量)信息:JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
- 域的相关信息包括:域名称、域类型、域修饰符。(public,private,protected,static,final,volatile,transient的某个子集)
- 方法(Method)信息:JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序。
- 方法名称。
- 方法的返回类型。(或void)
- 方法参数的数量和类型。(按顺序)
- 方法的修饰符。(public,private,protected,static,final,synchronized,native,abstract的一个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小。(abstract和native方法除外)
- 异常表。(abstract和native方法除外)每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引。
- 类型信息:对每个加载的类型(类class,接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
-
什么是运行时常量池?
-
class文件中的常量池,包括各种字面量和对类型、域和方法的符号引用,在运行时就被加载为方法区中的运行时常量池。
-
字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。
-
符号引用则属于编译原理方面的概念,包括下面三类常量:
-
类和接口的全限定名。
-
字段的名称和描述符。
-
方法的名称和描述符。
-
-
-
JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
-
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
-
-
字符串常量池为什么要调整(字符串常量从永久代放到堆)?
- jdk7中将字符串常量池放到了堆空间中。因为永久代的回收效率很低,永久代空间不足时才会触发GC。
- 这就导致字符串常量池回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
-
回收方法区中的类型数据,需要同时满足那些条件?
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
- 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
- 该类对应的Java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
- 即使满足了三个条件也仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。
-
对象实例化的方式有哪些?
- new直接创建。
- 静态方法和工厂方法。
- Class的newInstance方法,通过类模板调用空参构造反射。
- Constructor的newInstance方法,通过构造器调用带参的构造方法反射。
- 使用clone原型模式。
- 使用反序列化。
- 第三方库Objenesis。
-
创建对象的步骤?
- 判断对象的类模板是否已经被加载、链接、初始化。
- 为对象分配内存,指针碰撞或空闲列表。
- 处理并发安全问题,先尝试分配在TLAB上,否则使用CAS+失败重试分配在堆上。
- 初始化分配到的空间,将属性设置默认值。
- 设置对象的对象头。
- 执行init方法进行初始化。
-
对象访问定位的方法?
- 句柄访问。虚拟机栈中的对象引用指向句柄池中的句柄,然后从句柄中获取到对象实例的指针和对象类型数据的指针。
- 在对象实例的地址改变时,只需要改变句柄的值,不需要改变栈帧中reference引用的值。
- 直接指针。虚拟机栈中的对象引用直接指向堆中的对象实例数据,其中包含了指向对象类型数据的指针。
- 访问实例只需要一次索引;不需要额外占用句柄池空间,相对而言效率更高。
5、执行引擎
- 介绍一下执行引擎?
- 执行引擎是Java虚拟机核心的组成部分之一。
- JVM的主要任务是负责装载字节码,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。
- 如果想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。
- 执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于程序计数器。每当执行完一项指令操作后,程序计数器就会更新下一条需要被执行的指令地址。
- 什么是字节码?
- 字节码是一种中间状态(中间码)的二进制代码(文件),需要直译器转译后才能成为机器码。字节码主要为了实现特定软件运行和软件环境、与硬件环境无关。
- 字节码的实现方式是通过编译器和虚拟机。编译器将源码编译成字节码,特定平台上的虚拟机将字节码转译为可以直接执行的指令。
- 什么是解释器(Interpreter),什么是JIT(即时)编译器?
- 解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
- 当一条字节码指令被解释执行完成后,接着再根据程序计数器中记录的下一条需要被执行的字节码指令执行解释操作。
- JIT(Just In Time Compiler)编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。
- 为什么说Java是半编译半解释型语言?
- JDK1.0时代,将Java语言定位为“解释执行”还是比较准确的。再后来,Java也发展出可以直接生成本地代码的编译器。
- 现在JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。
- JIT编译器如何探测热点代码?
- 是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,需要根据代码被调用执行的频率而定。
- 关于那些需要被编译为本地代码的字节码,也被称之为“热点代码”,JIT编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能。
- 一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为OSR(On stack Replacement)编译。
- 目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。
6、StringTable
- String有哪些基本特性?
- string声明为final的,不可被继承。
- string实现了serializable接口:表示字符串是支持序列化的。实现了comparable接口:表示string可以比较大小string。
- 在jdk8及以前内部定义了final char[]用于存储字符串数据。jdk9时改为byte[]。
- string:代表不可变的字符序列。简称:不可变性。
- 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
- 字符串常量池中是不会存储相同内容的字符串的。
- String是怎样进行内存分配的?
- 在Java语言中有8种基本数据类型和一种比较特殊的类型string。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
- 常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,string类型的常量池比较特殊。它的主要使用方法有两种。
- 直接使用双引号声明出来的string对象会直接存储在常量池中。
- 如果不是用双引号声明的string对象,可以使用string提供的
intern()
方法将其放入常量池中。
- Java 6及以前,字符串常量池存放在永久代。
- Java 7中后,将字符串常量池的位置调整到Java堆内。
- 字符串拼接的底层原理是什么?
- 比如操作:
String s3 = s1 + s2
。 - 首先new一个空的StringBuilder。
- 调用两次
append()
操作,把s1和s2添加进去。 - 调用
toString()
方法并返回。 - 通过
StringBuilder.append()
方法拼接字符串的效率远高于使用String变量用+
拼接的方式。 - 因为后者会创建多个StringBuilder和String对象,而且会耗费时间GC。
- 比如操作:
- String的
intern()
方法怎么使用?- 如果不是用双引号声明的string对象,可以使用string提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。比如:
String myIhfo = new string("I ").intern()
。 - 也就是说,如果在任意字符串上调用String.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量(字面量)形式出现的字符串实例完全相同。
- Interned string就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(string Intern Pool)。
- 如果不是用双引号声明的string对象,可以使用string提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。比如:
7、垃圾回收算法
-
什么是垃圾(Garbage)呢?
- 垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
- 基本数据类型不存在垃圾回收。
- 如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。
-
为什么需要GC?
- 对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完。
- 除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象。
- 随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序的正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。
-
垃圾回收分为几个阶段?
- 标记阶段:判断对象是否存活,找出那些是垃圾。
- 在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间。
- 当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
- 清除阶段:清除标记的垃圾,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。
-
如何判断对象存活?
- 判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
- 引用计数算法:
- 引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
- 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
- 优点:
- 实现简单,垃圾对象便于辨识;
- 判定效率高,回收没有延迟性。
- 缺点:
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
- 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
- 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
- 可达性分析算法(根搜索算法、追踪性垃圾收集):
- 相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
- 所谓"GC Roots"根集合就是一组必须活跃的引用。
- 基本思路:
- 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)。
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
- 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。这点也是导致GC进行时必须"Stop The World"的一个重要原因。
-
在Java语言中,GC Roots包括哪几类元素?
- 虚拟机栈中引用的对象。比如:各个线程被调用的方法中使用到的参数、局部变量等。
- 本地方法栈内JNI(通常说的本地方法)引用的对象。
- 方法区中类静态属性引用的对象。比如:Java类的引用类型静态变量。
- 方法区中常量引用的对象。比如:字符串常量池(String Table)里的引用。
- 所有被同步锁synchronized持有的对象。
- Java虚拟机内部的引用。基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException,OutofMemoryError),系统类加载器。
- 反映java虚拟机内部情况的JMXBean,JVMTI中注册的回调、本地代码缓存等。
- 除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收(Partial Gc)。
- 如果只针对Java堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性(考虑老年代中对于新生代中对象的引用)。
- 由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。
-
对象的finalization机制是什么?
- Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
- 当垃圾回收器发现没有引用指向一个对象 即:垃圾回收此对象之前,总会先调用这个对象的
finalize()
方法。 finalize()
方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。- 永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由包括下面三点:
- 在
finalize()
时可能会导致对象复活。 finalize()
方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()
方法将没有执行机会。- 一个糟糕的
finalize()
会严重影响GC的性能。
- 在
- 由于
finalize()
方法的存在,虚拟机中的对象一般处于三种可能的状态。- 可触及的:从根节点开始,可以到达这个对象。
- 可复活的:对象的所有引用都被释放,但是对象有可能在
finalize()
中复活。 - 不可触及的:对象的
finalize()
被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()
只会被调用一次。
- 以上3种状态中,是由于
finalize()
方法的存在,进行的区分。只有在对象不可触及时才可以被回收。
-
如何判定一个对象objA是否可回收?
-
如果对象objA到GC Roots没有引用链,则进行第一次标记。
-
进行筛选,判断此对象是否有必要执行
finalize()
方法:- 如果对象objA没有重写
finalize()
方法,或者finalize()
方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。
- 如果对象objA没有重写
-
-
如果对象objA重写了
finalize()
方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()
方法执行。finalize()
方法是对象逃脱死亡的最后机会,稍后GX会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()
方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象如果再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。
-
常见的垃圾回收算法有哪些?
- 标记清除算法:
- 当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
- 标记:垃圾回收器从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象(也就是说标记的是非垃圾对象)。
- 清除:垃圾回收器对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
- 缺点:
- 效率不算高。
- 在进行GC的时候,需要停止整个应用程序,导致用户体验差。
- 这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表。
- 复制算法:
- 将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
- 优点:
- 没有标记和清除过程,实现简单,运行高效。
- 复制过去以后保证空间的连续性,不会出现“碎片”问题。
- 缺点:
- 此算法的缺点也是很明显的,就是需要两倍的内存空间。
- 对于G1这种分拆成为大量region(区域)的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
- 如果系统中的垃圾对象相对于存活对象很少,复制算法就不会太理想。因为复制算法适用于复制的存活对象数量相对较少的情况,比如堆空间中新生代的S0和S1区的情况。
- 标记压缩算法:
- 前两种算法在老年代垃圾回收中执行效率较低,对标记清除算法进行改进。
- 标记:与标记清除算法中类似。
- 压缩:将所有的存活对象压缩到内存的一端,按顺序排放。
- 清除:清理边界外所有的空间。
- 标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。
- 二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。
- 标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
- 优点:
- 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
- 消除了复制算法当中,内存减半的高额代价。
- 缺点:
- 从效率上来说,标记-压缩算法要低于复制算法和标记-清除算法。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
- 移动过程中,需要全程暂停用户应用程序。即STW。
- 标记清除算法:
-
还有哪些垃圾回收算法的思想?
- 分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的,回收算法,以提高垃圾回收的效率。
- 增量收集算法:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
- 分区算法:一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
8、垃圾回收器
-
System.gc()表示什么?
- 在默认情况下,通过
System.gc()
或者Runtime.getRuntime().gc()
的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。 - 然而
System.gc()
调用附带一个免责声明,无法保证对垃圾收集器的调用。实际上只是提醒JVM希望进行一次垃圾回收,但不确定是否会执行。 - JVM实现者可以通过
System.gc()
调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()
。
- 在默认情况下,通过
-
什么是内存溢出(OOM)?
- javadoc中对OutOfMemoryError的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。
- JVM的堆内存不够的主要原因:
- JVM的堆内存设置不够。
- 代码中创建了大量大对象,并且长时间不能被垃圾回收器收集。
- 在即将OOM时,JVM会进行一次独占式的Full GC操作,这时候会回收大量的内存,供应用程序继续使用。
- GC不是在任何情况下垃圾收骤器都会被触发的。比如分配一个超大对象,超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OOM。
-
什么是内存泄漏?
- 严格来讲,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
- 但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的内存泄漏。
- 尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终可能出现OutOfMemory异常,导致程序崩溃。
-
什么是Stop The World?
- Stop-The-World,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
- 可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。
- 分析工作必须在一个能确保一致性的快照中进行。
- 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上。
- 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证。
- 被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉卡顿,所以我们需要减少STW的发生。
-
垃圾回收的并行与并发?
- 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。如ParNew Parallel Scavenge,Parallel Old。
- 串行(Serial):相较于并行的概念,单线程执行。
如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收。回收完,再启动程序的线程(实际上并行也是如此)。 - 并发(Concurrent):在一个时间段内,指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;如CMS,G1。
-
什么是安全点和安全区域?
- 安全点:程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点(Safepoint)。
- 安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。我们也可以把safe Region看做是被扩展了的Safepoint。
-
什么是强引用?
- 在Java程序中,最常见的引用类型是强引用(普通系统99%以上都是强引用),也就是我们最常见的普通对象引用,也是默认的引用类型。
- 当在Java语言中使用new操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。
- 强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象。
-
什么是软引用?
- 软引用是用来描述一些还有用,但非必需的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收(第一次回收是针对不可达对象),如果这次回收还没有足够的内存,才会抛出内存溢出异常。
- 软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
- 垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地把引用存放到一个引用队列(Reference Queue)。
-
什么是弱引用?
- 弱引用也是用来描述那些非必需对象,只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在系统Gc时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。
- 但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。
- 弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收。
-
什么是虚引用?
- 是所有引用类型中最弱的一个。
- 一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收。
- 它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null。
- 为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。
- 虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。
-
垃圾回收器如何进行分类?
- 按线程数分:
- 串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。
- 并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了"stop-the-world"机制。
- 按照工作模式分:
- 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
- 独占式垃圾回收器(stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。
- 按碎片处理方式分:
- 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。
- 非压缩式的垃圾回收器不进行这步操作。
- 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。
- 按线程数分:
-
评估垃圾回收器的指标有哪些?
- 吞吐量:运行用户代码的时间占总运行时间的比例
(总运行时间:程序的运行时间+内存回收的时间)。 - 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
- 收集频率:相对于应用程序的执行,收集操作发生的频率。
- 内存占用:Java堆区所占的内存大小。
- 快速:一个对象从诞生到被回收所经历的时间。
- 最重要的是吞吐量和暂停时间。
- 高吞吐量较好因为这会让应用程序的最终用户感觉有应用程序线程在做“生产性”工作。直觉上,吞吐量越高程序运行越快。
- 低暂停时间(低延迟)较好因为从最终用户的角度来看不管是GC还是其他原因导致一个应用被挂起始终是不好的。因此,具有低的较大暂停时间是非常重要的,特别是对于一个交互式应用程序。
- 在设计(或使用)GC算法时,我们必须确定我们的目标:一个GC算法只可能针对两个目标之一(即只专注于较大吞吐量或最小暂停时间),或尝试找到一个二者的折衷。
- 吞吐量:运行用户代码的时间占总运行时间的比例
-
垃圾回收器分代划分?
- 新生代收集器:Serial,ParNew,Parallel scavenge;
- 老年代收集器:Serial old,Parallel old,CMS;
- 整堆收集器:G1。
-
垃圾回收器的组合关系是怎样的?
- Serial+Serial Old
- ParNew+CMS
- Parallel Scavenge+Parallel Old
- G1
- JDK8中废弃,JDK9中移除:Serial+CMS和ParNew+Serial Old
- JDK14中废弃:Parallel Scavenge+Serial Old
- JDK14中移除:CMS
- JDK6~8默认Parallel scavenge + Parallel Old
- JDK9及以后默认G1
-
介绍一下Serial回收器?
-
特点是:串行回收。
-
Serial收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的选择。
-
Serial收集器作为Hotspot中Client模式下的默认新生代垃圾收集器。
-
Serial 收集器采用复制算法、串行回收和"Stop-the-World"机制的方式执行内存回收。|
-
除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial old收集器。Serial old收集器同样也采用了串行回收和"Stop the World"机制,只不过内存回收算法使用的是标记-压缩算法。
- Serial old是运行在Client模式下默认的老年代的垃圾回收器
- Serial old在Server模式下主要有两个用途:①与新生代的Parallel Scavenge配合使用;②作为老年代CMS收集器的后备垃圾收集方案。
-
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(stop The world)。
-
优势:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
-
-
介绍一下ParNew回收器?
-
特点是:并行回收
-
如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本。
-
ParNew收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。
-
ParNew收集器在年轻代中同样也是采用复制算法、"stop-the-world"机制。
-
-
介绍一下Parallel Scavenge回收器?
-
特点是:吞吐量优先
-
arallel Scavenge收集器同样也采用了复制算法、并行回收和"Stop the World"机制。
-
那么Parallel收集器的出现是否多此一举?
- 和ParNew收集器不同,Parallel scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。
- 自适应调节策略也是Parallel Scavenge与ParNew一个重要区别。
-
高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。
-
Parallel收集器在JDK1.6时提供了用于执行老年代垃圾收集的Parallel old收集器,用来代替老年代的Serial old收集器。
-
Parallel old收集器采用了标记-压缩算法,但同样也是基于并行回收和"stop-the-world"机制
-
-
介绍一下CMS回收器?
-
特点是:低延迟
-
CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
-
CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
-
CMS的垃圾收集算法采用标记-清除算法,并且也会"stop-the-world"。
-
由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。
-
要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
-
-
CMS是怎样进行垃圾回收的?
-
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。
-
初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“stop-the-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GCRoots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。
-
并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
-
重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
-
并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
-
如何选择GC?
- 如果你想要最小化地使用内存和并行开销,请选Serial GC;
- 如果你想要最大化应用程序的吞吐量,请选Parallel GC;
- 如果你想要最小化GC的中断或停顿时间,请选CMS GC。
-
介绍一下G1回收器?
-
特点是:区域化分代式。
-
应用程序所应对的业务越来愈庞大,经常造成STW的GC又跟不上实际的需求。官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,担当起“全功能收集器”的期望。
-
为什么名字叫做Garbage First(G1)?。
- 因为G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使用不同的Region来表示Eden、幸存者0区,幸存者1区,老年代等。
- G1 GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
- 由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First)。
-
优点:
- 并行与并发:
- 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW。
- 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况。
- 分代收集:
- 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
- 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
- 和之前的各类回收器不同,它同时兼顾年轻代和老年代。其他回收器,或工作在年轻代,或工作在老年代。
- 空间整合:
- CMS:“标记-清除”算法、内存碎片、若干次GC后进行一次碎片整理。
- G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。
- Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)
算法,两种算法都可以避免内存碎片。 - 这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
- Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)
- 可预测的停顿时间模型(即:软实时soft real-time):
- 这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
- 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
- G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
- 相比于CMSGC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
- 并行与并发:
-
缺点:
- 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。
- 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。
-
-
G1是如何进行分区的?
- 使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂。所有的Region大小相同,且在JVw生命周期内不会被改变。
- 虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。通过Region的动态分配方式实现逻辑上的连续。
- 一个region 有可能属于Eden,Survivor 或者Old/Tenured内存区域。但是一个region只可能属于一个角色。E表示该region属于Eden内存区域,S表示属于survivor内存区域,O表示属于old内存区域。空白的表示未使用的内存空间。
- G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,H块。主要用于存储大对象,如果超过0.5个region,就放到H。
-
什么是Remembered Set:记忆集RSet?
- 一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,需要扫描整个Java堆才能保证准确。
- 在其他的分代收集器,也存在这样的问题(而G1更突出)回收新生代也不得不同时扫描老年代,这样的话会降低MinorGC的效率。
- 无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:每个Region都有一个对应的Remembered Set。
- 每次Reference类型数据写操作时,都会产生一个Write Barrier(写屏障)暂时中断操作;然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(对于除G1外的其他收集器:检查老年代对象是否引用了新生代对象);
- 如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;就可以保证不进行全局扫描,也不会有遗漏。
- 将引用信息记录时,会先将引用信息入队到dirty card queue(脏卡表)中,在进行垃圾收集时再更新RSet。这是为了减少RSet进行线程同步的开销。
-
G1是如何进行垃圾回收的?
- 主要包括三个环节:年轻代GC,老年代标记和混合GC。
- 年轻代GC(Young GC)。
- 应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式收集器。
- 在年轻代回收期,G1GC暂停所有应用程序线程(STW),启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到survivor区间或者老年区间,也有可能是两个区间都会涉及。
- 具体步骤是首先扫描根和RSet,然后赋值对象,最后处理引用。
- 老年代并发标记过程(Concurrent Marking)。
- 当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。
- 这个过程与CMS的类似。不过不会进行是指的垃圾收集,而是计算每个区域的回收价值。
- 混合回收(Mixed GC)。
- 标记完成马上开始混合回收过程。对于一个混合回收期,G1GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。
- 和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。
- 如果需要,单线程、独占式、高强度的FullGC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收
9、Class文件结构
-
什么是字节码文件?
- 是由javac编译器将Java源码编译的结果。是一种二进制的类文件,它的内容是JVM的指令,而不像C、C++经由编译器直接生成机器码。
- Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。
-
Class文件格式是怎样的?
- Class文件没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的。哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
- Class文件格式采用一种类似于C语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表。
- 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8编码构成字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。
- 表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明。
-
Class文件的结构是怎样的?
- 魔数:Class文件的标识符。
- Class文件的版本号。
- 常量池和常量池计数器。
- 访问标识:一些类或者接口层次的访问信息。
- 类索引、父类索引、接口索引集合:指向常量池中的索引。
- 字段表:Java中的成员变量,包括类变量和实例变量。
- 方法表:方法信息。比如方法的访问修饰符,方法的返回值类型以及方法的参数信息等。
- 属性表:class文件所携带的辅助信息,通常被用于Java虚拟机的验证和运行,以及Java程序的调试。
- 其中比较重要的是Code属性,就是存放方法体里面的代码。
- Code属性中还有LineNumberTable属性和LocalVariableTable属性。
-
常量池中存储的是什么?
- 字面量和符号引用。
- 字面量:文本字符串,或声明为final的常量值。
- 符号引用:类和接口的全限定名,字段的名称和描述符,或方法的名称和描述符。
- 描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。
- 用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。
- 虚拟机在加载Class文件时才会进行链接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。
- 符号引用和直接引用的区别与关联:
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
- 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。
10、字节码指令集
- 什么是字节码指令?
- 方法的字节码指令在方法表中各个方法的Code属性中。
- Java字节码对于虚拟机,就好像汇编语言对于计算机,属于基本执行指令。
- Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。即
操作码 操作数
- 由于Java虚拟机采用面向操作数栈而不是寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码。
- 字节码指令的分类?
- 加载与存储指令:将数据从栈帧中的局部变量表和操作数栈之间来回传递。加载一般指压栈到操作数栈中(从局部变量表或常量池),存储一般指从操作数栈出栈存入局部变量表中。
- 算术指令:算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新压入操作数栈。
- 类型转换指令:将两种不同的数值类型进行相互转换。
- 对象的创建与访问指令:用于对象操作,可进一步细分为创建指令、字段访问指令、数组操作指令、类型检查指令。
- 方法调用与返回指令:用于进行方法调用和结果返回。
- 操作数栈管理指令:可以用于直接操作操作数栈的指令。
- 比较控制指令:根据比较结果进行跳转。
- 异常处理指令:包括抛出异常指令、异常处理与异常表配合。
- 同步控制指令:同步方法和同步代码块。
11、类的加载和类加载器
-
类模板文件的生命周期?
- 在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。
- 从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:
- 加载阶段。将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象。
- 通过类的全名,获取类的二进制数据流。
- 解析类的二进制数据流为方法区内的数据结构(Java类模型)。
- 创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的入口。
- 链接阶段。
- 验证阶段:它的目的是保证加载的字节码是合法、合理并符合规范的。
- 准备阶段:为类的静态变量分配内存,并将其初始化为默认值。
- 这里不包含基本数据类型和String的字段用static final修饰(常量)的情况,因为final在编译的时候就会分配了,准备阶段会显式赋值(使用字面量而非调用方法或构造器)。
- 解析阶段:将类、接口、字和方法的符号引用转为直接引用。
- 初始化阶段。
- 为类的静态变量予正确的初始值。
- 执行类的初始化方法:
<clinit>()
方法。 - 包括static final修饰的基本数据类型和String,但是是由调用方法返回值赋值构造方法new的。
- 使用阶段。可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用new关键字为其创建对象实例。
- 卸载阶段。一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
- 加载阶段。将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象。
-
类、类的加载器、类的实例之间的引用关系是怎样的?
- 在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。
- 另一方面,一个Class对象总是会引用它的类加载器,调用class对象的getclassLoader()方法,就能获得它的类加载器。
- 由此可见,代表某个类的Class实例与其类的加载器之间为双向关联关系。
- 一个类的实例总是引用代表这个类的Class对象。在object类中定义了getclass()方法,这个方法返回代表对象所属类的Class对象的引用。
- 此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。
-
类加载器的作用?
- 类加载器是Java的核心组件,所有的Class都是由类加载器进行加载的。
- 类加载器负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。
- 因此,类加载器在整个装载阶段,只能影响到类的加载,而无法通过类加载器去改变类的链接和初始化行为。至于它是否可以运行,则由Execution Engine决定。
-
类的显式加载和隐式加载?
- class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式。
- 显式加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。
- 隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。
-
类的唯一性是什么?
- 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。
- 每一个类加载器,都拥有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。
-
类加载机制的三个基本特征是什么?
- 双亲委派模型。如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。但不是所有类加载都遵守这个模型,有的时候,启动类加载器所加载的类型,是可能要加载用户代码的,比如上下文加载器。
- 可见性,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑。
- 单一性,由于父加载器的类型对于子加载器是可见的度所以父加载器中加载过的类型,就不会在子加载器中重复加载。但是注意,类加载器“邻居”间,同一类型仍然可以被加载多次,因为互相并不可见。
-
类加载器的分类?
- JVM提供的启动类加载器、扩展类加载器和应用程序类加载器,以及用户自定义的用户自定义类加载器。
- 不同类加载器看似是继承关系,实际上是包含关系。在下层加载器中,包含着上层加载器的引用。
- 启动类加载器(引导类加载器,Bootstrap ClassLoader):
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
- 它用来加载Java的核心库。
- 不继承自java.lang.ClassLoader,没有父加载器。
- 扩展类加载器(Extension ClassLoader):
- Java语言编写,继承于ClassLoader类,父类加载器为启动类加载器。
- 它用来加载Java的扩展类库。
- 应用程序类加载器(系统类加载器,AppClassLoader):
- java语言编写,继承于ClassLoader类,父类加载器为扩展类加载器。
- 它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库。应用程序中的类加载器默认是系统类加载器。
- 它是用户自定义类加载器的默认父加载器。
- 用户自定义类加载器:自定义类加载器,来定制类的加载方式。
-
获取ClassLoader的途径?
- 获得当前类的ClassLoader:clazz.getClassLoader()。
- 获得当前线程上下文的ClassLoader:
Thread.currentThread().getContextClassLoader()。 - 获得系统的ClassLoader:ClassLoader.getSystemclassLoader()。
- 数组类的Class对象,不是由类加载器去创建的,而是在Java运行期JVM根据需要自动创建的。对于数组类的类加载器来说,是通过class.getClassLoader()返回的,与数组当中元素类型的类加载器是一样的;如果数组当中的元素类型是基本数据类型,数组类是没有类加载器的(不需要加载)。
-
Class.forName()与ClassLoader.loadClass()的区别?
Class.forName()
:是一个静态方法,最常用的是Class.forName(String className);根据传入的类的全限定名返回一个Class对象。该方法在将Class文件加载到内存的同时,会执行类的初始化。ClassLoader.loadClass()
:这是一个实例方法,需要一个ClassLoader对象来调用该方法。该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。
-
什么是双亲委派模型?
- 定义:如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。
- 本质:规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器加载,最后才会尝试由自定义的类加载器进行加载。
- 双亲委派机制优势:
- 避免类的重复加载,确保一类的全局唯一性。Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
- 保护程序安全,防止核心API被随意篡改。
- 双亲委派机制的弊端:
- 检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。
-
如何破坏双亲委派机制?
- 第一次:在JDK1.2即双亲委派机制出现之前,用户自定义类加载器需要重写loadClass()方法。在JDK1.2之后运行时,会将实现双亲委派机制的ClassLoader中的loadClass()方法覆盖。
- 第二次:
- 为了弥补双亲委派机制的缺陷,即启动类加载器向下请求应用程序类加载器加载其所需的类,在双亲委派机制下无法实现。
- 引入线程上下文加载器(也属于应用程序类加载器),作为委托中介。当启动类加载器需要应用程序类加载器进行类的加载时,就像将请求委托给线程上下文加载器,再由它调用应用程序类加载器。
- 第三次:
- 由于用户对程序动态性的追求,比如代码热替换、模块热部署。
- OSGi模块热部署中,每个程序模块都有一个自己的类加载器,类加载器之间不再是树状结构,而是网状结构。
- 在需要热替换时,创建一个新的自定义类加载器,用新的类加载器加载被替换的类。
-
什么是沙箱安全机制?
- 目的:保证程序安全,保护Java原生的JDK代码。
- 沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问。通过这样的措施来保证对代码的有限隔离,防止对本地系统造成破坏。
- 沙箱主要限制系统资源访问,包括:CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
-
为什么需要自定义类的加载器?
-
隔离加载类:在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。
-
修改类加载的方式:类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载。
-
扩展加载字节码文件的源。
-
防止源码泄漏:Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。
-
实现方式:
- 一般来说推荐重写
findClass()
方法。这样不会破坏双亲委派机制。 - 自定义加载器的父类时应用程序类加载器。
- 获取字节码文件为字节数组,调用
defineClass()
方法,将字节数组转换为Class实例。
- 一般来说推荐重写
-
-
Java9中对类加载器的修改?
- 扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform class loader)。JDK9时基于模块化进行构建,其中的Java类库就已天然地满足了可扩展的需求。
- 平台类加载器和应用程序类加载器都不再继承自java.net.URLClassLoader。现在启动类加载器、平台类加载器、应用程序类加载器全都继承于jdk.internal.loader.BuiltinClassLoader。
- 启动类加载器现在是在jvm内部和java类库共同协作实现的类加载器(以前是C++实现),但为了与之前代码兼容,在获取启动类加载器的场景中仍然会返回null,而不会得到BootClassLoader实例。
- 类加载的委派关系也发生了变动。当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。
iwehdio的博客园:https://www.cnblogs.com/iwehdio/