注:本文档是我学习周志明老师的《深入理解Java虚拟机》时所写的笔记,其中的主要内容来自于《深入理解Java虚拟机》以及尚硅谷宋红康老师的JVM课程,部分内容来自网络如有侵权请立即联系。文档仅供大家学习交流使用请勿用作商业用途。由于上传时格式有所变化,部分排版有问题请见谅,时间仓促、能力有限不足之处请包涵和积极指出,谢谢。
第一节 JVM 与Java体系结构
1.1JVM在Java中的地位与作用

实现了一次编写到处运行。
可以实现多语言混合编程:每个语言都需要转换成字节码文件,最后转换的字节码文件都能通过Java虚拟机进行运行和处理
1.2 JVM 的整体结构

JVM的架构模型
Java编译器输入的指令流基本上是一种基于栈的指令集架构(不同平台CPU架构不同,所以不能设计为基于寄存器的),另外一种指令集架构则是基于寄存器的指令集架构。
特点:
- 跨平台性
- 指令集小
- 相同一段代码的指令较多
- 执行性能比寄存器差
1.3 JVM的主要版本
HotSpot VM
目前Hotspot占有绝对的市场地位,称霸武林。
- 不管是现在仍在广泛使用的JDK6,还是使用比例较多的JDK8中,默认的虚拟机都是HotSpot
- Sun/oracle JDK和openJDK的默认虚拟机
- 名称中的HotSpot指的就是它的热点代码探测技术。
JRockit
专注于服务器端应用,它可以不太关注程序启动速度,因此JRockit内部不包含解析器实现,全部代码都靠即时编译器编译后执行。大量的行业基准测试显示,JRockit JVM是世界上最快的JVM。IBM的J9
市场定位与HotSpot接近,服务器端、桌面应用、嵌入式等多用途VM广泛用于IBM的各种Java产品。
目前,有影响力的三大商用虚拟机之一,也号称是世界上最快的Java虚拟机。
第二节 运行时数据区

