运行时数据区
- 程序计数器(线程私有)
- 虚拟机栈(线程私有)
- 本地方法栈(线程私有)
- 堆(线程共享)
- 方法区(线程共享)
方法区存储内容
类的所有字段和方法字节码,以及一些特殊方法如构造方法,接口代码也在此定义。也就是静态变量+常量+类信息(构造方法/接口定义)+运行时常量池都存在该方法区中。虚拟机栈的内容
描述Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息本地方法栈的内容:
使用的Native方法服务,在虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它,通常是调用本地的c++函数。永久代和元空间
- 永久代:永久代在jdk1.7之后就被元空间给取代了,永久代逻辑结构上属于堆,但是物理上不属于堆,会出现OOM异常。
- 元空间:元数据区取代了永久代,本质和永久代类似逻辑结构上属于堆,区别在于元数据区并不在虚拟机中,而是使用本地物理内存,永久代在虚拟机中,元数据区也有可能发生OutOfMemory异常。
这样就可以不用在单独为方法区去做一个内存管理了,而且本地物理内存不受JVM的内存大小限制,受操作系统限制。
GC Root的对象
- 堆内存溢出,堆上对象分配空间不足,有OutOfMemoryError。
- 栈内存溢出,有StackOverflow和OutOfMemoryError两类。
- 常量内存溢出
-
引用
在java中主要有以下四种引用类型:强引用,软引用,弱引用,虚引用.不同的引用类型主要体现在GC上:
强引用如果一个对象具有强引用,它就不会被垃圾回收器回收.即使当前内存空间不足,JVM也不会回收它.而是抛出 OutOfMemoryError 错误.使程序异常终止.如果想中断强引用和某个对象之间的关联.可以显式地将引用赋值为null,这样一来的话.JVM在合适的时间就会回收该对象。
- 软引用在使用软引用时,如果内存的空间足够,软引用就能继续被使用而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。
- 弱引用具有弱引用的对象拥有的生命周期更短暂,因为当 JVM 进行垃圾回收时,如果这个对象只被弱引用关联(没有任何强引用关联他),那么这个对象就会被回收,不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象。弱引用的作用是回收不再使用的键值对,不用弱引用只要桶活着,就不会被回收。
- 虚引用如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。唯一的作用是能在这个对象被收集器回收时收到一个系统通知,以此判断垃圾回收器的频率。
堆内存分配策略
- 对象优先分配在Eden区,如果Eden区没有足够的空间进行分配时,虚拟机执行一次MinorGC。
- 大对象直接进入老年代(需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄(Age Count)计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,直到达到阀值(默认15次),对象进入老年区。
- 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
- 空间分配担保。在Minor GC前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果成立就执行Minor GC;否则检查HandlePromotionFailure设置值是否允许担保失败,如果不允许,直接执行Full GC;否则检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于的话尝试Minor GC;否则改为Full GC。
FULL GC触发条件
- 新生代空间不足
- 永久代空间满了
- CMS GC时出现promotion failed和concurrent mode failure
-
判断对象是否存活
会有两次检查。第一次扫描并标记不可达对象,并在其中进行筛选没有事先finalize()或已经调用过finalize()的对象,让其死亡。第二次检查,会判断该对象是否有必要执行finalize()方法,稍后会由优先级较低的Finalizer线程执行,如果在执行前被重新引用,可以避免死亡。
三色标记算法
它是描述追踪式回收器的一种有效的方法,利用它可以推演回收器的正确性。
我们将对象分成三种类型: 黑色:根对象,或者该对象与它的子对象都被扫描过(对象被标记了,且它的所有field也被标记完了)。
- 灰色:对象本身被扫描,但还没扫描完该对象中的子对象(它的field还没有被标记或标记完)。
- 白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,既垃圾对象(对象没有被标记到)。
垃圾回收算法
- 标记清除算法缺点:标记和清除的两个动作效率都不高;清除后会产生大量不连续的空间碎片。
- 复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,一块用完了,就将存活着的对象复制到另外一块上面。优点:没有碎片问题。缺点:成本太高,内存小了一半。
- 标记整理算法根据老年代的特点,提出了此算法。标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,最后直接清理掉端边界以外的内存。
分代收集算法新生代使用复制算法;老年代使用标记清除/标记整理算法。
垃圾回收一般发生在哪些区域?
由于线程私有的几个区域会随着线程创建而创建,线程销毁而销毁,所以不用关注垃圾回收,主要是关注线程共享的部分,也就是堆和方法区。
垃圾回收器
Serial收集器
新生代收集器,它是最基本、发展历史最悠久的收集器。是一个单线程收集器,在垃圾收集时,必须暂停其他所有的工作线程,直到收集结束。适合运行在Client模式下的虚拟机。
Serial Old收集器
Serial收集器的老年代版本,同样也是单线程收集器,使用“标记-整理”算法。
ParNew收集器
新生代收集器,是Serial收集器的多线程版本,除了Serial收集器外,只有它能与CMS收集器配合工作。
Parallel Scavenge收集器
新生代收集器,也是使用复制算法的收集器,又是并行的多线程收集器。它更关注于达到一个可控制的吞吐量。其中,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。除此之外,它还有自适应调节的特性,虚拟机根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这被称为GC自适应的调节策略。它是与ParNew收集器的一个重要区别。
Parallel Old收集器
它是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
CMS收集器
老年代收集器,CMS收集器是一种以获取最短回收停顿时间为目标的收集器,使用“标记-清除”算法。回收过程如下:优点:并发收集,低停顿。缺点:吞吐量问题,CMS对CPU资源敏感,会因为占用一部分线程,导致应用程序变慢,使吞吐量降低;浮动垃圾问题,并发清除阶段产生的垃圾叫做浮动垃圾;碎片问题。
初始标记:需要Stop The World,标记能直接关联到的对象。
- 并发标记:可以与用户一起工作,进行GC Roots Tracing的过程。
- 重新标记:需要Stop The World,为了修正并发标记期间因用户程序继续运作而导致标记产生的那一部分对象的标记记录,停顿时间比初始标记稍长一些,远短于并发标记时间。
-
G1收集器
G1收集器在1.7时被认为达到足够的商用程度,是收集器技术发展的最前沿成果之一。它是一款面向服务端应用的垃圾收集器。
特点: 并行与并发可以使用多个CPU来缩短Stop-The-World时间。
- 分代收集可以管理整个GC堆(G1将内存划分为多个大小相等的区域,并维护一个优先列表,优先回收回收价值最大的Region),对不同年龄对象采用不同策略。
- 空间整合整体上采用“标记整理”,局部采用“复制”算法实现,所以不会产生碎片。
- 可预测的停顿
收集过程:
- 初始标记:需要停顿,标记GC Roots能直接关联到的对象。
- 并发标记:做可达性分析,找出存活的对象。
- 最终标记:需要停顿,但可以并行执行,为了修正在并发标记期间内用户程序继续运作而导致标记产生变动的那一部分标记记录。
- 筛选回收:需要停顿,对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
相较于CMS:
- 优点:
1、可以指定最大停顿时间;
2、分Region的内存布局;
3、按收益动态确定回收集;
4、整体标记-整理,局部标记-复制的特性使之没有内存空间碎片;
- 缺点:
1、内存占用高,每个Region都要维护一份卡表;
2、执行负载高,G1需要用异步队列处理写前屏障与写后屏障,写屏障是指在发生引用关系变更时, 对卡表进行更新。
一些提问
分成许多个Region后,如何解决跨Region引用造成的问题?
双向卡表来维护,使得内存空间消耗额外 10% 至 20% 的堆内存。
并发标记阶段如何保证收集线程与用户线程互不干扰地运行?
CMS 采用增量更新算法实现,G1 采用原始快照算法实现。
G1 为每个 Region 设计了两个名为 TAMS 的指针,把 Region 中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址必须要在这两个指针位置以上。
怎样建立可预测的停顿模型?
G1 收集器的停顿预测模型是以衰减均值为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。
Shenandoah收集器
OracleJDK 不存在但 OpenJDK 中存在的一款收集器,与 G1 有着高度的相似性。主要区别如下:
- 支持并发的整理算法;
- 默认不使用分代收集,即不存在新生代Region或者老年代Region的存在;
- 放弃维护记忆集,而去维护一个连接矩阵来记录跨Region的引用关系。
工作过程:
- 初始标记:标记与 GC Roots 直接关联的对象。
- 并发标记:遍历对象图,标记出全部可达的对象,与用户线程并发。
- 最终标记:处理剩余的 SATB 扫描,并统计出回收价值最高的 Region,将这些 Region 构成一组回收集。
- 并发清理:清理整个区域都没有存活对象的 Region,与用户线程并发。
- 并发回收:把回收集中的存活对象复制一份到其他未被使用的 Region,与用户线程并发。
- 初始引用更新:把堆中所有指向旧对象的引用修正到复制后的新地址。并未执行具体处理,只是确保所有回首阶段中进行的收集器线程都已完成分配给它们的对象移动任务。
- 并发引用更新:真正开始进行引用更新操作,与用户线程并发。与并发标记不同,不需要沿着对象图搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值。
- 最终引用更新:解决堆中引用更新后,还要修正存在于 GC Roots 中的引用。
- 并发清理:经过并发回收和引用更新后,回收集中的 Region 已经没有存活对象了,再调用一次并发清理过程来回收这些 Region 的内存空间,供以后的新对象分配使用,与用户线程并发。
转发指针
并发回收时,可能用户线程会对被移动对象不断地做读写访问,所以会有一个转发指针的概念,即在对象头前设置一个新的引用字段,在不处于移动情况下,引用指向对象自己。当有了新副本时,只需要修改旧对象上面的引用地址,这样就可以统一转发到对新地址的访问。
然而,转发指针也会面临写问题所造成的线程安全问题,即修改旧值的操作未适用到新副本上,实际上,Shenandoah 收集器是通过 CAS 操作来保证并发时对象的访问正确性的。ZGC收集器
ZGC 收集器是一款给予 Region 内存布局的,不设分代的,使用了读屏障、染色指针和内存多重映射等技术实现可并发的标记 - 整理算法的,以低延迟为首要目标的一款垃圾收集器。除了与 GC Roots 相关的几乎全程可并发。可将任何堆停顿都降低到 10ms 内。Region
ZGC 的 Region 分为了三类:
小型 Region:容量固定为 2MB,用于放置小于 256KB 的小对象。
中型 Region:容量固定为 32MB,用于放置大于等于 256KB 但小于 4MB 的对象。
大型 Region:容量不固定,可以动态变化,但必须是 2MB 的整数倍,用于放置 4MB 或以上的大对象。每个大型 Region 中只会放置一个大对象。染色指针
染色指针是一种直接将少量额外的信息存储在指针上的技术。
优势:
- 染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。
- 大幅减少在垃圾收集过程中内存屏障的使用数量。
- 染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。
虚拟内存映射技术
染色指针有个前置条件,即操作系统需要支持重定义指针的几位。在 Linux/x86-64 平台上的 ZGC 使用了多重映射将多个不同的虚拟内存地址映射到同一个物理内存地址上,这是一种多对一映射。
把染色指针的标志位看作是地址的分段符,那只要将这些不同的地址段都映射到同一个物理内存空间,经过多重映射转换后,就可以使用染色指针正常寻址了。工作过程
- 并发标记:做可达性分析,前后也要经过类似 G1 的初始标记、最终标记的短暂停顿,唯一不同的是标记阶段不是在对象上进行的,而是更新染色指针的 Marked 0、Marked 1 标志位。
- 并发预备重分配:根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集。与 G1 不同的是,它扫描的是全堆,而非以收益优先的增量回收,以此减少记忆集的维护成本。
- 并发重分配:把重分配集中的存货对象复制到新的 Region 上,并为重分配集中的每个 Region 维护一个转发表,记录从旧对象到新对象的转向关系。当用户线程并发访问了位于重分配集中的对象,这次访问会被预置的内存屏障捕获,然后立即根据 Region 上的转发表记录讲访问转发到新复制对象上,同时修正更新该引用的值,使其直接指向新对象,这被称为指针自愈。好处是只有第一次访问旧对象会陷入转发,也就只是慢一次。
并发重映射:修正整个堆中指向重分配集中旧对象的所有引用。由于指针自愈的特性,所以这一阶段并不急,可以合并到下次的并发标记阶段去完成。一旦完成,原来的转发表就可以被释放了。
劣势
Epsilon收集器
不回收垃圾,而是只负责堆的管理与布局、对象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等,适合只运行数分钟甚至数秒的服务。
对象的内存布局
对象头
对象头包括markword以及类型指针。
类型指针:
对象指向它的类元数据的指针。
markword:
主要有对象分代年龄、指向锁记录的指针、偏向线程的id以及hashCode等。实例数据
对齐填充
同类型指针一样,并不是必然存在的,仅仅起着占位符的作用,对象大小要求必须是8字节的整数倍,没有对齐的时候,需要对齐填充来补全。
对象的访问定位
Java通过栈上的reference数据来操作堆上的具体对象,主要有两种方式。
1、使用句柄访问
堆中划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含对象实例数据与类型数据各自的具体地址信息。
优点是:reference中存储的是稳定的句柄地址,对象被移动时只会改变句柄中的实例数据指针,reference本身不需要修改。
2、使用直接指针访问
reference中存储的直接就是对象地址:
优点是:速度更快,节省了一次指针定位的时间开销。创建对象的步骤
检查检查指令的参数是否能在常量池中定位到一个类的符号引用;检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有就执行类加载。
- 分配内存在类加载后,对象所需的内存已经可以确定了。对分配内存空间动作进行同步处理——虚拟机采用了CAS配上失败重试的方式保证更新的原子性。
- 指针碰撞指的是在堆中,空闲区和非空闲区界限分明,中间放着一个指针作为分界点的指示器,所分配的过程就只是将指针向空闲空间的那边挪动一段与对象大小相等的距离。适用于Serial、ParNew等带Compact过程的收集器。
- 空闲列表指的是已使用内存和空闲区内存交错的情况下,没办法指针碰撞,而需要维护一个列表,记录内存的可用和不可用,分配时进行更新,适用于基于Mark-Sweep算法的CMS收集器。
- 初始化将分配到的内存空间都初始化为零值(不包括对象头),可提前至TLAB(本地分配缓存区)分配前初始化。初始化保证了不赋初值就可直接使用,程序能访问到这些字段的数据类型所对应的零值。
- 设置对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到元数据信息、对象的哈希码、对象的GC分代年龄等信息。
执行init以上步骤来说,虚拟机已经产生了一个对象,但从Java程序视角来看,对象创建才刚开始,因为还没有执行init,这一步将对象按照程序员的意愿进行初始化。
类加载过程
加载通过一个类的全限定名获取定义此类的二进制字节流,将这个字节流所代表的的静态存储结构转化为元空间的运行时数据结构,同时在内存中生成一个代表这个类的java.lang.Class对象,作为元空间这个类的各种数据的访问入口。
- 验证确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不危害虚拟机自身的安全。
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
- 准备为类变量分配内存并设置初始值,内存都将在元空间中进行分配(被static修饰的变量,不包括实例变量),且分配的初始值是各个对象的类初始值。(final修饰的static变量除外,其在此分配对应的值)
- 解析将常量池内的符号引用替换为直接引用的过程。符号引用:符号引用以一组符号来描述所引用的目标,可以是任何形式的字面量。直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
- 初始化类加载过程的最后一步,真正开始执行类中定义的Java字节码。(执行
()方法的过程,为静态变量赋值) 双亲委派机制
工作过程
- 收到类请求;
- 请求委派给父类;
-
使用双亲委派的好处
保证稳定性,类随着类加载器具有统一优先级的层次关系;
- 虚拟机只有在两个类的类名相同且加载该类的加载器均相同的情况下才判定这是一个类。若不采用双亲委派机制,同一个类有可能被多个类加载器加载,这样该类会被识别为两个不同的类,相互赋值时会有问题。
破坏双亲委派机制的场景
- 基础类要调用用户的代码一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载,但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,但启动类加载器不可能“认识”这些代码。为了解决这个问题,Java团队引入了线程上下文类加载器(Thread Context ClassLoader)。有了线程上下文加载器,JNDI服务就可以使用它去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
用户对程序动态性的追求所谓的动态性是指代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构。
打破的方法
重写ClassLoader类中的loadClass()方法即可打破,重写findClass()是不会打破的,它的作用是加载无法被父类加载器加载的类。
Tomcat的类加载器
CommonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
- CatalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
- SharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
- WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见。
- CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。
- WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
- JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载。很显然Tomcat已经打破了双亲委派模型,tomcat是web容器,一个web容器可能需要部署多个应用程序,不同的应用程序可能会依赖同一个第三方库的不同版本,但是不同版本的库中某一个类的全路径名可能是一样的,为了实现隔离性,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。如果tomcat的Common ClassLoade 想加载WebApp ClassLoader中的类,可以使用线程上下文加载器实现。
JVM性能调优
常用命令
- jps显示系统内所有的HotSpot虚拟机进程。
- jstat虚拟机统计信息监视工具。
- jinfoJava配置信息工具。
- jmapJava内存映像工具。
- jhat虚拟机堆转储快照分析工具。
- jstackJava堆栈跟踪工具。
-
常用工具
JConsoleJava监视与管理控制台。
-
常用参数
参数说明
标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
- 非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;
- 非Stable参数(-XX),此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用。
在选项名前用 “+” 或 “-” 表示开启或关闭特定的选项,例:
-XX:+UseCompressedOops:表示开启 压缩指针
-XX:-UseCompressedOops:表示关闭 压缩指针
常见参数设置
- -Xms:初始堆大小
- -Xmx:最大堆大小
- -Xmn:新生代大小
- -Xss:每个线程的虚拟机栈大小
- -XX:NewSize:新生代初始大小
- -XX:NewSize:新生代最大值
- -XX:MetaspaceSize:设置元数据空间初始大小(取代-XX:PermSize)
- -XX:MaxMetaspaceSize:设置元数据空间最大值(取代之前-XX:MaxPermSize)
- -XX:+PrintGCDetails:打印GC信息
Linux cpu100%怎么办
- 确定cpu占用率高的进程ID - PID
在Linux下可以使用 jps -v 或者 top 命令直接查看。 - 查看进程中cpu占用率高的线程ID - TID
执行top -H -p [PID],查看结果 - 将线程ID - TID 转换成16进制 - XTID
执行printf “%x\n” [TID],输出结果记为 XTID - 将进程中的所有线程输出到文件
执行命令jstack [PID] >> jstack.txt。 - 在输出文件中查找对应的线程ID
执行命令cat jstack.txt | grep -i [XTID]。