前面我们提到了堆、方法区以及常量池的概念,相信第一次看的朋友们一定是云里雾里,此篇特来梳理一下完整版,填坑:
JVM可以总结为6部分:内存模型、垃圾回收、类加载、执行模式、性能调优、编译器优化。 其中前3部分是最常提到的,也会重点说。
- 内存模型:包含堆、方法区、栈、本地方法栈、程序计数器:
堆: 年轻代(s0区,s1区,eden区)、老年代
(1).堆空间数据是线程共享的,主要存储对象实例、数组、成员变量、初始化的对象;
(2).对象是GC隐式释放并回收内存,垃圾回收主要在堆中进行。
(3).TLAB:堆为每个线程初始化对象时会分配独立空间(TLAB),避免加锁同步来高效率,堆分配的TLAB区在eden区上,即eden既有TLAB这样的私有区又有本身eden减去TLAB的剩余公有区。
(4). TLAB区的对象创建是私有的,但读取对象、垃圾回收都是线程共享行为。
(5).大对象无法TLAB中分配,可能在eden剩余空间或老年代(共享空间)中分配,则需同步控制,效率降低。
(6).一个线程TLAB空间不大,当创建的对象将要占满TLAB,剩余空间不足创建新对象时,有最大浪费空间策略:例如某线程的TLAB空间为100KB,用了80KB剩下20KB,设置最大浪费阈值25KB,新对象占用空间>25KB则直接共享空间中分配;如果<=25KB,则重新分配一个TLAB 空间供新对象用。
(7).年轻代空间占比:eden:s0:s1=8:1:1, eden的空间用于创建对象,s0或s1每次回收都交换状态,1个存上次回收存活的对象,1个为空;每次垃圾回收eden区+s0/s1(有存活对象的1个),回收后的存活对象再放入s0/s1(空的那1个),每次回收后分代年龄+1;这就是复制算法。
(8).对象晋升规则:一般对象先进入eden区,大对象连续对象直接进老年代,分代年龄超过15的对象进入老年代,空间分配担保(当年轻代中相同分代年龄的对象数量占总空间的50%以上时,大于等于此年龄的对象都进入老年代)。
(9)年轻代与年老代的空间占比为1:2,如果年轻代过大,对象就会过早进入老年代容易被占满;如果年轻代空间过小,复制算法操作会变频繁。
(10).有年轻代防止对象全进入年老代,再回收,效率低;年轻代中的对象大多生命短,使用复制法成本低。
方法区:(1.7版方法区的永久代替代为1.8内存的元空间,各种常量池也由堆划分)
(1).方法区为线程共享,其存储程序中唯一的数据(class、static变量、类结构等)。
(2).1.8版主要存储:静态变量、静态常量、对象类型数据、字节码/变量符等。
(3).运行时常量池(1.8版堆中):是类或接口的字节码文件中常量池的运行形式,主要包含2种:编译时已知数字字面量值、需要运行时解析的方法或变量引用。
(4).1.7版方法区中永久代改为1.8版的本地内存的元空间,原因简述:
永久代存放多种类型的数据不易分类且回收复杂;字符串也存在永久代中有内存溢出问题以及性能问题;永久代空间大小制定难,空间太小容易溢出,空间太大会内存紧张。
元空间简化了Full GC,并且每一个回收器有专门的元数据迭代器。
程序计数器:
(1).线程私有,任意时刻一线程只能执行一个方法代码,线程切换时,要切换到程序计数器记录的位置。
栈:
(1).线程私有,调方法存入栈中,栈中存放的每个数据叫栈帧。
(2).调用方法创建帧入栈,正常完成或抛出异常会销毁帧出栈,每个线程都有方法栈。
(3).帧结构:局部变量表(大小根据帧大小提前确定)、操作数栈(传参与接返回值给父级,存储JVM正在执行指令)、动态链接(将方法引用符号转为运行时常量池的具体方法引用),除这3种外还有一些其他,主要是这3部分。
(4).局部变量表:存储8大基础值、地址值;32bit一个单位,存一个类型的值,long和double为64bit占2单位,读时从前32bit开始,但写时从后32bit写可能数据错乱,32位CPU写可能会有Long/Double异常、64位CPU写不会异常。
(5).静态帧(静态方法)的局部变量表索引从0开始;普通帧的局部变量表索引从1开始,因为0位存除的是this引用。
本地方法栈:
(1).native方法不是Java所写,JVM不加载,也无需提供对应的方法栈。
(2).本地方法:Java调用非Java代码的接口,方法非Java实现。
- 垃圾回收:
(1).栈、本地方法栈、方法区、程序计数器都不需GC回收,GC主要在堆中进行。
(2).finalize方法:年轻内存充足时不会执行、其主要用于回收-JNI调用程序(C或C++)如本地方法。
(3).垃圾识别方式:引用计数(存在相互引用的问题); 可达路径分析(判断有无gcRoot对象的引用),无gcRoot引用,初次执行finalize方法,若其中添加gcRoot引用则不回收,若初次执行finalize方法未添加gcRoot引用,第一次也不回收,第二次回收时不再执行finalize方法,直接进行垃圾回收。
(4).gcRoot对象: 可以是栈或本地栈或方法区静态量以及方法区常量所引用的对象。
垃圾回收算法:
(1).复制:将存活的对象复制到另一分区,无内存碎片,但总闲置一半空间。
(2).标记清除:可达性判断标记,清除后产生内存碎片,无法存连续的对象。
(3).标记整理:可达性判断标记,移动至尾端,再清除,但频繁移动效率低。
(4).分代回收:年轻代-复制,年老代-标记清除或标记整理,依据垃圾回收器而不同。
(5).FullGC回收年轻和年老代造成STW性能消耗大,MinorGC也造成STW但消耗小。
(6).StopTheWorld(STW):除垃圾回收线程工作,其他工作线程挂起避免边回收边生产。
(7).SafePoint(安全点):此时没有线程工作,此时是GC垃圾回收线程切入的时机。
(8).SafePoint规则:太少造成GC时间长停顿长、太多造成GC较频繁负荷高。
垃圾回收器:
(1).Serial:单线程GC回收器适于Client模式虚拟机,年轻代默认复制算法。
(2).SerialOld:单线程GC回收器,年老代标记整理算法,适用于单核CPU。
(3).PerNew:多GC线程版Serial,适于Server模式,响应快且STW时间短,复制算法。
(4).CMS:多线程版,适于Server模式,年老代标记清除,以STW时间最短为目标,用标记清除节省整理内存的时间消耗。分为四步:初始标记、并发标记、重新标记、并发清除;初始标记和重新标记均STW,重新标记是因并发标记期有用户线程可能产新垃圾,缺点是吞吐量低(并发标记/并发清除可能有用户线程工作,默认启GC线程(CPU数+3)/4个)、浮动垃圾(并发标记/并发清除可能有用户线程产生垃圾,只能下次执行回收)、标记清除(内存碎片化,容易造成FullGC); CMS有2种机制回收触发:主动触发(内存不足)、被动触发(周期性2s执行回收一次)。
(5).ParallelScavenge:多线程、年轻代复制算法,关注吞吐量控制而非响应时间,适和用户交互少的后台运算,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
(6).ParallelOld:多线程、适于Server、年老代标记整理,吞吐量优先为目标。
(7).G1:多线程、适于Server、年轻年老同时用标记整理,支持用户线程并发且吞吐大,GC停顿时间可预测调整,优先回收大对象来防FullGC的STW时间长;缺点是需要记录新老代关系会占内存大、执行效率低、维护难;也是分为四步:初始标记、并发标记(有用户线程)、最终标记、筛选回收;通过记忆集记录新老对象跨代的引用关系,避免回收有跨代引用的同时不必全扫描查找。记忆集存储跨代引用指针的集合,根据记录精度分3种:字长精度(记录机器字长)、对象精度(记录对象)、卡精度(记录1块内存区);G1用卡精度:卡表将内存切为多个部分,1个部分中只要有一个新老关系引用,此部分标记为扫描,以此代替全空间扫描,卡精度的新老两者关系类似复制算法。
(8).ZGC:支持TB级内存、停顿时间10ms内、程序吞吐小于15%,无新老代区分。标记/复制/迁移过程均是并发,比G1更好的处理大对象,高吞吐低延迟,但也有浮动垃圾。内存分三类容量:小型容量2MB空间(存<256KB的对象)、中型容量32MB空间(存>256K且<4MB的对象)、大型容量动态变化(必需2MB的整数倍)存>4MB的对象。
GC类型:因为需要判断栈上数据的类型则有以下类型GC
(1).保守式GC:据一些条件猜测,不符则回收,符合的不一定都是不回收的,但都保留。
(2).半保守式GC:在对象上记录类型信息,知晓对象所包含信息,不完全用条件猜测。
(3).准确式GC:准确知道对象的类型、栈上的引用的类型,映射表(OopMap)记录此类型对象的偏移量和对应的类型数据,形成(映射表)OopMap供GC使用。
(4).安全点介入时机:循环尾部(非counted循环)、方法返回前/call指令后、异常位置。
(5).增量式GC:应用线程执行中,穿插执行GC可抽象为三色标记(代表GC中对象状态):
三色标记算法:白代表未搜索到的对象、灰代表未搜索完的对象、黑代表已搜索完的对象;GC执行前,对象均为白色,有gcRoot引用的对象压栈并标为灰色,搜索其子对象标为灰入栈,子对象均为灰入栈后,根对象标黑结束,黑根的子级再搜索,重复上述过程。GC结束后,没有灰色,只有黑色存活,白色回收掉,下次GC重置黑色为白色,再计算。
(6).多标问题:黑对象的父引用被删,黑无可达根,但为黑则需下次GC回收,可接受。
(7).漏标问题:使用的对象被当垃圾回收,问题严重不可接受;2种情况对应2种解决:1-插入黑对白的引用,当黑不再搜索时则白也不被搜索、2-删灰对白的引用,白未被搜索就不会标灰。解决漏标:1-增量更新(写屏障-将白标灰)、2-STAB(写屏障-保留要删的引用-将白标灰)
3. 类加载:
(1).前置流程:java代码文件、经过编译器(词法分析,语法分析,语义分析,字节码生成器)后变为.class文件、进入JVM执行类加载。
(2).类加载:.class文件验证(判断名称合法性)、准备(开辟存储空间)、解析(符号转为直接引用)、初始化、销毁(GC回收)。
(3).进入系统:类加载后执行字节码校验与翻译和编译执行JIT,最后进入操作系统。
(4).双亲委派机制:启动类加载器、扩展类加载器、系统类加载器、自定义类加载器;上级可以创建则上级创建,避免类重复加载和java核心Api被篡改风险。
(5).跨平台性:JVM通过加载.class文件(字节码文件),实现不同平台安装JVM能运行。
(6).两个对象只有全路径名相同并且类加载器也相同时,确定为同一对象。
4. 性能调优
(1).jps指令:显示指定系统所有虚拟机进程。
(2).jstat指令:监控虚拟机运行状态,显示类加载、内存、垃圾回收等运行数据。
(3).jmap指令:生成heap dump文件。
(4).jhat指令:分析生成的dump文件 可在浏览器中查看。
(5).jstack指令:生成虚拟机当前线程快照。
(6).jinfo指令:实时查看和调整虚拟机运行参数。
(7).jconsole工具:监控JVM的内存、线程、类等。
(8).jvisualvm工具:分析内存和线程快照,监控内存和GC变化。
(9).MAT工具:查找内存泄漏和减少内存消耗。
(10).GChisto工具:分析GC日志工具。
(11).调优目标: GC低停顿、GC低频率、低内存占⽤、⾼吞吐量。
(12).JVM调优:分析GC⽇志和dump⽂件(判断是否需优化),确定瓶颈点、调优量化⽬标。
5. 执行模式
(1).解释执行模式:代码全解释执行不做JIT编译,立即执行,节省时间和内存。
(2).编译执行模式:代码全JIT编译执行,代码编译为本地代码,执行效率高。
(3).混合执行模式:部分解释/部分编译-执行,高频调的函数-编译执行,默认混合模式
6. 编译器优化:编译器优化技术:
(1).公共子表达式消除(即公共部分已计算不必再计算)。
(2).数组边界检查消除(确定访问的数组位不超过范围时不进行边界检查)。
(3).方法内联(剔除无用方法,没必要的方法跳转放入发起调用的方法中)。
(4).逃逸分析(定义的对象,外部方法访问-方法逃逸,外部线程访问-线程逃逸),无逃逸的对象可优化 (栈上分配空间、消除线程同步、对象不拆解-整体使用)。
以上就是个人的总结,一些知识点还是没说到,篇幅有限多多包涵,后记一些操作笔记吧:
JVM调优前工作:
配置jstatd的远程RMI服务(远程服务器上看JAVA程序GC情况),可JVM工具查看使用情况
grant codebase "file:${java.home}/../lib/tools.jar" { permission java.security.AllPermission; };
代码存为文件jstatd.all.policy,放JAVA_HOME/bin中,执行which java查JAVA_HOME目录
执行命令jstatd -J-Djava.security.policy=jstatd.all.policy -J-Djava.rmi.server.hostname =服务器ip地址 & 执行C:\glassfish4\jdk7\bin\jvisualvm.exe 打开JVM控制台。
工具--插件--中找到Visual GC插件进行安装.
对要执行java程序进行调优,在该jar包所在目录下建立一个start.sh文件,文件内容如下
java
-server
-Xms4G
-Xmx4G
-Xmn2G
-XX:SurvivorRatio=1
-XX:+UseConcMarkSweepGC
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=1100
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
-jar c1000k.jar&
通过配置,使用JVM控制台即可查看JVM(CPU/内存)及垃圾回收的情况
控制台配置 打开\jvisualvm.exe,远程---添加远程主机---输入远程IP----添加JMX连接
JVM调优核心:调整年轻代、年老代的内存空间大小、GC回收器的类型等,分析start.sh内容
S0C是:Survivor0区的分配空间
S0U是:Survivor1区的已经使用的空间
EC是:Eden区所使用的空间
EU是:Eden区当前使用的空间
OC是:老年代分配的空间
OU是:老年代当前使用的空间
PC是:永久代分配的空间
PU是:永久代当前使用的空间
YGC是:年轻代发生的次数,这里是354次
YGCT是:年轻代发送的总时长,这里是54.272秒,因此每次年轻代发生GC,即平均每次stop-the-world的时长为54.272/354=0.153秒。
FGC是:年老代回收的次数,或者成为FullGC的次数。
FGCT是:年老代发生回收的总时长。
GCT是:包括年轻代YGC及年老代FGC的总时间长。
MAC电脑上可以通过jconsole调出图形化分析工具。
声明:因文章是个人笔记,顾偏向于知识总结,文中提到的相关知识还需细化学习,因篇幅有限无法逐一讲解,最后谢谢支持与指正。