- 线程私有:PC寄存器、栈、本地方法栈。
- 线程间共享:堆、方法区(永久代或元空间)。
2.1PC寄存器
- 是线程私有的,生命周期与线程的生命周期保持一致。
- 它是唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域。
- 存储指向下一条指令的地址,由执行引擎读取下一条指令;或者,如果是在执行native方法,则是空值。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖它来完成。字节码解释器工作时就是通过改变它的值来选取下一条需要执行的字节码指令。
2.2栈(Stack)
1.概述
首先栈是运行时的单位,而堆是存储的单位
- 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
- 堆解决的是数据存储的问题,即数据怎么放,放哪里
- 生命周期和线程一致,也就是线程结束了,该虚拟机栈也销毁了.
- 主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
- 对于栈来说不存在垃圾回收问题(栈存在溢出的情况)。
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
2.栈的大小和异常
Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverFlowError 异常。
如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError 异常。
- 设置栈内存大小
3.栈的存储单位
栈中的数据都是以栈帧(Stack Frame)的格式存在。在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。
4.栈帧的内部结构
- 局部变量表(Local Variables)
- 操作数栈(operand Stack)(或表达式栈)
- 动态链接(DynamicLinking)(或指向运行时常量池的方法引用)
- 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
- 一些附加信息
1.局部变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。局部变量表所需的容量大小是在编译期确定下来的。在方法运行期间是不会改变局部变量表的大小的。2.操作数栈(Operand Stack)
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了。
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)。3.动态链接:Dynamic Linking
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接 (Dynamic Linking)。4.方法返回地址
存放调用该方法的pc寄存器的值。5.一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。
2.3本地方法栈
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
- 允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
- 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。
2.4堆(Heap)
1.概述
《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。
2.堆内存细分
3. 设置堆内存大小与OOM
-Xms10m:最小堆内存
-Xmx10m:最大堆内存
一旦堆区中的内存大小超过“-Xmx”所指定的最大内存时,将会抛出OOM异常。
通常会将-Xms和-Xmx两个参数配置相同的值:因为在垃圾回收中当最小堆内存不足时,并不是直接进行扩容而是先执行GC,只有GC后内存依然不足时才会进行扩容,同时每次GC完成后会JVM会重新分配内存,所以为了避免频繁GC引起的STW和内存分配的开销,我们将最小和最大堆内存设置为相同值来减少GC的发生。
**
4.对象分配过程(内存分配与回收)【重点】
- new的对象先放伊甸园区。
- 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
- 然后将伊甸园中的剩余对象移动到幸存者0区。
- 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
- 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
- 啥时候能去养老区呢?可以设置次数。默认是15次。
- 在养老区,相对悠闲。当养老区内存不足时,再次触发GC:Major GC/Full GC,进行养老区的内存清理
- 若养老区执行了GC之后,发现依然无法进行对象的保存,就会产生OOM异常。
内存分配与回收策略
1. 对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
2. 大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
3. 长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。-XX:MaxTenuringThreshold 用来定义年龄的阈值。
4. 动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold 中要求的年龄。
5. 空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
JDK1.6之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。HandlePromotionFailure参数不会再影响到虚拟机的策略。
5. 垃圾收集(GC)
- 部分收集
- Minor GC:只针对新生代的垃圾收集,Eden区满时才会触发,Suvivor区满时不会触发(直接全部晋升)。
- Major GC:一般指只针对老年代的垃圾收集,只有CMS收集器才会有只收集老年代的行为。
- Mixed GC: 目标是收集全部新生代和部分老年代,只有G1收集器才会有该行为。
- 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集行为。
- 触发Fu11GC执行的情况有如下五种:
- 调用System.gc()时,系统建议执行Fu11GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、From区向To 区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
我们都知道,JVM的调优的一个环节,也就是垃圾收集,我们需要尽量的避免垃圾回收,因为在垃圾回收的过程中,会出现STW(stop the word)的问题:暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
所有垃圾回收器的所有垃圾回收策略都会触发STW,只是发生的阶段不同时间长短不同而已。Major GC 和 Full GC出现STW的时间,是Minor GC的10倍以上。
6.线程私有内存:TLAB
TLAB:Thread Local Allocation Buffer,也就是从内存模型角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,一旦对象在TLAB空间分配失败时,JVM就会尝试着通过使用“CAS锁+失败重试”的机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
image-20200707103547712
7.逃逸分析
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
逃逸分析的基本行为就是分析对象动态作用域:
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方法中。
小结:堆空间的参数设置
-XX:+PrintFlagsInitial //查看所有的参数的默认初始值
-XX:+PrintFlagsFinal //查看所有的参数的最终值(可能会存在修改,不再是初始值)
-Xms //初始堆空间内存(默认为物理内存的1/64)
-Xmx //最大堆空间内存(默认为物理内存的1/4)
-Xmn //设置新生代的大小。(初始值及最大值)
-XX:NewRatio //配置新生代与老年代在堆结构的占比 默认为1:2
-XX:SurvivorRatio //设置新生代中Eden和S0/S1空间的比例,默认8:1:1
-XX:MaxTenuringThreshold//设置新生代垃圾的最大年龄,默认为15
-XX:+PrintGCDetails //输出详细的GC处理日志,在发生垃圾收集行为时打印内存回收日志,并且在进程退出的 时候输出当前的内存各区域分配情况。
-XX:PretenureSizeThreshold //令大于这个设置值的对象直接在老年代分配。目的是避免在Eden区及两个Survivor区直接发生大量的内存复制(新生代采用复制算法收集内存)。该参数只对Serial和ParNew两款收集器有效。
2.5方法区
1.栈、堆、方法区的交互关系
下面就涉及了对象的访问定位
- Person:存放在元空间,也可以说方法区
- person:存放在Java栈的局部变量表中
-
2.方法区的理解
JVM启动时被创建,关闭时释放,它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
- 它用于存储已被虚拟机加载的类型信息、域信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等。
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。
-
3.HotSpot中方法区的演进
JDK6的时候

