可以先看看我的深入理解java虚拟机笔记
深入理解java虚拟机笔记Chapter2
深入理解java虚拟机笔记Chapter3-垃圾收集器
深入理解java虚拟机笔记Chapter3-内存分配策略
深入理解java虚拟机笔记Chapter4
深入理解java虚拟机笔记补充-JVM常见参数设置
深入理解java虚拟机笔记Chapter7
深入理解java虚拟机笔记Chapter8
深入理解java虚拟机笔记Chapter11
深入理解java虚拟机笔记Chapter12
JVM中的内存是怎么划分的?(详见Chapter2)
JVM中的内存主要划分为5个区域,即方法区,堆内存,程序计数器,虚拟机栈以及本地方法栈
- 方法区:方法区是一个线程之间共享的区域。常量,静态变量以及JIT编译后的代码都在方法区。主要用于存储已被虚拟机加载的类信息,也可以称为“永久代”,垃圾回收效果一般,通过-XX:MaxPermSize控制上限。
- 堆内存:堆内存是垃圾回收的主要场所,也是线程之间共享的区域,主要用来存储创建的对象实例,通过-Xmx 和-Xms 可以控制大小。
- 虚拟机栈(栈内存):栈内存中主要保存局部变量、基本数据类型变量以及堆内存中某个对象的引用变量。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。栈中的栈帧随着方法的进入和退出有条不紊的执行着出栈和入栈的操作。
- 程序计数器: 程序计数器是当前线程执行的字节码的位置指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,是内存区域中唯一一个在虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
- 本地方法栈: 主要是为JVM提供使用native 方法的服务。
对象创建过程中的内存分配(详见Chapter12)
一般情况下我们通过new指令来创建对象,当虚拟机遇到一条new指令的时候,会去检查这个指令的参数是否能在常量池中定位到某个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。如果没有,那么会执行类加载过程。
通过执行类的加载,验证,准备,解析,初始化步骤,完成了类的加载,这个时候会为该对象进行内存分配,也就是把一块确定大小的内存从Java堆中划分出来,在分配的内存上完成对象的创建工作。
对象的内存分配有两种方式,即指针碰撞和空闲列表方式。
指针碰撞方式:
假设Java堆中的内存是绝对规整的,用过的内存在一边,未使用的内存在另一边,中间有一个指示指针,那么所有的内存分配就是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
空闲列表方式:
如果Java堆内存中不是规整的,已使用和未使用的内存相互交错,那么虚拟机就必须维护一个列表用来记录哪块内存是可用的,在分配的时候找到一块足够大的空间分配对象实例,并且需要更新列表上的记录。
需要注意的是,Java 堆内存是否规整是由所使用的垃圾收集器是否拥有压缩整理功能来决定的,关于垃圾收集器我们在下一小节重点介绍。
那么内存的分配如何保证线程安全呢?
- 对分配内存空间的动作进行同步处理,通过“CAS + 失败重试”的方式保证更新指针操作的原子性
- 把分配内存的动作按照线程划分在不同的空间之中,即给每一个线程都预先分配一小段的内存,称为本地线程分配缓存(TLAB),只有TLAB用完并分配新的TLAB时,才需要进行同步锁定。 虚拟机是否使用TLAB,可以通过
-XX: +/-UserTLAB
参数来设定
对象被访问的时候是怎么被找到的(详见Chapter2)
当创建一个对象的时候,在栈内存中会有一个引用变量,指向堆内存中的某个具体的对象实例。
Java虚拟机规范中并没有规定这个引用变量应该以何种方式去定位和访问堆内存中的具体对象。目前常见的对象访问方式有两种,即句柄访问方式和直接指针访问方式,分别介绍如下。
句柄访问方式:
在JVM的堆内存中划分出一块内存来作为句柄池,引用变量中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。在内存垃圾收集之后,对象会移动,但是引用reference中存储的是稳定的句柄地址,但是句柄地址方式不直接,访问速度较慢。
直接指针访问方式:
引用变量中存储的就是对象的直接地址,通过指针直接访问对象。直接指针的访问方式节省了一次指针定位的时间开销,速度较快。Sun HotSpot使用了直接指针方式进行对象的访问。
内存分配与垃圾回收(详见Chapter3)
JVM的内存可以分为堆内存和非堆内存。堆内存分为年轻代和老年代。年轻代又可以进一步划分为一个Eden(伊甸)区和两个Survivor(幸存)区组成。
JVM堆内存的分配:
JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64。JVM最大分配的堆内存由-Xmx指定,默认是物理内存的1/4。
默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。因此我们一般设置-Xms和-Xmx相等以避免在每次GC 后调整堆的大小。
通过参数-Xmn2G 可以设置年轻代大小为2G。通过-XX:SurvivorRatio可以设置年轻代中Eden区与Survivor区的比值,设置为8,则表示年轻代中Eden区与一块Survivor的比例为8:1。注意年轻代中有两块Survivor区域。
JVM非堆内存的分配:
JVM使用-XX:PermSize 设置非堆内存初始值,默认是物理内存的1/64。由-XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。
堆内存上对象的分配与回收:
我们创建的对象会优先在Eden分配,如果是大对象(很长的字符串数组)则可以直接进入老年代。虚拟机提供一个
-XX:PretenureSizeThreshold参数,令大于这个参数值的对象直接在老年代中分配,避免在Eden区和两个Survivor区发生大量的内存拷贝。
另外,长期存活的对象将进入老年代,每一次MinorGC(年轻代GC),对象年龄就大一岁,默认15岁晋升到老年代,通过
-XX:MaxTenuringThreshold设置晋升年龄。
堆内存上的对象回收也叫做垃圾回收,那么垃圾回收什么时候开始呢?
垃圾回收主要是完成清理对象,整理内存的工作。上面说到GC经常发生的区域是堆区,堆区还可以细分为新生代、老年代。新生代还分为一个Eden区和两个Survivor区。垃圾回收分为年轻代区域发生的Minor GC和老年代区域发生的Full GC,分别介绍如下。
Minor GC(年轻代GC):
对象优先在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,因为Java大多数对象都是朝生夕灭,所以Minor GC非常频繁,而且速度也很快。
Full GC(老年代GC):
Full GC是指发生在老年代的GC,当老年代没有足够的空间时即发生Full GC,发生Full GC一般都会有一次Minor GC。
接下来,我们来看关于内存分配与回收的两个重要概念吧。
动态对象年龄判定:
如果Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,那么年龄大于等于该对象年龄的对象即可晋升到老年代,不必要等到-XX:MaxTenuringThreshold。
空间分配担保:
发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小。如果大于,则进行一次Full GC(老年代GC),如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,那只会进行一次Minor GC,如果不允许,则改为进行一次Full GC。
JVM如何判定一个对象是否应该被回收?(详见Chapter3)
引用计数法:
是一种比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只需要收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
root根搜索方法:
root搜索方法的基本思路就是通过一系列可以做为root的对象作为起始点,从这些节点开始向下搜索。当一个对象到root节点没有任何引用链接时,则证明此对象是可以被回收的。
以下对象会被认为是root对象(重点):
- 栈内存中引用的对象
- 方法区中静态引用和常量引用指向的对象
- 被启动类(bootstrap加载器)加载的类和创建的对象
- Native方法中JNI引用的对象。
对象的引用
如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表一个引用。JDK1.2以后将引用分为强引用,软引用,弱引用和虚引用四种。
- 强引用:普通存在, P p = new P(),只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用:通过SoftReference类来实现软引用,在内存不足的时候会将这些软引用回收掉。
- 弱引用:通过WeakReference类来实现弱引用,每次垃圾回收的时候肯定会回收掉弱引用。
- 虚引用:也称为幽灵引用或者幻影引用,通过PhantomReference类实现。设置虚引用只是为了对象被回收时候收到一个系统通知。
JVM垃圾回收算法有哪些?(详见Chapter3)
基础:标记 - 清除算法
- 算法描述:
- 先标记出所有需要回收的对象(图中深色区域);
- 标记完后,统一回收所有被标记对象(留下狗啃似的可用内存区域……)。
- 不足:
- 效率问题:标记和清理两个过程的效率都不高。
- 空间碎片问题:标记清除后会产生大量不连续的内存碎片,导致以后为较大的对象分配内存时找不到足够的连续内存,会提前触发另一次 GC。
解决效率问题:复制算法
- 算法描述:
- 将可用内存分为大小相等的两块,每次只使用其中一块;
- 当一块内存用完时,将这块内存上还存活的对象复制到另一块内存上去,将这一块内存全部清理掉。
- 不足: 可用内存缩小为原来的一半,适合GC过后只有少量对象存活的新生代。
- 节省内存的方法:
- 新生代中的对象 98% 都是朝生夕死的,所以不需要按照 1:1 的比例对内存进行划分;
- 把内存划分为:
- 1 块比较大的 Eden 区;
- 2 块较小的 Survivor 区;
- 每次使用 Eden 区和 1 块 Survivor 区;
- 回收时,将以上 2 部分区域中的存活对象复制到另一块 Survivor 区中,然后将以上两部分区域清空;
- JVM 参数设置:
-XX:SurvivorRatio=8
表示Eden 区大小 / 1 块 Survivor 区大小 = 8
。
解决空间碎片问题:标记 - 整理算法
- 算法描述:
- 标记方法与 “标记 - 清除算法” 一样;
- 标记完后,将所有存活对象向一端移动,然后直接清理掉边界以外的内存。
- 不足: 存在效率问题,适合老年代。
进化:分代收集算法
- 新生代: GC 过后只有少量对象存活 —— 复制算法
- 老年代: GC 过后对象存活率高 —— 标记 - 整理算法
垃圾收集器(详见Chapter3,重点是G1和CMS)
7 个垃圾收集器
垃圾收集器就是内存回收操作的具体实现,HotSpot 里足足有 7 种,为啥要弄这么多,因为它们各有各的适用场景。有的属于新生代收集器,有的属于老年代收集器,所以一般是搭配使用的(除了万能的 G1)。关于它们的简单介绍以及分类请见下图。
Serial 收集器
- 单线程收集器
- 在进行垃圾收集时,必需暂停其他所有的工作线程(打扫卫生时,必需要求房间里停止工作产生垃圾)
- 简单而高效,专心做垃圾收集
- 虚拟机运行在Client模式下的默认新生代收集器
ParNew 收集器
ParNew收集器其实就是Serial收集器的多线程版本,是运行在Server模式下的虚拟机中首选的新生代收集器。
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替运行),用户程序继续运行,而垃圾收集线程运行在另一个CPU上。
Parallel Scavenge
Parallel Scavenge收集器是一个使用复制算法的并行的多线程收集器,它关注于提高吞吐量(Throughput,CPU用于运行用户代码的时间和CPU消耗时间的比值)。另外,自适应调节策略也是Parallel Scavenge和ParNew收集器的区别.
Serial Old 收集器
是Serial收集器的老年版本。
Parallel Old 收集器
是Parallel Scavenge 收集器的老年版本。
介绍下上述垃圾收集器搭配使用的方法
Serial / ParNew 搭配 Serial Old 收集器
Serial 收集器是虚拟机在 Client 模式下的默认新生代收集器,它的优势是简单高效,在单 CPU 模式下很牛。
ParNew 收集器就是 Serial 收集器的多线程版本,虽然除此之外没什么创新之处,但它却是许多运行在 Server 模式下的虚拟机中的首选新生代收集器,因为除了 Serial 收集器外,只有它能和 CMS 收集器搭配使用。
Parallel 搭配 Parallel Scavenge 收集器
首先,这两个收集器肯定是要搭配使用的,不仅仅如此,它们的关注点与其他收集器不同,其他收集器关注于尽可能缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目的是达到一个可控的吞吐量。
吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )
因此,Parallel Scavenge 收集器不管是新生代还是老年代都是多个线程同时进行垃圾收集,十分适合于应用在注重吞吐量以及 CPU 资源敏感的场合。
可调节的虚拟机参数:
-
-XX:MaxGCPauseMillis
:最大 GC 停顿的秒数; -
-XX:GCTimeRatio
:吞吐量大小,一个 0 ~ 100 的数,最大 GC 时间占总时间的比率 = 1 / (GCTimeRatio + 1)
; -
-XX:+UseAdaptiveSizePolicy
:一个开关参数,打开后就无需手工指定-Xmn
,-XX:SurvivorRatio
等参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,自行调整。
CMS 收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于标记-清除算法实现的,是一种老年代收集器,通常与ParNew一起使用。
CMS的垃圾收集过程分为4步:
- 初始标记:需要“Stop the World”,初始标记仅仅只是标记一下GC Root能直接关联到的对象,速度很快。
- 并发标记:是主要标记过程,这个标记过程是和用户线程并发执行的。
- 重新标记:需要“Stop the World”,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(停顿时间比初始标记长,但比并发标记短得多)。
- 并发清除:和用户线程并发执行的,基于标记结果来清理对象。
优点是:并发收集、低停顿
缺点是:对CPU资源非常敏感、无法处理浮动垃圾、收集结束后会产生大量的空间碎片以致于在给大对象分配空间时带来麻烦
更完善的缺点解释:
- 对CPU资源非常敏感,因为并发标记和并发清理阶段和用户线程一起运行,当CPU数变小时,性能容易出现问题。
- 收集过程中会产生浮动垃圾,所以不可以在老年代内存不够用了才进行垃圾回收,必须提前进行垃圾收集。通过参数
-XX:CMSInitiatingOccupancyFraction
的值来控制内存使用百分比。如果该值设置的太高,那么在CMS运行期间预留的内存可能无法满足程序所需,会出现Concurrent Mode Failure失败,之后会临时使用Serial Old收集器做为老年代收集器,会产生更长时间的停顿。 - 标记-清除方式会产生内存碎片,可以使用参数
-XX:UseCMSCompactAtFullCollection
来控制是否开启内存整理(无法并发,默认是开启的)。参数-XX:CMSFullGCsBeforeCompaction
用于设置执行多少次不压缩的Full GC后进行一次带压缩的内存碎片整理(默认值是0)。
浮动垃圾:
由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。
- 参数设置:
-
-XX:+UseCMSCompactAtFullCollection
:在 CMS 要进行 Full GC 时进行内存碎片整理(默认开启) -
-XX:CMSFullGCsBeforeCompaction
:在多少次 Full GC 后进行一次空间整理(默认是 0,即每一次 Full GC 后都进行一次空间整理)
-
G1收集器
G1(Garbage First)收集器是当前收集器技术发展的最新成果,相对于上文的 CMS 收集器有两个显著改进:
- 基于“标记-整理”算法,也就是说它不会产生空间碎片
- 非常精确地控制停顿
G1的特点:
- 并行与并发:G1能充分利用多CPU,多核环境下的硬件优势,来缩短Stop the World,是并发的收集器。
- 分代收集:G1不需要其他收集器就能独立管理整个GC堆,能够采用不同的方式去处理新建对象、存活一段时间的对象和熬过多次GC的对象。
- 空间整合:G1从整体来看是基于标记-整理算法,从局部(两个Region)上看基于复制算法实现,G1运作期间不会产生内存空间碎片。
- 可预测的停顿:能够建立可以预测的停顿时间模型,预测停顿时间。
G1收集器的垃圾回收工作也分为了四个阶段:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
其中,筛选回收阶段首先对各个Region的回收价值和成本进行计算,根据用户期望的GC停顿时间来制定回收计划。
G1 收集器可以实现在基本不牺牲吞吐量的情况下完成低停顿的回收,它将整个Java堆划分为多个大小固定的独立区域(Region),并跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这也是Garbage First名称的由来)。总而言之,区域划分和有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。
后续更新
- 类加载机制(详见Chapter7)
- 双亲委派(详见Chapter7)
- JVM调优