1、JVM内存模型总览
2022-05-02 09:38:52 18 举报
对JVM的内存模型进项详细的描述和全面的总结,包含了N多JVM的相关知识体系
作者其他创作
大纲/内容
双亲委派机制底层主要源码
解析
动态链接
对其填充
5、JVM优化之常见线上问题处理
方法区(元空间)
对象头
运行时常量池
运行时数据区(JVM内存模型)
栈上分配内存
CONSTANT_Utf8_info: utf8字符串
应用程序类加载器
类型的常量池
G1
1、对象栈上分配JVM内存分配时JAVA中的对象主要都是在堆上进行分配,当对象没有被引用的时候,需要依靠GC进行回收内存,如果对象数量较多的时候,会给GC带来较大压力,也间接影响了应用的性能。为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象会不会被外部访问,如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。对象逃逸分析其实就是分析对象的动态作用域,判断一个对象是否是逃逸对象,就看这个对象能否被外部对象访问到,如果对象在方法内部被定义后,会被外部方法所引用,则认为该对象就会发生逃逸,否则就不会逃逸。通俗点讲,如果一个对象的指针被多个方法或者线程引用时,那么我们就称这个对象的指针(或对象)逃逸。如果对象发生了逃逸,就将对象放入堆中,如果对象未发生逃逸,则将对象分配在栈中,让其在方法结束时跟随栈帧的出栈而销毁,减少了堆中对象的分配和销毁,减轻GC的压力,从而优化性能。逃逸分析JDK1.7之后默认开启。标量替换标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量。当通过逃逸分析后,如果一个对象只会作用于方法内部,虚拟机可以通过使用标量替换来进行优化。如果对象未发生逃逸(即栈上分配)并且可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解,使其被若干个这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。栈上分配依赖于逃逸分析和标量替换。2、对象堆内分配Eden区分配大多数情况下,对象在堆内存的新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。堆内存中,Eden与Survivor区内存默认比例为8:1:1(堆内存分配原则:让eden区尽量的大,survivor区够用即可)。大对象直接进入老年代如果对象超过大对象的设置大小,就会直接进入老年代,不会进入新生代,目的为了避免为大对象分配内存时的复制操作(新生代垃圾收集采用的是复制算法)而降低效率。长期存活的对象进入老年代对象每经过一次Minor GC其年龄age就会加1,当age≥15岁(默认15岁,CMS为6岁,不同的垃圾收集器略有不同)时,就会进入老年代。对象动态年龄判断对象动态年龄判断机制一般是在minor gc之后触发的。当一批对象的总大小≥Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时≥这批对象年龄最大值的对象,就可以直接进入老年代了,这么做的目的是让那些可能是长期存活的对象,尽早进入老年代。老年代空间分配担保机制
黑色:已经被GC访问过的对象, 且这个对象的所有引用都已经被扫描过。
方法表
内存回收就是释放掉在内存中已经没用的对象。内存回收的两种方式:引用计数法内存中的每个对象均拥有一个引用计数器,有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的,这些对象就是需要内存回收的对象。根可达性分析算法将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾。GC Roots根节点包括:-- 线程栈的本地变量-- 静态变量-- 本地方法栈的变量引用类型-- 强引用普通引用变量,比如new出来的对象。无论内存是否足够,不会回收。-- 软引用SoftReference,当GC之后内存不足时,会回收该对象。-- 弱引用WeakReference,GC会直接回收。-- 虚引用PhantomReference,GC会直接回收,虚引用主要用来跟踪对象被垃圾回收的活动。如何判断一个类是无用的类同时满足以下三个条件,就是无用的类:-- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。-- 加载该类的 ClassLoader 已经被回收。-- 该类对应的对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
字面量
卸载
获取类的二进制字节流
抽象语法树
初始化
N
Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,下文统称JIT编译器)。
5、输入 thread -b 可以查看线程死锁
基于计数器的热点探测方法调用计数器回边计数器
白色:尚未被GC访问过的对象,如果全部标记已完成依旧为白色的,称为不可达对象,既垃圾对象。
7、使用 ognl 命令可以修改线上动态对象的属性值
1、类加载检查虚拟机遇到一条new指令时,会首先进行类加载的检查:-- 检查这个指令的参数是否能在常量池中定位到一个类的符号引用;-- 检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载。2、分配内存对象所需内存的大小在类加载完成后便可完全确定,于是分配空间等同于把一块确定大小的内存从堆中划分出来,具体分配方式取决于堆内存是否规整,而是否规整又取决于所采用的GC收集器是否带有压缩整理功能。内存分配方式-- 指针碰撞堆内存规整,移动指针(对象内存大小的距离)-- 空闲列表堆内存不规整,虚拟机维护记录可用内存的列表,从列表中查找对应对象大小的内存区域分配给对象,并更新空闲列表。为保证内存分配时并发情况下线程安全问题,有两种方案:-- CAS+失败重试虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。-- TLAB把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。3、初始化内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。4、设置对象头初始化零值之后,虚拟机要对对象进行必要的设置,这些设置信息存放在对象的对象头Object Header之中。对象头包括三部分:MarkWord、实例数据、类型指针。5、执行<init>方法为对象的属性赋值,执行对象的构造方法。
执行编译后的机器码
垃圾收集共分为五个阶段:上图所示初始标记和重新标记会有STW。重新标记,会用到三色标记里的增量更新算法。并发清理:这个阶段如果有新增对象会被标记为黑色不做任何处理主要优点:并发收集、低停顿。-XX:+UseConcMarkSweepGC:启用cms
young
提交编译请求
是否已编译
老年代(2/3)
根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。优点:消除了标记-清除算法中,内存区域分散不连续的缺点。消除了复制算法中内存减半的高额代价缺点:移动对象开销较大
CONSTANT_InterfaceMethodref_info:接口方法符号引用
栈(线程)
记录出栈地址:方法返回的地址,即记录当本方法执行完之后,回到主方法时该从哪一行执行。
Server Compiler(C2)质量
扩展类加载器
方法返回
程序计数器
3、jstackjstack [option] pid,查看进程信息,排查死锁。option的选项说明:-l:long listings,会打印出额外的锁信息,在发生死锁时可以用jstack -l pid来观察锁持有情况-m:mixed mode,不仅会输出Java堆栈信息,还会输出C/C++堆栈信息(比如Native方法)
Serial
方法出口
符号引用
设计思想
符号引用常量池类型
是
concurrent mode failure
各参数详解:S0C:第一个幸存区的大小,单位KB S1C:第二个幸存区的大小 S0U:第一个幸存区的使用大小S1U:第二个幸存区的使用大小 EC:伊甸园区的大小 EU:伊甸园区的使用大小 OC:老年代大小 OU:老年代使用大小 MC:方法区大小(元空间)MU:方法区使用大小 CCSC:压缩类空间大小 CCSU:压缩类空间使用大小YGC:年轻代垃圾回收次数 YGCT:年轻代垃圾回收消耗时间,单位s FGC:老年代垃圾回收次数 FGCT:老年代垃圾回收消耗时间,单位s GCT:垃圾回收消耗总时间,单位s
TLAB?
每个方法都有自己的栈帧空间,都有自己的局部变量表,用来存储方法内部的局部变量的值。存放boolean,byte,char,short,int,float,long,double种类型的数据,以变量槽(slot)为最小单位(32位),long,double需要2个slot,所以线程不安全;基本数据类型会直接存值,引用数据类型会存放对象的引用
类型指针
Parallel(多线程)复制算法
垃圾判断算法
常量池的类型
本地方法栈
字段信息
(2)String s = new String(\"xxx\"); 这种方式会保证字符串常量池和堆中都有这个对象,没有就创建,最后返回堆内存中的对象引用。因为有\"xxx\"这个字面量,所以会先检查字符串常量池中是否存在字符串\"xxx\":不存在,先在字符串常量池里创建一个字符串对象\"xxx\",再去堆内存中创建一个字符串对象\"xxx\";存在,就直接去堆内存中创建一个字符串对象\"xxx\",最后,将堆内存中的引用返回。注:如果\"xxx\"这个字面量在字符串常量池中存在,则只会创建一个对象(在堆中创建),如果不存在,则会创建两个对象(在字符串常量池和堆中均创建)。
操作数栈
字符串常量池
为什么要设计双亲委派机制:遵循实现沙箱安全机制,保证JDK核心类库的类不被随意篡改、加载保证类的唯一性,避免重复加载,当父类已经加载了该类时,就没有必要子ClassLoader再加载一次。全盘负责委托机制“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,否则该类所依赖及引用的类也由这个ClassLoder载入。
当发生minor gc时,S1和S0有一个是空的,数据来回切换
字节码校验器
我们把Gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色:
标记过程:(以左侧5张图为例)1、1图,初始时,所有对象都在 【白色集合(White Set)】中;2、2图,将GC Roots直接引用到的对象全部挪到 【灰色集合(Grey Set)】中,因为对象A和D是GC Roots的直接引用,所以对象A和D会被标记为灰色;3、3图,遍历灰色对象,A对象上没有其他引用,直接标记为黑色,挪到【黑色集合(Black Set)】中,D对象上有其他引用E对象,将其他引用(E对象)标记为灰色挪到【灰色集合(Grey Set)】中,然后把D对象标记为黑色,挪到【黑色集合(Black Set)】中;4、4图,重复3图的操作,会把E对象标记为黑色,挪到【黑色集合(Black Set)】中,会把F和G对象标记为灰色,挪到【灰色集合(Grey Set)】中;5、5图,重复3图的操作,会把F和G对象标记为黑色,挪到【黑色集合(Black Set)】中,如果F和G对象还有其他引用,就继续重复3图的操作;6、最终,被标记为黑色的对象有ADEFG对象,被标记为灰色的对象没有,被标记为白色的对象有BCH对象,那么BCH对象就是本地GC要回收的垃圾对象。
4、JVM优化之GC日志
创建一个引导类加载器的示例(底层是C++实现)
自定义类加载器
动态链接(多态,编译期没有指明,运行时才指明对象),符号引用指向数据在内存中的真实地址的指针或句柄等(即直接引用)
垃圾回收算法
存放编译期生成的各种字面量和符号引用
数组长度
Y
否
1.使用top命令查到Cpu占用最高的进程id2.利用top-H-p <pid>查看该进程内cpu最高的线程pid3.将得到的pid转成16进制的Xid(执行命令:printf \"%x\\" <pid>)4.使用jstack <pid> | grep -A 10 Xid得到线程堆栈的信息中这个xid线程的后10行,可以找到导致cpu过高的调用的方法。
3、显示调用System.gc()造成多余的full gc,这种一般线上尽量通过XX:+DisableExplicitGC参数禁用,如果加上了这个JVM启动参数,那么代码中调用System.gc()没有任何效果
ZGC(多线程)
Arthas的使用
可以理解为用于计算的临时数据存储区,使用load指令将数据加载到此,主要用于保存计算过程的中间结果值,同时作为计算过程中临时的存储空间。
后台执行编译
方法区(元空间Matespace)
JIT
堆(heap)
sun.misc.Launcher#getLauncher()
实例数据
是否已加载?
CMS(多线程)标记清除算法
由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。因此,如果对象实例数据部分没有对齐的话(大小不是8字节的整数倍),就需要通过对齐填充来补全。
多线程,STW新生代:复制算法老年代:标记整理算法重点关注吞吐量,效率高,JDK8默认
分配内存
方法/回边计数器+1
语法分析
每个线程会有一个独有的程序计数器,记录线程运行到哪一行的代码的位置,程序计数器值的改变是由字节码执行引擎去改变的。由于CPU执行指令是可中断的,会有线程切换,程序计数器会记录当前线程执行的字节码指令地址(行号),以便线程切换后能恢复到正确的执行位置
Client Compiler(C1)速度
主要操作1、读取类的字节流信息2、将静态结构转化为方法区运行时的数据接口3、内存中生成类对象,作为数据入口
编译器是以整个方法作为编译对象,编译完成后,方法的调用入口地址被替换成编译后的方法地址
垃圾回收算法主要有以下三种:
频繁发生Full GC问题的处理
CONSTANT_InvokeDynamic_info:动态方法调用点
SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象。而CMS对增量引用的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度扫描。
底层源码
TLAB(线程私有)
类装载子系统
2、输入dashboard可以查看整个进程的运行情况,线程、内存、GC、运行环境信息:
验证
常见的优化命令1、jps(全称:Java Virtual Machine Process Status Tool)是 java 提供的一个用来显示当前所有 java 进程的 pid 的命令。使用 jps 命令,可以查看到底启动了多少个 java 进程,并且可以通过 option 参数来参看进程的详细信息。java 的每一个程序,均独占一个 java 虚拟机实例,且都是一个独立的进程。每个进程都有自己的 id,称为pid。常用参数如下:
直接引用(动态链接)
引导类加载器
Epsilon
1、JVM优化之常用参数
向下加载
OLD
2、老年代空间分配担保机制
加载完成时,JVM会执行Math类的main方法入口
Age≥15?
END
CONSTANT_Long_info: 长整型字面量
类加载的底层流程
常见的垃圾收集器
C++调用java代码创建JVM启动器实例sun.misc.Launcher,该类由引导类加载器负责加载并创建其他类加载器
局部变量表
使用
-dump
生成Java堆转储快照。格式为:-dump:llive,]fomat-b,file=<filename>,其中live子参数说明是否只dump出存活的对象
-finalizerinfo
显示在F-Queue中等待Finalizer线程执行finalize方法的对象。只在Linux/Solaris平台下有效
-heap
显示Java堆详细信息,如使用哪种回收器、参数配置、分代状况等。只在Linux/Solaris平台下有效
-histo
显示堆中对象统计信息,包括类、实例数量、合计容量
-permstat
以ClassLoader为统计口径显示永久代内存状态。只在Linux/Solaris平台下有效
-F
当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照。只在Linux/Solaris平台下有效
类型指针Klass Pointer,开启压缩时占4个字节,关闭压缩时占8个字节,类的元数据的指针(D)
CONSTANT_MethodHandle_info: 方法句柄表
整体预估后,再从以下几个方面入手,对JVM进行多方位的评估和优化1、年轻代对象增长的速率可以执行命令 jstat -gc pid 1000 10 (每隔1秒执行1次命令,共执行10次),通过观察EU(eden区的使用)来估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。2、Young GC的触发频率和每次耗时知道年轻代对象增长速率我们就能推根据eden区的大小推算出Young GC大概多久触发一次,Young GC的平均耗时可以通过 YGCT/YGC公式算出,根据结果我们大概就能知道系统大概多久会因为Young GC的执行而卡顿多久。3、每次Young GC后有多少对象存活和进入老年代这个因为之前已经大概知道Young GC的频率,假设是每5分钟一次,那么可以执行命令 jstat -gc pid 300000 10 ,观察每次结果eden、survivor和老年代使用的变化情况,在每次gc后eden区使用一般会大幅减少,survivor和老年代都有可能增长,这些增长的对象就是每次Young GC后存活的对象,同时还可以看出每次Young GC后进去老年代大概多少对象,从而可以推算出老年代对象增长速率。4、Full GC的触发频率和每次耗时知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出。
EDEN
符号引用在程序加载或运行时会被转变为被加载到内存区域的代码的直接引用
age>15时,进入老年代
(1)String s = \"xxx\"; 创建对象s时,JVM会先去常量池中通过 equals(key) 方法,判断是否有相同的对象:如果有,则直接返回该对象在常量池中的引用,如果没有,则会在常量池中创建一个新对象,再返回引用。 这种方式创建的字符串对象,只会在常量池中。s最终指向常量池中的引用。
5、jstatjstat命令可以查看堆内存各部分的使用量,以及加载类的数量。jstat命令格式:jstat [Options] pid [interva] [count] --pid.当前运行的java进程号 --interval,间隔时间,单位为秒或者毫秒 --count.打印次数,如果缺省则打印无数次jstat常用的命令有:jstat -gc pid:显示垃圾回收统计信息(该命令最常用)
新生代(1/3)
系统CPU过高问题的处理
单线程,STW新生代:复制算法老年代:标记整理算法适用于内存在几百M以下的JVM
compute()栈帧
2、对象内存分配
将常量池内的符号引用替换为直接引用
硬件
字符串常量池的位置
执行<init>方法
类型信息
热点探测
分代收集理论
jstat -gccapacity pid:堆内存统计信息jstat -gcnew pid:新生代垃圾回收统计信息jstat -gcnewcapacity pid:新生代内存统计信息jstat -gcold pid:老年代垃圾回收统计信息jstat -gcoldcapacity pid:老年代内存统计信息jstat -gcutil pid:堆内存统计信息,以使用空间的百分比表示
对象
class字节码
MarkWord
概念:当加载一个类时,先委托其父类加载器(扩展类加载器)进行查找,如果查找不到,再委托上层父类加载器(引导类加载器)进行查找,如果查找到了,就加载该目标类。如果所有父类加载器在各自的加载类路径下均找不到目标类,则在自己的类加载器(应用程序加载器)路径中进行查找,并加载该目标类。原理:向上委派,向下加载。
注解抽象语法树
对象在内存中有3部分组成:对象头、实例数据和对其填充
多线程,STW新生代:复制算法老年代:标记整理算法(CMS或Serial)只能用在新生代
Math.class
解释器
From SurvivorS0(1/10)
栈
(3)String s1 = new String(\"xxx\"); String s2 = s1.intern();String中的intern方法是一个 native 的方法,当调用 intern方法时,如果常量池中已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回常量池中的字符串。否则,将intern返回的引用指向当前字符串 s1(jdk1.6版本需要将 s1 复制到字符串常量池里)。
存储字符串常量信息
CONSTANT_MethodType_info:方法类型表
START
为类变量分配内存并设置初始值
实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。
CodeCacheJIT编译代码产物
Class实例引用
类元信息
windows系统下,java.exe程序调用底层的jvm.dll文件,创建java虚拟机(底层是C++实现)
1.通过jps查看进程的启动pid,然后根据 jstat-gc pid 1000 10000命令查看出现问题的时间点,以及当前FullGC的频率2.了解该时间段之前有没有新的程序上线,基础组件升级等情况,如果是程序上线的话,查看代码中是否有内存泄漏,显式的调用gc()方法等进行排查3.如果没有的话,用Jinfo命令查看运行java应用程序的扩展参数,包括:堆空间各个区域的大小设置,新生代和老年代使用了那些垃圾回收器,然后分析这些参数配置是否合理4.通过jmap-histo进程id导出日志信息,定位到可疑对象,看一下是不是有大对象或者是长生命周期的对象导致的FullGC的发生5.根据具体的对象,定位到具体的代码再次分析,这时候要结合GC原理和jvm参数设置,弄清楚可疑的对象是否满足了进入老年代的条件,然后进行调试处理问题。
自定义加载器
sun.misc.Launcher.AppClassLoader#loadClass(\"com.study.tuling.jvm.Math\")
4、jinfojinfo命令,用来查看正在运行的java应用程序的扩展参数。jinfo常用的命令有:jinfo -flags pid:查看jvm的参数jinfo -sysprops pid:查看进程的全部配置信息
3、对象内存回收
双亲委派
Serial Old(单线程)标记整理算法
CONSTANT_Fieldref_info:字段信息表
垃圾收集--GC
将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。优点:不会出现内存碎片问题对于存活对象少的区域(新生代),简单高效缺点:浪费空间并且移动对象开销大
三色标记
这个非常有用,因为并不是每个人都比较精通VM核心原理和性能优化,所以如果有一套为团队或者公司定制一套基本的JVM参数模板,基本可以保证JVM性能不会太差,避免许多初中级工程师直接使用默认的JVM参数,并不能满足生产需要。
-XX:+UseSerialGC(年轻代) -XX:+UseSerialOldGC(老年代)
C++发起调用
JIT动态编译
常见三种字符串操作
Extention ClassLoader(ExtClassLoader),负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包。
CONSTANT_Methodref_info:方法
To SurvivorS1(1/10)
根可达性算法
Eden区(8/10)
两个计数器之和是否超过阈值
JIT代码生成器(热点代码)字节码-->JIT编译(代码膨胀10X)-->执行-->结果
字面量常量池类型
Math.main()
向上委托其父加载器查找目标类
解释器字节码-->解释执行-->结果
javac 编译器
S2
8大基本类型
类和结构的全限定名字段名称和描述符方法名称和描述符
Application ClassLoader(AppClassLoader),负责加载ClassPath路径下的类包,主要就是加载自己写的那些类。即该类加载器是我们写的 Java 类的默认加载器,我们写的类会首先尝试使用这个类加载器进行加载。
栈上替换
CONSTANT_Integer_info: 整型字面量
三色标记的问题多标多标会产生浮动垃圾。在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过(被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“浮动垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。漏标漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案: 增量更新(IncrementalUpdate)和原始快照(Snapshot At The Beginning,SATB) 。-- 增量更新增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再以这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。-- 原始快照原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后,再以这些记录过引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)。增量更新和原始快照的底层操作,都是通过写屏障来实现的。不同的垃圾处理器对漏标的处理方案:CMS:写屏障 + 增量更新,G1:写屏障 + SATB,ZGC:读屏障
方法信息
思考:为什么G1用SATB(原始快照),CMS用增量更新?
1、当空间满了会触发FULL GC2、大对象直接进入老年代3、对象动态年龄判断:当Survivor空间中一批对象的总大小超过Survivor空间的50%时,大于等于该批对象中年龄最大值的所有对象直接进入老年代(一般是minor gc后触发)4、老年代空间分配担保机制
线程3
ParNew
每个线程独享的内存区域
CONSTANT_Float_info: 浮点型字面量
Jmap常用的命令有:jmap -histo pid > jmap.txt:查看并导出java进程的内存信息,实例个数以及占用内存大小。jmap -heap pid:查看堆内存信息。font color=\"#ff0000\
POP
JDK1.8默认垃圾收集器-XX:+UseParallelGC(年轻代)-XX:+UseParallelOldGC(老年代)不能与CMS收集器配合使用
GC overhead limt exceed检查是Hotspot VM 1.6定义的一个策略,通过统计GC时间来预测是否要OOM了,提前抛出异常,防止OOM发生。超过98%的时间都在用来做GC并且回收了不到2%的堆内存。连续多次的GC,都回收了不到2%的极端情况下才会抛出。Sun 官方对此的定义是:并行/并发回收器在GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存。用来避免内存过小造成应用不能正常工作。假如生产环境中遇到了这个问题,在不知道原因时不要简单的猜测和规避。可以通过-verbose:gc -XX:+PrintGCDetails看下到底什么原因造成了异常。通常原因都是因为old区占用过多导致频繁Full GC,最终导致GC overhead limit exceed。如果gc log不够可以借助于JProfile等工具查看内存的占用,old区是否有内存泄露。分析内存泄露还有一个方法-XX:+HeapDumpOnOutOfMemoryError,这样OOM时会自动做Heap Dump,可以拿MAT来排查了。还要留意young区,如果有过多短暂对象分配,可能也会抛这个异常。
CONSTANT_String_info:String类型的常量对象
获取运行类自己的类加载器ClassLoader,默认的是AppClassLoader的示例: this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
(4)String字符串不可变String s = \"a\" + \"b\" + \"c\"; //就等价于String s = \"abc\";String a = \"a\";String b = \"b\";String c = \"c\";String s1 = a + b + c;s1 这个就不一样了,可以通过观察其JVM指令码发现s1的\"+\"操作会变成如下操作:StringBuilder temp = new StringBuilder();temp.append(a).append(b).append(c);String s = temp.toString();这就是符号\"+\"在JVM底层的源码操作,所以可知,当用符号\"+\"来拼接引用类型时,就会生成一个新的String对象。
Arthas的使用1、通过java -jar arthas-boot.jar启动arthas服务,然后选择进程号1进入进程信息操作界面
ParNew(多线程)复制算法
标记清除算法
CMS
总结:当字符串用“+”来拼接时,拼接后的字符串是否会生成新的对象,要根据“+”两侧的字符串来判断。如果“+”两侧的字符串均为字符串常量(即有确切的常量值),则JVM会在编译期对其进行优化,会将两个字符串常量拼接为一个字符串常量,如果拼接后的字符串常量在字符串常量池中存在,则不创建对象,如果不存在,则创建对象;如果“+”两侧的字符串有字符串引用存在,因为引用的值在JVM的编译期是无法确定的,所以“+”无法被JVM编译器进行优化,只有在程序运行期来动态分配并为拼接后的字符串创建新的对象(分配新的内存地址)。
问题一:元空间内存溢出--java.Lang.0utofMemoryError:Metaspace原因分析:(1)系统上线时,Metaspace直接使用的默认的参数,而默认参数往往比较小,很容易不够用。(2)代码中使用cglib之类的技术生成类,没有控制好,导致生成的类过多,也容易塞满Metaspace,导致内存溢出。解决办法:(1)合理分配Metaspace区域(2)避免无限生成动态类问题二:栈内存溢出--java.lang.StackOverflowError原因分析:如果不停的让线程调用方法,不停的往栈里面放入栈帧,最终会有一个时刻,大量栈帧会消耗完这个栈内存,最终就会出现栈内存溢出的情况。要么是方法调用层次过多(比如存在无限递归调用),要么是线程栈太小。 解决方法:优化程序设计,减少方法调用层次(避免不合理的调用递归方法等);调整-Xss参数增加线程栈大小。 问题三:堆内存溢出堆内存溢出的情况比较多,主要分为以下5种:1、java.lang.OutOfMemoryError: Java heap space原因分析:这种是java堆内存不够,一个原因是真不够,另一个原因是程序中有死循环;解决办法:调大JVM堆内存参数:-Xms和-Xmx,排查程序中是否有死循环。2、java.lang.OutOfMemoryError: GC overhead limit exceeded 原因分析:JDK6新增错误类型,当GC为释放很小空间占用大量时间时抛出;一般是因为堆太小,导致异常的原因,没有足够的内存。 解决办法:查看系统是否有使用大内存的代码或死循环;通过添加JVM配置,来限制使用内存:-XX:-UseGCOverheadLimit(关闭这个特性);调大JVM堆内存参数:-Xms和-Xmx;3、java.lang.OutOfMemoryError: Direct buffer memory 原因分析:(1)内存设置不合理,导致DirectByteBuffer对象一直慢慢进入老年代,导致堆外内存一直程放不掉。(2)设置了-XX:+DisableExplicitGC导致Java NIO没法主动提醒去回收掉一些垃圾DirectByteBuffer对象,同样导致堆外内存无法释放掉。解决办法:限制DirectMemory的容量,通过调整JVM参数-XX:MaxDirectMemorySize=128m来实现,如果不指定,则与Xmx(堆的最大值一致)相同。4、java.lang.OutOfMemoryError: PermGen space原因分析:永久代内存PermGen space不够解决办法:调整JVM PermGen区域的内存大小:XX:MaxPermSize=128m XX:PermSize=128m5、java.lang.OutOfMemoryError: unable to create new native thread 原因分析:Stack空间不足以创建额外的线程,要么是创建的线程过多,要么是Stack空间确实小了。 解决办法:(1)通过 -Xss启动参数减少单个线程栈大小,这样便能开更多线程(当然不能太小,太小会出现StackOverflowError); (2)通过-Xms -Xmx 两参数减少Heap大小,将内存让给Stack(前提是保证Heap空间够用)。
灰色:已经被GC访问过的对象, 但这个对象上至少存在一个引用还没有被扫描过。属于中间态。
Shenandoah
无参数(-V)
默认显示 pid、应用程序 main class 类名
-q
只显示 pid
-m
显示 pid、应用程序 main class 类名 和 传递给main方法的参数
-l
显示 pid 和 应用程序 main class 的完整包名 或者 应用程序的 jar 路径
-v
显示 pid 、应用程序 main class 类名 和 传递给 jvm 的参数
对于java应用我们可以通过一些配置把程序运行过程中的gc日志全部打印出来,然后分析gc日志得到关键性指标,分析GC原因,调优JVM参数。打印GC日志方法,在JVM参数里增加参数,%t 代表时间:-Xloggc:./gc-%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M如何分析GC日志运行程序时,添加对应gc日志的命令:1、Parallel垃圾收集器的日志相关参数-Xloggc:./gc-%t.log -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M2、CMS垃圾收集器的日志相关参数-Xloggc:d:/gc-cms-%t.log -Xms50M -Xmx50M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC 3、G1垃圾收集器的日志相关参数-Xloggc:d:/gc-g1-%t.log -Xms50M -Xmx50M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M -XX:+UseG1GC
标记整理算法
2、jmapJmap是一个可以输出所有内存中对象的工具,甚至可以将JVM中的heap,以二进制输出成文本。打印出某个java进程(使用pid)内存信息、实例个数、占用内存大小,所有对象的情况(如:产生哪些对象,及其数量)。常用参数如下:
4核8GLinux系统JVM参数模板
G1(多线程)整理+清除
加载类
解释执行
类加载器
class常量池
HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据--Mark Word, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元信息的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
Serial(单线程)复制算法
当前虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
与虚拟机栈非常相似,区别在于本地方法栈为虚拟机的Native方法服务;本地方法底层是C++/C语言实现的。在运行过程中需要给这些方法分配一块内存空间。Hotspot将JVM栈和本地方法栈合二为一。
语义分析
执行栈上替换
Custom ClassLoader,负责加载用户自定义路径下的类包。
词法分析
3、JVM优化之优化思路
大对象?
执行如下代码,即可在代码中的hashSet中添加元素
设置对象头
Math.java
main()栈帧
S1
调用AppClassLoader的loadClass()方法,加载要运行的类
java运行结束
标记复制算法
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化:(1)为字符串开辟一个字符串常量池,类似于缓存区;(2)创建字符串常量时,首先查询字符串常量池是否存在该字符串;(3)存在该字符串,返回引用实例,不存在,实例化该字符串(即创建字符串对象)并放入池中;
TLAB:JVM在堆内的Eden区开辟的每一个线程私有的很小的缓冲空间(Thread Local Allocation Buffer)。线程创建对象时,只要TLAB空间足够就可在此空间创建。
底层源码流程:应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass()方法最终会调用其父类ClassLoader的loadClass方法,该方法的大体逻辑如下:(1)首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。(2)如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用font color=\"#ff0000\
向上委托
栈内分配?
字节码生成器
JVM销毁
ZGC
1、元空间不够导致的多余full gc
CONSTANT_NameAndType_info:名称和类型表
经典面试题
数组
reference
(1)下面代码创建了几个对象,最终结果是什么?String s1 = new String(\"he\") + new String(\"llo\");String s2 = s1.intern();System.out.println(s1 == s2);// 在 JDK 1.6 下输出是 false,创建了 6 个对象,因为s1.intern()会将字符串“hello”放入到字符串常量池,并创建新对象,返回新对象的引用地址,所以为false// 在 JDK 1.7 及以上的版本输出是 true,创建了 5 个对象,因为s1.intern()返回的是s1对象的引用地址,并没有创建新对象,所以为true
逃逸分析
Arthas 是 Alibaba 在 2018 年 9 月开源的 Java 诊断工具。支持 JDK6+, 采用命令行交互模式,可以方便的定位和诊断线上程序运行问题。
父加载器先加载,如果加载失败,则交由子加载器进行加载
CONSTANT_Class_info:表示类或接口
Bootstrap ClassLoader,负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar、resources.jar等。
Parallel Old(多线程)标记整理算法
更多命令使用可以用help命令查看,或查看Arthas官方文档:https://alibaba.github.io/arthas/commands.html#arthas
1、对象的创建过程
文本字符串final常量值其他数据类型值
new对象
3、输入thread可以查看线程详细情况
运行时被加载到内存后
4、输入 thread加上线程ID,可以查看线程堆栈
双亲委派底层源码
class对象(类元信息、属性、方法、静态变量等)
old
6、输入 jad加类的全名 可以反编译,这样可以方便我们查看线上代码是否是正确的版本
class字节码文件由魔数,主次版本号,常量池,类信息,类的构造方法,类的中的方法信息,类变量与成员变量等信息组成。
-Xms:堆最小空间-Xmx:堆最大空间-XX:NewRatio:Old/New的比例-Xmn:年轻代大小,调整会影响老年代大小,官方建议为堆大小的3/8-XX:SurvivorRatio:调整Survivor和Eden去的大小比例-XX:MetaspaceSize:元空间初始化大小,64位JVM默认20.75M-XX:MaxMetaspaceSize:元空间最大大小,逻辑限制为屋里内存上限一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般会将这两个值都设置为256M。-XX:PretenureSizeThreshold:大对象直接进入老年代的阈值-XX:MaxTenuringThreshold:进入老年代的分代年龄阀值-XX:-XX:TargetSurvivorRatio:动态年龄判断比例设置-verbose:gc:输出JVM的gc情况-XX:+PrintGCDetails:输出GC详细信息-XX:+PrintTenuringDistribution:输出对象GC年龄信息-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如2013-05-04T21:53:59.234+0800)-XX:+PrintHeapAtGC在进行GC的前后打印出堆的信息-Xloggc..…/logs/gc.log日志文件的输出路径-XX:+UseG1GC:用G1-GC收集器-XX:-UseConcMarkSweepGC:用CMS-GC收集器-XX:+PrintCommandLineFlags-version:输出默认的垃圾回收器CMS的相关核心参数-XX:+UseConcMarkSweepGC:启用cms -XX:ConcGCThreads:并发的GC线程数-XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)-XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次 -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)-XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整-XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,降低CMS GC标记阶段(也会对年轻代一起做标记,如果在minor gc就干掉了很多对垃圾对象,标记阶段就会减少一些标记时间)时的开销,一般CMS的GC耗时 80%都在标记阶段-XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW-XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
线程2
ParNew只能用在年轻代,对应的老年代要用CMS。-XX:+UseParNewGC
线程1
双亲委派机制
思考:full gc比minor gc还多的原因有哪些?
Minor JC?
returnAddress
(3)字符串常量和字符串对象的比较String s0=\"zhuge\";String s1=new String(\"zhuge\");String s2=\"zhu\" + new String(\"ge\");System.out.println( s0==s1 ); // falseSystem.out.println( s0==s2 ); // falseSystem.out.println( s1==s2 ); // false用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。s0还是常量池 中\"zhuge”的引用,s1因为无法在编译期确定,所以是运行时创建的新对象\"zhuge\"的引用,s2因为有后半部分 new String(\"ge\")所以也无法在编译期确定,所以也是一个新创建对象\"zhuge\"的引用;明白了这些也就知道为何得出此结果了。
加载
对类的静态变量初始化为指定的值,执行静态代码块
准备
JVM运行情况预估优化之前,首先要对JVM整体的运行情况进行一个预估,用 jstat gc -pid 命令可以计算出一些关键数据:Young GC次数和耗时,Full GC次数和耗时,有了这些数据就可以采用之前介绍过的优化思路,先给自己的系统设置一些初始性的JVM参数,比如堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值等。
sun.misc.Launcher#getClassLoader()
引用计数器法
CONSTANT_Double_info: 双精度字面量
验证类的字节流信息是否准确
2、JVM优化之常用命令
类加载检查
应用程序加载器
(2)字符串常量的比较String s0=\"zhuge\";String s1=\"zhuge\";String s2=\"zhu\" + \"ge\";System.out.println( s0==s1 ); //trueSystem.out.println( s0==s2 ); //true因为例子中的 s0和s1中的”zhuge”都是字符串常量,它们在编译期就被确定了,所以s0==s1为true;而”zhu”和”ge”也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被优化为一个字符串常量\"zhuge\",所以s2也是常量池中” zhuge”的一个引用。所以我们得出s0==s1==s2;
-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:MetaspaceSize=256m-XX:MaxMetaspaceSize=256m -XX:SurvivorRatio=8 -XX:+UseParNewGC-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=10-XX:PretenureSizeThreshold=2M -XX:CMSInitiatingOccupancyFaction=92-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0-XX:+CMSParallellnitialMarkEnabled -XX:+CMSScavengeBeforeRemark-XX:+DisableExplicitGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps-Xloggc:./gclog/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/app/oom
优化思路其实简单来说主要就是以下三点:1、尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。2、尽量别让对象进入老年代。3、尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。
Parallel
new Object()
类加载器的引用
com.study.tuling.jvm.Math
java方法入口
Full GC
数组长度,占用4个字节,只有数组才有
多线程新生代:复制算法(ParNew或Serial)老年代:标记清除(只能用在老年代)实现垃圾收集线程与用户线程同时运行
收藏
收藏
0 条评论
下一页