image-20200708211541300
JDK7的时候
image-20200708211609911
JDK8的时候,元空间大小只受物理内存影响
image-20200708211637952
1.为什么永久代要被元空间替代? 为永久代设置空间大小是很难确定的
- 在某些场景下,如果动态加载类过多,容易产生Perm区的OOM。而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
- 对永久代进行调优是很困难的
- 主要是为了降低Full GC:一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。
- 方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
2.StringTable为什么要调整位置?
因为永久代的回收效率很低,在Full GC的时候才会触发。而Full GC是老年代的空间不足、永久代不足时才会触发。这就导致String Table回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
3.静态变量存放在那里?
静态引用对应的对象实体始终都存在堆空间。
4.设置方法区大小与OOM
jdk7及以前
- 通过-xx:Permsize来设置永久代初始分配空间。默认值是20.75M
- -XX:MaxPermsize来设定永久代最大可分配空间。32位机器默认是64M,64位机器默认是82M
- 当JVM加载的类信息容量超过了这个值,会报异常OutofMemoryError:PermGen space
JDK8以后
元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定
默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即没有限制。
与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace
如何解决这些OOM
- 要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
- 内存泄漏就是 有大量的引用指向某些对象,但是这些对象以后不会使用了,但是因为它们还和GC ROOT有关联,所以导致以后这些对象也不会被回收,这就是内存泄漏的问题
- 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的。
- 如果不存在内存泄漏,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
5.class 文件常量池 VS 运行时常量池
- 常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
- 运行时常量池(Runtime Constant Pool)是方法区的一部分。JVM为每个已加载的类型(类或接口)都维护一个运行时常量池。池中的数据项像数组项一样,是通过索引访问的。运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用,它相比常量池具备动态性,。此时不再是常量池中的符号地址了,这里换为真实地址。
6.方法区的回收
主要是对常量池的回收和对类的卸载。
类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:
- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法
Java中的对象
1.对象的创建过程

Step1:类加载检查
虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。
Step2:分配内存
在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载完成后便可确定。
内存管理方式:1. 碰撞指针 2. 维护空闲列表
多线程下分配内存:1.使用TLAB 2.加锁:CAS+失败重试
Step3:初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。
Step4:设置对象头
初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅式。
Step5:执⾏ init ⽅法
执行构造函数:即Class文件中的() 方法。
2.对象的内存布局

