本文已参与「新人创作礼」活动,一起开启掘金创作之路。
-
JVM概述
- JVM 即 Java 虚拟机。它能识别 .class后缀的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作。
\
-
C++和Java区别
- C++ 程序是编译成操作系统能够识别的 .exe 文件,而 Java 程序是编译成 JVM 能够识别的 .class 文件,然后由 JVM 负责调用系统函数执行程序。\
-
JVM,JDK,JRE区别\
\
-
Java代码运行过程\
- 一个 Java 程序,首先经过 javac 编译成 .class 文件,然后 JVM 将其加载到
元数据区,执行引擎将会通过混合模式执行这些字节码。执行时,会翻译成操作系统相关的函数。JVM 作为 .class 文件的黑盒存在,输入字节码,调用操作系统函数。\
- 过程如下:Java 文件->编译器>字节码->JVM->机器码。\
- 一个 Java 程序,首先经过 javac 编译成 .class 文件,然后 JVM 将其加载到
-
JVM内存区域\
-
运行时数据区域\
-
不同版本的Java运行时数据区\
-
JDK1.8之前\
\
-
JDK1.8之后\
- 增加了元空间
\
- 增加了元空间
-
线程私有区域\
- 程序计数器\
- 虚拟机栈\
- 本地方法栈\
- 线程私有区域的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。\
-
线程共享区域\
- 堆\
- 方法区\
- 直接内存 (非运行时数据区的一部分)\
-
-
程序计数器\
- 当前线程所执行的字节码的行号指示器。通过改变该计数器的值来选取下一条需要执行的字节码指令。\
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,线程切回来的时候用来恢复。\
- 若线程执行的是Java方法,计数器内记录的是正在执行的虚拟机字节码指令的地址。若执行的是本地方法(虚拟机内的方法),该计数器值为空。\
- 程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。\
-
Java虚拟机栈\
-
每个方法被执行时,Java虚拟机栈都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到完成,对应一个栈帧在虚拟机栈中入栈到出栈。\
- Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。
\
- Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。
-
局部变量表\
- 主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置),returnAddress类型(指向了一条字节码指令的地址)。\
- 64位长度的long和double使用2个局部变量槽,其他使用1个。\
-
操作数栈\
- 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。\
-
动态链接\
- 主要服务一个方法需要调用其他方法的场景。在 Java 源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在Class 文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用。\
-
StackOverFlowError\
- 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。\
-
OutOfMemoryError\
- 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。\
-
-
本地方法栈\
- 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。\
-
堆\
- Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。 基本数据类型分配内存是在虚拟机栈中。\
- Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆\
-
Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。Java 虚拟机将堆划分为新生代和老年代。其中,新生代又被划分为 Eden 区,以及两个大小相同的 Survivor 区。\
\
-
堆内存的组成\
-
JDK7及之前:新生代内存,老生代,永久代\
\
-
JDK8:新生代内存,老生代,元空间\
\
- 大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。\
-
- Java堆可以处于物理上不连续的内存空间,但是逻辑上要连续。\
- Java堆既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。\
-
方法区\
- 各个线程共享的区域,在不同的虚拟机实现上,方法区的实现是不同的。\
- 当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。\
-
方法区和永久代以及元空间的关系\
-
永久代以及元空间是 HotSpot 虚拟机对方法区的两种具体实现方式。\
\
-
为什么要将永久代 替换为元空间\
- 整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。\
- 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。\
- 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。\
-
- 方法区如果无法满足新的内存分配需求时,会抛出OutOfMemoryError异常\
-
运行时常量池\
- 是方法区的一部分\
- Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量和符号引用的常量池表。常量池表会在类加载后存放到方法区的运行时常量池中。\
- 运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进人方法区运行时常量池,运行期间也可能将新的常量放入池中。\
- 当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。\
-
字符串常量池的作用\
- 字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
\
- JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区。JDK1.7 的时候,字符串常量池被从方法区拿到了堆中。\
-
JDK 1.7 为什么要将字符串常量池移动到堆中?\
- 主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。\
- 字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
-
直接内存\
- JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。\
- 本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。\
-
元空间\
- 元空间并不在虚拟机里面,而是直接使用本地内存\
- 为什么有 Metaspace 区域?它有什么问题?\
-
关于OOM\
-
发生OOM的区域\
\
-
OOM产生的原因\
- 内存的容量太小了,需要扩容,或者需要调整堆的空间。\
- 错误的引用方式,发生了内存泄漏。即对象没有及时的释放自己的引用;没有及时的切断与 GC Roots 的关系。比如线程池里的线程,在复用的情况下忘记清理 ThreadLocal 的内容。\
- 接口没有进行范围校验,外部传参超出范围。比如数据库查询时的每页条数等。\
- 对堆外内存无限制的使用。这种情况一旦发生更加严重,会造成操作系统内存耗尽。\
-
-
-
Hotspot虚拟机对象探秘\
-
对象创建方式\
- new -通过调用构造器来初始化实例字段\
- 反射-通过调用构造器来初始化实例字段\
- Object.clone-通过直接复制已有的数据,来初始化新建对象的实例字段\
- 反序列化-通过直接复制已有的数据,来初始化新建对象的实例字段\
- Unsafe.allocateInstance-没有初始化对象的实例字段\
-
对象的创建\
-
第一步:类加载检查\
- 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。\
-
第二步:分配内存\
-
虚拟机为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。\
-
指针碰撞 :\
- 适用场合 :堆内存规整(即没有内存碎片)的情况下。\
- 原理 :用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。\
- 使用该分配方式的 GC 收集器:Serial, ParNew\
-
空闲列表 :\
- 适用场合 : 堆内存不规整的情况下。即使用过的和没使用过的内粗交错在一起。\
- 原理 :虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。\
- 使用该分配方式的 GC 收集器:CMS\
-
-
内存分配并发问题\
- 线程安全问题:正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。\
-
CAS+失败重试\
- CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。\
-
TLAB\
- 将线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个线程的本地线程分配缓冲上分配。当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配\
-
-
第三步:初始化零值\
- 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。\
-
第四步:设置对象头\
- 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。\
-
第五步:执行init方法\
- 完成上面工作后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。\
-
-
对象的内存布局\
- 在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。\
-
Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。\
- 并不是所有虚拟机实现都必须在对象数据上保留类型指针。\
- 如果对象是Java数组,那么对象头还必须有一块用于记录数组长度的数据。\
- 实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。\
- 对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。\
-
对象的访问定位\
- Java 程序通过栈上的 reference 数据来操作堆上的具体对象, reference只是一个指向对象的引用。对象的访问方式由虚拟机实现而定,目前主流的访问方式有:使用句柄、直接指针。\
-
句柄\
- 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
\
- 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
-
直接指针\
- 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
\
- 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
- 这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。\
-
-
OOM异常\
- 除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOMemoryError(下文称O0M)异常的可能\
-
Java堆溢出\
- Java堆用来存储对象实例。异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。\
- 首先确定导致OOM的对象是否是必要的,即发生了内存泄漏还是内存溢出。\
- 找出泄漏对象位置进行处理,删除泄漏对象。\
- 内存溢出即所有对象都要存活,检查堆参数设置,看是否还有上调的空间。\
-
虚拟机栈和本地方法栈溢出\
-
两种异常情况\
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowEror异常。\
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。\
- HotSpot虚拟机不支持栈的动态扩容。所以除非是在创建线程时没有申请到足够的内存而出现OutOfMemoryError。其余都是栈容量无法容纳新的栈帧导致的StackOverflowEror\
-
- 方法区和运行时常量池溢出\
- 本机直接内存溢出\
-
-
JVM垃圾回收(GC)\
-
JVM内存分配和内存回收\
-
Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。\
\
-
对象优先在Eden区分配\
-
大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.\
- 给 allocation2分配好内存后,在给 allocation2 分配内存的时候 eden 区内存几乎已经被分配完了,我们刚刚讲了当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.GC 期间虚拟机又发现 allocation1 无法存入 Survivor 空间,所以只好通过 分配担保机制 把新生代的对象提前转移到老年代中去,老年代上的空间足够存放 allocation1,所以不会出现 Full GC。执行 Minor GC 后,后面分配的对象如果能够存在 eden 区的话,还是会在 eden 区分配内存。\
- 如果Survivor空间不够,就要依赖老年代进行分配担保,把放不下的对象直接进入老年代。\
-
-
大对象直接进入老年代\
- 大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。\
- 为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。\
-
长期存活的对象进入老年代\
- 虚拟机给每个对象一个对象年龄(Age)计数器。\
- 如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。\
-
动态对象年龄判定\
- 大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。\
-
进行GC的区域\
-
部分收集 (Partial GC):\
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;\
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;\
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。\
- 整堆收集 (Full GC):收集整个 Java 堆和方法区。(因为程序计数器,虚拟机栈,本地方法栈随线程产生和消耗)。\
-
-
空间分配担保\
- 确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。\
- 分配担保是︰当新生代进行垃圾回收后,新生代的存活区放置不下,那么需要把这些对象放置到老年代去的策略,也就是老年代为新生代的GC做空间分配担保.\
\
-
对于跨代的引用的处理:卡片标记\
- 其实,老年代是被分成众多的卡页的(一般数量是 2 的次幂)。\
- 卡表就是用于标记卡页状态的一个集合,每个卡表项对应一个卡页。\
- 如果年轻代有对象分配,而且老年代有对象指向这个新对象, 那么这个老年代对象所对应内存的卡页,就会标识为 dirty,卡表只需要非常小的存储空间就可以保留这些状态。\
- 垃圾回收时,就可以先读这个卡表,进行快速判断。\
-
-
对象已经死亡?\
- 堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡。\
-
引用计数法\
- 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。\
- 很难解决对象之间相互循环引用的问题。 对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用,它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。\
-
可达性分析算法\
- 通过一系列的称为 “GC Roots” 的根对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
\
-
哪些对象可以作为 GC Roots 呢?\
- 虚拟机栈(栈帧中的本地变量表)中引用的对象\
- 本地方法栈(Native 方法)中引用的对象\
- 方法区中类静态属性引用的对象\
- 方法区中常量引用的对象\
- 所有被同步锁持有的对象\
\
-
对象可以被回收,就代表一定会被回收吗?两次标记\
- 即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。\
- 被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。\
\
- 通过一系列的称为 “GC Roots” 的根对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
-
四种引用分类\
- JDK1.2 之前,对引用的定义:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。\
-
四种引用\
-
强引用\
- 如 String s=new String() 这种。如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。\
-
软引用\
- 有用但不是必须的对象\
- 如果内存空间足够,垃圾回收器就不会回收它。如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。\
- 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。\
-
弱引用\
- 具有弱引用的对象拥有更短暂的生命周期。当垃圾收集器开始工作时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。\
- 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。\
-
虚引用\
- 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。\
- 为对象设置虚引用的目的是为了让对象被收集器回收时收到一个系统通知。\
-
-
回收方法区\
- 方法区垃圾收集回收两部分,废弃的常量和不再使用的类。\
-
如何判定一个常量是废弃常量\
- 无对象引用即废弃常量。
\
- 无对象引用即废弃常量。
-
如何判定一个类是无用的类\
- 同时满足下面三个条件。
\
- 同时满足下面三个条件。
-
垃圾收集算法\
-
标记-清除算法\
- 该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有未被标记的对象。\
-
缺点\
-
效率问题\
- 执行效率不稳定,若大量对象要被回收,需要大量标记和清除操作,导致效率随对象的增加而降低。\
-
空间问题\
- 碎片问题:标记清除后会产生大量不连续的碎片,导致下一次分配较大对象时无法找到足够的连续内存。\
-
-
标记-复制算法\
- 它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。(好像有点像redis的字典由两个哈希表组成,新生代中Survivor的from和to也是类似的)\
-
缺点\
- 若内存内大多数对象是存活的,会导致大量的复制开销。\
- 将可用内存空间缩小一半,空间浪费了。\
-
标记-整理算法\
- 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
\
-
缺点:因为它是移动式的回收算法\
- 对象移动会全程暂停用户应用程序。\
- 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
-
分代收集算法\
- 根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。\
-
比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。\
-
新生代:Eden、from、to 的默认比例是 8:1:1,所以只会造成 10% 的空间浪费。
\
- TLAB 的全称是 Thread Local Allocation Buffer,JVM 默认给每个线程开辟一个 buffer 区域,用来加速对象分配。这个 buffer 就放在 Eden 区中。\
- 这个道理和 Java 语言中的 ThreadLocal 类似,避免了对公共区的操作,以及一些锁竞争。对象的分配优先在 TLAB上 分配,但 TLAB 通常都很小,所以对象相对比较大的时候,会在 Eden 区的共享区域进行分配。
\
-
-
HotSpot 为什么要分为新生代和老年代?\
\
-
三色标记-清除算法\
- 三色标记算法利用三种颜色进行标记。白色代表需要回收的节点;黑色代表不需要回收的节点;灰色代表会被回收,但是没有完成标记的节点。\
-
-
垃圾收集器\
-
收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。\
\
- JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old\
-
Serial收集器\
- 单线程收集器了。它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。\
-
stop the world\
- Java 虚拟机中的 Stop-the-world 是通过安全点(safepoint)机制来实现的。当 Java 虚拟机收到Stop-the-world 请求,它便会等待所有的线程都到达安全点,才允许请求 Stop-the-world 的线程进行独占的工作。\
- 新生代采用标记-复制算法,老年代采用标记-整理算法。
\
-
优缺点\
- 优点:简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择\
-
ParNew 收集器\
- Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。\
- 新生代采用标记-复制算法,老年代采用标记-整理算法。 有多线程上下文切换的开销。
\
-
Parallel Scavenge 收集器\
-
使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。\
- Parallel Scavenge 收集器关注点是吞吐量(提高CPU利用率)。而CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。
\
- Parallel Scavenge 收集器关注点是吞吐量(提高CPU利用率)。而CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。
- 新生代采用标记-复制算法,老年代采用标记-整理算法。
\
-
-
Serial old收集器\
- Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。\
-
Parallel old收集器\
- Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。\
-
CMS(Concurrent Mark Sweep)收集器\
- 一种以获取最短回收停顿时间为目标的收集器。希望系统停顿时间尽可能短。\
- CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。\
-
CMS 收集器是一种 “标记-清除”算法实现的,整个过程分为四个步骤:\
- 初始标记: stop the world暂停所有的其他线程,并记录下直接与GC Roots 相连的对象,速度很快 ;\
- 并发标记: 从GC Roots 的直接关联对象开始遍历整个对象图的过程。不需要停顿用户线程,可与垃圾收集线程一起并发运行。\
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短\
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。\
\
-
优缺点\
-
优点\
- 并发收集、低停顿并发收集、低停顿\
-
缺点\
-
对 CPU 资源敏感;\
- 因为他占用了一部分线程而导致应用程序变慢,降低总吞吐量。\
- 无法处理浮动垃圾(在处理过程中产生的垃圾),可能导致full GC;\
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。\
-
-
-
G1收集器\
- G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.\
-
特点\
- 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。\
-
分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。把内存分为多个区域Region。\
- G1的年轻代和老年代都是逻辑上的。
\
- G1的年轻代和老年代都是逻辑上的。
- 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。 整体标记-整理,局部是标记-清除\
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。\
-
步骤\
-
初始标记\
- 跟CMS初始标记一样。\
-
并发标记\
- 跟CMS并发标记一样\
-
最终标记\
- 跟CMS重新标记一样\
-
筛选回收\
- 根据时间来进行价值最大化的回收\
- G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。\
-
-
ZGC 收集器\
- JDK11加入的低延迟收集器\
- ZGC里面的新技术:着色指针和读屏障\
-
- \
-
GC性能指标\
- 吞吐量=应用代码执行的时间/运行的总时间\
- GC负荷:是GC时间/运行的总时间\
- 暂停时间,就是发生Stop-the-World的总时间\
- GC频率,就是GC在一个时间段发生的次数\
- 反应速度,就是从对象成为垃圾到被回收的时间\
- 交互式应用通常希望暂停时间越少越好\
-
JVM内存配置原则\
-
新生代尽可能设置大点,如果太小会导致∶\
- 1.新生代GC次数更加频繁\
- 2.可能导致新生代GC后的对象进入老年代,如果此时老年代满了,会触发FullGC\
-
对老年代,针对响应时间优先的应用:由于老年代通常采用并发收集器,因此其大小要综合考虑并发量和并发持续时间等参数\
- 如果设置小了,可能会造成内存碎片,高回收频率会导致应用暂停\
- 如果设置大了,会需要较长的回收时间\
-
对老年代,针对吞吐量优先的应用:\
- 通常设置较大的新生代和较小的老年代,这样可以尽可能回收大部分短期对象,减少中期对象,而老年代尽量存放长期存活的对象\
- 依据对象的存活周期进行分类,对象优先在新生代分配,长时间存活的对象进入老年代\
- 根据不同代的特点,选取合适的收集算法︰少量对象存活,适合复制算法﹔大量对象存活,适合标记清除或者标记整理\
-
-
-
类文件结构\
-
字节码(即扩展名为 .class 的文件),只面向虚拟机。Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。\
- .class文件是不同的语言在 Java 虚拟机之间的重要桥梁,同时也是支持 Java 跨平台很重要的一个原因
\
- .class文件是不同的语言在 Java 虚拟机之间的重要桥梁,同时也是支持 Java 跨平台很重要的一个原因
-
Class文件结构\
- Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前[的方式分割成若干个8个字节进行存储。\
-
Class文件格式只有两种数据类型:“无符号数”和“表”。\
- 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数。\
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。\
- 集合:无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的“集合”。\
- Class文件的组成
\
- \
- -----------------------Class类文件结构----------------------\
-
魔数\
- 每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。\
\
-
Class文件的版本号\
- 第 5 和第 6 位是次版本号,第 7 和第 8 位是主版本号。
\
- 第 5 和第 6 位是次版本号,第 7 和第 8 位是主版本号。
-
常量池\
- 常量池的数量是 constant_pool_count-1(常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项”)。
\
-
常量池主要存放两大常量:字面量和符号引用。 常量池即Class文件的资源仓库\
- 字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。\
-
而符号引用则属于编译原理方面的概念。\
- 类和接口的全限定名\
- 字段的名称和描述符\
- 方法的名称和描述符\
-
常量池中每一项常量都是一个表,这 14 种表有一个共同的特点:开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型.\
\
- 在Class文件中不会保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。\
- 常量池的数量是 constant_pool_count-1(常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项”)。
-
访问标志\
- 在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 等等。
\
- 在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 等等。
-
类索引、父类索引与接口索引集合\
- 类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。\
- 接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 implements (如果这个类本身是接口的话则是extends) 后的接口顺序从左到右排列在接口索引集合中。\
\
-
字段表集合\
- 字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。\
\
-
方法表集合\
- methods_count 表示方法的数量,而 method_info 表示方法表。\
- Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。\
\
-
属性表集合\
- 在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与 Class 文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写 入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性\
\
-
-
类加载机制\
-
类加载时机\
- 解析可以在初始化阶段之后开始。
\
-
六种情况必须对类进行初始化\
-
遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:\
- 使用new关键字实例化对象的时候。\
- 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。\
- 调用一个类型的静态方法的时候。\
- 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。\
- 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。\
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。\
- 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。\
- 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。\
-
-
JVM必须在每个类或接口“首次主动使用”时才初始化它们。主动使用的情况∶\
- 1)创建类实例\
- 2)访问某个类或接口的静态变量\
- 4)反射某个类\
- 5)初始化某个类的子类,而父类还没有初始化\
- 6 )JVM启动的时候运行的主类\
- 解析可以在初始化阶段之后开始。
-
类加载过程\
- 加载、验证、准备、解析和初始化这五个阶段(不包括使用,卸载)\
-
加载\
-
通过全类名获取定义此类的二进制字节流,将字节流所代表的静态存储结构转换为方法区的运行时数据结构,在内存(堆)中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口\
- 未指明要从哪里获取、如何获取。比较常见的就是从 ZIP 包中读取(日后出现的 JAR、EAR、WAR 格式的基础)、其他文件生成(典型应用就是 JSP)\
- 即加载的主要作用是将外部的 .class 文件,加载到 Java 的方法区内。加载是指查找字节流,并且据此创建类的过程。加载需要借助类加载器\
-
非数组类和数组类加载阶段\
- 对引用类型如:类、接口、数组类,进行类加载。(数组类是由 Java 虚拟机直接生成的,其他两种则有对应的字节流)\
-
非数组类型\
- 加载阶段既可以使用Java虚拟机里内置的引导类加载器来完成,也可以由用户自定义的类加载器去完成,\
-
数组类\
- 数组类本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态构造出来的。\
-
-
验证\
- 确保被加载类的正确性\
- 文件格式验证:验证字节流是否符合Class文件格式的规范\
-
元数据验证:保证其描述的信息符合《Java语言规范》的要求\
\
-
字节码验证:通过数据流分析和控制流分析,确定程序语义是合法的。\
\
- 符号引用验证:最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。\
\
-
准备\
-
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。\
- 类变量所使用的内存都应当在 方法区 中进行分配,但是方法区是一个逻辑区域。JDK7及之前,虚拟机是用永久代实现方法区。 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。\
-
这里所设置的初始值"通常情况"下是数据类型默认的零值(如 0、0L、null、false 等)\
- 比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111。\
\
-
进行内存分配的只有类变量,不包括实例变量,实例变量会在对象实例化随对象一起分配到Java堆中。\
\
-
-
解析\
- 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。\
-
符号引用\
- 符号引用就是一组符号来描述目标,可以是任何字面量,只需使用时无歧义定位到目标即可。\
-
直接引用\
- 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。\
-
初始化\
-
为类的静态变量赋初始值即执行。\
\
-
除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码(字节码),将主导权移交给应用程序。初始化阶段是执行初始化方法 ()方法的过程。\
- ·()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。\
- 类和接口,父类初始化顺序可以看慕课网视频。\
- () 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个进程阻塞,并且这种阻塞很难被发现。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。\
-
5 种情况下,必须对类进行初始化\
-
当遇到 new 、 getstatic、putstatic 或 invokestatic 这 4 条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。\
- 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。\
- 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。\
- 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。\
- 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。\
- 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname("..."), newInstance() 等等。如果类没初始化,需要触发其初始化。\
- 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。\
- 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。\
- MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类。\
\
-
-
方法和 方法的区别\
- 类初始化,static代码只会执行一次\
- ,用来初始化对象。每次新建对象的时候,都会执行。\
-
-
-----------------不属于五个阶段的卸载-----------------------\
- 卸载类即该类的 Class 对象被 GC。\
-
卸载类需要满足 3 个要求:\
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。\
- 该类没有在其他任何地方被引用\
- 该类的类加载器的实例已被 GC\
- 所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。\
-
类加载器\
- 所有的类都由类加载器加载,加载的作用就是将 .class文件加载到内存。\
-
类与类加载器\
- 两个类相等,同一个Class文件,被同一个Java虚拟机加载,并且加载它们的类加载器相同。\
- 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。\
-
类加载器分类\
- 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分;\
- 所有其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。\
- 三层类加载器
\
-
双亲委派模型\
-
类加载器双亲委派模型\
\
- 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。一个类加载器首先将类加载请求转发到父类加载器,最终所有加载请求传送到最顶层的启动类加载器。只有当父类加载器无法完成时才尝试自己加载。\
-
好处\
- 使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。\
- 双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。\
-
-
破坏双亲委派机制\
- 自定义类加载器的话,则需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。( loadClass()中实现了双亲委派)\
-
自定义类加载器\
- 除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader。\
-
(Tomcat是不支持双亲委派机制的)\
\
-
-
字节码执行引擎\
- JVM的字节码执行引擎,功能基本就是输入字节码文件,然后对字节码进行解析并处理,最后输出执行的结果。\
-
执行引擎在执行字节码的时候,通常会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备.\
- Java 虚拟机需要将字节码翻译成机器码。\
- 在HotSpot里面,上述翻译过程有两种形式:第一种是解释执行,即逐条将字节码翻译成机器码并执行;第二种是即时编译,即将一个方法中包含的所有字节码编译成机器码后再执行。\
- 前者的优势在于无需等待编译,而后者的优势在于实际运行速度更快(但是占内存)。HotSpot 默认采用混合模式,综合了解释执行和即时编译两者的优点。它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译。\
-
栈帧\
- 栈帧是用于支持JVM进行方法调用和方法执行的数据结构。\
- 在活动线程中,只有位于栈顶的方法才是在运行的(当前方法),只有位于栈顶的栈帧才是生效的(当前栈帧)。\
- 每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。\
-
局部变量表\
- 一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。\
-
以槽作为最小单位,一个槽能放32位,boolean、byte、char、short、int、float、reference或returnAddress类型的数据。\
- reference类型表示对一个对象实例的引用\
- returnAddress类型指向了一条字节码指令的地址\
- 对于64位的数据类型如long,double,Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间。\
-
对于实例方法,第0位slot存放的是this,然后从1到n ,依次分配给参数列表\
- 实例方法
\
- 静态方法static,无this,第0位是参数接着是局部变量啥的。\
- 实例方法
- 然后根据方法体内部定义的变量顺序和作用域来分配slot\
- slot是复用的,以节省栈帧的空间,这种设计可能会影响到系统的垃圾收集行为 ??????\
-
Java八个基本类型\
- boolean 类型在 Java 虚拟机中被映射为整数类型:“true”被映射为 1,而“false”被映射为0。Java 代码中的逻辑运算以及条件跳转,都是用整数相关的字节码来实现的。\
- 除 boolean 类型之外,Java 还有另外 7 个基本类型。它们拥有不同的值域,但默认值在内存中均为 0。这些基本类型之中,浮点类型比较特殊。基于它的运算或比较,需要考虑 +0.0F、-0.0F 以及NaN 的情况。\
-
操作数栈\
- 1∶操作数栈中元素的数据类型必须和字节码指令的顺序严格匹配\
- 2︰虚拟机在实现栈帧的时候可能会做一些优化,让两个栈帧出现部分重叠区域,以存放公用的数据\
-
操作数栈∶用来存放方法运行期间,各个指令操作的数据。\
- 操作数栈中元素的数据类型必须和字节码指令的顺序严格匹配\
- 虚拟机在实现栈帧的时候可能会做一些优化,让两个栈帧出现部分重叠区域,以存放公用的数据\
-
动态连接\
- 动态连接:每个栈帧持有一个指向运行时常量池中该栈帧所属方法的引用,以支持方法调用过程的动态连接\
- 静态解析∶类加载的时候,符号引用就转化成直接引用\
- 动态连接:运行期间转换为直接引用\
-
方法返回地址\
-
当一个方法开始执行后,只有两种方式退出这个方法。\
- 第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者或者主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为“正常调用完成”(Normal Method Invocation Completion)。\
- 外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为“异常调用完成(Abrupt Method Invocation Completion)”。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者提供任何返回值的。\
- 无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。一般来说,方法 正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。\
-
-
方法调用\
- 方法调用不等于方法中的代码被执行,而是指确定调用哪一个方法。\
-
解析\
- 调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。\
-
分派\
-
静态分派\
-
重载\
- 方法名相同、方法参数类型不同\
-
选取重载方法的过程\
- 在不考虑对基本类型自动装拆箱,以及可变长参数的情况下选取重载方法;\
- 如果在第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法;\
- 如果在第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法。\
- 在解析时JVM便知道该调用那个目标方法\
-
-
动态分派\
-
重写\
- 方法名相同、方法参数类型相同、方法返回值相同,类之间有继承关系,便构成方法重写。\
- 在运行时JVM需要根据对应的类类型来具体定位应该调用那个目标方法。\
-
-
单分派和多分派\
- 单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。\
-
- JVM定位目标方法的关键是类名+方法名+方法参数类型+方法返回值类型,于是就出现了两种JVM找目标方法的方式,静态绑定、动态绑定\
-
Java 字节码中与调用相关的指令共有五种\
- invokestatic:用于调用静态方法。\
- invokespecial:用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法。\
- invokevirtual:用于调用非私有实例方法。\
- invokeinterface:用于调用接口方法。\
- invokedynamic:用于调用动态方法。\
-
JVM参数\
-
堆内存相关\
-
显示指定堆内存\
\
- heap size 表示要初始化内存的具体大小。\
- unit 表示要初始化内存的单位。单位为***“ g”*** (GB) 、“ m”(MB)、“ k”(KB)。
\
-
显式新生代内存\
- 默认情况下,新生代内存YG 的最小大小为 1310 MB,最大大小为无限制。\
- 显式指定元空间/永久代的大小\
-
-
垃圾收集相关\
-
选择正确的垃圾收集器\
- 串行垃圾收集器\
- 并行垃圾收集器\
- CMS垃圾收集器\
- G1垃圾收集器\
- 时刻检查JVM的垃圾回收性能\
-
-
-
JVM性能调优\
- 内存方面\
- 线程方面\
\
-
JMM内存模型\
-
JMM中支持主内存和工作内存的八种原子操作\
\
- \
-
JMM的三大特性\
- 原子性\
- 可见性\
- 有序性\
-