image-20210403195234587
- 对象头(Header)
- Mark Word部分:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向锁ID、偏向锁时间戳等。
- Class Pointer:类型指针,指向实例对象的在方法区中的类型元数据。
- 实例数据(Instance Data):对象的真正有效的数据,在代码里面定义的各种类型的字段内容。
- 对齐填充(Padding):由于对象大小必须是8的倍数,头部的大小满足8的倍数,实例数据区并不一定满足,所以用来补齐对象大小。
3.对象的访问定位
⽬前主流的访问⽅式有①使用句柄和②直接指针两种:
- 句柄: 如果使⽤句柄的话,那么Java堆中将会划分出⼀块内存来作为句柄池,栈中reference
存储的就是对象的句柄地址,⽽句柄中包含了对象实例数据与类型数据各⾃的具体地址信息;
image-20210403193922271
- 直接指针: 如果使⽤直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类
型数据的相关信息,⽽reference 中存储的直接就是对象的地址。
image-20210403193950545
这两种对象访问⽅式各有优势。使⽤句柄来访问的最⼤好处是 reference 中存储的是稳定的句柄
地址,在对象被移动时只会改变句柄中的实例数据指针,⽽ reference 本身不需要修改。使⽤直
接指针访问⽅式最⼤的好处就是速度快,它节省了⼀次指针定位的时间开销。
第三节 垃圾回收
1. 概述
关于垃圾收集有三个经典问题:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
什么是垃圾?
垃圾是指在运行的程序中没有任何指针指向的对象。
如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序的结束,被保留的空间无法被其它对象使用,甚至可能导致内存溢出。
C/C++中的垃圾回收
使用new关键字进行内存申请,并使用delete关键字进行内存释放。如以下代码:
MibBridge pBridge= new cmBaseGroupBridge();
//如果注册失败,使用Delete释放该对象所占内存区域
if(pBridge->Register(kDestroy)!=NO ERROR)
delete pBridge;
*Java中的垃圾回收
- 优点
自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险。
- 缺点
对于Java开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于“自动”,那么这将会是一场灾难,最严重的就会弱化Java开发人员在程序出现内存溢出时定位问题和解决问题的能力。
2. 垃圾收集算法
1.标记阶段
引用计数算法
对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。当引用为零时表明可以回收该对象。但是该算法有一个严重的问题,即无法处理循环引用的情况。这导致在Java的垃圾回收器中没有使用这类算法。
可达性分析算法
可达性分析算法的思路是以GCRoots为起始点,按照从上至下的方式遍历所有对象判断对象是否可达,只有能够被根对象集合直接或者间接连接的对象才是存活对象,被标记为可达对象。Java采用该算法。
GC Roots可以是哪些?
- Java虚拟机栈中,栈帧中局部变量表中引用的对象;
- 方法区中,类的静态变量引用的对象,类中常量引用的对象;
- 本地方法栈中,Native方法中引用的对象;
- 同步锁持有的对象 ;
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。。
生存还是死亡?
一个不可达的对象不会直接标记为垃圾对象,在可能的条件下也许会复活。
如果一个不可达对象重写了finalize(),那么JVM会将它插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。如果在finalize()方法中它与GCRoots引用链上的任何一个对象建立了联系那么他就复活了。如果它之前已经调用过finalize方法(finalize方法只能调一次)或不能复活就会被标记为垃圾。
2.清除阶段
标记一清除算法
- 标记阶段:收集器(Collector)先用可达性分析算法的思想,从GC Roots对象出发,在对象的Header中标记所有可达的对象。
- 清除阶段:收集器对堆内存从头到尾的线性遍历一次,发现没有标记为可达的对象便对其进行回收。清除是指把需要清除的对象地址加入空闲的地址列表里。

image-20200712150935078
缺点:
- 标记清除算法的效率不算高
这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表而不能使用简单的指针碰撞分配内存,可能导致无法找到连续内存空间分配给大对象。
标记——复制算法
将内存空间分为两块,每次只使用其中一块。在垃圾回收阶段将from区中存活的对象复制到to区中去,把from区置空变为下一次的to区,交换两个区的角色。这种算法用在一般新生代,因为新生代中的对象大多朝生夕死,需要移动的对象不是很多,两倍空间的浪费也不是很多。

image-20200712151916991
优点:复制过去以后保证空间的连续性,不会出现“碎片”问题,也提高了运行效率。
缺点:需要两倍的内存空间。移动对象的同时需要调整对象的引用地址。
标记—压缩算法
第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
image-20200712153236508
优点:解决了清除算法内存碎片问题,可以使用更简单的指针碰撞来分配内存;也克服了复制算法的两倍内存的内存消耗问题。
缺点:效率上低于复制算法;移动对象的同时,需要调整引用的地址
分代收集算法
分代收集的思想是,根据对象存活的周期特性将内存划分为几块,不同的区域采用不同的收集算法。
新生代使用:复制算法
老年代使用:标记 - 清除 或者 标记 - 压缩 算法
增量收集算法
总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。由于传统垃圾收集算法在垃圾收集的过程中要STW如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
缺点:由于线程的切换使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
分区算法
为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。这种算法的好处是可以控制一次回收多少个小区间。缺点是多个分区让内存的管理又变得复杂。
3.垃圾回收相关概念
内存泄漏
对象不会再被程序用到了,但是GC又不能回收他们的情况,叫内存泄漏。
安全点与安全区域
安全点
程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点(Safepoint)”。
安全区域
解决线程处于阻塞状态时无法到达安全点的问题,安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始Gc都是安全的。
4.再谈引用
- 强引用(StrongReference):
- 最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
- 软引用(SoftReference):
- 在内存不够时,就会回收软引用的可达对象如果这次回收后还没有足够的内存,才会抛出内存溢出异常。软引用通常用来实现内存敏感的非必需的缓存对象。
- 弱引用(WeakReference):
- 被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。用来引用非必需的对象。
- 虚引用(PhantomReference):
- 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
5.垃圾回收器
1.评估GC的性能指标
- 吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)
- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
- 收集频率:相对于应用程序的执行,收集操作发生的频率。
- 内存占用:Java堆区所占的内存大小。
- 快速:一个对象从诞生到被回收所经历的时间。
主要抓住两点:
- 吞吐量
- 暂停时间
2. 七种经典的垃圾收集器
- 串行回收器:Serial、Serial old
- 并行回收器:ParNew、Parallel Scavenge、Parallel old
- 并发回收器:CMS、G1
关系
image-20200713094745366
- 其中Serial o1d作为CMs出现”Concurrent Mode Failure”失败的后备预案。
- (红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial old这两个组合声明为废弃(JEP173),并在JDK9中完全取消了这些组合的支持,即:移除。
- (绿色虚线)JDK14中:弃用Paralle1 Scavenge和Serialold GC组合
- (青色虚线)JDK14中:删除CMS垃圾回收器
Serial/SerialOld收集器:串行回收(低内存开销)
Serial(串行)是最基本也是最古老的垃圾收集器,Serial新生代使用复制算法,SerialOld老年代使用标记整理算法。缺点是限定于单核CPU,GC是单线程执行的。是独占式的回收器,执行过程中暂停所有用户线程。用于客户端。
ParNew收集器:并行回收
Par是Parallel的缩写,New:只能处理的是新生代,ParNew是Serial收集器的多线程版本,除了GC过程是多线程执行的之外其余的与Serial一样。ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。【Parallel:平行的】ParNew是虚拟机运行在 Server 模式下新生代的默认垃圾收集器。
在并发能力比较强的CPU上,它产生的停顿时间要短于串行回收器。在单CPU或者并发能力较弱的系统中,由于线程切换的资源消耗, 并行回收器的效果不会比串行回收器好。
Parallel Scavenge / Parallel Old 收集器:吞吐量优先
Parallel Scavenge也是多线程的垃圾收集器,整体与ParNew非常类似但是他重点关注的是程序达到一个可控制的吞吐量,高吞吐量可以最高效率地利用 CPU 时间来执行用户代码。
CMS回收集器:低延迟
用于老年代的CMS收集器(Concurrent-Mark-Sweep:并发收集)的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。适合与用户交互的程序使用。
整个回收过程分为4个主要阶段
- 初始标记阶段:在这个阶段中会发生短暂的STW,这个阶段的主要任务仅仅只是标记出GCRoots能直接关联到的对象。
- 并发标记阶段:主要任务是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
- 重新标记阶段:发生短暂的STW,这个阶段的任务是修正并发标记期间因用户线程继续运作而导致标记产生变动的那一部分对象的标记。
- 并发清除阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象(标记清除算法),所以这个阶段也是可以与用户线程同时并发的
Garbage First收集器:区域化分代式
G1收集器是全功能收集器,能够实现在延迟可控的情况下获得尽可能高的吞吐量,G1的特点是:并行清理+并发标记+分区分阶段;
G1的特点
G1收集器与之前的收集器最大的不同就在于堆内存的划分,之前的收集器只区分新生代与老年代,而G1收集器则是把堆内存划分成多个独立的Region【2048个,最小为1MB,最大为32MB,中间值都是2的n次幂】。每个Region有可能是eden、survivor、old,但是他们的身份仅仅是逻辑上的,是可以变化的,G1可以根据情况动态的调整各种Region的数量,还可以通过控制回收的Region数量来控制STW的时间,以达到STW时间的可控制。回收时计算出每个Region回收所获得的空间以及所需时间的经验值,根据记录这两个值来判断哪个区域最具有回收价值,所以叫Garbage First(垃圾优先)。G1在两个Region之间的复制可以看作是复制算法但是在整体上可以看做是标记——整理算法。
G1的一些概念知识:
CSet(Collection Set):GC过程记录的可被回收的Region的集合。在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自eden空间、survivor空间、或者老年代。
RSet(Remembered Set ):记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构 (谁引用了我的对象)。作用是不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet即可。
Humongous regions:用来存放大于标准的Region内存50%的大对象区域,如果有些对象大于整个Region就会去找连续的Region保存,如果没有就会触发GC。
G1收集过程
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
- 初始标记:标记出从GC Roots开始直接可达的对象,并且修改TAMS指针的值,让下一阶段用户程序并发运行时,能在正确可以用的Region中创建新对象。
- 并发标记:GC Roots开始对堆中对象进行可达性分析,找出存活对象。
- 最终标记:修正并发标记期间因用户线程继续运作而导致标记产生变动的那一部分对象的标记。
- 筛选回收:首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region。
G1中提供了两种模式垃圾回收模式,Young GC和Mixed GC。
3.垃圾回收器总结

第四节 类加载机制
1.类加载子系统
类加载器子系统负责从文件系统或者网络中加载Class文件,将类信息加载到方法区。
注意:类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。

image-20200705081813409
2.类的生命周期

3.类的加载过程
1. 加载 loading
- 通过一个类的全限定名获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的对象,作为方法区这个类的各种数据的访问入口。
2. 验证 Verify
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。如果出现不合法的字节码文件,那么将会验证不通过。3. 准备 Prepare
为类变量分配内存并且设置该类变量的默认初始值,即零值。
public class Hello {
private static int a = 1; // 准备阶段为0,在下个阶段,也就是初始化的时候才是1;这里不包含用final 修饰的static,因为final在编译的时候就会分配了
public static void main(String[] args) {
System.out.println(a);
}
}4.解析 Resolve
将常量池内的符号引用转换为直接引用的过程。
符号引用就是一组符号来描述所引用的目标,直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。解析操作往往会在初始化阶段之后再执行,这是为了支持Java的动态绑定。5. 初始化阶段
初始化阶段就是执行类构造器法()的过程。
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。也就是说,当我们代码中包含static变量的时候,就会有clinit方法,默认为空方法,构造器方法中指令按语句在源文件中出现的顺序执行。父类的 () 方法先执行。
4.类加载时机
- 创建类的实例,也就是new一个对象
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射
- 初始化一个类的子类(会首先初始化子类的父类)
- JVM启动时标明的启动类,即文件名和类名相同的那个类
5. 类加载器的分类
从JVM的角度来分有两种加载器:
引导类加载器(Bootstrap ClassLoader):由C++实现,是虚拟机的一部分,开发者无法直接获取到启动类加载器的引用(返回为null)。
自定义类加载器(User-Defined ClassLoader):自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
从开发者的角度来看有多种类加载器:
6.双亲委派机制
1.工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

image-20200705105151258
2. 沙箱安全机制
沙箱安全机制是由基于双亲委派机制上 采取的一种JVM的自我保护机制,假设你要写一个java.lang.String 的类,由于双亲委派机制的原理,此请求会先交给Bootstrap试图进行加载,但是Bootstrap在加载类时首先通过包和类名查找rt.jar中有没有该类,有则优先加载rt.jar包中的类,因此就保证了java的运行机制不会被破坏.

