Jvm
关于jvm:Jvm监控工具Jvisualvm的安装配置
阐明:在linux零碎中对jvm有一套监控命令,命令的应用办法即作用本文不做赘述,可自行学习(参考地址:http://blog.csdn.net/fenglibing/article/details/6411999)。 本文重点介绍监控工具jvisualvm(另称为VisualVM)的应用。 Jvisualvm的启动1、 须要装置jdk 2、 在jdk的装置门路下的bin目录中找到jvisualvm.exe,双击即可启动 (我本地的门路为D:Program FilesJavajdk1.8.0_20binjvisualvm.exe) 启动后的界面如图 Jvisualvm监控本地资源如果java程序运行在本机,则jvisualvm启动之后,默认在“本地”一栏中能够间接看到 Jvisualvm监控近程资源应用jmx形式因为我监控的指标机应用的是tomcat,因而以tomcat为例进行阐明 Tomcat加war包的配置这种比拟场景,将服务打成war包放在tomcat中,批改tomcat的配置即可进行监控。 步骤1:在须要监控的指标机上找到tomcat的配置(linux为catalina.sh,Windows为catalina.bat,上面以linux的catalina.sh为例进行阐明) 步骤2:在catalina.sh中增加以下配置 JAVA_OPTS="-Dcom.sun.management.jmxremote.port=9998 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Djava.rmi.server.hostname=172.31.7.37" 增加后的成果如图; 步骤三:增加之后重启tomcat 步骤四:在jvisualvm的“近程”栏下右键增加近程主机 步骤五:在近程主机中右键抉择增加jmx链接 步骤六:在jmx链接的配置中输出指标机ip及端口,连贯即可 步骤七:双节已增加的近程机器,即可关上次要的监控界面 ~~~~ 服务内嵌tomcat的配置这种形式在linux下也比拟常见,服务不是war包放在tomcat下,而是通过援用tomcat的jar包调用内置的tomcat来运行。这种形式通常在服务的启动脚本中增加相干监控配置,常见的启动脚本命名为start.sh、server.sh等 以基线我的项目的安全监管服务文字鉴黄接口为例: 须要监控的服务为safety-server,启动目录:safety-server/bin/ start.sh 在服务的bin目录下,编辑启动脚本:vim start.sh 操作1:执行脚本中增加配置项参数: 操作2:给配置参数设定配置信息 操作3:重启服务 Jstatd形式VisualVM在监控本地JVM的时候是很不便的。只有利用程序运行起来,咱们就能够从VisualVM外面监控进去。然而有些性能在该监控模式下会有限度 设置的步骤如下: 步骤一:在远程目标机上找到java的jdk装置门路(能够应用这个命令进行查找:find / -name java) 步骤二:在jdk的bin目录下查找是否存在jstatd小插件 查找命令:ls -l | grep jst 查找后果: 步骤三:在目录下创立安全策略文件,文件名为jstatd.all.policy (文件名能够本人取) 策略文件内容为 grant codebase "file:${java.home}/../lib/tools.jar" {permission java.security.AllPermission; }; 即如图所示: 步骤四:查看homename值,是否为指标机本人的ip ...
关于jvm:初识JVM
JVM是什么jvm和Java的关系 JVM是什么虚拟机是一种抽象化的计算机,通过在理论的计算机上仿真模仿各种计算机性能来实现的。Java虚拟机有本人欠缺的硬体架构,如处理器、堆栈、寄存器等,还具备相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相干的信息,使得Java程序只需生成在Java虚拟机上运行的指标代码(字节码),就能够在多种平台上不加批改地运行。 所谓的java虚拟机,就是一台虚构的机器。它是一款软件,用来执行一系列虚构计算机指令,大体上虚拟机能够分为零碎虚拟机和程序虚拟机。visual Box、VMare就属于零碎虚拟机。他们齐全是对物理计算机的仿真,提供一个可运行残缺操作系统的软件平台。而java虚拟机就是典型程序虚拟机,它专门为执行单个计算机程序而设计,在java虚拟机中执行的指令咱们称之为java字节码指令。 比方一台服务器上运行着两个用Java编写的程序,一个博客零碎,一个电商零碎,那么这两个Java程序、Java虚拟机、服务器之间是什么关系呢? 从上图能够看进去,Java写的博客零碎,电商零碎都是运行在jvm之上,这两个Java程序启动也对应着会启动两个JVM实例。JVM是运行在服务器的零碎之上,服务器能够是linux零碎,能够是windows零碎,能够是centos零碎,各种零碎都能够。这也就是常说的Java是一次编译到处运行。可能这么说还是不通俗易懂,至多我刚开始学Java的时候我的老师这么说我就没有了解。那么我就说说我本人工作后的了解。 咱们跑Java程序之前首先必定是装jdk,咱们在windows装jdk环境是用的windows版本的jdk,在linux上装jdk用的linux版本的jdk;jdk咱们就能够了解为由Java程序设计语言、Java虚拟机、Java类库组成(Java Development kit)。咱们写的Java程序能够在服务器上跑的前提是这个服务器装了jdk环境。也就是只有是服务器装了jdk环境,咱们用maven或者gradle或者其余工具编译打包好的Java程序jar包就能够间接运行,我不须要在Java程序外面去适配你的windos环境还是linux环境还是centos环境。 jvm和Java的关系jvm和Java是什么关系呢,jvm和Java是不是只能相互依存呢?我刚开始学Java的时候接触到jvm,我就说这么认为的,因为jvm又叫Java虚拟机,所以我认为jvm就是专门为Java设计的。Java有Java本人的标准,依据这个标准也衍生出多种不同的jdk,比方Oracle的jdk,sun的jdk(sun曾经被oracle收买)IBM的jdk;虚拟机也有虚拟机本人的标准,依据这个标准也衍生出了多种不同的虚拟机,比方:武林盟主HotSpot虚拟机,小家碧玉Mobile Embedded VM,天下第二BEA JRockit/IBM J9虚拟机等。然而2018年4月,Oracle Labs新出了一个虚拟机Graal VM,口号是:“Run Programs Faster Anywhere”与Java刚诞生时候的“Write Once,Run Anywhere”一唱一和,Graal VM是一个增强版虚拟机,能够作为“任何语言”的运行平台应用,这里“任何语言”包含了Java、Scala、Groovy、Kotlin等基于Java虚拟机之上的语言,还包含了C、C++、Rust等基于LLVM的语言,同时反对其余像JavaScript、Ruby、Python和R语言等。Graal VM能够无额定开销地混合应用这些编程语言,反对不同语言中混用对方的接口和对象,也可能反对这些语言应用曾经编写好的本地库文件。 虚拟机的介绍就先说到这,置信聪慧的你曾经对虚拟机有一个大抵的意识,下一章将虚拟机的内存构造,这也是当初Java面试的高频题,然而大多数人必定都只是理解堆,栈,程序计数器,本地办法栈,而后再理解一下堆中分了年老代,老年代这些,这也是培训机构教jvm局部的模板,然而每年培训出那么多人,你难道就不想比他们多晓得那么一点吗?各位人才应该都懂的,咱们下一章见。 各位人才,各位大佬,有不对的中央各位请不吝指教!都看到这儿了,点赞珍藏转发三连一下?祝各位早日找到女朋友。喜爱的敌人能够关注一下我的公众号敲代码的蛋蛋,一起成长,一起骚起来呀!!! 我是敲代码的蛋蛋,一个致力触摸编程门栏的老手。明天的你是否比昨天的你更优良了呢? 参考文档: <<深刻了解Java虚拟机>> ----周志明 <<百度百科>>
关于jvm:记一次堆外内存泄漏排查过程
本文波及以下内容 开启NMT查看JVM内存应用状况通过pmap命令查看过程物理内存应用状况smaps查看过程内存地址gdb命令dump内存块背景最近收到运维反馈,说有我的项目的一个节点的RSS曾经是Xmx的两倍多了,因为是ECS机器所以我的项目能够始终运行,幸好机器内存短缺,不然就可能影响到其余利用了。 排查问题通过跳板机登录到指标机器,执行top命令,再按c,看到对应的过程所占用的RES有8个多G(这里过后遗记截图了),然而实际上咱们配置的Xmx只有3G,而且程序还是失常运行的,所以不会是堆占用了这么多,于是就把问题方向指向了非堆的内存。 首先想到通过Arthas查看内存散布状况,执行dashboard命令,查看内存散布状况 发现堆和非堆的内存加起来也就2个G不到,然而这里看到的非堆只蕴含了code_cache和metaspace的局部,那有没有可能是其余非堆的局部有问题呢? NMTNMT是Native Memory Tracking的缩写,是Java7U40引入的HotSpot新个性,开启后能够通过jcmd命令来对JVM内存应用状况进行跟踪。留神,依据Java官网文档,开启NMT会有5%-10%的性能损耗。-XX:NativeMemoryTracking=[off | summary | detail] # off: 默认敞开 # summary: 只统计各个分类的内存应用状况.# detail: Collect memory usage by individual call sites.增加-XX:NativeMemoryTracking=detail命令到启动参数中,而后重启我的项目。 跑了一段时间后top看了下过程的RES,发现曾经5个多G了 执行jcmd <pid> VM.native_memory summary scale=MB 从图中能够看到堆和非堆的总应用内存(committed)也就2G多,那还有3个G的内存去哪里了呢。 pmappmap命令是Linux上用来开过程地址空间的执行pmap -x <pid> | sort -n -k3 > pmap-sorted.txt命令能够依据理论内存排序 查看pmap-sorted.txt文件,发现有大量的64M内存块难道是linux glibc 中经典的 64M 内存问题?之前看挖坑的张师傅写过一篇文章(一次 Java 过程 OOM 的排查剖析(glibc 篇))讲过这个问题,于是筹备参考一下排查思路 尝试设置环境变量MALLOC_ARENA_MAX=1,重启我的项目,跑了一段时间当前,再次执行pmap命令查看内存状况,发现并没有什么变动,看来并不是这个起因,文章前面的步骤就没有什么参考意义了。 smaps + gdb既然能够看到有这么多异样的内存块,那有没有方法晓得外面存的是什么内容呢,答案是必定的。通过一番材料查阅,发现能够通过gdb的命令把指定地址范畴的内存块dump进去。 要执行gdb的dump须要先晓得一个地址范畴,通过smaps能够输入过程应用的内存块详细信息,包含地址范畴和起源 cat /proc/<pid>/smaps > smaps.txt查看smaps.txt,找到有问题的内存块地址,比方下图中的 7fb9b0000000-7fb9b3ffe000 启动gdb ...
关于jvm:七款经典垃圾回收器
SerialSerial 是一款单线程的垃圾回收器,在进行垃圾回收的时候会产生Stop the world 。对于其余垃圾收集器而言,他的内存占用是最小的。 ParNewParNew 实际上是Serial 收集器的并行版本 Parallel Scavenge重视吞吐量的垃圾收集器。吞吐量= 运行用户代码的工夫/(垃圾回收的工夫+运行用户代码的工夫)他有两个参数用于准确管制吞吐量大小:-XX:MaxGCPauseMills:管制垃圾回收的工夫 这个值并不是越小越好,咱们晓得,内存越小,进行垃圾回收的工夫就越短。如果咱们将这个值设置地较小,那么回收的内存局部也就小。这也就导致gc 进行垃圾回收的频率也越高,这样吞吐量也就降落了。-XX:GCTimeRatio:垃圾回收工夫的比例这个值默认是99 .也就是说 垃圾回收的工夫只占1%(1/1+99) Serial Old是serial收集器的老年代版本有两个用处 在jdk5及之前的版本中与Parallel Scavenge 收集器搭配应用在cms收集器产生失败的后备预案Parallel Old是Parallel Scavenge 的老年代版本 CMS(Current Mark and Sweep)真正意义上的并发收集器,第一次实现了让_垃圾_收集线程与用户线程(基本上)同时工作整个过程能够分为四局部: 初始标记扫描和GCRoots间接关联的对象,速度很快并发标记遍历整个对象图,工夫较长从新标记标记产生变动的那一部分对象的标记记录并发分明清标记阶段断定死亡的对象毛病: Garbage First基于region的垃圾回收器
关于jvm:jvm疯狂吞占内存罪魁祸首是谁
导读 JVM是Java Virtual Machine的缩写,中文名为Java虚拟机。它是一种用于计算设施的标准,是一个虚构进去的计算机,次要通过在理论的计算机上仿真模仿各种计算机性能来实现的。在理论使用过程中,易观技术人员留神到一台开发机上各个微服务过程占用内存很高,随即便开展了考察...... 景象:前段时间发现某台开发机上各个微服务过程占用内存很高,这里记录下解决思路,仅供参考。 Centos6.10+Jdk1.8+SpringBoot1.4.4环境下各个JVM过程所占内存应用状况VIRT和RES都很高...... 以其中某个过程为例(过程启动日期为8月9日,排查时候的工夫是8月10日10:54:58,也就是说该过程运行工夫应该不会超过48小时) top命令查看该过程占用内存状况(能够看到此过程曾经占用2.7G物理内存) 为了排除掉是因为中途有压力测试的嫌疑,将此服务进行了重启,然而刚起的过程(19146), 占内存状况RES:1.8G, VIRT:33.4G … JVM过程动不动就是2G以上的内存,然而开发环境并没什么业务申请,谁是罪魁祸首 ? 解决问题之前,先温习下几个基础知识。1. 什么是RES和VIRT? RES:resident memory usage 常驻内存(1)过程以后应用的内存大小,但不包含swap out (2)蕴含其余过程的共享 (3)如果申请100m的内存,理论应用10m,它只增长10m,与VIRT相同 (4)对于库占用内存的状况,它只统计加载的库文件所占内存大小 RES = CODE + DATA VIRT:virtual memory usage(1)过程“须要的”虚拟内存大小,包含过程应用的库、代码、数据等 (2)如果过程申请100m的内存,但理论只应用了10m,那么它会增长100m,而不是理论的使用量 VIRT = SWAP + RES 2. Linux与过程内存模型 3. JVM内存模型(1.7与1.8之间的区别) 所以JVM过程内存大小大抵为: 非heap(非heap=元空间+栈内存+…)+heap+JVM过程运行所需内存+其余数据 那么会是jvm内存透露引起的吗?应用Jmap命令将整个heap dump下来,而后用jvisualvm剖析 能够看到,堆内存一切正常(dump会引起FGC,但并不影响此论断) 那么可能是SpringBoot的起因吗?为了验证此问题,通过部署零碎在开发机上起了1个没有任何业务代码的springboot过程,仅仅是引入注册核心 查看此过程内存占用状况: 显著曾经设置了Xmx为512MB,尽管Xmx不等于最终JVM所占总内存,但至多也不会偏差太多; 那么应用jmap命令查看以后jvm堆内存配置和应用状况(上面的图2是在图1现场5分钟之后截取的) **(图1) ** (图2) 所以从2次的jmap后果中,能够得出以下几个论断: 咱们的Xmx设置并没有失效,因为MaxHeapSize≠Xmx图1中jvm占用内存计算:元空间(20.79MB)+ eden(834MB)+年轻代(21MB)+线程栈(38*1024KB)+JVM过程自身运行内存+ NIO的DirectBuffer +JIT+JNI+…≈top(Res) 1.1G ...
关于jvm:JVM-类加载机制
1. JVM细节版架构图 2. 类加载器子系统的作用 类加载器子系统负责从文件系统或者网络中加载Class文件,Class文件在文件结尾有特定的文件标识。ClassLoader只负责class文件的加载,至于它是否能够运行,则由Execution Engine决定。加载的类信息寄存于一块称为办法区的内存空间。除了类的信息外,办法区中还会寄存运行时常量池信息,可能还包含字符串字面量和数字常量(这部分常量信息是Class文件中常量池局部的内存映射)3. 类的加载过程 1. 加载 loading通过一个类的全限定名获取定义此类的二进制字节流。将这个字节流所代表的动态存储构造转化为办法区的运行时存储构造。在内存中生成一个代表这个类的 java.lang.Class 对象,作为办法区这个类的各种数据的拜访入口。 2. 链接 linking1. 验证1.目标在于确保Class文件的字节流中蕴含信息合乎以后虚拟机要求,保障被加载类的正确性,不会危害虚拟机本身平安。 2.次要包含四种验证,文件格式验证,源数据验证,字节码验证,符号援用验证。 2. 筹备 prepare1.为类变量分配内存并且设置该类变量的默认初始值,即零值。2.这里不蕴含用 final 润饰的 static 变量,因为 final 在编译的时候就调配了,筹备阶段会显式初始化。3.这一阶段不会为实例变量调配初始化,类变量会调配在办法区中,而实例变量是会随着对象一起调配到java堆中。 3. 解析1.将常量池内的符号援用转换为间接援用的过程。 2.事实上,解析过程在某些状况下能够在初始化阶段之后再开始,这是为了反对 Java 的动静绑定。 3.符号援用就是一组符号来形容所援用的指标。符号援用的字面量模式明确定义在《java虚拟机标准》的class文件格式中。间接援用就是间接指向指标的指针、绝对偏移量或一个间接定位到指标的句柄。 4.解析动作次要针对类或接口、字段、类办法、接口办法、办法类型等。对应常量池中的CONSTANT_Class_info/CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。 3. 初始化初始化阶段就是执行类结构器办法<clinit>()的过程。此办法不需定义,是 javac 编译器主动收集类中的所有类变量的赋值动作和动态代码块中的语句合并而来。结构器办法中指令按语句在源文件中呈现的程序执行。<clinit>()不同于类的结构器。(关联:结构器是虚拟机视角下的<init>())若该类具备父类,JVM会保障子类的<clinit>()执行前,父类的<clinit>()曾经执行结束。虚拟机必须保障一个类的<clinit>()办法在多线程下被同步加锁,因为类只能加载一次。public class ClassInitTest{ private static int num = 1; static{ num = 2; number = 20; System.out.println(num); // System.out.println(number); // 报错:非法的前向援用 } private static int number = 10; // linking之prepare:number=0 --> initial: 20 -->10 public static void main(String[] args){ System.out.println(ClassIniTest.num); // 2 System.out.println(ClassInitTest.number); //10 } }上述代码<clinit>()办法的字节码:只收集动态变量和动态代码块中的赋值语句 ...
关于jvm:JVM-垃圾收集器与内存分配策略
1. 判断对象是否被回收1. 援用计数算法在对象中增加一个援用计数器,每当有一个中央援用它时, 计数器值加1;当援用生效时, 计数器值减1;任何时刻计数器为零的对象就是不可能再被应用的。(很难解决对象互相循环援用的问题。) 两个对象互相援用时,虚拟机也会进行回收,因而Java虚拟机并不是通过计数算法来判断对象存活的。 2. 可达性剖析算法以 GC Roots 为起始点依据援用关系进行搜寻,可达的对象都是存活的,不可达的对象可被回收。 虚拟机栈中局部变量表中援用的对象本地办法栈中 JNI(Native 办法) 中援用的对象办法区中类动态属性援用的对象办法区中的常量援用的对象3. 援用无论是通过援用计数算法判断对象的援用数量, 还是通过可达性剖析算法判断对象是否援用可达, 判断对象是否存活都和“援用相干”。 1. 强援用被强援用的对象不会被回收。 应用 new 一个新对象的形式来创立强援用。 Object obj = new Object()2. 软援用被软援用关联的对象只有在内存不够的状况下才会被回收。应用 SoftReference 类来创立软援用。 3. 弱援用被弱援用关联的对象只能存活到下一次垃圾收集产生为止。所以肯定会被回收。 应用 WeakReference 类来实现弱援用。 4. 虚援用又称为幽灵援用或者幻影援用,一个对象是否有虚援用的存在,不会对其生存工夫造成影响,也无奈通过虚援用失去一个对象。 为一个对象设置虚援用的惟一目标是能在这个对象被回收时收到一个零碎告诉。 应用 PhantomReference 来创立虚援用。 4. finalize()当一个对象可被回收时,如果须要执行该对象的 finalize() 办法,那么就有可能在该办法中让对象从新被援用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 办法自救,前面回收时不会再调用该办法。 finalize() 办法运行代价很高,不确定性大,无奈保障各个对象的调用程序,因而最好不要应用。应用 try-finally 或者其余形式能够做得更好。 5. 回收办法区因为办法区次要寄存永恒代对象,而永恒代对象的回收率比新生代低很多,所以在办法区上进行回收性价比不高。 次要是对常量池的回收和对类的卸载。 为了防止内存溢出,在大量应用反射和动静代理的场景都须要虚拟机具备类卸载性能。 类的卸载条件很多,须要同时满足以下三个条件,并且满足了条件也不肯定会被卸载: 该类所有的实例都曾经被回收,此时堆中不存在该类的任何实例。加载该类的 ClassLoader 曾经被回收。该类对应的 Class 对象没有在任何中央被援用,也就无奈在任何中央通过反射拜访该类办法。2. 垃圾收集算法1. 分代收集实践将回收对象根据其年龄(对象熬过垃圾收集过程的次数)将Java堆划分出不同的区域。个别将堆分为新生代和老年代。 新生代应用:标记 - 复制算法老年代应用:标记 - 革除 或者 标记 - 整顿 算法长处: ...
关于jvm:JVM-运行时数据区域
1. 程序计数器记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本中央法令为空)。每条线程都须要一个独立的程序计数器。 2. Java虚拟机栈与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程公有的,它的生命周期与线程雷同。 每个 Java 办法在执行的同时会创立一个栈帧用于存储局部变量表、操作数栈、常量池援用等信息。从办法调用直至执行实现的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 局部变量表所需内存空间在编译期间实现调配,寄存了: 根本数据类型(boolean、byte、char、shrot、int、float、long、double)对象援用(指向对象起始地址的援用指针)两种异样:StackOverflowError:线程申请栈深度大于虚拟机所容许深度OutofMemoryError: 无奈申请到足够空间 3. 本地办法栈本地办法栈与 Java 虚拟机栈相似,它们之间的区别只不过是本地办法栈为本地办法服务。 4. Java 堆是被所有线程共享的一块内存区域,所有对象都在这里分配内存,是垃圾收集的次要区域("GC 堆")。异样 OutofMemoryError:Java堆中没有内存实现实例调配时抛出该异样。 5. 办法区与Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、动态变量、即时编译器编译后的代码缓存等数据。 对这块区域进行垃圾回收的次要指标是对常量池的回收和对类的卸载,然而个别比拟难实现。 异样 OutofMemoryError:办法区无奈满足新的内存调配要求时。 6. 运行时常量池运行时常量池是办法区的一部分。 Class 文件中的常量池表(寄存编译器生成的字面量和符号援用)会在类加载后被放入办法区的运行时常量池中。 动态性: 解决编译器产生常量,运行期间也能够将新的常量放入池中,例如String类的intern()办法. 异样 OutofMemoryError:收到办法区内存的限度,常量池无奈再申请到内存时抛出该异样。 7. 间接内存在 JDK 1.4 中新引入了 NIO 类,它能够应用 Native 函数库间接调配堆外内存,而后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的援用进行操作。这样能在一些场景中显著进步性能,因为防止了在堆内存(Java堆)和堆外内存(Native堆)来回拷贝数据。 异样 OutofMemoryError:本机间接内存的调配会受到本机总内存大小以及处理器寻址空间的限度。 参考资料https://github.com/CyC2018/CS...https://www.cnblogs.com/czwbi...
关于jvm:十个问题弄清JVMGC二
简介: 每个java开发同学不论是日常工作中还是面试里,都会遇到JDK、JVM和GC的问题。本文会从以下10个问题为切入点,带着大家一起全面理解一下JVM的方方面面。 每个java开发同学不论是日常工作中还是面试里,都会遇到JDK、JVM和GC的问题。本文会从以下10个问题为切入点,带着大家一起全面理解一下JVM的方方面面。 JVM、JRE和JDK的区别和分割JVM是什么?以及它的次要作用JVM的外围性能有哪些类加载机制和过程运行时数据区的逻辑构造JVM的内存模型如何确定对象是垃圾垃圾收集的算法有哪些各种问世的垃圾收集器JVM调优的参数配置上一篇文章结尾时咱们谈到,就JVM的设计规范,从应用用处角度JVM的内存大体的分为:线程公有内存区 和 线程共享内存区。 线程公有内存区在类加载器编译某个class文件时就确定了执行时须要的“程序计数器”和“虚构栈帧”等所需的空间,并且会随同着以后执行线程的产生而产生,执行线程的沦亡而沦亡,因而“线程公有内存区”并不需要思考内存治理和垃圾回收的问题。线程共享内存区在虚拟机启动时创立,被所有线程共享,是Java虚拟机所治理内存中最应该关注的和最大的一块。首先咱们来一起看一下“线程共享内存区”的内存模型是什么样的? 6、JVM的内存模型 如图所示,JVM的内存构造分为堆和非堆两大块区域。 其中“非堆”就是上篇文章咱们提到的办法区或叫元数据区,用来存储class类信息的。而“堆”是用来存储JVM各线程执行期间所创立的实例对象或数组的。堆辨别为两大块,一个是Old区,一个是Young区。Young辨别为两大块,一个是Survivor区(S0+S1),一块是Eden区S0和S1一样大,也能够叫From和To。之所以这样划分,设计者的目标无非就是为了内存治理,也就是咱们说的垃圾回收。那么什么样的对象是垃圾?垃圾回收算法有哪些?目前罕用的垃圾回收器又有哪些?这篇文章咱们一起弄清楚这些问题和知识点。 7、如何确定一个对象是垃圾?要想进行垃圾回收,得先晓得什么样的对象是垃圾。目前确认对象是否为垃圾的算法次要有两种:援用计数法和可达性分析法。 1、援用计数法:在对象中增加了一个援用计数器,当有中央援用这个对象时,援用计数器的值就加1,当援用生效的时候,援用计数器的值就减1。当援用计数器的值为0时,JVM就开始回收这个对象。对于某个对象而言,只有应用程序中持有该对象的援用,就阐明该对象不是垃圾,如果一个对象没有任何指针对其援用,它就是垃圾。这种办法尽管很简略、高效,然而JVM个别不会抉择这个办法,因为这个办法会呈现一个弊病:当对象之间互相指向时,两个对象的援用计数器的值都会加1,而因为两个对象时互相指向,所以援用不会生效,这样JVM就无奈回收。 2、可达性分析法:针对援用计数算法的弊病,JVM采纳了另一种算法,以一些"GC Roots"的对象作为起始点向下搜寻,搜寻所走过的门路称为援用链(Reference Chain),当一个对象到GC Roots没有任何援用链相连时,则证实此对象是不可用的,即能够进行垃圾回收。否则,证实这个对象有用,不是垃圾。 上图中的obj7和obj8尽管它们相互援用,但从GC Roots登程这两个对象不可达,所以会被标记为垃圾。JVM会把以下几类对象作为GC Roots: (1) 虚拟机栈(栈帧中本地变量表)中援用的对象;(2) 办法区中类动态属性援用的对象;(3) 办法区中常量援用的对象;(4) 本地办法栈中JNI(Native办法)援用的对象。注:在可达性剖析算法中不可达的对象,并不是间接被回收,这时它们处于缓刑状态,至多须要进行两次标记才会确定该对象是否被回收:第一次标记:如果对象在进行可达性剖析后发现没有与GC Roots相连接的援用链,那它将会被第一次标记; 第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()办法(该办法可将此对象与GC Roots建立联系)。在finalize()办法中没有从新与援用链建设关联关系的,将被进行第二次标记。 第二次标记胜利的对象将真的会被回收,如果对象在finalize()办法中从新与援用链建设了关联关系,那么将会逃离本次回收,持续存活。 8、垃圾收集的算法有哪些晓得了如何JVM确定哪些对象是垃圾后,上面咱们来看一下,面对这些垃圾对象,JVM的回收算法都有哪些。 1、 标记-革除算法(Mark-Sweep)第一步“标记”,如下图所示把堆里所有的对象都扫描一遍,找出哪些是垃圾须要回收的对象,并且把它们标记进去。 第二步“革除”,把第一步标记为“UnReference Object”(无援用或不可达)的对象革除掉,开释内存空间。 这种算法的毛病次要有两点: (1) 标记和革除两个过程都比拟耗时,效率不高 (2) 革除后会产生大量不间断的内存碎片空间,碎片空间太多可能会导致当程序后续须要创立较大对象时,无奈找到足够间断的内存空间而不得不再次触发垃圾回收。 2、 标记-复制算法(Mark-Copying)将内存划分为两块区域,每次应用其中一块,当其中一块用满,触发垃圾回收的时候,将存活的对象复制到另一块下来,而后把之前应用的那一块进行格式化,一次性革除洁净。 (革除前) (革除后) “标记-复制”算法的毛病不言而喻,就是内存空间利用率低。 3、 标记-整顿算法(Mark-Compact)标记整顿算法标记过程依然与"标记-革除"算法一样,然而后续步骤不是间接对可回收对象进行清理,而是让所有存活的对象都向一端挪动,而后间接清理掉端边界以外的内存。 将所有存活的对象向一边挪动,清理掉存活边界以外的全副内存空间。 联合这三种算法咱们能够看到, “标记-复制”算法的长处是回收效率高,但空间利用率上有肯定的节约。而“标记-整顿”算法因为须要向一侧挪动等一系列操作,其效率绝对低一些,但对内存空间治理上非常优异。因而,“标记-复制”算法实用于那些生命周期短、回收频率高的内存对象,而标记-整顿”算法实用于那些生命周期长、回收频率低,但重视回收一次内存空间失去足够开释的场景。因而JVM的设计者将JVM的堆内存,分为了两大块区域Young区和Old区,Young区存储的就是那些生命周期短,应用一两次就不再应用的对象,回收一次基本上该区域十之有八的对象全副被回收清理掉,因而Young区采纳的垃圾回收算法也就是“标记-复制”算法。Old区存储的是那些生命周期长,通过屡次回收后依然存活的对象,就把它们放到Old区中,平时不再去判断这些对象的可达性,直到Old区不够用为止,再进行一次对立的回收,开释出足够的间断的内存空间。 9、各种问世的垃圾收集器鉴于Young区和Old区须要采纳不同的垃圾回收算法,因而在JVM的整个垃圾收集器的演进各个时代里,针对Young区和Old区每个时代都是不同的垃圾收集机制。从JDK1.3开始到目前,JVM垃圾收集器的演进大体分为四个时代:串行时代、并行时代、并发时代和G1时代。 1、串行时代:Serial(Young区)+ Serial Old(Old区)JDK3(1.3)的时候,大略是2000年左右,那个时代根本计算机都是单核一个CPU的,因而垃圾回收最后的设计实现也是基于单核单线程工作的。并且垃圾回收线程的执行绝对于失常业务线程执行来说还是STW(stop the world)的,应用一个CPU或者一条收集线程去实现垃圾收集工作,这个线程执行的时候其它线程须要进行。 串行收集器采纳单线程stop-the-world的形式进行收集。当内存不足时,串行GC设置进展标识,待所有线程都进入平安点(Safepoint)时,利用线程暂停,串行GC开始工作,采纳单线程形式回收空间并整顿内存。单线程也意味着复杂度更低、占用内存更少,但同时也意味着不能无效利用多核优势。因而,串行收集器特地适宜堆内存不高、单核甚至双核CPU的场合。 2、并行时代:Parallel Scavenge(Young区) + Parallel Old(Old区)并行收集器是以关注吞吐量为指标的垃圾收集器,也是server模式下的默认收集器配置,对吞吐量的关注次要体现在年老代Parallel Scavenge收集器上。 ...
关于jvm:从10个问题切入带大家了解JVM的方方面面
每个java开发同学不论是日常工作中还是面试里,都会遇到JDK、JVM和GC的问题。本文会从以下10个问题为切入点,带着大家一起全面理解一下JVM的方方面面。 JVM、JRE和JDK的区别和分割JVM是什么?以及它的次要作用JVM的外围性能有哪些类加载机制和过程运行时数据区的逻辑构造JVM的内存模型如何确定对象是垃圾垃圾收集的算法有哪些各种问世的垃圾收集器JVM调优的参数配置上一篇文章结尾时咱们谈到,就JVM的设计规范,从应用用处角度JVM的内存大体的分为:线程公有内存区 和 线程共享内存区。 线程公有内存区在类加载器编译某个class文件时就确定了执行时须要的“程序计数器”和“虚构栈帧”等所需的空间,并且会随同着以后执行线程的产生而产生,执行线程的沦亡而沦亡,因而“线程公有内存区”并不需要思考内存治理和垃圾回收的问题。线程共享内存区在虚拟机启动时创立,被所有线程共享,是Java虚拟机所治理内存中最应该关注的和最大的一块。首先咱们来一起看一下“线程共享内存区”的内存模型是什么样的? 6、JVM的内存模型 如图所示,JVM的内存构造分为堆和非堆两大块区域。 其中“非堆”就是上篇文章咱们提到的办法区或叫元数据区,用来存储class类信息的。而“堆”是用来存储JVM各线程执行期间所创立的实例对象或数组的。堆辨别为两大块,一个是Old区,一个是Young区。Young辨别为两大块,一个是Survivor区(S0+S1),一块是Eden区S0和S1一样大,也能够叫From和To。之所以这样划分,设计者的目标无非就是为了内存治理,也就是咱们说的垃圾回收。那么什么样的对象是垃圾?垃圾回收算法有哪些?目前罕用的垃圾回收器又有哪些?这篇文章咱们一起弄清楚这些问题和知识点。 7、如何确定一个对象是垃圾?要想进行垃圾回收,得先晓得什么样的对象是垃圾。目前确认对象是否为垃圾的算法次要有两种:援用计数法和可达性分析法。 1、援用计数法:在对象中增加了一个援用计数器,当有中央援用这个对象时,援用计数器的值就加1,当援用生效的时候,援用计数器的值就减1。当援用计数器的值为0时,JVM就开始回收这个对象。对于某个对象而言,只有应用程序中持有该对象的援用,就阐明该对象不是垃圾,如果一个对象没有任何指针对其援用,它就是垃圾。这种办法尽管很简略、高效,然而JVM个别不会抉择这个办法,因为这个办法会呈现一个弊病:当对象之间互相指向时,两个对象的援用计数器的值都会加1,而因为两个对象时互相指向,所以援用不会生效,这样JVM就无奈回收。 2、可达性分析法:针对援用计数算法的弊病,JVM采纳了另一种算法,以一些"GC Roots"的对象作为起始点向下搜寻,搜寻所走过的门路称为援用链(Reference Chain),当一个对象到GC Roots没有任何援用链相连时,则证实此对象是不可用的,即能够进行垃圾回收。否则,证实这个对象有用,不是垃圾。 上图中的obj7和obj8尽管它们相互援用,但从GC Roots登程这两个对象不可达,所以会被标记为垃圾。JVM会把以下几类对象作为GC Roots: (1) 虚拟机栈(栈帧中本地变量表)中援用的对象;(2) 办法区中类动态属性援用的对象;(3) 办法区中常量援用的对象;(4) 本地办法栈中JNI(Native办法)援用的对象。注:在可达性剖析算法中不可达的对象,并不是间接被回收,这时它们处于缓刑状态,至多须要进行两次标记才会确定该对象是否被回收:第一次标记:如果对象在进行可达性剖析后发现没有与GC Roots相连接的援用链,那它将会被第一次标记; 第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()办法(该办法可将此对象与GC Roots建立联系)。在finalize()办法中没有从新与援用链建设关联关系的,将被进行第二次标记。 第二次标记胜利的对象将真的会被回收,如果对象在finalize()办法中从新与援用链建设了关联关系,那么将会逃离本次回收,持续存活。 8、垃圾收集的算法有哪些晓得了如何JVM确定哪些对象是垃圾后,上面咱们来看一下,面对这些垃圾对象,JVM的回收算法都有哪些。 1、 标记-革除算法(Mark-Sweep)第一步“标记”,如下图所示把堆里所有的对象都扫描一遍,找出哪些是垃圾须要回收的对象,并且把它们标记进去。 第二步“革除”,把第一步标记为“UnReference Object”(无援用或不可达)的对象革除掉,开释内存空间。这种算法的毛病次要有两点: (1) 标记和革除两个过程都比拟耗时,效率不高 (2) 革除后会产生大量不间断的内存碎片空间,碎片空间太多可能会导致当程序后续须要创立较大对象时,无奈找到足够间断的内存空间而不得不再次触发垃圾回收。 2、 标记-复制算法(Mark-Copying)将内存划分为两块区域,每次应用其中一块,当其中一块用满,触发垃圾回收的时候,将存活的对象复制到另一块下来,而后把之前应用的那一块进行格式化,一次性革除洁净。 (革除前) (革除后) “标记-复制”算法的毛病不言而喻,就是内存空间利用率低。 3、 标记-整顿算法(Mark-Compact)标记整顿算法标记过程依然与"标记-革除"算法一样,然而后续步骤不是间接对可回收对象进行清理,而是让所有存活的对象都向一端挪动,而后间接清理掉端边界以外的内存。 将所有存活的对象向一边挪动,清理掉存活边界以外的全副内存空间。 联合这三种算法咱们能够看到, “标记-复制”算法的长处是回收效率高,但空间利用率上有肯定的节约。而“标记-整顿”算法因为须要向一侧挪动等一系列操作,其效率绝对低一些,但对内存空间治理上非常优异。因而,“标记-复制”算法实用于那些生命周期短、回收频率高的内存对象,而标记-整顿”算法实用于那些生命周期长、回收频率低,但重视回收一次内存空间失去足够开释的场景。因而JVM的设计者将JVM的堆内存,分为了两大块区域Young区和Old区,Young区存储的就是那些生命周期短,应用一两次就不再应用的对象,回收一次基本上该区域十之有八的对象全副被回收清理掉,因而Young区采纳的垃圾回收算法也就是“标记-复制”算法。Old区存储的是那些生命周期长,通过屡次回收后依然存活的对象,就把它们放到Old区中,平时不再去判断这些对象的可达性,直到Old区不够用为止,再进行一次对立的回收,开释出足够的间断的内存空间。 9、各种问世的垃圾收集器鉴于Young区和Old区须要采纳不同的垃圾回收算法,因而在JVM的整个垃圾收集器的演进各个时代里,针对Young区和Old区每个时代都是不同的垃圾收集机制。从JDK1.3开始到目前,JVM垃圾收集器的演进大体分为四个时代:串行时代、并行时代、并发时代和G1时代。 1、串行时代:Serial(Young区)+ Serial Old(Old区)JDK3(1.3)的时候,大略是2000年左右,那个时代根本计算机都是单核一个CPU的,因而垃圾回收最后的设计实现也是基于单核单线程工作的。并且垃圾回收线程的执行绝对于失常业务线程执行来说还是STW(stop the world)的,应用一个CPU或者一条收集线程去实现垃圾收集工作,这个线程执行的时候其它线程须要进行。 串行收集器采纳单线程stop-the-world的形式进行收集。当内存不足时,串行GC设置进展标识,待所有线程都进入平安点(Safepoint)时,利用线程暂停,串行GC开始工作,采纳单线程形式回收空间并整顿内存。单线程也意味着复杂度更低、占用内存更少,但同时也意味着不能无效利用多核优势。因而,串行收集器特地适宜堆内存不高、单核甚至双核CPU的场合。 2、并行时代:Parallel Scavenge(Young区) + Parallel Old(Old区)并行收集器是以关注吞吐量为指标的垃圾收集器,也是server模式下的默认收集器配置,对吞吐量的关注次要体现在年老代Parallel Scavenge收集器上。 并行收集器与串行收集器工作模式类似,都是stop-the-world形式,只是暂停时并行地进行垃圾收集。年老代采纳复制算法,老年代采纳标记-整顿,在回收的同时还会对内存进行压缩。关注吞吐量次要指年老代的Parallel Scavenge收集器,通过两个指标参数-XX:MaxGCPauseMills和-XX:GCTimeRatio,调整新生代空间大小,来升高GC触发的频率。并行收集器适宜对吞吐量要求远远高于提早要求的场景,并且在满足最差延时的状况下,并行收集器将提供最佳的吞吐量。 3、 并发时代:CMS(Old区)并发标记革除(CMS)是以关注提早为指标、非常优良的垃圾回收算法,CMS是针对Old区的垃圾回收实现。 老年代CMS每个收集周期都要经验:初始标记、并发标记、从新标记、并发革除。其中,初始标记以STW的形式标记所有的根对象;并发标记则同利用线程一起并行,标记出根对象的可达门路;在进行垃圾回收前,CMS再以一个STW进行从新标记,标记那些由mutator线程(指引起数据变动的线程,即利用线程)批改而可能错过的可达对象;最初失去的不可达对象将在并发革除阶段进行回收。值得注意的是,初始标记和从新标记都已优化为多线程执行。CMS非常适合堆内存大、CPU核数多的服务器端利用,也是G1呈现之前大型利用的首选收集器。 - 但CMS有以下两个缺点:(1)因为它是标记-革除不是标记-整顿,因而会产生内存碎片,Old区会随着工夫的推移而究竟被耗尽或产生无奈调配大对象的状况。最初不得不通过底层的担保机制(CMS背地有串行的回收作为兜底)进行一次Full GC,并进行内存压缩。(2)因为标记和革除都是通利用线程并发进行,两类线程同时执行时会减少堆内存的占用,一旦某一时刻内存不够用,就会触发底层担保机制,又采纳串行回收进行一次STW的垃圾回收。4、G1时代:Garbage FirstG1收集器时代,Java堆的内存布局与就与其余收集器有很大差异,它将整个Java堆划分为多个大小相等的独立区域(Region),尽管还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不须要间断)的汇合。 ...
关于jvm:偏向锁的批量重偏向与批量撤销机制
前言从网上看了很多批量重偏差与批量撤销的介绍,但都只知其一;不知其二,本着钻研的精力,应用数据进行剖析批量重偏差与批量撤销的工作机制。 筹备首先,要先晓得偏差锁的偏差锁机制,着重看下撤销机制。而后,要晓得【批量重偏差与批量撤销】的钻研对象是Class,即锁对象对应的Class,而非锁对象自身。 // 钻研的是对象.classsynchronized(对象) {}// 并不是说的这种,这种不思考synchronized(对象.class) {}如对机制不理解,能够看下这个博客https://blog.csdn.net/sinat_41832255/article/details/89309944另外几个jvm参数: -XX:BiasedLockingBulkRebiasThreshold = 20 // 默认偏差锁批量重偏差阈值-XX:BiasedLockingBulkRevokeThreshold = 40 // 默认偏差锁批量撤销阈值-XX:+UseBiasedLocking // 应用偏差锁,jdk6之后默认开启-XX:BiasedLockingStartupDelay = 0 // 提早偏差工夫, 默认不为0,意思为jvm启动多少ms当前开启偏差锁机制(此处设为0,不提早)在这里,我把博客中的图拷过去,不便大家思考。 注释咱们次要看下图中的 “是否开启重偏差?” 这个条件分支,来看下什么是重偏差? 重偏差字面了解就是从新偏差,那什么状况下会从新偏差呢批量重偏差咱们首先要晓得的是,锁对象Class刚开始的状况下,是没有开启重偏差的,意思就是 “是否开启重偏差?” 这个分支刚开始的时候是始终走 “否” 的,即会始终撤销偏差锁 ,当达到BiasedLockingBulkRebiasThreshold(20)次数的时候,容许重偏差。 上面咱们看下代码 public class TestBiasedLocking { public static void main(String[] args) throws DecoderException, InterruptedException { // 首先咱们创立一个list,来寄存锁对象 List<TestBiasedLocking> list = new LinkedList<>(); // 线程1 new Thread(() -> { for (int i = 0; i < 50; i++) { TestBiasedLocking testBiasedLocking = new TestBiasedLocking(); list.add(testBiasedLocking); // 新建锁对象 synchronized (testBiasedLocking) { System.out.println("第" + (i + 1) + "次加锁-线程1"); System.out.println(ClassLayout.parseInstance(testBiasedLocking).toPrintable()); // 打印对象头信息 } } }, "线程1").start(); // 让线程1跑一会儿 Thread.sleep(2000); // 线程2 new Thread(() -> { for (int i = 0; i < 30; i++) { TestBiasedLocking testBiasedLocking = list.get(i); synchronized (testBiasedLocking) { System.out.println("第" + (i + 1) + "次加锁-线程2"); System.out.println(ClassLayout.parseInstance(testBiasedLocking).toPrintable()); // 打印对象头信息 } } }, "线程2").start(); LockSupport.park(); }} ...
关于jvm:深入理解Java虚拟机JVM高级特性与最佳实践-第2版
深刻了解Java虚拟机_JVM高级个性与最佳实际 第2版 下载地址: https://pan.baidu.com/s/15_ADwgj7VPdMJhcMVUxYlQ 扫码上面二维码关注公众号回复100013 获取分享码 本书目录构造如下: 前言 第一局部 走近Java 第1章 走近Java 1.1 概述 1.2 Java技术体系 1.3 Java发展史 1.4 Java虚拟机发展史 1.4.1 Sun Classic Exact VM 1.4.2 Sun HotSpot VM 1.4.3 Sun Mobile-Embedded VM Meta-Circular VM 1.4.4 BEA JRockit IBM J9 VM 1.4.5 Azul VM BEA Liquid VM 1.4.6 Apache Harmony Google Android Dalvik VM 1.4.7 Microsoft JVM及其他 1.5 瞻望Java技术的将来 1.5.1 模块化 1.5.2 混合语言 1.5.3 多核并行 1.5.4 进一步丰盛语法 1.5.5 64位虚拟机 1.6 实战:本人编译JDK 1.6.1 获取JDK源码 1.6.2 零碎需要 1.6.3 构建编译环境 1.6.4 进行编译 1.6.5 在IDE工具中进行源码调试 1.7 本章小结 第二局部 主动内存管理机制 第2章 Java内存区域与内存溢出异样 2.1 概述 2.2 运行时数据区域 2.2.1 程序计数器 2.2.2 Java虚拟机栈 2.2.3 本地办法栈 2.2.4 Java堆 2.2.5 办法区 2.2.6 运行时常量池 2.2.7 间接内存 2.3 HotSpot虚拟机对象探秘 2.3.1 对象的创立 2.3.2 对象的内存布局 2.3.3 对象的拜访定位 2.4 实战:OutOfMemoryError异样 2.4.1 Java堆溢出 2.4.2 虚拟机栈和本地办法栈溢出 2.4.3 办法区和运行时常量池溢出 2.4.4 本机间接内存溢出 2.5 本章小结 第3章 垃圾收集器与内存调配策略 3.1 概述 3.2 对象已死吗 3.2.1 援用计数算法 3.2.2 可达性剖析算法 3.2.3 再谈援用 3.2.4 生存还是死亡 3.2.5 回收办法区 3.3 垃圾收集算法 3.3.1 标记-革除算法 3.3.2 复制算法 3.3.3 标记-整顿算法 3.3.4 分代收集算法 3.4 HotSpot的算法实现 3.4.1 枚举根节点 3.4.2 平安点 3.4.3 平安区域 3.5 垃圾收集器 3.5.1 Serial收集器 3.5.2 ParNew收集器 3.5.3 Parallel Scavenge收集器 3.5.4 Serial Old收集器 3.5.5 Parallel Old收集器 3.5.6 CMS收集器 3.5.7 G1收集器 3.5.8 了解GC日志 3.5.9 垃圾收集器参数总结 3.6 内存调配与回收策略 3.6.1 对象优先在Eden调配 3.6.2 大对象间接进入老年代 3.6.3 长期存活的对象将进入老年代 3.6.4 动静对象年龄断定 3.6.5 空间调配担保 3.7 本章小结 第4章 虚拟机性能监控与故障解决工具 4.1 概述 4.2 JDK的命令行工具 4.2.1 jps:虚拟机过程情况工具 4.2.2 jstat:虚拟机统计信息监督工具 4.2.3 jinfo:Java配置信息工具 4.2.4 jmap:Java内存映像工具 4.2.5 jhat:虚拟机堆转储快照剖析工具 4.2.6 jstack:Java堆栈跟踪工具 4.2.7 HSDIS:JIT生成代码反汇编 4.3 JDK的可视化工具 4.3.1 JConsole:Java监督与治理控制台 4.3.2 VisualVM:多合一故障解决工具 4.4 本章小结 第5章 调优案例剖析与实战 5.1 概述 5.2 案例剖析 5.2.1 高性能硬件上的程序部署策略 5.2.2 集群间同步导致的内存溢出 5.2.3 堆外内存导致的溢出谬误 5.2.4 外部命令导致系统迟缓 5.2.5 服务器JVM过程解体 5.2.6 不失当数据结构导致内存占用过大 5.2.7 由Windows虚拟内存导致的长时间进展 5.3 实战:Eclipse运行速度调优 5.3.1 调优前的程序运行状态 5.3.2 降级JDK 1.6的性能变动及兼容问题 5.3.3 编译工夫和类加载工夫的优化 5.3.4 调整内存设置管制垃圾收集频率 5.3.5 抉择收集器升高提早 5.4 本章小结 第三局部 虚拟机执行子系统 第6章 类文件构造 6.1 概述 6.2 无关性的基石 6.3 Class类文件的构造 6.3.1 魔数与Class文件的版本 6.3.2 常量池 6.3.3 拜访标记 6.3.4 类索引、父类索引与接口索引汇合 6.3.5 字段表汇合 6.3.6 办法表汇合 6.3.7 属性表汇合 6.4 字节码指令简介 6.4.1 字节码与数据类型 6.4.2 加载和存储指令 6.4.3 运算指令 6.4.4 类型转换指令 6.4.5 对象创立与拜访指令 6.4.6 操作数栈治理指令 6.4.7 管制转移指令 6.4.8 办法调用和返回指令 6.4.9 异样解决指令 6.4.10 同步指令 6.5 私有设计和公有实现 6.6 Class文件构造的倒退 6.7 本章小结 第7章 虚拟机类加载机制 7.1 概述 7.2 类加载的机会 7.3 类加载的过程 7.3.1 加载 7.3.2 验证 7.3.3 筹备 7.3.4 解析 7.3.5 初始化 7.4 类加载器 7.4.1 类与类加载器 7.4.2 双亲委派模型 7.4.3 毁坏双亲委派模型 7.5 本章小结 第8章 虚拟机字节码执行引擎 8.1 概述 8.2 运行时栈帧构造 8.2.1 局部变量表 8.2.2 操作数栈 8.2.3 动静连贯 8.2.4 办法返回地址 8.2.5 附加信息 8.3 办法调用 8.3.1 解析 8.3.2 分派 8.3.3 动静类型语言反对 8.4 基于栈的字节码解释执行引擎 8.4.1 解释执行 8.4.2 基于栈的指令集与基于寄存器的指令集 8.4.3 基于栈的解释器执行过程 8.5 本章小结 第9章 类加载及执行子系统的案例与实战 9.1 概述 9.2 案例剖析 9.2.1 Tomcat:正统的类加载器架构 9.2.2 OSGi:灵便的类加载器架构 9.2.3 字节码生成技术与动静代理的实现 9.2.4 Retrotranslator:逾越JDK版本 9.3 实战:本人入手实现近程执行性能 9.3.1 指标 9.3.2 思路 9.3.3 实现 9.3.4 验证 9.4 本章小结 第四局部 程序编译与代码优化 第10章 晚期(编译期)优化 10.1 概述 10.2 Javac编译器 10.2.1 Javac的源码与调试 10.2.2 解析与填充符号表 10.2.3 注解处理器 10.2.4 语义剖析与字节码生成 10.3 Java语法糖的滋味 10.3.1 泛型与类型擦除 10.3.2 主动装箱、拆箱与遍历循环 10.3.3 条件编译 10.4 实战:插入式注解处理器 10.4.1 实战指标 10.4.2 代码实现 10.4.3 运行与测试 10.4.4 其余利用案例 10.5 本章小结 第11章 早期(运行期)优化 11.1 概述 11.2 HotSpot虚拟机内的即时编译器 11.2.1 解释器与编译器 11.2.2 编译对象与触发条件 11.2.3 编译过程 11.2.4 查看及剖析即时编译后果 11.3 编译优化技术 11.3.1 优化技术概览 11.3.2 公共子表达式打消 11.3.3 数组边界查看打消 11.3.4 办法内联 11.3.5 逃逸剖析 11.4 Java与CC++的编译器比照 11.5 本章小结 第五局部 高效并发 第12章 Java内存模型与线程 12.1 概述 12.2 硬件的效率与一致性 12.3 Java内存模型 12.3.1 主内存与工作内存 12.3.2 内存间交互操作 12.3.3 对于volatile型变量的非凡规定 12.3.4 对于long和double型变量的非凡规定 12.3.5 原子性、可见性与有序性 12.3.6 后行产生准则 12.4 Java与线程 12.4.1 线程的实现 12.4.2 Java线程调度 12.4.3 状态转换 12.5 本章小结 第13章 线程平安与锁优化 13.1 概述 13.2 线程平安 13.2.1 Java语言中的线程平安 13.2.2 线程平安的实现办法 13.3 锁优化 13.3.1 自旋锁与自适应自旋 13.3.2 锁打消 13.3.3 锁粗化 13.3.4 轻量级锁 13.3.5 偏差锁 13.4 本章小结 附 录 附录A 编译Windows版的OpenJDK 附录B 虚拟机字节码指令表 附录C HotSpot虚拟机次要参数表 附录D 对象查询语言(OQL)简介 附录E JDK历史版本轨迹 ...
关于jvm:深入理解Java虚拟机JVM高级特性与最佳实践-第2版
深刻了解Java虚拟机_JVM高级个性与最佳实际 第2版 下载地址: https://pan.baidu.com/s/15_ADwgj7VPdMJhcMVUxYlQ 扫码上面二维码关注公众号回复100013 获取分享码 本书目录构造如下: 前言 第一局部 走近Java 第1章 走近Java 1.1 概述 1.2 Java技术体系 1.3 Java发展史 1.4 Java虚拟机发展史 1.4.1 Sun Classic Exact VM 1.4.2 Sun HotSpot VM 1.4.3 Sun Mobile-Embedded VM Meta-Circular VM 1.4.4 BEA JRockit IBM J9 VM 1.4.5 Azul VM BEA Liquid VM 1.4.6 Apache Harmony Google Android Dalvik VM 1.4.7 Microsoft JVM及其他 1.5 瞻望Java技术的将来 1.5.1 模块化 1.5.2 混合语言 1.5.3 多核并行 1.5.4 进一步丰盛语法 1.5.5 64位虚拟机 1.6 实战:本人编译JDK 1.6.1 获取JDK源码 1.6.2 零碎需要 1.6.3 构建编译环境 1.6.4 进行编译 1.6.5 在IDE工具中进行源码调试 1.7 本章小结 第二局部 主动内存管理机制 第2章 Java内存区域与内存溢出异样 2.1 概述 2.2 运行时数据区域 2.2.1 程序计数器 2.2.2 Java虚拟机栈 2.2.3 本地办法栈 2.2.4 Java堆 2.2.5 办法区 2.2.6 运行时常量池 2.2.7 间接内存 2.3 HotSpot虚拟机对象探秘 2.3.1 对象的创立 2.3.2 对象的内存布局 2.3.3 对象的拜访定位 2.4 实战:OutOfMemoryError异样 2.4.1 Java堆溢出 2.4.2 虚拟机栈和本地办法栈溢出 2.4.3 办法区和运行时常量池溢出 2.4.4 本机间接内存溢出 2.5 本章小结 第3章 垃圾收集器与内存调配策略 3.1 概述 3.2 对象已死吗 3.2.1 援用计数算法 3.2.2 可达性剖析算法 3.2.3 再谈援用 3.2.4 生存还是死亡 3.2.5 回收办法区 3.3 垃圾收集算法 3.3.1 标记-革除算法 3.3.2 复制算法 3.3.3 标记-整顿算法 3.3.4 分代收集算法 3.4 HotSpot的算法实现 3.4.1 枚举根节点 3.4.2 平安点 3.4.3 平安区域 3.5 垃圾收集器 3.5.1 Serial收集器 3.5.2 ParNew收集器 3.5.3 Parallel Scavenge收集器 3.5.4 Serial Old收集器 3.5.5 Parallel Old收集器 3.5.6 CMS收集器 3.5.7 G1收集器 3.5.8 了解GC日志 3.5.9 垃圾收集器参数总结 3.6 内存调配与回收策略 3.6.1 对象优先在Eden调配 3.6.2 大对象间接进入老年代 3.6.3 长期存活的对象将进入老年代 3.6.4 动静对象年龄断定 3.6.5 空间调配担保 3.7 本章小结 第4章 虚拟机性能监控与故障解决工具 4.1 概述 4.2 JDK的命令行工具 4.2.1 jps:虚拟机过程情况工具 4.2.2 jstat:虚拟机统计信息监督工具 4.2.3 jinfo:Java配置信息工具 4.2.4 jmap:Java内存映像工具 4.2.5 jhat:虚拟机堆转储快照剖析工具 4.2.6 jstack:Java堆栈跟踪工具 4.2.7 HSDIS:JIT生成代码反汇编 4.3 JDK的可视化工具 4.3.1 JConsole:Java监督与治理控制台 4.3.2 VisualVM:多合一故障解决工具 4.4 本章小结 第5章 调优案例剖析与实战 5.1 概述 5.2 案例剖析 5.2.1 高性能硬件上的程序部署策略 5.2.2 集群间同步导致的内存溢出 5.2.3 堆外内存导致的溢出谬误 5.2.4 外部命令导致系统迟缓 5.2.5 服务器JVM过程解体 5.2.6 不失当数据结构导致内存占用过大 5.2.7 由Windows虚拟内存导致的长时间进展 5.3 实战:Eclipse运行速度调优 5.3.1 调优前的程序运行状态 5.3.2 降级JDK 1.6的性能变动及兼容问题 5.3.3 编译工夫和类加载工夫的优化 5.3.4 调整内存设置管制垃圾收集频率 5.3.5 抉择收集器升高提早 5.4 本章小结 第三局部 虚拟机执行子系统 第6章 类文件构造 6.1 概述 6.2 无关性的基石 6.3 Class类文件的构造 6.3.1 魔数与Class文件的版本 6.3.2 常量池 6.3.3 拜访标记 6.3.4 类索引、父类索引与接口索引汇合 6.3.5 字段表汇合 6.3.6 办法表汇合 6.3.7 属性表汇合 6.4 字节码指令简介 6.4.1 字节码与数据类型 6.4.2 加载和存储指令 6.4.3 运算指令 6.4.4 类型转换指令 6.4.5 对象创立与拜访指令 6.4.6 操作数栈治理指令 6.4.7 管制转移指令 6.4.8 办法调用和返回指令 6.4.9 异样解决指令 6.4.10 同步指令 6.5 私有设计和公有实现 6.6 Class文件构造的倒退 6.7 本章小结 第7章 虚拟机类加载机制 7.1 概述 7.2 类加载的机会 7.3 类加载的过程 7.3.1 加载 7.3.2 验证 7.3.3 筹备 7.3.4 解析 7.3.5 初始化 7.4 类加载器 7.4.1 类与类加载器 7.4.2 双亲委派模型 7.4.3 毁坏双亲委派模型 7.5 本章小结 第8章 虚拟机字节码执行引擎 8.1 概述 8.2 运行时栈帧构造 8.2.1 局部变量表 8.2.2 操作数栈 8.2.3 动静连贯 8.2.4 办法返回地址 8.2.5 附加信息 8.3 办法调用 8.3.1 解析 8.3.2 分派 8.3.3 动静类型语言反对 8.4 基于栈的字节码解释执行引擎 8.4.1 解释执行 8.4.2 基于栈的指令集与基于寄存器的指令集 8.4.3 基于栈的解释器执行过程 8.5 本章小结 第9章 类加载及执行子系统的案例与实战 9.1 概述 9.2 案例剖析 9.2.1 Tomcat:正统的类加载器架构 9.2.2 OSGi:灵便的类加载器架构 9.2.3 字节码生成技术与动静代理的实现 9.2.4 Retrotranslator:逾越JDK版本 9.3 实战:本人入手实现近程执行性能 9.3.1 指标 9.3.2 思路 9.3.3 实现 9.3.4 验证 9.4 本章小结 第四局部 程序编译与代码优化 第10章 晚期(编译期)优化 10.1 概述 10.2 Javac编译器 10.2.1 Javac的源码与调试 10.2.2 解析与填充符号表 10.2.3 注解处理器 10.2.4 语义剖析与字节码生成 10.3 Java语法糖的滋味 10.3.1 泛型与类型擦除 10.3.2 主动装箱、拆箱与遍历循环 10.3.3 条件编译 10.4 实战:插入式注解处理器 10.4.1 实战指标 10.4.2 代码实现 10.4.3 运行与测试 10.4.4 其余利用案例 10.5 本章小结 第11章 早期(运行期)优化 11.1 概述 11.2 HotSpot虚拟机内的即时编译器 11.2.1 解释器与编译器 11.2.2 编译对象与触发条件 11.2.3 编译过程 11.2.4 查看及剖析即时编译后果 11.3 编译优化技术 11.3.1 优化技术概览 11.3.2 公共子表达式打消 11.3.3 数组边界查看打消 11.3.4 办法内联 11.3.5 逃逸剖析 11.4 Java与CC++的编译器比照 11.5 本章小结 第五局部 高效并发 第12章 Java内存模型与线程 12.1 概述 12.2 硬件的效率与一致性 12.3 Java内存模型 12.3.1 主内存与工作内存 12.3.2 内存间交互操作 12.3.3 对于volatile型变量的非凡规定 12.3.4 对于long和double型变量的非凡规定 12.3.5 原子性、可见性与有序性 12.3.6 后行产生准则 12.4 Java与线程 12.4.1 线程的实现 12.4.2 Java线程调度 12.4.3 状态转换 12.5 本章小结 第13章 线程平安与锁优化 13.1 概述 13.2 线程平安 13.2.1 Java语言中的线程平安 13.2.2 线程平安的实现办法 13.3 锁优化 13.3.1 自旋锁与自适应自旋 13.3.2 锁打消 13.3.3 锁粗化 13.3.4 轻量级锁 13.3.5 偏差锁 13.4 本章小结 附 录 附录A 编译Windows版的OpenJDK 附录B 虚拟机字节码指令表 附录C HotSpot虚拟机次要参数表 附录D 对象查询语言(OQL)简介 附录E JDK历史版本轨迹 ...
关于jvm:基础篇JVM运行时内存布局
1 JVM的内存区域布局java代码的执行步骤有三点 java源码文件->编译器->字节码文件字节码文件->JVM->机器码机器码->零碎CPU执行JVM执行的字节码须要用类加载来载入;字节码文件能够来自本地文件,能够在网络上获取,也能够实时生成。就是说你能够跳过写java代码阶段,间接生成字节码交由JVM执行其中Java虚拟机栈、程序计数器、Heap、本地办法栈、Metaspace属于JVM运行时的内存;按是否线程共享则能够分两类 JAVA堆和MetasSpace元空间属于线程共享的;虚拟机栈和本地办法栈、程序计数器是线程公有的2 JVM五大数据区域介绍2.1 程序计数器(Progarm Counter Register) 一块较小的内存空间, 是以后线程所执行的字节码的行号指示器。线程有一个独属的程序计数器,字节码解析工作时须要程序计数器来选取下一指令,分支、循环、跳转等依赖它正在执行java办法线程的计数器记录的是虚拟机字节码指令的地址;如果还是Native办法,则为空程序计数器内存区域是惟一一个在虚拟机中没有规定任何OutOfMemoryError谬误的区域2.2 虚拟机栈(Virtual Machine Stack) Java办法执行的内存模型:每个办法在执行的同时都会创立一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动静链接、办法进口等信息每一个办法从调用直至执行实现的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程栈帧是用来存储数据和局部过程后果的数据结构,同时也被用来解决动静链接(Dynamic Linking)、 办法返回值和异样分派(Dispatch Exception)。栈帧随着办法调用而创立,随着办法完结而销毁(无论办法是失常实现还是异样实现)如果线程申请的栈深度大于虚拟机容许深度,则抛出StackOverflowError;扩大时无奈申请到足够内存,则抛出OutOfMemeryError2.3 本地办法栈(Native Method Stack) 本地办法栈和虚拟机栈作用相似,区别是虚拟机栈为执行Java办法服务,而本地办法栈则为Native办法服务。(HopShot的实现 间接把本地办法栈和虚拟机栈合二为一)上述3类区域,生命周期与Thread雷同,即:线程创立时,相应的内存区创立,线程销毁时,开释相应内存2.4 堆(Heap) 线程共享的一块内存区域,简直所有的对象实例在这里分配内存,也是垃圾收集器进行垃圾收集的最重要的内存区域。因而很多时候也叫GC堆线程公有的调配缓存区(Thread Local Alloaction Buffer)也是在堆划分进去的JDK8的版本,因应用元空间代替永恒代,字符串常量池和类的动态变量也放入java堆中 2.5 元空间(MetaSpace) 次要存储类的元数据,比方类的各种形容信息,类名、办法、字段、拜访限度等,既编译器编译后的代码等数据运行时常量池:Class文件中除了有类的版本、字段、办法等形容等信息外;还有一项信息是常量池,用于寄存编译期生成的各种字面量和符号援用,这部分将在类加载后寄存到元空间的运行时常量池中应用元空间代替永恒代起因 永恒代的大小是在启动时固定好的,很难进行调优;太大则容易导致永恒代溢出;太小在运行时,容易抛出OutOfMemeryError字符串存在永恒代中,应用时易出问题,因为永恒代内存常常不够用,爆出异样OutOfMemoryError: PermGenCodeCache JVM生成的native code寄存的内存空间称之为Code Cache;JIT编译、JNI等都会编译代码到native code,其中JIT生成的native code占用了Code Cache的绝大部分空间间接内存 它并不是虚拟机运行时数据区的个别分,也不在标准定义。JDK1.4,引入了Channel(通道)与Buffer(缓存区)的I/O形式,它能够应用Native函数调配堆外内存,可通过DirectByteBuffer操作。3 JVM运行时内存布局和JMM内存模型区别 JVM内存区域是指JVM运行时将内存数据分区域存储,强调对内存空间的划分JAVA内存模型是Java语言在多线程并发状况下对于共享变量内存操作的标准:解决变量在多线程的可见性、原子性的问题4 JMM内存模型交互操作内存交互操作有八种,虚拟机的实现保障每一个操作都是原子性的 lock(锁定):作用于主内存的变量,标识变量为线程独占状态unlock(解锁):作用于主内存的变量,开释一个处于锁定状态的变量,开释后的变量才能够被其余线程锁定read(读取):作用于主内存变量,从主内存中读取出前面load操作要用到的变量load(载入):作用于主内存中的变量,把方才read的值放入工作内存的正本中use(应用):作用于工作内存中的变量,当线程执行某个字节码指令须要用到相应的变量时,把工作内存中的变量正本传给执行引擎assign(赋值):作用于工作内存中的变量,把一个从执行引擎中承受到的值放入工作内存的变量正本中store(存储):作用于工作内存中的变量,把工作内存中的变量送到主内存,给后续的write应用write(写入):作用于主内存中的变量,把store的工作内存中的变量值,写入主内存中read和load 如同是雷同的操作?各位有何高见,请指教下JMM对这八种指令的应用,制订了如下规定 read和load、store和write必须程序执行,而且两个指令绑定呈现;就是说呈现read就要有load不容许一个线程抛弃最近的assign操作,工作内存中的变量扭转后,必须write同步到主内存不容许一个线程把没有产生assign操作的变量同步到主内存新的变量必须诞生于主内存,不容许工作内存应用一个没有初始化的变量;use、store操作变量之前,必须通过load和assign操作变量同一时刻只容许一个线程对其lock,该线程能够对该变量加锁屡次,开释锁须要执行雷同次数的unlock,lock和unlock要成对呈现一个变量没有lock,不能unlock;并且一个线程不能unlock被其余线程锁住的变量执行unlock前,必须把工作内存中的变量同步到主内存中执行lock操作,须要清空工作内存(所有),并且须要应用该变量之前,要从新执行load和assign操作欢送指注释中谬误关注公众号,一起交换 参考文章深刻了解Java虚拟机JVM之内存布局超具体整顿Metaspace 之一:Metaspace整体介绍
关于jvm:ParallelScavenge-+-Parallel-Old
ParalletScavenge:复制算法,多线程,关注吞吐率Copying GC(GC复制算法): 最早是Robert R.Fenichel和Jerome C.Yochelson提出,简略说将空间分为From和To,当From空间齐全占满时,gc将流动对象全副复制到To空间,复制实现后,From和To的使命调换。 为了保障From中所有活着的对象都能复制到To里,要求From和To空间大小统一。 coping(){ free = to_start; for(r : roots){ *r = copy(*r) } swap(from_start,to_start)}/***free设在To空间的结尾*第四行copy函数,复制能从根援用到的对象及其子对象,返回*r所在的新空间。*而后替换From和To,对From空间进行gc*/ copy(obj){ if(obj.tag != COPIED){ copy_data(free,obj,obj.size); obj.tag = COPIED; obj.forwarding = free; free = obj.size for(child : children(obj.forwarding){ *child = copy(*child) } } return obj.forwarding}/***obj.tag 并不是独自的一个域,只是占用了obj的field1,一旦obj被复制结束,其在From空间的域值更改没有关系*obj.forwarding也不是一个独自的域,占用了obj.field2,用来标识新空间的地位* 由更上可知,须要每个对象至多有两个域* 深度遍历*/ //调配new_obj(size){ if(free + size > from_start + HEAP_SIZE /2){ copying(); //空间有余,gc if(free + size > from_start + HEAP /2){ allocation_fail();//还是不够,调配失败 } } obj = free; obj.size = size; free += size; return obj;}copying算法的总结: ...
关于jvm:ParallelScavenge-+-Parallel-Old
ParalletScavenge:复制算法,多线程,关注吞吐率Copying GC(GC复制算法): 最早是Robert R.Fenichel和Jerome C.Yochelson提出,简略说将空间分为From和To,当From空间齐全占满时,gc将流动对象全副复制到To空间,复制实现后,From和To的使命调换。 为了保障From中所有活着的对象都能复制到To里,要求From和To空间大小统一。 coping(){ free = to_start; for(r : roots){ *r = copy(*r) } swap(from_start,to_start)}/***free设在To空间的结尾*第四行copy函数,复制能从根援用到的对象及其子对象,返回*r所在的新空间。*而后替换From和To,对From空间进行gc*/ copy(obj){ if(obj.tag != COPIED){ copy_data(free,obj,obj.size); obj.tag = COPIED; obj.forwarding = free; free = obj.size for(child : children(obj.forwarding){ *child = copy(*child) } } return obj.forwarding}/***obj.tag 并不是独自的一个域,只是占用了obj的field1,一旦obj被复制结束,其在From空间的域值更改没有关系*obj.forwarding也不是一个独自的域,占用了obj.field2,用来标识新空间的地位* 由更上可知,须要每个对象至多有两个域* 深度遍历*/ //调配new_obj(size){ if(free + size > from_start + HEAP_SIZE /2){ copying(); //空间有余,gc if(free + size > from_start + HEAP /2){ allocation_fail();//还是不够,调配失败 } } obj = free; obj.size = size; free += size; return obj;}copying算法的总结: ...
关于jvm:对象的创建
基于HotSpot 根本类型对象的创立 1 当虚拟机遇到一条new指令时,尝试在常量池中定位到相干类的符号援用 2 查看这个符号援用代表的类是否曾经加载,解析,初始化,如果没有先执行类加载 3 实现2后可确定对象所需的内存大小 4 从堆中划分出相应大小的内存。 5 初始化对象:对象的哈希码,gc分代年龄,对应类的信息。 6 依据代码设置对象初始值。 问题一: 步骤3是如何确定对象所需内存的大小的? 首先看一下对象在内存中的存储布局: hotspot规定对象的起始地址必须是8字节的整倍数,即对象的大小必须是8字节的整数倍。对象头个别格局较固定,能够保障这一点,当实例数据不满足的时候,须要对齐填充局部来一起保障。 问题二: 步骤4中是如何从堆中划分内存的? 堆中划分内存有两种办法:指针 和 闲暇列表。应用哪种形式取决于堆是否是规整的(堆是否能规整取决于应用的垃圾回收算法)。 规整的意思是说堆中应用过的内存和闲暇内存有显著的划分界线,而不是混在一起的。 如果是规整的,那么就能够应用指针法,在应用过的内存和闲暇内存的分界点用一个指针示意,分配内存时只有将指针向闲暇内存方向挪动一段与对象内存大小相等的间隔。 如果不是规整的,那只能应用闲暇列表,闲暇列表是指虚拟机保护一个列表,记录哪些内存块是应用过的哪些是闲暇的,分配内存时从列表中抉择一块满足对象大小的内存块。这里如何抉择内存块,预计就是学生时代学的操作系统讲的那些内容了,如何保障内存碎片较少之类的(后续整顿) hotspot应用的应该是指针 分配内存的时候还波及到并发的问题,两种解决形式: 1 对分配内存这种操作进行同步解决,CAS保障操作的原子性 2 本地线程调配缓冲TLAB,事后为每个线程调配一小块内存TLAB,须要内存时各线程先在本人的TLAB上调配,当TLAB不够用的时候才会须要同步。 CAS: 简略形容下,大体采纳的是乐观锁的思维,不应用锁,多个线程去改同一个变量时,都能够尝试去改,只会有一个线程胜利,失败的线程也会立即失去反馈,不会阻塞。 CAS中有三个变量,内存中的值V,预期原值A,新值B,每个线程尝试去扭转量的时候,先判断A和V是否相等,如果雷同,则将B更新到内存,否则更新失败。
关于jvm:对象的实例化以及对象的内存布局
创建对象的形式new常见的形式变形1: xxx静态方法变形2: xxxBuilde/xxxFactory 的静态方法Class的 newinstance() :反射形式,只能调用空参结构器,权限必须是publicConstructor 的newInstance(Xxx): 反射形式,能够调用空参,带参的结构器,权限没要求应用clone():不调用任何结构器,以后的类须要实现Cloneable接口,实现clone()办法适应反序列化:从文件或文本中获取一个对象的二进制流应用三方库Objenesis创建对象的步骤1.判断对象对应的类是否加载、链接、初始化虚拟机遇到一条new指令,首先查看这个指令的参数是否在Metaspace的常量池中定位到一个类的符号援用,并查看这个符号援用代表的类是否被加载、解析、初始化(即判断类元信息是否存在)。如果没有,那么在双亲委派机制下,应用以后类加载器以ClassLoader+ 包名+ 类名为key进行查找对应的.class文件。如果没有找到文件,则抛出 ClassNotFoundException异样,如果找到,则进行类加载,并生成对应的Class类对象。 2.为对象分配内存首先计算对象占用空间大小,截止在堆中划分一块内存给新对象。如果实例成员变量是援用变量,仅调配援用变量空间即可,即4个字节大小。 如果内存工整 指针碰撞如果内存是规整的,那么虚拟机将采纳的是指针碰撞法(Bump the pointer )来为对象分配内存。 意思是所有用过的内寄存一边,闲暇的内寄存另一边,两头放着一个指针作为分界点的指示器,分配内存就仅仅把指针指向闲暇的那边移动一段与对象大小相等的间隔罢了。如果垃圾收集器抉择的是Serial、 ParNew这种基于压缩算法的,虚拟机采纳这种调配形式。个别应用带有compact(整顿)过程的收集器时,应用指针碰撞。 如果内存不工整 虚拟机须要保护一个列表闲暇列表调配如果内存是不工整的,已应用的内存和未应用的内存互相交织,那么虚拟机采纳的是闲暇散列表法来为对象分配内存。 意思是虚拟机保护了一个列表,记录了哪些内存是可用的,再调配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种调配形式成为“闲暇列表(Free List)” 阐明抉择哪种调配形式有Java堆是否规整决定,而java堆是否规整又由所采纳的垃圾收集器是否带有压缩整顿性能决定。 3.解决并发平安问题采纳CAS配上失败重试保障更新的原子性每个线程事后调配一块TLAB 通过-XX:+/-UseTLAB参数来设定4.初始化调配到空间所有属性设置默认值,保障对象实例字段在不赋值时能够间接应用5.设置对象的对象头将对象的所属类(即类的元数据信息)、对象HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置取决于JVM的实现。 6.执行init办法进行初始化在Java程序的视角来看,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给援用变量。 因而一般来说(由字节码中是否追随有invokespecial指令所决定),new指令之后会接着就是执行办法,把对象依照程序员的志愿进行初始化,这样一个真正可用的对象才算齐全创立进去了。 对象内存布局对象头 (Header)蕴含两局部 运行时元数据(MarkWorld) 哈希值 (hash code)GC 分代年龄锁状态标记线程持有的锁偏差锁ID偏差工夫戳类型指针 指向类元数据InstanceKlass,确定该对象所属的类型。阐明: 如果是数组,还要记录数组长度实例数据(Instance Data)阐明 它是对象真正存储的无效信息,包含程序代码中定义的各种类型的字段(包含从父类继承下来的和自身领有的字段)规定 如果CompactFiled参数为true(默认为true);子类的窄变量可能插入代父类变量的空隙对齐填充(Padding)不是必须的,也没特地含意,仅仅起到占位符作用。对象拜访定位创建对象的目标是为了应用它JVM是如果通过栈帧中的对象援用拜访到其外部的对象实例 的呢?定位,通过栈上reference援用对象拜访形式次要有两种句柄拜访 益处reference中存储稳固句柄地址,对象被挪动(垃圾收集时挪动对象很广泛)时只会扭转句柄中实例数据指针即可,reference自身不须要被批改。 间接指针(HotSpot采纳) 益处间接指向对象 思维导图如下:
关于jvm:对象的实例化以及对象的内存布局
创建对象的形式new常见的形式变形1: xxx静态方法变形2: xxxBuilde/xxxFactory 的静态方法Class的 newinstance() :反射形式,只能调用空参结构器,权限必须是publicConstructor 的newInstance(Xxx): 反射形式,能够调用空参,带参的结构器,权限没要求应用clone():不调用任何结构器,以后的类须要实现Cloneable接口,实现clone()办法适应反序列化:从文件或文本中获取一个对象的二进制流应用三方库Objenesis创建对象的步骤1.判断对象对应的类是否加载、链接、初始化虚拟机遇到一条new指令,首先查看这个指令的参数是否在Metaspace的常量池中定位到一个类的符号援用,并查看这个符号援用代表的类是否被加载、解析、初始化(即判断类元信息是否存在)。如果没有,那么在双亲委派机制下,应用以后类加载器以ClassLoader+ 包名+ 类名为key进行查找对应的.class文件。如果没有找到文件,则抛出 ClassNotFoundException异样,如果找到,则进行类加载,并生成对应的Class类对象。 2.为对象分配内存首先计算对象占用空间大小,截止在堆中划分一块内存给新对象。如果实例成员变量是援用变量,仅调配援用变量空间即可,即4个字节大小。 如果内存工整 指针碰撞如果内存是规整的,那么虚拟机将采纳的是指针碰撞法(Bump the pointer )来为对象分配内存。 意思是所有用过的内寄存一边,闲暇的内寄存另一边,两头放着一个指针作为分界点的指示器,分配内存就仅仅把指针指向闲暇的那边移动一段与对象大小相等的间隔罢了。如果垃圾收集器抉择的是Serial、 ParNew这种基于压缩算法的,虚拟机采纳这种调配形式。个别应用带有compact(整顿)过程的收集器时,应用指针碰撞。 如果内存不工整 虚拟机须要保护一个列表闲暇列表调配如果内存是不工整的,已应用的内存和未应用的内存互相交织,那么虚拟机采纳的是闲暇散列表法来为对象分配内存。 意思是虚拟机保护了一个列表,记录了哪些内存是可用的,再调配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种调配形式成为“闲暇列表(Free List)” 阐明抉择哪种调配形式有Java堆是否规整决定,而java堆是否规整又由所采纳的垃圾收集器是否带有压缩整顿性能决定。 3.解决并发平安问题采纳CAS配上失败重试保障更新的原子性每个线程事后调配一块TLAB 通过-XX:+/-UseTLAB参数来设定4.初始化调配到空间所有属性设置默认值,保障对象实例字段在不赋值时能够间接应用5.设置对象的对象头将对象的所属类(即类的元数据信息)、对象HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置取决于JVM的实现。 6.执行init办法进行初始化在Java程序的视角来看,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给援用变量。 因而一般来说(由字节码中是否追随有invokespecial指令所决定),new指令之后会接着就是执行办法,把对象依照程序员的志愿进行初始化,这样一个真正可用的对象才算齐全创立进去了。 对象内存布局对象头 (Header)蕴含两局部 运行时元数据(MarkWorld) 哈希值 (hash code)GC 分代年龄锁状态标记线程持有的锁偏差锁ID偏差工夫戳类型指针 指向类元数据InstanceKlass,确定该对象所属的类型。阐明: 如果是数组,还要记录数组长度实例数据(Instance Data)阐明 它是对象真正存储的无效信息,包含程序代码中定义的各种类型的字段(包含从父类继承下来的和自身领有的字段)规定 如果CompactFiled参数为true(默认为true);子类的窄变量可能插入代父类变量的空隙对齐填充(Padding)不是必须的,也没特地含意,仅仅起到占位符作用。对象拜访定位创建对象的目标是为了应用它JVM是如果通过栈帧中的对象援用拜访到其外部的对象实例 的呢?定位,通过栈上reference援用对象拜访形式次要有两种句柄拜访 益处reference中存储稳固句柄地址,对象被挪动(垃圾收集时挪动对象很广泛)时只会扭转句柄中实例数据指针即可,reference自身不须要被批改。 间接指针(HotSpot采纳) 益处间接指向对象 思维导图如下:
JVM中栈的frames详解
简介咱们晓得JVM运行时数据区域专门有一个叫做Stack Area的区域,专门用来负责线程的执行调用。那么JVM中的栈到底是怎么工作的呢?快来一起看看吧。 JVM中的栈小师妹:F师兄,JVM为每个线程的运行都调配了一个栈,这个栈到底是怎么工作的呢? 小师妹,咱们先看下JVM的整体运行架构图: 咱们能够看到运行时数据区域分为5大部分。 堆区是存储共享对象的中央,而栈区是存储线程公有对象的中央。 因为是栈的构造,所以这个区域总是LIFO(Last in first out)。咱们思考一个办法的执行,当办法执行的时候,就会在Stack Area中创立一个block,这个block中持有对本地对象和其余对象的援用。一旦办法执行结束,则这个block就会出栈,供其余办法拜访。 FrameJVM中的stack area是由一个个的Frame组成的。 Frame次要用来存储数据和局部后果,以及执行动静链接,办法的返回值和调度异样。 每次调用办法时都会创立一个新Frame。当Frame的办法调用实现时,无论该办法是失常完结还是异样完结(它引发未捕捉的异样),这个frame都会被销毁。 Frame是从JVM中的stack area中调配的。 每个frame都由三局部组成,别离是本人的local variables数组,本人的operand stack,以及对以后办法的run-time constant pool的援用。 在线程的执行过程中,任何一个时刻都只有一个frame处于活动状态。这个frame被称为current frame,它的办法被称为current 办法,定义以后办法的类是以后类。 如果frame中的办法调用另一个办法或该frame的办法完结,那么这个frame将不再是current frame。 每次调用新的办法,都会创立一个新的frame,并将控制权转移到调用新的办法生成的框架。 在办法返回时,以后frame将其办法调用的后果(如果有的话)传回上一个frame,并完结以后frame。 请留神,由线程创立的frame只能有该线程拜访,并且不能被任何其余线程援用。 Local Variables本地变量每个frame都蕴含一个称为其本地局部变量的变量数组。frame的局部变量数组的长度是在编译的时候确定的。 单个局部变量能够保留以下类型的值:boolean, byte, char, short, int, float, reference, 或者 returnAddress。 如果对于long或double类型的值须要应用一对局部变量来存储。 局部变量因为存储在数组中,所以间接通过数字的索引来定位和拜访。 留神,这个数组的索引值是从0开始,到数组长度-1完结。单个局部变量间接通过索引来拜访就够了,那么对于占用两个间断局部变量的long或者double类型来说,怎么拜访呢? 比如说一个long类型占用数组中的n和n+1两个变量,那么咱们能够通过索引n值来拜访这个long类型,而不是通过n+1来拜访。 留神,在JVM中,并不一定要求这个n是偶数。那么这些局部变量有什么用呢? Java虚拟机应用局部变量在办法调用时传递参数。 咱们晓得在java中有两种办法,一种是类办法,一种是实例办法。 在类办法调用中,所有参数都从局部变量0开始在间断的局部变量中传递。 在实例办法调用中,局部变量0始终指向的是该实例对象,也就是this。也就是说实在的参数是从局部变量1开始存储的。 Operand Stacks在每个frame外部,又蕴含了一个LIFO的栈,这个栈叫做Operand Stack。 刚开始创立的时候,这个Operand Stack是空的。而后JVM将local variables中的常量或者值加载到Operand Stack中去。 而后Java虚拟机指令从操作数堆栈中获取操作数,对其进行操作,而后将后果压回操作数堆栈。 比如说,当初的Operand Stack中曾经有两个值,1和2。 这个时候JVM要执行一个iadd指令,将1和2相加。那么就会先将stack中的1和2两个数取出,相加后,将后果3再压入stack。 最终stack中保留的是iadd的后果3。 留神,在Local Variables本地变量中咱们提到,如果是long或者double类型的话,须要两个本地变量来存储。而在Operand Stack中,一个值能够示意任何Java虚拟机类型的值。也就是说long和double在Operand Stack中,应用一个值就能够示意了。Operand Stack中的任何操作都必须要确保其类型匹配。像之前提到的iadd指令是对两个int进行相加,如果这个时候你的Operand Stacks中存储的是long值,那么iadd指令是会失败的。 ...
干货分享丨jvm系列dump文件深度分析
摘要:java内存dump是jvm运行时内存的一份快照,利用它能够剖析是否存在内存节约,能够查看内存治理是否正当,当产生OOM的时候,能够找出问题的起因。那么dump文件的内容是什么样的呢?JVM dumpjava内存dump是jvm运行时内存的一份快照,利用它能够剖析是否存在内存节约,能够查看内存治理是否正当,当产生OOM的时候,能够找出问题的起因。那么dump文件的内容是什么样的呢?咱们一步一步来 获取JVM dump文件获取dump文件的形式分为被动和被动 i.被动形式: 1.利用jmap,也是最罕用的形式:jmap -dump:[live],format=b,file= 2.利用jcmd,jcmd GC.heap_dump 3.应用VisualVM,能够界面操作进行dump内存 4.通过JMX的形式 MBeanServer server = ManagementFactory.getPlatformMBeanServer();HotSpotDiagnosticMXBean mxBean = ManagementFactory.newPlatformMXBeanProxy(server, "com.sun.management:type=HotSpotDiagnostic", HotSpotDiagnosticMXBean.class);mxBean.dumpHeap(filePath, live);参考(https://www.baeldung.com/java... ii.被动形式: 被动形式就是咱们通常的OOM事件了,通过设置参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath= dump文件剖析构造示意图 构造详解 dump文件是堆内存的映射,由文件头和一系列内容块组成 文件头 由musk, 版本,identifierSize, 工夫4局部组成 1、musk:4个byte,内容为'J', 'A', 'V', 'A'即JAVA 2、version:若干byte,值有以下三种 " PROFILE 1.0\0"," PROFILE 1.0.1\0"," PROFILE 1.0.2\0"3、identifierSize:4个byte数字,值为4或者8,示意一个援用所占用的byte数 4、time:8个byte,dump文件生成工夫 阐明:java一个类的成员变量有两种类型 根本类型(8种根本类型),它们占用byte数固定不变,每生成一个对象它们就须要给它们赋初始值,调配空间是援用类型,示意一个对象,在类中只有一个援用,援用只是一个数值,所占用的空间大小为identifierSize,被援用对象行将在堆中的另一个中央 例如定义一个类public class Person { private int age;//4个byte private String name;//identifierSize个byte private double weight;//8个byte}当咱们在new Person()的时候 它就须要申请一个空间,空间大小为 对象头大小+4+identifierSize+8个byte 对象大小的测量: jdk提供一个测试对象占用内存大小的工具Instrumentation,然而Instrumentation没法间接援用到,须要通过agent来援用到 定义一个Premain类, javac Premain.java ...
JvisualVM查找前N个最大的对象
下图是应用Guava的布隆过滤器,预估量是1亿,误判率是1百万分之1。的堆dump查看。下图是查问后果
AlarmClock-slow-alarmAlarm-问题排查-cms-perm-gen内存使用满
项目启动成功 但是无法正常访问控制台有报错信息AlarmClock slow alarm Alarm项目端口为8083使用netstat -ano| findstr "8083" 查找对应的pid使用jamp -heap 12576 查看内存使用情况可以看到maxpermsize最大为82M 目前已经使用81M多 永久代使用完思考项目启动时加载的类较多 解决方案:项目启动时设置 maxpermsize 增大后无报错
为什么需要-JVM它处在什么位置
JVM 和操作系统的关系JVM 全称 Java Virtual Machine,也就是我们耳熟能详的 Java 虚拟机。它能识别 .class后缀的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作。 一般情况下,使用 C++ 开发的程序,编译成二进制文件后,就可以直接执行了,操作系统能够识别它;但是 Java 程序不一样,使用 javac 编译成 .class 文件之后,还需要使用 Java 命令去主动执行它,操作系统并不认识这些 .class 文件。 你可能会想,我们为什么不能像 C++ 一样,直接在操作系统上运行编译后的二进制文件呢?而非要搞一个处于程序与操作系统中间层的虚拟机呢? 这就是 JVM 的过人之处了。大家都知道,Java 是一门抽象程度特别高的语言,提供了自动内存管理等一系列的特性。这些特性直接在操作系统上实现是不太可能的,所以就需要 JVM 进行一番转换。 有了上面的介绍,我们就可以做如下的类比。JVM:等同于操作系统;Java 字节码:等同于汇编语言。 如果你还是对上面的介绍有点模糊,可以参考下图:从图中可以看到,有了 JVM 这个抽象层之后,Java 就可以实现跨平台了。JVM 只需要保证能够正确执行 .class 文件,就可以运行在诸如 Linux、Windows、MacOS 等平台上了。 而 Java 跨平台的意义在于一次编译,处处运行,能够做到这一点 JVM 功不可没。比如我们在 Maven 仓库下载同一版本的 jar 包就可以到处运行,不需要在每个平台上再编译一次。 现在的一些 JVM 的扩展语言,比如 Clojure、JRuby、Groovy 等,编译到最后都是 .class 文件,Java 语言的维护者,只需要控制好 JVM 这个解析器,就可以将这些扩展语言无缝的运行在 JVM 之上了。 我们用一句话概括 JVM 与操作系统之间的关系:JVM 上承开发语言,下接操作系统,它的中间接口就是字节码。 ...
你不得不掌握的-JVM-内存管理
Java 引以为豪的就是它的自动内存管理机制。相比于 C++的手动内存管理、复杂难以理解的指针等,Java 程序写起来就方便的多。然而这种呼之即来挥之即去的内存申请和释放方式,自然也有它的代价。为了管理这些快速的内存申请释放操作,就必须引入一个池子来延迟这些内存区域的回收操作。我们常说的内存回收,就是针对这个池子的操作。我们把上面说的这个池子,叫作堆,可以暂时把它看成一个整体。 JVM 内存布局Java 程序的数据结构是非常丰富的。其中的内容,举一些例子:静态成员变动态成员变量区域变量短小紧凑的对象声明庞大复杂的内存申请 我们先看一下 JVM 的内存布局。随着 Java 的发展,内存布局一直在调整之中。比如,Java 8 及之后的版本,彻底移除了持久代,而使用 Metaspace 来进行替代。这也表示着 -XX:PermSize 和 -XX:MaxPermSize 等参数调优,已经没有了意义。但大体上,比较重要的内存区域是固定的。JVM 内存区域划分如图所示,从图中我们可以看出: JVM 堆中的数据是共享的,是占用内存最大的一块区域。可以执行字节码的模块叫作执行引擎。执行引擎在线程切换时怎么恢复?依靠的就是程序计数器。JVM 的内存划分与多线程是息息相关的。像我们程序中运行时用到的栈,以及本地方法栈,它们的维度都是线程。本地内存包含元数据区和一些直接内存。虚拟机栈Java 虚拟机栈是基于线程的。哪怕你只有一个 main() 方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程一样的。 栈里的每条数据,就是栈帧。在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦完成相应的调用,则出栈。所有的栈帧都出栈后,线程也就结束了。每个栈帧,都包含四个区域: 局部变量表操作数栈动态连接返回地址我们的应用程序,就是在不断操作这些内存空间中完成的。 本地方法栈是和虚拟机栈非常相似的一个区域,它服务的对象是 native 方法。你甚至可以认为虚拟机栈和本地方法栈是同一个区域,这并不影响我们对 JVM 的了解。 这里有一个比较特殊的数据类型叫作 returnAdress。因为这种类型只存在于字节码层面,所以我们平常打交道的比较少。对于 JVM 来说,程序就是存储在方法区的字节码指令,而 returnAddress 类型的值就是指向特定指令内存地址的指针。 这里有一个两层的栈。第一层是栈帧,对应着方法;第二层是方法的执行,对应着操作数。注意千万不要搞混了。你可以看到,所有的字节码指令,其实都会抽象成对栈的入栈出栈操作。执行引擎只需要傻瓜式的按顺序执行,就可以保证它的正确性。程序计数器既然是线程,就代表它在获取 CPU 时间片上,是不可预知的,需要有一个地方,对线程正在运行的点位进行缓冲记录,以便在获取 CPU 时间片时能够快速恢复。 程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。这里面存的,就是当前线程执行的进度。下面这张图,能够加深大家对这个过程的理解。可以看到,程序计数器也是因为线程而产生的,与虚拟机栈配合完成计算操作。程序计数器还存储了当前正在运行的流程,包括正在执行的指令、跳转、分支、循环、异常处理等。 我们可以看一下程序计数器里面的具体内容。下面这张图,就是使用 javap 命令输出的字节码。大家可以看到在每个 opcode 前面,都有一个序号。就是图中红框中的偏移地址,你可以认为它们是程序计数器的内容。 堆 堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。 堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。 随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection)。 由于对象的大小不一,在长时间运行后,堆空间会被许多细小的碎片占满,造成空间浪费。所以,仅仅销毁对象是不够的,还需要堆空间整理。这个过程非常的复杂。 那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和在 Java 类中存在的位置。 ...
小师妹学JVM之JIT中的PrintAssembly
简介想不想了解JVM最最底层的运行机制?想不想从本质上理解java代码的执行过程?想不想对你的代码进行进一步的优化和性能提升? 如果你的回答是yes。那么这篇文章非常适合你,因为本文将会站在离机器码最近的地方来观看JVM的运行原理:Assembly。 使用PrintAssembly小师妹:F师兄,上次你给我介绍了java中的字节码,还有JIT中的LogCompilation和PrintCompilation的用法。虽然都非常有用,但是能不能更进一步,让我能以机器的眼光来看待JVM的执行? 小师妹,如果要探究JVM的运行本质,那就应该是机器码了。难道你要去读懂机器码?虽然我不是机器码的专家,但我猜那应该是个非常复杂的过程。 小师妹:F师兄,当然不是机器码,有没有比机器码更高级一点点的,我记得上大学的时候学过汇编语言,好像就是离机器码最近的语言了,JVM有没有相应的汇编语言呢? 必须有的,我们可以使用-XX:+PrintAssembly来将assembly打印出来。 但是打印assembly是有条件的,它就像一个高傲的姑娘,不是你想追求就能追求得上的。 我们使用下面的命令来查看系统对PrintAssembly的支持程度: java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -versionJava HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional outputCould not load hsdis-amd64.dylib; library not loadable; PrintAssembly is disabledjava version "1.8.0_171"Java(TM) SE Runtime Environment (build 1.8.0_171-b11)Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode)根据大家的运行环境的不同,得到的结果可能也是不同的,我是mac的系统,从上面的结果可以看到,在我的JDK8的环境中,显示缺少hsdis-amd64.dylib,所以PrintAssembly其实是禁用的。 小师妹:F师兄,那现在咋办呀?没有hsdis-amd64.dylib就用不了PrintAssembly了。 巴甫洛夫说过:问号是开启任何一门科学的钥匙。没有问题我们就创造问题,没有困难我们就制造困难,没有hsdis-amd64.dylib当然是安装咯。 具体怎么安装,大家自行探索吧,网上有很多安装的教程,这里就不一一介绍了。 这里想讨论一个很奇怪的事情,虽然在JDK8环境中,我们不能使用PrintAssembly,因为没有hsdis-amd64.dylib。但是当我切到最新的JDK14环境中,一切都很美好,PrintAssembly可以正常运行了。 如果我们在JDK14中同样运行上面的命令,我们会得到下面的结果: 上图说明JDK14中虽然可以正常运行但是结果却不是assembly code,说明在JDK14中还是需要安装hsdis-amd64.dylib才能够得到正确的assembly结果。 注意,JDK14也需要安装hsdis-amd64.dylib才能正确使用。输出过滤默认情况下,PrintAssembly输出的是所有的信息,但是JDK内部的代码我们不可能进行修改,一般来说并不关心他们的assembly输出,如果要指定我们自己编写的方法,可以使用CompileCommand: CompileCommand=print,*MyClass.myMethod prints assembly for just one methodCompileCommand=option,*MyClass.myMethod,PrintOptoAssembly (debug build only) produces the old print command outputCompileCommand=option,*MyClass.myMethod,PrintNMethods produces method dumps例如: ...
JVM系列分享4垃圾回收
JVM系列分享3内存机制
JVM系列分享2方法调用
Java内存区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有的区域则是依赖用户线程的启动和结束而建立和销毁。根据Java虚拟机规范的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的本地Native方法服务 Java堆是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里"几乎"所有的对象实例都在这里分配内存。 方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
JVM学习笔记双亲委派方式
简介本篇包括以下内容: 什么是双亲委派机制。双亲委派机制的处理流程。什么是双亲委派机制。运行原理一个类加载器收到类加载请求,不会自己立刻尝试加载类,而是把请求委托给父加载器去完成,每一层都是如此,所有的加载请求最终都传递到最顶层的引导类加载器进行处理。如果父加载器不存在了,那么尝试判断该类能不能被引导类加载器加载。如果父加载器无法加载,子加载器才会尝试自己加载。为什么要有如此复杂的双亲委派机制防止类的重复加载。保护程序安全,防止核心API被篡改。例:我们自己编写一个java.lang.Object用自己的类加载器进行加载,系统中就会存在多个Object类。双亲委派机制的处理流程。以下是java.lang.Classload.loadClass()方法的实现。`` protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; }}``转换成流程图: ...
JVM学习笔记类加载器ClassLoader
类加载器和类加载过程本篇主要内容:JVM工作结构简图。类加载过程。类加载器(ClassLoader)。自定义类加载器实现步骤。JVM工作结构简图 类加载过程上图中我们看到了字节码文件经过类加载子系统加载到JVM中,那么现在我们在看类加载子系统中发生了什么。先放一张类加载过程的简图。其中包括了三个大步骤,他们分别是: 加载加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象。 通过类的全限定名获取此类的二进制字节流。将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存中生成一个Class对象,作为方法区这个类的各种数据的访问入口。加载通常由类加载器完成,加载类的方式具体有以下几项: 本地资源加载。网络加载。Web Applet。zip压缩包加载。jar,war。运行时计算生成。动态代理技术。其他文件生成。JSP应用。从加密文件中读取,主要是为了防止class文件被反编译。链接链接分为三个步骤: 验证 确保class文件的字节流中包含信息符合虚拟机要求。 主要包括,文件格式验证,元数据验证,字节码验证,符号引用验证。准备 为类变量分配内存,并为变量赋零值。 这里不包括用final修饰的static,因为final在变异的时候就会分配了,准备阶段会显式初始化。 不会为实例变量分配初始化。解析 将常量池内的符号引用变为直接引用。初始化初始化阶段就是执行类构造器方法<clinit>()方法。<clinit>()方法是由javac将类变量赋值动作和静态代码块中的语句合并自动产生的。当类不存在类变量和静态代码块时不会自动生成此方法。一个类只会被加载一次,<clinint》()方法是同步加锁的。(后付测试代码)注:如果想看此方法,可以使用JClassLib软件或IDEA中的JClassLib插件。附一张clinit<>()方法图和同步测试代码。 package cn.lele;public class ClinitTest02 { public static void main(String[] args) { Runnable r = ()->{ System.out.println(Thread.currentThread().getName() + "start"); DeadThread deadThread = new DeadThread(); System.out.println(Thread.currentThread().getName() + "finish"); }; Thread a = new Thread(r,"A"); Thread b = new Thread(r,"B"); a.start(); b.start(); }}class DeadThread { static { if (true) { System.out.println(Thread.currentThread().getName() + "初始化当前类"); while (true) { } } }}图中我们看到在number声明之前我们就可以在同步代码块中为他赋值,这是因为在链接的准备阶段,number变量已经分配了内存空间并且赋予了零值。 ...
面试准备-JVM面试准备
整理一下JVM的面试题,别让自己在面试官面签懵逼了就好。。。1. JVM虚拟机1.1 什么是java虚拟机Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。 1.2 平台无关性一次编译,到处执行(Write Once ,Run Anywhere)。Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。 1.3 JVM 的主要组成部分?及其作用?类加载器(ClassLoader)运行时数据区(Runtime Data Area)执行引擎(Execution Engine)本地库接口(Native Interface)组件的作用:首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。 1.4 引用的分类强引用:GC时不会被回收软引用:描述有用但不是必须的对象,在发生内存溢出异常之前被回收弱引用:描述有用但不是必须的对象,在下一次GC时被回收虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用PhantomReference实现虚引用,虚引用用来在GC时返回一个通知。1.5 垃圾回收机制算法标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。1.6 JVM性能调优方法设定堆内存大小-Xmx:堆内存最大限制。 设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代-XX:NewSize:新生代大小-XX:NewRatio 新生代和老生代占比-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比设定垃圾回收器 年轻代用 -XX:+UseParNewGC 老生代用-XX:+UseConcMarkSweepGC1.7 JVM调优工具常用调优工具分为两类:jdk自带监控工具:jconsole和jvisualvm第三方有:MAT(Memory Analyzer Tool)、GChisto。详细描述为:jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗GChisto,一款专业分析gc日志的工具1.8 么判断对象是否可以被回收引用计数器:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;可达性分析:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。1.9 什么是双亲委派机制当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。 1.10 双亲委派机制的作用防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。1.11 堆栈功能方面:堆是用来存放对象的,栈是用来执行程序的。共享性:堆是线程共享的,栈是线程私有的。空间大小:堆大小远远大于栈。 个人网站http://www.zhouzhaodong.xyz
从main方法分析内存溢出
内存溢出OutOfMemoryError不常遇到,起码没有姨妈空指针异常(NullPointerException)来的那么频繁。现在就用最简单的main方法复现堆内存溢出并做分析。 概念先行JVM内存模型(JMM): 堆,方法区,本地方法栈,虚拟机栈,程序计数器 (后面三个线程共享)栈和堆:栈是运行空间,堆是存储空间,类似于我小米手机的运行内存(RAM)8G和存储空间(ROM)128G。java中基本类型和堆对象的引用存在栈中。堆:堆在JVM中占据了很大的空间,用来存放实例对象,等会儿我们就拿它下手!堆:我当时害怕急了。堆内存分为年轻代和老年代,java8之后没有了永久代。(往细了说年轻代还有伊甸园(eden)和两个幸存区(from、to))当java对象在年轻代存活一段时间经历过N次回收没有被回收掉后(N还可以自己设置),就会进入老年代,在老年代中又经历回收后积累的没有被回收的对象超负荷后就会抛出内存溢出的异常。 图做的有点粗糙,还请见谅! 我把堆内存设置小一点先。在idea的配置 (VM options)加上启动参数 -Xms10m -Xmx20m。运行以下代码,不断的生成People对象并放入集合中防止被回收。 public static void main(String[] args) { List<People> peoples = new ArrayList<>(); int i = 0; while (true) { People abc = new People(); i++; peoples.add(abc); System.out.println(abc.toString() + i); }}结果在生成540217个对象的时候抛出了内存溢出的异常。 ......People{name='null', sex='null'}540216People{name='null', sex='null'}540217Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.Arrays.copyOf(Arrays.java:3181) at java.util.ArrayList.grow(ArrayList.java:265) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231) at java.util.ArrayList.add(ArrayList.java:462) at com.example.demo.DemoMain.main(DemoMain.java:17)开启GC日志添加启动参数,不同的参数打印不同格式的日志。 参数说明-XX:-PrintGC开启GC日志-XX:-PrintGCDetails打印详细信息-XX:+PrintGCDateStamps打印时间-Xloggc:./gclogs.logGC日志的生成路径和日志名称加上后看到的GC日志如下,太多了,我截取了后半段,可以看到短期内GC之后都是Full GC,应该是老年代满了,触发Full GC但是还是有大量的对象不能被回收,最后抛出OOM的异常。年轻代满了会触发GC,也叫Minor GC,老年代满了会触发Full GC,CPU空闲时也会回收垃圾,JVM调优的目的就是减少GC的频率和Full GC的次数。 ...
深入理解Java虚拟机-线程的上下文类加载器
更多JVM内容,欢迎个人网站 https://www.zhoutao123.com 线程上下文类加载器 线程上下文类加载器( Thread Context ClassLoader) 是从JDK1.2 引入的,类Thread 的getContextClassLoader() 与 setContextClassLoader(Classloader var1) 分别用来设置线程的上下文类加载器。如果没有指定线程的上下文的加载器,那么线程将会继承父线程的上下文类加载器。Java 的初始化线程的上下文加载器,可以通过上下文类加载器加载类与资源。 基本的获取和使用方法: public class ContextClassLoader { /** * 每个类都会使用自己的类加载器尝试去加载所依赖的类 * * <p>如果ClassX 依赖了 ClassY ,那么在ClassX的加载器将会在主动引用ClassY 并且ClassY尚未被加载的时候加载ClassY 这个类 */ public static void main(String[] args) { System.out.println(Thread.currentThread().getContextClassLoader()); System.out.println(Thread.class.getClassLoader()); }} 从 JDBC 说起在以前学习JDBC 的时候我们最深刻的印象应该是 Class.forName("xxxxxx"); 简单的伪代码如下图: import java.sql.Connection;import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.Statement;public class DbUtil { public static final String URL = "jdbc:mysql://localhost:3306/db"; public static final String USER = "root"; public static final String PASSWORD = "123456"; public static void main(String[] args) throws Exception { Class.forName("com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT user_name, age FROM student"); } }}从上面的代码中,import导入的 jdbc 的相关类可以看到,他们均来自于 java.sql 包下,根据类的双亲委派机制可以非常清晰的知道,这些接口肯定是来自于 系统类加载器加载。那么具体的其实现类使用我们的应用类加载器加载,所以问题出现 其中的JDBC 标准是由启动器类加载器加载,而具体的实现的类是由系统类加载器加载,所以这就会导致启动类加载器加载的JDBC标准类无法找到子加载器加载的JDBC实现 ...
深入理解Java虚拟机Launcher-源码分析
更多JVM文章,欢迎访问个人网站 https://www.zhoutao123.com Launcher 源码分析拓展类加载器是Launcher 类的一个内部类,为了学习应用类加载器和拓展类加载器的实现细节,我们需要对Launcher 类的源码进行学习。 由于 Launcher 的代码属于 Oracle 闭源的代码部分,这一块代码的分析属于 IDEA 反编译出来的,所以部分变量的命名为var1, var2 这种奇怪的形式 1、获取系统类加载器想要获取系统类加载器,可以通过 ClassLoader.getSystemClassLoader() 获取到, 这个方法中实现了对系统类加载的创建,他的说明文档中也阐述了部分细节。 /** * Returns the system class loader for delegation. This is the default * delegation parent for new <tt>ClassLoader</tt> instances, and is * typically the class loader used to start the application. * * <p> This method is first invoked early in the runtime's startup * sequence, at which point it creates the system class loader and sets it * as the context class loader of the invoking <tt>Thread</tt>. * * <p> The default system class loader is an implementation-dependent * instance of this class. * * <p> If the system property "<tt>java.system.class.loader</tt>" is defined * when this method is first invoked then the value of that property is * taken to be the name of a class that will be returned as the system * class loader. The class is loaded using the default system class loader * and must define a public constructor that takes a single parameter of * type <tt>ClassLoader</tt> which is used as the delegation parent. An * instance is then created using this constructor with the default system * class loader as the parameter. The resulting class loader is defined * to be the system class loader.大致翻译一下: ...
模拟实战排查堆内存溢出javalangOutOfMemoryError-Java-heap-space问题
前言:模拟实战中排查堆内存溢出(java.lang.OutOfMemoryError: Java heap space)的问题。堆内存溢出的原因:一般都是创建了大量的对象,这些对象一直被引用着,无法被GC垃圾回收掉,最终导致堆内存被占满,没有足够的空间存放新创建的对象时,就会出现堆内存溢出问题。 在实际的业务场景中出现内存溢出的问题,排查起来一般是十分困难繁琐的,本文将通过结合一个简单的实例来阐述排查的具体思路和步骤。 准备:注意:在实际场景中,一般都是部署在Linux服务器中的项目报出内存溢出的问题;为了尽可能还原出实际场景,本文也是将提前编写好的可以触发内存溢出的代码并打包成可运行的Jar包,然后放到服务器中执行的。1、准备可导致内存溢出的代码:// 创建一个Java类public class OutOfMemory { private String test; public OutOfMemory(String test){ this.test = test; } }// 模拟内存溢出的发生public class TestOOM { public static void main(String[] args) { List<OutOfMemory> list = new ArrayList<OutOfMemory>(); while(true){ /** * 无限创建OutOfMemory对象,直至将堆空间占满,并且创建的OutOfMemory对象一直被list集合对象引用着, * 导致GC也无法回收,最终出现堆内存溢出问题 */ list.add(new OutOfMemory("5656")); System.out.println("5656"); } }}代码编写完成后,使用开发工具导出可运行的Jar包(TestOOM.jar)2、准备Linux服务器可以直接使用centos或者Red Hat等都可以;实战:1、将可运行的Jar包放到服务器中执行:①、可使用xshell、xftp工具将可运行的Jar包(Jar包叫:TestOOM.jar)放入到服务器中;②、使用命令执行Jar包;命令: java -Xms40m -Xmx70m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/tmp -jar TestOOM.jar 注意:为了尽快模拟发生堆内存溢出,所以在启动Jar包时,设置了一些参数;参数解析: 1)、 -Xms40m 初始堆大小设置为40m 2)、 -Xmx70m 最大堆大小设置为70m 3)、 -XX:+HeapDumpOnOutOfMemoryError 出现堆内存溢出时,自动导出堆内存 dump 快照 4)、 -XX:HeapDumpPath=/usr/tmp 设置导出的堆内存快照的存放地址为 /usr/tmp ...
这一次终于系统的学习了-JVM-内存结构
最近在看《 JAVA并发编程实践 》这本书,里面涉及到了 Java 内存模型,通过 Java 内存模型顺理成章的来到的 JVM 内存结构,关于 JVM 内存结构的认知还停留在上大学那会的课堂上,一直没有系统的学习这一块的知识,所以这一次我把《 深入理解Java虚拟机JVM高级特性与最佳实践 》、《 Java虚拟机规范 Java SE 8版 》这两本书中关于 JVM 内存结构的部分都看了一遍,算是对 JVM 内存结构有了新的认识。JVM 内存结构是指:Java 虚拟机定义了若干种程序运行期间会使用的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁,另一些则与线程一一对应,随着线程的开始而创建,随着线程的结束而销毁。具体的运行时数据区如下图所示: 在 Java 虚拟机规范中,定义了五种运行时数据区,分别是 Java 堆、方法区、虚拟机栈、本地方法区、程序计数器,其中 Java 堆和方法区是线程共享的。接下来就具体看看这 五种运行时数据区。 Java 堆(Heap)Java 堆是所有线程共享的一块内存区域,它在虚拟机启动时 就会被创建,并且单个 JVM 进程有且仅有一个 Java 堆。Java 堆是用来存放对象实例及数组,也就是说我们代码中通过 new 关键字 new 出来的对象都存放在这里。所以这里也就成为了垃圾回收器的主要活动营地了,于是它就有了一个别名叫做 GC 堆,根据垃圾回收器的规则,我们可以对 Java 堆进行进一步的划分,具体 Java 堆内存结构如下图所示: 我们可以将 Java 堆划分为新生代和老年代两个大模块,在新生代中,我们又可以进一步分为 Eden 空间、From Survivor 空间(s0)、To Survivor 空间(s1),Survivor 空间有一个为空,用于发生 GC 时存放存活对象,老年代存放的是经过多次 Minor GC 仍然存活的对象或者是一些大对象,FGC 就是发生在老年代。 ...
应用程序嵌入JVM使用
有时候我们会想jvm和程序在同一进程中,和jvm交互或者做一些定制工作,需要把jvm嵌入到程序中。简单来说过程可以分为三步:初始化jvm/执行java字节码/退出jvm 初始化jvm加载libjvm.so到进程中并且调用JNI_CreateJavaVm JNI_CreateJavaVM(JavaVM **vm, void **penv, JavaVMOption* options)options我们按照launcher中的设置就好,包含了classpath/command/pid pid_t pid = getpid(); char *op_pid = NEW_STRING(256); sprintf(op_pid,"-Dsun.java.launcher.pid=%d",pid); options[op_count++].optionString = "-Djava.class.path=."; if(class_path != nullptr && strcmp(class_path,".") != 0){ char *op_class_path = NEW_STRING(strlen(class_path)+50); sprintf(op_class_path,"-Djava.class.path=%s",class_path); options[op_count++].optionString = op_class_path; } options[op_count++].optionString = "-Dsun.java.command=test_jvm"; options[op_count++].optionString = "-Dsun.java.launcher=SUN_STANDARD"; options[op_count++].optionString = op_pid;退出jvm if(vm->DetachCurrentThread() != JNI_OK || vm->DestroyJavaVM() != JNI_OK){ return -1; }执行java字节码 JavaVM *vm = init_jvm(".:shen.jar"); JNIEnv *env = get_jni_env(); if(vm == nullptr || env == nullptr) return -1; jclass cla = env->FindClass("test"); jmethodID method = env->GetStaticMethodID(cla,"main","([Ljava/lang/String;)V"); jclass cla_string = env->FindClass("java/lang/String"); jobjectArray args = env->NewObjectArray(1,cla_string, nullptr); char *cwd = NEW_STRING(256); getcwd(cwd,256); env->SetObjectArrayElement(args,0,env->NewStringUTF(cwd)); env->CallStaticObjectMethod(cla,method,args); destroy_jvm();本文代码jvm_helper.cpp ...
浅谈Java垃圾回收
上一次我们已经介绍了Java内存模型,今天来简单介绍一下Java的垃圾回收机制。 JVM垃圾回收Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是堆内存中对象的分配与回收。 Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。 堆内存常见分配策略对象优先在eden区分配大对象直接进入老年代长期存活的对象将进入老年代对象优先在eden区分配目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。 大多数情况下,对象在新生代中eden区分配。当eden区没有足够空间进行分配时,虚拟机将发起一次 Minor GC. 我们先来看看 Minor GC 和 Full GC 有什么不同呢? 新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Full GC 经常会伴随至少一次的 Minor GC(并非绝对),Full GC 的速度一般会比 Minor GC 的慢 10 倍以上。大对象直接进入老年代大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。 为什么要这样呢?这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。 长期存活的对象将进入老年代既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。 如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。 注意:为了更好的适应不同程序的内存情况,虚拟机不是永远要求对象年龄必须达到了某个值才能进入老年代,如果 Survivor 空间中相同年龄对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到要求的年龄。
JVM内存模型
JVM内存模型 程序计数器: 较小的内存空间, 线程所执行的字节码的行号指示器,线程私有,不会抛出内存溢出异常虚拟机栈: 方法在执行的同时都会创建一个栈帧(栈桢大小缺省为1M,可用参数 –Xss调整大小,例如-Xss256k),进行压栈,执行完弹栈。线程私有,会有两种异常, StackOverflowError, OutOfMemoryError本地方法栈:和虚拟机栈相似, 不同的是本地方法栈为Native方法服务。堆: 存放对象实例, 可细分为新生代和老年代,再细分可分为Eden空间、From Survivor空间、To Survivor空间。线程共享,抛出OutOfMemoryError -Xms:堆的最小值;-Xmx:堆的最大值;-Xmn:新生代的大小;-XX:NewSize;新生代最小值;-XX:MaxNewSize:新生代最大值;方法区: 存储已被虚拟机加载的类信息、常量、静态变量,线程共享, 抛出OutOfMemoryError不同版本jdk内存区域的变化jdk1.6运行时常量池在方法区中jdk1.7运行时常量池在堆中jdk1.8多出个元空间的概念 jdk1.7及以前:-XX:PermSize;-XX:MaxPermSize;jdk1.8以后:-XX:MetaspaceSize; -XX:MaxMetaspaceSizejdk1.8以后大小就只受本机总内存的限制如:-XX:MaxMetaspaceSize=3M运行时常量池:方法区的一部分 栈上分配虚拟机提供的一种优化技术,基本思想是,对于线程私有的对象,将它打散分配在栈上,而不分配在堆上,好处是对象跟着方法调用自行销毁,不需要进行垃圾回收,可以提高性能-server JVM运行的模式之一, server模式才能进行逃逸分析-XX:+DoEscapeAnalysis:启用逃逸分析(默认打开)-XX:+PrintGC:打印GC日志-XX:+EliminateAllocations:标量替换(默认打开)-XX:-UseTLAB 关闭本地线程分配缓冲 对象的分配1.类加载2.分配内存:指针碰撞和空闲列表,为了并发安全,CAS和TLAB解决,TLAB是为每一个线程分配一块私有的区域。3.对内存里的变量进行初始化,int为0,boolean默认false等4.设置对象的信息到对象头,包括类的元数据信息,对象的哈希码,对象的GC分代年龄等5.刚刚开始初始化对象的 内存布局在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。 对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对对象的大小必须是8字节的整数倍。当对象其他数据部分没有对齐时,就需要通过对齐填充来补全。 堆溢出参数 : -Xms5m -Xmx5m -XX:+PrintGC出现java.lang.OutOfMemoryError: GC overhead limit exceeded 一般是(某个循环里可能性最大)在不停的分配对象,但是分配的太多,把堆撑爆了。出现java.lang.OutOfMemoryError: Java heap space一般是分配了巨型对象 栈溢出参数:-Xss256kjava.lang.StackOverflowError 一般的方法调用是很难出现的,如果出现了要考虑是否有无限递归。虚拟机栈带给我们的启示:方法的执行因为要打包成栈桢,所以天生要比实现同样功能的循环慢,所以树的遍历算法中:递归和非递归(循环来实现)都有存在的意义。递归代码简洁,非递归代码复杂但是速度较快。
JVM垃圾回收
带着三个问题看垃圾回收 1.回收谁2.什么时候回收3.怎么回收 1.回收谁引用计数法:给对象中添加一个引用计数器,每当有对象引用它时,计数器就加1,引用失效就减1,计数器到0的时候代表不能使用该对象,不能解决循环引用的问题 可达性分析:通过GCRoots做为起点,从这个起点向下搜索,当一个对象到GCRoots没有任何引用链相连的时候,这个对象是不可用的,可以回收。虚拟机栈(栈帧变量表中引用的对象),方法区(类的静态属性引用的对象,常量引用的对象),本地方法中JNI(Native引用的对象) "食之无味,弃之可惜"强引用(认可OOM,也不会回收)软引用(系统OOM之前,这些对象被回收)弱引用(无论内存够不够,都会回收)虚引用(只会收到回收通知) "最后一刻挣扎"一个对象的死亡,至少要被标记两次,第一次看有没有必要执行该对象中的finalize方法,如果该方法被调用过或者对象没有覆盖整个方法,就没有必要执行finalize。如果执行了finalize,可以在方法里面自救,自救方案是与引用链上任何一个对象关联即可。不建议使用 方法区的回收回收效率低,回收严谨。只有满足以下三点才会回收1.该类的所有实例都被回收,堆中不存在任何该类的实例2.加载该类的ClassLoader已被回收3.该类的java.lang.Class对象没有任何地方被应用,无法通过反射来访问该类 2.什么时候回收应用线程空闲时内存满的时候 3.怎么回收标记清除算法先标记要回收的对象,在统一清除。缺陷是会产生大量不连续的内存碎片,在分配大对象时,不得不提前触发另一次垃圾收集动作 复制算法将内存分AB两块,每次只用一块,A的内存用完了,回收的时候就将A还存活的对象放在B上,然后统一清理A。缺陷是对象多的时候浪费复制时间,对内存的开销也比较大。 标记整理算法标记出所有要回收的对象,标记完成后,将存活的对象移动一端,然后清理掉端边界以外的内存。 分代收集算法根据新生代和老年代的特点,使用分代收集算法因为新生代朝生夕死,所以用复制算法,仅需要复制少量对象。老年代存活率高,对象多,没有额外空间进行分配,就使用标记-整理算法。可以自由搭配很多种,不过大致的类型就是以下几种。 串行收集器(Serial)-XX:+SerialGC单线程收集器,这里的单线程不是指垃圾回收的线程只有一个,而是相对于应用程序来讲,在回收垃圾的时候要暂停应用程序(STW)在内存不足时,串行GC设置停顿标识,当所有线程到达安全点后,应用程序暂停,开始垃圾收集。适合堆内存不高且单核的cpu使用。 并行收集器(ParNew,Parallel Scavenge)-XX:+UseParNewGCParNew是Serial收集器的多线程版本,搭配老年代CMS首选。适合多核cpu。ParallelScavenge更关注吞吐量,同样需要STW,适合和前台交互少的系统,后台处理任务量大的 并发收集器(CMS)更关注低延迟的收集器,分为以下四个阶段,适合内存大,多核cpu。缺陷:消耗内存过的大,容易引起fullGC。有碎片,为防止fullGC,默认开启碎片整理参数 初始标记:以STW的方式标记所有根对象,很快并发标记,与程序并发执行,标记出所有根路径的可达路径重新标记,以STW标记有可能在这期间错过的,同样很快并发清除,将不可达对象并发回收 G1收集器引入了 Region概念,和CMS比较像,只不过有Region的优势 观看GC日志33.125代表虚拟机启动到现在,经过了多少秒Full GC和GC代表停顿类型,不是为了区分新生代和老年代的,如果有Full,代表是以SWT触发的垃圾收集DefNew,Tenured,Perm才是区域,发生在什么区域上的3324K - > 152K(3712K) :GC前该内存区域已使用的容量 -> GC后该内存已使用的容量(总容量) 内存分配与回收策略 对象优先在Eden分配,如果说Eden内存空间不足,就会发生Minor GC大对象直接进入老年代,大对象需要大量连续内存空间的Java对象,比如很长的字符串和大型数组,1、导致内存有空间,还是需要提前进行垃圾回收获取连续空间来放他们,2、会进行大量的内存复制。-XX:PretenureSizeThreshold 参数 ,大于这个数量直接在老年代分配,缺省为0 ,表示绝不会直接分配在老年代。长期存活的对象将进入老年代,默认15岁,-XX:MaxTenuringThreshold调整动态对象年龄判定,为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄空间分配担保:新生代中有大量的对象存活,survivor空间不够,当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代.只要老年代的连续空间大于新生代对象的总大小或者历次晋升的平均大小,就进行Minor GC,否则FullGC。内存泄漏和内存溢出内存泄漏是该释放的对象没有得到释放内存溢出是撑爆了内存,对象太多了 JDK为我们提供的工具 jps列出当前机器上正在运行的虚拟机进程-p :仅仅显示VM 标示,不显示jar,class, main参数等信息.-m:输出主函数传入的参数. 下的hello 就是在执行程序时从命令行输入的参数-l: 输出应用程序主类完整package名称或jar完整名称.-v: 列出jvm参数, -Xms20m -Xmx50m是启动程序指定的jvm参数 jstat是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。假设需要每250毫秒查询一次进程2764垃圾收集状况,一共查询20次,那命令应当是:jstat-gc 2764 250 20常用参数:-class (类加载器)-compiler (JIT)-gc (GC堆状态)-gccapacity (各区大小)-gccause (最近一次GC统计和原因)-gcnew (新区统计)-gcnewcapacity (新区大小)-gcold (老区统计)-gcoldcapacity (老区大小)-gcpermcapacity (永久区大小)-gcutil (GC统计汇总)-printcompilation (HotSpot编译统计) jmapjstack.........
宜信开源UAVStack功能上新新增JVM监控分析工具
摘要:UAVStack推出的JVM监控分析工具提供基于页面的展现方式,以图形化的方式展示采集到的监控数据;同时提供JVM基本参数获取、内存dump、线程分析、内存分配采样和热点方法分析等功能。 引言作为AllInOne的智能化服务技术栈,UAVStack提供了非常全面的监控数据采样功能,同时支持数据监控与预警。近期,我们整合了原有的数据采集展示功能,新增JVM分析功能,推出了更易用的JVM监控分析工具。 熟悉JDK的开发者都知道,JDK本身提供了一套JVM分析工具,包括jinfo、jmap、jstack等。用户可以通过命令行轻松获取JVM内存堆栈信息、内存对象分配以及JVM启动基本参数信息。但这些工具需要在命令行环境中执行,且生产环境下则需要通过堡垒机转发。 开源社区一些不错的JVM分析工具也可以提供获取JVM基本信息、追踪堆栈、获取内存信息等功能,但同样需要命令行CLI的支持。 UAVStack推出的JVM监控分析工具提供基于页面的展现方式,以图形化的方式展示采集到的监控数据;同时提供JVM基本参数获取、内存dump、线程分析、内存分配采样和热点方法分析等功能。 一、架构JVM监控分析工具基于UAVStack既有架构,整体分为前端、后台及中间件增强框架(MOF)。其中: 前端负责展示数据、向后台发送用户执行指令;后台负责下发指令、响应用户查询、处理采集到的数据;中间件增强框架(MOF)负责接收后台下发的指令、执行指令并返回数据或将数据写入文件,然后通过UAV提供的文件归集功能上送数据。整体架构流程如下图所示: 二、关键技术2.1 JMXJMX提供相关接口,获取基础的JVM监控数据,如内存堆大小、GC情况等,是JVM监控数据的基础。 2.2 中间件增强框架(MOF)作为分析工具整条链路的基础,MOF依附于用户应用,主要提供以下基础支持: 基础数据采集:MOF植入应用中,JMX定期获取并上报相关JVM的基本信息数据,为展示和预警提供数据基础。请求捕获、指令执行:JVM监控分析工具的大多数功能都需要下发指令至应用所在的服务器。MOF负责把下发指令的请求拦截下来,执行并返回对应的结果。2.3 Java Attach APIJava Attach API是由Sun提供的一套非标准API,可以将用户连接到运行中的虚拟机进程上,进行agent的挂载等操作。 在JVM监控分析工具中,Java Attach API主要用于Attach到虚拟机进程,进行如下操作: 获取JMX Connection:从外部获取JVMConnection,得到MXBean,抓取运行数据。(CPU采样分析)获取VirtualMachine对象:调用接口,得到堆内存分布信息。(内存采样分析)三、功能展示3.1 基本监控选择应用实例后即可进入基本监控页面。 该页面主要展示CPU使用率、线程情况、内存占用和GC情况。用户可以根据需求调整时间范围,查看不同时段的监控数据。 3.2 JVM摘要JVM摘要页面显示当前虚拟机的基本参数信息,包括基本信息、JVM参数和系统属性。其中: 基本信息包括pid、主机信息、启动参数以及JVM的启动时间等最基本、最重要的信息;JVM参数包括所有JVM启动参数,用户可查看指定的堆大小、垃圾回收器信息等;系统属性包括写入System.Properties中的所有配置信息以及Javaagent的配置属性。 3.3 线程分析线程分析通过执行jstack获取线程基本信息,并对输出结果进行分析,得到线程状态数量、有无死锁等信息。 3.4 内存Dump内存Dump通过执行jmap获取指定JVM的堆栈dump文件。 用户可以便捷地在前端一键生成dump,不需要再登录堡垒机。点击“刷新”可以查看近期dump内存的操作记录。 3.5 CPU分析CPU分析是基于线程栈的采样分析,主要提供两个功能:线程执行时间以及方法热点采样。 线程执行时间是指线程在采样期间的活动时间。查询结果按照线程活动总时间排序,同时提供线程名称和线程执行时间信息,用户可据此判断应用的执行情况。 方法热点采样统计所有方法的执行时间,提供方法的类名和方法名信息。其中,方法的自用执行时间不包括方法调用其他方法的执行时间。查询结果按照方法的自用执行时间降序排序,用户可以查看当前应用内部耗时较长的执行方法,判断应用是否异常、是否需要优化。 3.6 内存分析内存分析是基于线程以及堆的统计采样分析,主要提供两个功能:每个线程的内存分配和堆内分配细节。 线程内存分配提供每个线程的内存分配大小和线程名称等信息,按照内存分配大小降序排列。用户可查看当前占用内存较大的线程。 堆内分配提供了各个类在堆内的分配实例数以及所占用的堆内存,按照堆内存大小降序排列。用户可把该功能当作简易的dump及分析工具,快速分析内存分配情况,发现内存分配问题。 总结JVM监控分析工具是从监控、分析到展示的一体化工具。JDK自带的工具虽然也可以实现除CPU分析之外的其他功能,但不够便捷,也无法实现从采样、分析到图形化展示的一体化效果。JVM监控分析工具解决了开发人员没有线上应用堡垒机权限、无法分析采集到的数据等痛点,同时提供CPU与内存采样分析等功能,以较低的性能开销获取较为全面的JVM运行数据,帮助应用开发人员发现与分析问题,为应用开发优化提供参考依据。 UAVStack已在Github上开放源码,并提供了安装部署、架构说明和用户指南等双语文档。 官方网站:https://uavorg.github.io/main/ 开源地址:https://github.com/uavorg 作者:张明明 首发于:UAVStack智能运维
InvokedynamicJava的秘密武器
最早关于invokedynamic的工作至少可以追溯到2007年,首次成功进行的动态调用是在2008年8月26日进行的。这早于Sun被Oracle收购之前,并且按照大多数开发人员的标准,该功能已经开发了很长时间。 。 invokedynamic的卓越之处在于它是自Java 1.0以后的第一个新增的字节码。它加入了现有的调用字节码invokevirtual,invokestatic,invokeinterface和invokespecial。这四个现有操作码实现了Java开发人员通常熟悉的所有形式的方法分派,特别是: invokevirtual -实例方法的标准调用invokestatic -用于分派静态方法invokeinterface -用于通过接口调用方法invokespecial -在需要非虚拟(即“精确”)调度时使用一些开发人员可能对平台为何需要全部四个操作码感到好奇,所以让我们看一个使用不同的调用操作码的简单示例,以说明它们之间的区别: public class InvokeExamples { public static void main(String[] args) { InvokeExamples sc = new InvokeExamples(); sc.run(); } private void run() { List<String> ls = new ArrayList<>(); ls.add("Good Day"); ArrayList<String> als = new ArrayList<>(); als.add("Dydh Da"); }}这将产生字节码,我们可以使用javap工具将其反汇编: javap -c InvokeExamples.class结果输出: public class kathik.InvokeExamples { public kathik.InvokeExamples(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class kathik/InvokeExamples 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: aload_1 9: invokespecial #4 // Method run:()V 12: return private void run(); Code: 0: new #5 // class java/util/ArrayList 3: dup 4: invokespecial #6 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #7 // String Good Day 11: invokeinterface #8, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 16: pop 17: new #5 // class java/util/ArrayList 20: dup 21: invokespecial #6 // Method java/util/ArrayList."<init>":()V 24: astore_2 25: aload_2 26: ldc #9 // String Dydh Da 28: invokevirtual #10 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 31: pop 32: return}这展示了四种调用操作码中的三种(其余一种,invokestatic 是微不足道的扩展)。首先,我们可以看到两个调用(在run方法的字节11和28处): ...
Java-中的强引用软引用弱引用虚引用以及-GC-策略
在介绍各种引用之前,先简单介绍下垃圾回收 什么是垃圾回收垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。Java 语言出来之前,大家都在拼命的写 C 或者 C++ 的程序,而此时存在一个很大的矛盾,C++ 等语言创建对象要不断的去开辟空间,不用的时候又需要不断的去释放控件,既要写构造函数,又要写析构函数,很多时候都在重复的 allocated,然后不停的析构。于是,有人就提出,能不能写一段程序实现这块功能,每次创建,释放控件的时候复用这段代码,而无需重复的书写呢?1960年,基于 MIT 的 Lisp 首先提出了垃圾回收的概念,用于处理C语言等不停的析构操作,而这时 Java 还没有出世呢!所以实际上 GC 并不是Java的专利,GC 的历史远远大于 Java 的历史!Java中的垃圾回收是根据可达性分析算法来判断对象是否存活的 可达性分析算法在主流的商用程序语言(Java、C#,甚至包括前面提到的古老的Lisp)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图3-1所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。 在Java语言中,可作为GC Roots的对象包括下面几种: 虚拟机栈(栈帧中的本地变量表)中引用的对象。方法区中类静态属性引用的对象。方法区中常量引用的对象。本地方法栈中JNI(即一般说的Native方法)引用的对象。各种引用对象是否存活与“引用”有关。 在JDK 1.2以前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力(这里说的就是强引用,最基本的引用方式)。 我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。 在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。 引用类型GC策略简介强引用(Strong Reference)永远不会回收(GC ROOT可引用到的前提下)最基本的引用Object obj=new Object()软引用(Soft Reference)OOM之前回收SoftReference弱引用(Weak Reference)下一次GC前WeakReference虚引用(Phantom Reference)未知,也就是随时可能被回收PhantomReference强引用强引用就是最基本的引用方式,Object obj=new Object(),引用的是另一块内存的起始地址。强引用的对象回收基于“可达性分析”算法,当对象不可达时才可能会被回收。 比如方法中new的对象,引用赋值给方法内的局部变量(局部变量存储在栈帧中的局部变量表),当方法结束之后,栈帧出栈,对象就自然不可达了,不可达就可能会被回收 软引用软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。SoftReference<RefObj> ref = new SoftReference<RefObj>(refObj);写个例子来测试下软引用的GC策略: # JVM OPTIONS: -XX:+PrintGCDetails -Xmx5mpublic class ReferenceTest { private List<RefObj> refObjs = new ArrayList<>(); private SoftReference<RefObj> ref = new SoftReference<RefObj>(createRefObj(4096*256));//1m public void add(){ refObjs.add(createRefObj(4096)); } private RefObj createRefObj(int dataSize){ RefObj refObj = new RefObj(); byte[] data = new byte[dataSize]; for (int i = 0; i < dataSize; i++) { data[i] = Byte.MAX_VALUE; } refObj.setData(data); return refObj; } public void validRef(){ System.out.println(ref.get()); } public static void main(String[] args) { ReferenceTest referenceTest = new ReferenceTest(); for (int i = 0; i < 1200; i++) { //不停新增堆大小 referenceTest.add(); //新增后查看SoftReference中的对象是否被回收 referenceTest.validRef(); } } private class RefObj{ private byte[] data; public byte[] getData() { return data; } public void setData(byte[] data) { this.data = data; } }}ReferenceTest中维护一个RefObjList和一个SoftReference,往RefObjList不断添加对象,增加堆大小,直至内存溢出。来观察下SoftReference中引用的对象是否还存在 ...
推荐收藏系列一文理解JVM虚拟机内存垃圾回收性能优化解决面试中遇到问题
一. JVM内存区域的划分1.1 java虚拟机运行时数据区java虚拟机运行时数据区分布图: JVM栈(Java Virtual Machine Stacks): Java中一个线程就会相应有一个线程栈与之对应,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈,因此栈存储的信息都是跟当前线程(或程序)相关信息的,包括局部变量、程序运行状态、方法返回值、方法出口等等。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。堆(Heap): 堆是所有线程共享的,主要是存放对象实例和数组。处于物理上不连续的内存空间,只要逻辑连续即可方法区(Method Area): 属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据常量池(Runtime Constant Pool): 它是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。本地方法栈(Native Method Stacks):其中,堆(Heap)和JVM栈是程序运行的关键,因为: 栈是运行时的单位(解决程序的运行问题,即程序如何执行,或者说如何处理数据),而堆是存储的单位(解决的是数据存储的问题,即数据怎么放、放在哪儿)。堆存储的是对象。栈存储的是基本数据类型和堆中对象的引用;(参数传递的值传递和引用传递)那为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗? 从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据,分工明确,处理逻辑更为清晰体现了“分而治之”以及“隔离”的思想。堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这样共享的方式有很多收益:提供了一种有效的数据交互方式(如:共享内存);堆中的共享常量和缓存可以被所有栈访问,节省了空间。栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。堆和栈的结合完美体现了面向对象的设计。当我们将对象拆开,你会发现,对象的属性即是数据,存放在堆中;而对象的行为(方法)即是运行逻辑,放在栈中。因此编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。1.2 堆(Heap)和JVM栈:1.2.1 堆(Heap) Java堆是java虚拟机所管理内存中最大的一块内存空间,处于物理上不连续的内存空间,只要逻辑连续即可,主要用于存放各种类的实例对象。该区域被所有线程共享,在虚拟机启动时创建,用来存放对象的实例,几乎所有的对象以及数组都在这里分配内存(栈上分配、标量替换优化技术的例外)。 在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor(S0)、To Survivor(S1)。如图所示: 堆的内存布局: 这样划分的目的是为了使jvm能够更好的管理内存中的对象,包括内存的分配以及回收。 而新生代按eden和两个survivor的分法,是为了 有效空间增大,eden+1个survivor;有利于对象代的计算,当一个对象在S0/S1中达到设置的XX:MaxTenuringThreshold值后,会将其挪到老年代中,即只需扫描其中一个survivor。如果没有S0/S1,直接分成两个区,该如何计算对象经过了多少次GC还没被释放。两个Survivor区可解决内存碎片化1.2.2 堆栈相关的参数参数描述-Xms堆内存初始大小,单位m、g-Xmx堆内存最大允许大小,一般不要大于物理内存的80%-Xmn年轻代内存初始大小-Xss每个线程的堆栈大小,即JVM栈的大小-XX:NewRatio年轻代(包括Eden和两个Survivor区)与年老代的比值-XX:NewSzie(-Xns)年轻代内存初始大小,可以缩写-Xns-XX:MaxNewSize(-Xmx)年轻代内存最大允许大小,可以缩写-Xmx-XX:SurvivorRatio年轻代中Eden区与Survivor区的容量比例值,默认为8,即8:1-XX:MinHeapFreeRatioGC后,如果发现空闲堆内存占到整个预估堆内存的40%,则放大堆内存的预估最大值,但不超过固定最大值。-XX:MaxHeapFreeRatio预估堆内存是堆大小动态调控的重要选项之一。堆内存预估最大值一定小于或等于固定最大值(-Xmx指定的数值)。前者会根据使用情况动态调大或缩小,以提高GC回收的效率,默认70%-XX:MaxTenuringThreshold垃圾最大年龄,设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率-XX:InitialTenuringThreshold可以设定老年代阀值的初始值-XX:+PrintTenuringDistribution查看每次minor GC后新的存活周期的阈值Note: 每次GC 后会调整堆的大小,为了防止动态调整带来的性能损耗,一般设置-Xms、-Xmx 相等。     新生代的三个设置参数:-Xmn,-XX:NewSize,-XX:NewRatio的优先级: (1).最高优先级: -XX:NewSize=1024m和-XX:MaxNewSize=1024m (2).次高优先级: -Xmn1024m (默认等效效果是:-XX:NewSize==-XX:MaxNewSize==1024m) (3).最低优先级:-XX:NewRatio=2 推荐使用的是-Xmn参数,原因是这个参数很简洁,相当于一次性设定NewSize和MaxNewSIze,而且两者相等。 1.3 jvm对象1.3.1 创建对象的方式各个方式的实质操作如下: 方式实质使用new关键调用无参或有参构造器函数创建使用Class的newInstance方法调用无参或有参构造器函数创建,且需要是publi的构造函数使用Constructor类的newInstance方法调用有参和私有private构造器函数创建,实用性更广使用Clone方法不调用任何参构造器函数,且对象需要实现Cloneable接口并实现其定义的clone方法,且默认为浅复制第三方库Objenesis利用了asm字节码技术,动态生成Constructor对象1.3.2 jvm对象分配在虚拟机层面上创建对象的步骤: ...
Java杂货铺JVMClass类结构
代码编译的结果从本地机器码转为字节码,是储存格式发展的一小步,却是编程语言的一大步。——《深入理解Java虚拟机》 计算机只认识0和1.所以我们写的编程语言只有转义成二进制本地机器码才能让机器认识。然而随着虚拟机的发展,包括Java在内的很多语言,都选择了一种和操作系统、机器指令集无关的中立储存格式来储存编译后的数据。 无关性我们都知道Java经典标语,“一次编译,到处运行”。实现这一目标,每个平台上定制的虚拟机,需要读取统一的数据。这种数据不依赖于任何一种平台,甚至不关心是由哪种语言编译来的,只要统一了格式,虚拟机就能正确的使用它。这种统一的格式就是——字节码(Class文件)。 Class文件中储存了Java虚拟机指令集和符号表以及若干其他辅助和结构化约束。处于安全考虑,Class文件中使用了许多强制性的语法和结构化约束。 Class类文件的结构下面来看下本文的硬菜,Class文件的结构。虽说大佬书中是以JDK1.4为版本讲述的,但是它所包含的指令、属性是Class文件中最重要最基础的。后续不同的版本都是对它的增强。 任何一个Class文件都对应着唯一一个类或者接口的定义信息,但是反过来说,类和接口并不一定都得定义在文件里(譬如类和接口也可以通过类加载器直接生成)。 Class文件是以一组以8位字节为基础单位的二进制流,这个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中储存的内容几乎是程序运行的必要数据,没有空格存在。 Class有两种数据类型(虽然用十六进制编辑器打开,看上去都是十六进制字符):无符号数和表。无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。表是由多个无符号数或者其他表作为数据项构造成的符合数据类型,所有表都习惯性地以“info_”结尾。表用于描述层次关系的复合结构数据,整个Class文件实质上就是一张表。 其中类似于紧挨着的constant_pool_count、constant_pool 这样的数据可视为一个整体(一个表),前面记录后者数据的数量。 魔数与Class文件的版本看class文件结构那张表,第一个就是u4 magic。这是一个占了4个字节的魔数,它的唯一作用就是确定这个文件是否为一个Class文件。它就是一个标志,告诉虚拟机自己是Class文件,这样做更加安全,四个字节储存的值是固定的,十六进制下为“0xCAFEBABE”,咖啡宝贝。 接下来分别是两个字节的minor(次版本)和两个字节的major(主版本)。分别储存着此Class文件时何种版本的编译器编译的,例如50.3,50就是主版本3就是次版本。在运行时可以向下兼容,比如51版本虚拟机可以运行50.3版本的class文件,但是反过来就不行了。 常量池紧接着 constant_pool_count、constant_pool就是常量池部分。常量池可以理解为Class文件的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件最大的数据项目之一。 首先两字节的constant_pool_count是统计后面constant_pool的常量数量的。注意后面的数量是从1开始,例如constant_pool_count储存的数字是22,那么constant_pool中就储存了21个数据项。这么设计是为了让“第0个位置”储存写特殊的数据。Class文件只有这一部分计数是从1开始的,其他部分还是从0开始。 常量池中主要储存两大类常量:字面量和符号引用。字面量好理解就是注入字符串、final修饰的常量值等等。符号引用主要包含一下三个常量: 类和接口的全限定名字段的名称和描述符方法的名称和描述符Class文件中不会储存各个方法、字段的最终内存分布,只有在执行到特定的代码时才会知道真正的内存入口(某信息的地址)。在JDK1.4中,常量池可包含的常量项如下(以后的版本会对内容进行扩充): 最麻烦的这些类型分别有自己的结构,不过共同的特点是第一个字节都储存着tag,即告诉虚拟机自己那种常量项。从这部分内容可以看出很多东西,比如说一个变量名称最大时两个字节,即64KB英文字符大小,当然按常理来说不会出现这样变态的变量名吧。 访问标志在常量池结束之后,紧接着两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息。 访问标志使用或来计算,比如一个类被ACC_PIBLIC(0x0001)、ACC_SUPER(0x0020)所修饰,那么计算为0x0001|0x0020 = 0x0021,该值就是被访问标志储存的值。Java中有专门计算关键字的包。 类索引、父类索引与接口索引集合类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引是一组u2类型的数据的集合。Class文件中有这三项来确定继承关系。除了Object类以外,所有的夫索引都不是0。如果结构计数器的大小是0,那么后面那部分就没有数据。 字段表集合字段表用于描述接口或者类中声明的变量。字段包括类级变量和实例级(对象级)变量,但不包括方法内部的局部变量。以下是字段表结构和字段表的第一个属性访问标志。 access_flags 的计算方式和前面类或者接口的访问表示相同。后面紧跟着两个属性是name_index和 descriptor_index,分别代表着简易名称和方法描述符。 字段表集合中不会列出从超类或者父接口中继承下来的字段,但是可以列出本来Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性自动添加的字段。另外在Java中,同一个类不能出现简易名称相同的字段名,例如int name,后面紧跟着String name。但是在字节码层面,简易名称可以相同,后面的描述不同就好了。 方法表集合方法表的结构和字段表的机构基本类似。 与字段表集合相对应,如果父类方法在子类中没有被重写,方法表集合中就不会出现父类的方法信息。在Java语言中,要重载一个方法,除了要与原方法具有相同的简单名称之外,还要求拥有一个与原方法不同的特征签名。特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,所以仅仅是返回值不同,不是重载。 属性表集合在Class文件、字段表、方法表都携带自己的属性表集合。属性表的数据项目相对于其他部分比较宽松一点,但是内容也有很多。下面来看一下比较重要的。 Code属性Java类的程序方法体中的代码经过编译后储存在Code属性中,但是接口和抽象类中的方法就不存在Code属性中。 max_locals代表了局部变量表所需要的储存空间,其中最小单位是Slot。其中Slot可以复用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用,极大节省了空间。 code_length和code值储存的时Java源代码编译后生成的字节码指令。由于每个code只占了一个字节,所以能表示的指令数只有256个。code_length的长度虽然时四个字节,但是由于虚拟机的规定只能使用两个字节,所以最大只能编译65535条指令,一般来说也是够用了,但是在编译复杂的JSP的时候要注意,某些编译器会把JSP内容和页面输出的信息归并于一个方法中,就可能导致编译失败。 值得一提的是,Javac在编译方法的时候,参数即使你没有填,agrs_size也可能是1,这是由于隐式传进去了this,当然static修饰的方法参数就是0(不填写的情况下)。 曾经使用try-catch的时候,注意到finlly不会改变局部变量的值,以为是try已经return了,return之后才去执行的finlly中的数据,其实不然。例如下面这段代码。 public int inc(){ int x; try{ x=1; return x; }catch(Exception e){ x=2; return x; }finally{ x=3; }}这段代码永远不会输出x=3,执行顺序是这样的(以不会抛出异常为例):首先执行x=1,此时局部变量等于1.然后读到return指令,然后将x的值赋给一个空间,这个空间是return时返回的值,我们暂且将这块空间起个名字,叫做returnX,然后代码进入finally,注意此时,还在这个inc()方法的作用域中。然后将x赋值等于3,最后执行return指令,返回刚才那块returnX空间的值给调用者。离开inc()作用域,此时x那块Slot可以被复用。 其他Exceptions 储存方法throws后面的异常。LineNumberTable 不是必填项,但是默认填上,如果不填,抛异常栈的时候就无法定位到哪一行了。LocalVariableTable 不是必填项,用于描述栈帧中局部变量表中的变量于Java源码中定义的变量之间的关系。SourceFile 记录生成这个Class文件源码的名称ConstantValue 通知虚拟机自动为静态变量赋值,在<init>初始化之前就进行赋值。InnerClasser 记录内部类和宿主类之间的关联。Signature 这个写AOP的时候经常见,此属性会为泛型记录信息,因为Java在编译的时候会进行泛型擦除,所以需要记录一下,让Java在运行的时候可以拿到泛型的原始信息。BootstrapMethods 这个属性保存invokedynamic指令引用的引导方法限定符,和Invoke包有很大关系。字节码指令字节码指令不会超过256个,一般来说一个指令后面会跟着参数,这很自然,就像我们写方法时需要加入参数(没有参数也是种参数)。但是由于Java虚拟机采用面向操作数栈而不是寄存器(编译语言)的架构,所以大多数情况下只包含一个操作码。 ...
Java杂货铺JVMJava高墙之GC与内存分配策略
Java与C++之间有一堵由内存动态分配和垃圾回收技术所围成的“高墙”,墙外的人想进去,墙外的人想出来。——《深入理解Java虚拟机》 前言上一章看了高墙的一半,接下来看另一半——GC。 为什么需要GC和内存分配策略?当需要排查各种内存溢出、内存泄漏问题时,当垃圾回收成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的控制和调节。 程序计数器、虚拟机栈、本地方法栈生命周期时伴随着线程的,所以更多的需要考虑Java堆和方法区的垃圾回收。我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的。 对象已死吗?如何判断对象是没有用了,该块内存可以被GC回收掉了。主要有两个方法。 引用计数算法就是每个对象都有个计数器,如果有一个地方对该对象有引用,计数器就加1,否则就减1,知道计数器的值为0的时候就说明这个对象没有被使用了,可以回收之。但是,主流的Java虚拟机都没有使用这个方法,因为无法解决循环引用的问题。比如有个对象A,引用了对象B,同时对象B又引用了对象A,此时两个对象的计数器都是1,但是这两个对象在逻辑上已经没有用了,白白占用了内存空间。 可达性分析算法主流的虚拟机使用的都是这个算法来判断对象是否存活(或者被使用)。这个算法的基本思路就是通过一系列的被称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所经过的路径被称为引用链(搜索的是引用,不是对象本身)。当一个对象到GC Roots没有任何引用链相连接的时候,就被视为不可用了。例如大佬书中非常经典的图,Object5、Object6、Object7 都是可以被回收的对象。 作为GC Roots的对象包括一下几种: 虚拟机栈(栈帧中的本地本地变量表)中引用的对象。方法去中类静态属性引用的对象。方法区中常量引用的对象。本地方法栈中JNL(Native方法)引用的对象。引用类型Java引用的定义很传统:如果reference类型的数据中储存的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。但是有些引用符合引用定义,但是此引用所指向的对象可能已经不可用了。所以对传统定义增强的解释就是:<mark>当内存空间还足够时,则能保存在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象,很多系统的缓存功能都符合这个定义。</mark> 所以,引用就被分成了4种类型。 强引用:最常见的引用,就是new个对象,该对象可达GC Roots。只要又强引用在,GC永远不会回收该空间。软引用:软引用用来描述一些还有用但不是必须的对象。软引用在内存溢出异常之前,将会对这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够空间,才会抛出内存溢出异常。弱引用:弱引用也是用来描述非必须的对象的,只是强度弱于软引用,弱引用所关联的对象只能生存到下一次垃圾回收发生之前,无论内存是否够用,都会回收之。虚引用:形同虚设的引用。一个对象是否虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收器回收时收到一个系统通知。finalize()的作用被检测到可达性不可达的对象,并不是立即就被收回内存,至少需要经历两次标记。第一次标记并进行一次筛选,筛选条件是是否重写了finalize()方法,如没有,或者此对象已经执行过finalize()方法(<mark>一个对象最多只能执行一次finalize()方法</mark>)了,虚拟机将它视为“没有必要执行”。 如果此对象重写了finalize()方法,并且没有执行,此对象就会被放到一个F-Queue队列中,并且根据低优先级的Finalizer线程去执行它。<mark>由于Finalizer线程优先级很低,所以需要在执行线程中sleep一会儿等待它的执行。</mark>Finalizer线程的执行也不一定要等它执行完才进行垃圾回收,毕竟这里面执行的任务可能是非常耗时的。 在重写的finalize()方法,此对象有一次(只有一次机会,毕竟finalize()方法只能执行一次)机会挽救自己,此时可以将自己(使用this关键字)重新与引用链上的对象建立关联,可达性可达就好。 但是finalize()方法机会很少有业务上的需求,毕竟它的功能try-finally也可以完成,毕竟这对于你的某个方法来说更具有实时性,并且更好控制。 回收方法区这部分不是重点,毕竟现在流行的JDK1.8已经没有了方法区,并且这块空间的垃圾回收效率极低。只需要知道这块空间只要被回收的是两部分,废弃常量和无用的类就好。 废弃常量好理解,就比方说一个字符串"abc",没有再被引用,根据可达性算法这个很好判断。对于无用的类判断条件需要符合以下三条: 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。加载该类的类加载器ClassLoader已经被回收。该对象的java.lang.Class对象没有在任何一个地方被引用,无法在任何地方通过反射访问该类的方法。垃圾收集算法标记-清除算法最基础的算法就是“标记-清除”(Mark-Sweep)算法,算法分为“标记”和“清除”两个阶段:首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。标记清除算法有两点不足:第一就是效率问题,两个阶段效率都不高。第二个问题就是空间问题,标记清楚会产生大量碎片,让物理空间不连续,导致给较大对象分配空间的时候,很容易触发一次垃圾回收机制。 复制算法复制算法将空间分成两个部分,一次只是用一个部分,当这一部分的空间用完了,直接将还存活的对象复制到另外一部分上去,然后将这一部分使用过的空间一次性清理掉。这样就是每次只对空间的一般及逆行GC操作。这样就不需要考虑碎片整理的问题了,只要移动堆顶指针,按顺序分配内存就行了。 现在的商业虚拟机基本都用这种算法回收新生代的数据。当一次GC,新生代分为两部分,一个Eden空间和两个Survivor,这两部分大小比一般是8:1:1。当一次GC操作存活的对象超过新生代的Survivor时,就需要老年代分配担保,来补充不足的空间。 标记-整理算法“标记-整理”算法首先将不可达的对象进行标记,然后将存活的对象向一端移动,然后直接清除掉端边界以外的内存。这样空间物理上就是连续的了。 分代收集算法分代收集算法,是指不同的空间根据自己的实际情况选择不同的回收机制。一般来说新生代使用复制算法。老年代一般使用标记-整理算法。 HotSpot算法实现枚举根节点由于JVM管理的内存十分的大,对象引用所占的空间可能很小并且十分零散,避免在一次GC消耗过长的时间,所以需要有种方式快速获取到对象引用。在HotSpot的虚拟机实现里面,有一个叫做OopMap的数据结构来储存这些对象引用,用于快速定位。在执行一个方法的时候,字节码层面会遇到一个OopMap记录,它通过偏移量记录着本次方法操作的字节码什么位置有引用,这样就可以找到引用了。 安全点虽说OopMap可以快速找到所有的引用,但是不可能为每一条指令都添加OopMap记录,毕竟这样的内存消耗是十分大的。只有在一些特定的地方才会添加OopMap记录,这些地点被称为安全点。安全点的选取需要符合“是否让程序长时间执行”的特征。“长时间执行”的最明显的特征就是指令序列的复用。比方说,方法调用、循环跳转、异常跳转等功能上。这里还需要注意一个问题,某一个线程就是当达到安全点了,要开始启动GC了,需要让整个程序都停下来,防止在GC的过程中产生新的垃圾,让本次垃圾回收不彻底。所以需要让所有的线程都到安全点,然后进行统一的垃圾回收。这里又两种机制,抢先式中断和主动式中断。 抢先式中断:在GC发生时,先把所有线程中断,如果发现有些线程没有在安全点,让它们恢复活跃,重新跑到安全点再中断,然后进行垃圾回收。 主动式中断:不直接对线程操作,仅仅简单设置一个标志,各个线程去轮询访问这个标志,当某个线程执行到安全点就去轮询一下,发现标志是中断状态,就将自己挂起,当所有线程都挂起的时候,就进行一次GC操作。 安全区域进行一次GC,都需要在安全点完成,但是有些线程是没有办法等它到达安全点的,比如说sleep(),不可能所有线程都等它睡完了再继续执行。所以除了安全点,还要引入安全区域的概念。<mark>安全区域是指在一段代码片段之中,引用关系不会再发生变化,所以GC是安全的。</mark>在某个线程执行在安全区域的时候,可以随意GC,当这个线程要离开安全区域的时候,需要查看此时是否又GC操作,没有的话就可以离开,如果有GC操作,就需要等待GC完成后再离开安全区域。 垃圾收集器 几个简单的垃圾回收器Serial收集器:这个垃圾回收器是线程工作的,当它开始回收的时候,所有线程都需要中断,用于新生代。 ParNew收集器:ParNew就是Serial的多线程版本。除了Serial以外,ParNew是唯一可以与CMS收集器配合工作的。ParNew在单线程或者数量较少的多线程下(CPU数量少)性能并不比Serial优秀,毕竟切换线程也很需要成本。此收集器也是用在新生代。 Parallel Scavenge收集器:也是用在新生代,这个收集器更在乎吞吐量,即用户代码运行的时间占用用户代码和垃圾回收总时间的比重。此收集器可以动态调整参数来保证适当的停顿时间和最大的吞吐量。 Serial Old收集器:单线程的用于老年代的收集器。 Parallel Old收集器:多线程的老年代收集器。 CMS收集器CMS是一种获取最短回收时间为目标的收集器,目前很大一部分的Java应用集中在互联网站或者B/S系统的服务器上,这类应用尤其重视服务的响应,希望更短的停顿时间。 CMS需要四个步骤:初始标记、并发标记、重新标记、并发清除。其中初始标记和重新标记都需要让所有线程都终止。并发标记可以让用户的工作线程同时运行,所以可能出现新的垃圾,重新标记就是为了解决这个问题的。 CMS有三个明显的缺点: CMS收集器对CPU资源非常敏感,当CPU数量少的时候性能极差。CMS阈值低,由于需要一部分空间留给并发,所以不能达到100%就需要开启GC。现在最高占用空间达到92%。由于使用的“标记-清除”功能,所以会产生大量的碎片。G1收集器G1收集器是一款面向服务端应用的垃圾收集器。G1收集器可以作用于新生代和老年代。并且有非常好的并发并行机制,可以进行空间整理,还有个非常优秀的特点是可以预测停顿时间,可以让使用者指定在固定的时间M毫秒内,垃圾回收所占用的时间不能超过N毫秒。 G1 收集器可以让Java堆划分成多个Region空间(其中仍然有新生代和老年代)独自管理,这样就可以根据某个区域内进行垃圾回收。并且后台维护者一个优先列表,指定哪些Region空间先被手机。 同时为了解决不同的Region通讯问题,比如ARegion中的对象引用了BRegion内的对象,每个Region维护着一个Remembered Set记录着这些信息。 内存分配与收回策略对象主要分配在新生代的Eden区上,或者分配在TLAB(线程独享)上,少数情况也可以直接分配在老年代上。这取决于你使用的垃圾收集器和参数设定。下面有几条普遍的内存分配规则。 对象有限在Eden上分配如果发现Eden上的空间不够了,会进行一次新生代GC。2个Survivor一个叫FROM,一个叫TO。当进行新生代GC的时候,Eden中的数据会复制到TO中,FROM内的数据根据年龄看是去往TO还是进入老年代。接着TO和FROM互换姓名,然后清空Eden和TO的数据。另外老年的GC收集是新生代时间的10倍。 大对象直接进入老年代一般来说新生的对象会在新生代,过了一段时间,一定数量的新生代GC(默认15次)之后,存活下来的对象再被放进老年代中。但是有些比较大型的对象,比如字符串或者非常大的数组就直接放到老年代了,这样就避免了多次新生代GC,来回复制这种超长的空间了。 长期存活的对象进入老年代一定数量的新生代GC(MaxTenuringThreshold默认15次)之后,存活下来的对象再被放进老年代中。 动态对象年龄判定如果再Servivor空间中相同年龄(经历GC次数)所有对象大小的总数大于Servivor空间的一半的时候,年龄大于或等于这一数值的对象直接进入老年代,无需等待MaxTenuringThreshold要求的年龄。 空间担保机制空间担保机制就是在新生代GC的时候,如果Servivor空间不够放来自Eden的对象,可以由担保人老年代来放些数据。 在新生代GC之前,虚拟机会区检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,这次GC是安全的。如果不大于,就回去看是否开启了空间担保机制,如果开启了就会继续检查老年代最大可用的连续空间是否大于历次晋升老年代对象的平均大小,如果大于就可以冒险试一下GC,如果不大于,就会触发全局的GC(Full GC)。 为什么会有这样的冒险?因为新生代多出来的数据老年代不一定放的下,毕竟没人为老年代做担保了。究竟多出来的数据能不能放下呢,这就需要经验来判断,算下历次从新生代过来的数据平均值,假定频率等于概率,来和老年代剩余的空间作比较。
Java杂货铺JVMJava高墙之内存模型
Java与C++之间有一堵由内存动态分配和垃圾回收技术所围成的“高墙”,墙外的人想进去,墙外的人想出来。——《深入理解Java虚拟机》 前言《深入理解Java虚拟机》,学习JVM的经典著作,几乎学习JAVA的小伙伴人手一本。当初买了,翻看了一部分,到了字节码那边彻底读不下去了,遂弃之。最近打算看Spring源码,反射、动态代理、设计模式等基础工具的确可以让我更加容易理解源码内容。然而,看着看着才发现,这个平常我们几乎用不到的东西(除了面试),才应该是理解java生态的出发站。所以,停下手来,重新看下这本书,再全面的了解下虚拟机,这次无论多么困难,也要把书读完,同时记好内容笔记和思考补充。作为Java围城之一的内存模型,比当时第一个要看的内容。 出发,看看JVM大工厂刚开始学Java的时候,被贯彻最多的两句话就是“一次编译,到处运行”和“Java不需要手动释放内存”。能做到这两点都是由于Jvm的存在。记得大学第一个启蒙语言c,电脑安装了一个cfree(一个体积超小的ide)就可以直接写了。而Java还需要下载一个叫JDK的东西,来开发。JDK包含一个叫JRE的东西,是Java的运行环境,之所以可以运行,是jre下拥有着JVM虚拟机。JVM作为一个程序,一定会占用电脑内存,而它所管辖内存间数据的互动,驱动着Java的工作。 线程的指挥官:程序计数器作为面向对象语言,Java每个类都有自己的属性和使命,并且暴露方法出来供其他成员调用。一个业务逻辑,不同对象之间调用方法、返回调用者,一个方法内部分支、循环等基础功能,都需要一个指挥官来完成,指挥官告诉这个线程内的对象执行的先后顺序。这个指挥官就叫做程序计数器。<mark>程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。</mark>因为一个CPU同一时间只能操作一个线程中的指令,所以每个线程需要私有一个指挥官,所以程序计数器这类内存也叫做线程私有内存。 如果一个线程正在执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果是正在执行的Native方法,这个计数器值则为空(Undefined)。Native方法就是Java调取本地其他语言的方法,此方法实现不受JVM管控,所以无法感知到地址,计数器值自然为空。 另外,程序计数器区域是唯一一个Java虚拟机规范中没有规定任何OutOfMemoryError情况的内存区域。 引用的地盘: Java虚拟机栈我们使用Java新建一个对象,首先需要声明类型,此时就出现了一个引用,引用指向创建出的对象。我们都知道引用在栈中,对象在堆中,此时说的栈就特指Java虚拟机栈。Java虚拟机栈同样属于线程私有的,所以生命周期和线程相同。每个方法在创建的同时,都会创建一个栈帧用于储存局部变量表、操作数栈、动态链接、方法出入口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 局部变量表存放了编译时克制的基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference)。对象引用直接或者间接指向堆中对象的地址。由于此过程是在编译时期完成的,所以局部变量内存分配大小是固定的,不会在运行时改变大小。其中64位长度的long和double类型的数据都占用了2个局部变量空间(Slot),其他数据类型只占1位。 在这个区域可能会出现两种异常:如果线程请求的栈深度过大,也就是说虚拟机栈在自己管辖的内存造成的原因,会抛出StackOverflowError异常,这个一般比较深的递归可能会造成。如果虚拟机栈发现自己内存不够,动态扩展,并且无法申请到足够的空间时,就会抛出OutMemoryError异常。 虚拟机栈的孪生兄弟:本地方法栈本地方法栈几乎与虚拟机栈发挥的作用基本相似,毕竟孪生兄弟嘛。区别是Java虚拟机栈是为字节码服务的,也就是Java方法本身。而本地方法栈是为了Native方法服务的,这个涉及调取本地的语言,例如C。 这里插个小曲,native对于咱们Java编程者来说很少直接操作,但是这东西无处不在,比如说Object类,你看源码,很多方法都有native关键字。这些方法具体实现在java代码里面无论如何都找不到的,因为具体实现就是调取的本地,并且调取本地的代码不受JVM控制!在编译的过程中,如果发现一个类没有显示继承,那么就会被隐式继承Object类,也就有了Object类所有的方法。 GC最喜欢的地方:Java堆我们常说的堆栈,说的就是这个堆。可以说Java堆是虚拟机所管辖最大的一块内存空间,并且此空间是所有线程共享的。<mark>几乎所有的对象实例都分配在这里</mark>,所有的对象实例和数组都要在堆上索取空间。Java堆也是垃圾收集器管理的主要区域,这个以后会细讲。Java堆可以处于物理上不连续的空间中,只要逻辑上是连续的即可。如果堆中没有内存完成实例分配,并且对也无法再拓展时,将会抛出OutOfMemoryError异常。 永久代的伪装:方法区大佬书中讲这部分内容的时候还是以JDK1.6为范本,但是直接被堆内存所托管了。JDK1.8这部分已经变成元空间了,并且成为了堆外内存,不受JVM直接管辖。但是为了更好的理解JVM内存模型的设计理念还是看下这部分内容。 方法区也属于线程共享区间,它储存着<mark>类信息、常量、静态变量即时编译后的代码等数据</mark> 相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这群有同样的内存回收目标主要是针对常量池的回收和堆类型的卸载,但是回收条件相当苛刻。同堆一样,可能会导致OutOfMemeoryError异常。 运行可变区域:运行时常量池既然有运行时常量池,就会有普通的常量池(简称常量池)。常量池用于存放编译期生成的各种字面量和符号引用,字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:类和接口的全限定名、字段名称和描述符、方法名称和描述符。 运行时常量池相对于普通的常量池(又称Class文件常量池)有一个重要特征动态性。Java语言并不要求常量只能在比那一起才能产生,运行期间也可以加入常量到常量池(运行时常量池)中,比如String的intern()方法。 运行时常量池属于方法区的一部分,自然受到方法去内存的限制,也会抛出OutOfMemoryError异常。 JVM外的世界:直接内存直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域。还记着前面说的有native关键字的方法吗?包括netty模块的一些Native函数库都是直接分配堆外内存的,然后通过一个储存在Java堆中的DirectByteBuffer对象作为这块内存的引用来操作。这样做,就是以为需要操作的数据在Native堆(你电脑上不被JVM管辖的内存空间)上,避免了将Java堆数据和Native堆数据来回复制。当然这块内存也不能无限放大,比如超过你电脑的内存,所以也可能出现OutOfMemoryError异常。 让数据动起来内存空间不在于划分,在于使用。大佬在书中继续以HotStop虚拟机堆内存为例,讲解了数据的创建、分布、与访问。 一个对象的诞生内存分配虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能够在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。接下来,虚拟机会为这个新生儿分配内存(加载完成后的内存是完全确定大小的)。和计算机管理内存的方式一样,Java堆维护内存,有一张空闲列表,用于记录堆内哪些空间没有被使用过。由于堆在物理上是不连续的,所以就需要有个地方记录哪些空间是被使用的,哪些是空闲的。还有一种记录方式叫指针碰撞,假定Java堆中的内存是绝对规整的连续的(这显然很难做到,需要GC做压缩整理)。在这条十分规整的,十分长的堆内存空间上,有一个指针,左右两侧分别是空闲区间和已使用空间,如果有空间需要被申请或者释放,指针就左右移动。就好像温度计,水银好似已使用空间,上方空闲部分就是空闲空间,当温度达到100度,到了温度计的量程,就会炸了(出现OutOfMemoryError异常)。 原子操作为了保证内存在使用的时候是线程安全的,需要采用一些机制。第一种就是CAS机制,这是一种乐观锁机制,再加上失败重试,可以保证操作的原子性。还有一种就是本地线程分配缓冲,把内存的动作按照线程划分在不同的空间上进行,即每个线程在Java堆中预想分配一小块内存供自己使用,让Java堆的共享强制编程线程私有。 对象设置接下来,虚拟机要对对象头进行必要的设置,例如这个对象是哪个类的实例、如何才能找到<mark>类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息都存放在对象的对象头之中。</mark>完成上述操作,一个对象在虚拟机的层面已经完成了,但是在代码层面还需要设置初始值,按照程序员的意愿选择不同的构造函数,传入不同的参数进行初始化。 对象的内存分布在HotSpot的虚拟机中,对象在内存中储存的布局可以分为3块区域:<mark>对象头、实例数据、对齐填充。</mark> HotStop虚拟的对象头包含两部分信息,第一部分用于储存对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程II、偏向时间戳。官方叫这部分是Mark Word,这部分虽然在对空间上,但是这部分会根据对象的状态服用自己的储存空间。除了储存自身状态外,还有一部分内容叫类型指针,即指向它的类元数组的指针,虚拟机通过这个指针来确定这个给对象是哪个类的实例。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录组长度的数据。 接下了就是实例数据部分,即真实储存的有效信息,也就是程序代码中所定义的各种类型的字段内容。包含从弗雷继承的,和子类定义的。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops,从分配策略中可以看出,相同宽度的字段总是被安排在一起。在满足这个前提条年间的情况下,在父类中定义的变量会出现在子类之前。 第三部分就是对齐填充,没有什么特别的意义,就是个占位符。由于对象的大小必须是8字节的整数倍,由于对象头部分正好是8字节的倍数,实例数据不一定是,所以就需要填充一下。 对象的访问定位我们都知道真正的对象实在堆上,但是我们操作对象使用的是引用,在虚拟机栈上的引用是如何访问对上的数据呢?主流的有两种方式。 句柄Java堆中将会划分出一块内存来作为句柄池,reference中储存的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。 直接指针Java堆对象的布局中就必须考虑如何防止访问类型数据的相关信息,而reference中储存的直接就是对象地址。 这两种方式的优缺点就好像数组和链表一样,一个访问速度快,一个操作快。毕竟世界是公平的,省功不省力,省力不省功。句柄访问的最大优点就是reference中储存的是稳定的句柄地址,在对象被移动时指挥改变句柄中的实例数据指针,而reference本身不需要修改。所以修改数据特别快。 相应的直接指针访问最大的优势就是访问对象本身更快,毕竟少了一次指针的地址定位。HotShot最主要就是采用这种方式访问对象。 一些补充大佬在本章还进行了抛OutOfMemoryError异常的实战,内容较长,还是看书讲的更清楚些。更主要的是,我觉得实战这种东西不能只看,具体问题还得具体分析,等遇到的多了,自然解决起来就会得心应手。不过这部分内容有一些值得记录的知识点。 一般来说,栈深度(比如递归)达到1000~2000是没有问题的,所以我们写代码的时候一定要注意栈的深度,不要过深,但也要充分使用递归这种用空间省时间的方式。JDK1.6~JDK1.8常量池的位置变动,导致一些方法展现出来的现象不同。例如String.intern()方法,在1.6时代,intern()方法会将首次遇到的字符串实例复制到永久代中,返回永久代中这个字符串实例的引用。而1.7的intern()方法不会复制实例,只是在常量池中记录首次出现的实例引用。动态代理(例如CGLib)是对类的一种增强,增强的类越多,就需要更大的内存来保存这些数据。还有种动态生成就是JSP(虽然现在大多数都是前后端分离,不用这个了),JSP第一次运行需要编译成Servlet,也需要产生大量的空间。值得一提的是,原来我在上家公司,有个系统是JDK1.7,当时JSP编译出来的东西还存放在方法堆中,当时可能设置的堆内存不大,本地跑一天,每次打开JSP页面,电脑都会卡顿一下(当然机子差也是原因之一),普通的Java文件就没事,我想是不是也是这个原因呢。另外对于同一个文件,不同的加载器加载也会视为不同的类。结束感觉每次看JVM这块内容都会有新的体会。JVM作为Java运行的基石,是每一个Javaer都需要了解的。和很多面试JVM总结内容相比,看本文确实是浪费时间,但我还是想记录下看书的感受,为了将来回忆起看书时灵光一现的小想法留个笔记吧。这本书真的不错,如果想了解JVM的小伙伴还是买来看一看吧。我一直觉得,从长远来看,比起看博客看视频,看书是效益最高的方式,毕竟伴随者大量的思考。
Java杂货铺JVM虚拟机加载机制
代码编译的结果从本地机器码变为字节码,是储存格式发展的一小步,却是编程语言发展的一大步——《深入理解Java虚拟机》 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转化解析和初始化,最终形成了可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。 类型的加载、连接和初始化都是在程序运行期间完成的,虽说加大了运行时期的开销,但是大大增加了Java的灵活度,方便动态加载和连接。Java不仅可以从Class文件获取属于,也可以从其他地方例如网络中直接获取二进制流数据,这极大提高了Java的延展性。 时机类的生命周期类从开始加载到卸载一共经过了七个过程,如下图。 其中验证、准备、解析统称为连接。另外,加载、验证、准备、初始化和卸载这5个过程只是开始要按照顺序,可以同时执行,不用等待上一个过程结束之后才执行。例如,我在9点开始准备,9点10分开始初始化,9点20准备结束。 初始化时机有且只有下面五种情况,才可以称为“初始化”: 遇到 new、getstatic、putstatic、invokestatic这4个字节码指令的时候,发现类没有进行初始化,才进行初始化。其中关于new的理解,除了生成普通的类实例,当调用类的静态方法的时候也会触发初始化。使用java.lang.reflect包内的方法对类进行反射调用的时候。当一个要初始化的类发现父类还没有初始化的时候,首先需要初始化父类。当虚拟机启动的时候初始化要执行的主类(main()方法所在的类)。使用JDK1.7+版本的动态语言支持时,发现类没有初始化需要初始化之。除此之外,所有引用类的方式都不会触发初始化,仅被称为被动引用。 开个小差,在一个类的静态代码块中,如果某变量提前被被赋值,就可以被使用;如果某变量之后才赋值的,在静态代码块中使用就会报错。但是无论何时赋值,只要声明了,在静态代码块中再赋值是被允许的。看下这个例子: public class Test{ static{ i=0;//给变量赋值可以正常编译通过 System.out.print(i);//这句编译器会提示"非法向前引用" } static int i=1;}对于接口来说,有且仅有前三种情况才会被称为初始化。另外,对于接口,不需要满足提前让父接口初始化,除非你有用到父接口的时候。 过程逐步看下加载、验证、准备、解析和初始化这5个过程。 加载加载过程需要完成以下三个事情: 通过类的全限定名来获取此类的二进制字节流。将这个字节流所代表的静态储存结构转化为方法区的运行时数据结构。再内存种生成一个代表这个类的java.lang.Class对象,这种对象有别于其他普通对象,是在方法区的。对于非数组的类,加载可以通过虚拟提供的类加载器,也可以通过一用户自定义的加载器。对于数组类,数组本身不是通过加载器加载的,而是通过Java虚拟机直接创建的,数组中的元素是通过加载器创建的。 加载过程结束后,内存中就会得到一个该类的java.lang.Class对象,为后续铺垫。 验证在加载开始的同时,验证择机开启。验证是为了确保Class文件的字节流种包含的信息符合上章讲的规格,不会危害虚拟机本身。这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击,从执行性能的角度讲,验证阶段的工作量在虚拟机类加载子系统中又占了相当大的一部分。 文件格式验证首先需要验证是否符合Class文件格式的规范,比如魔数(咖啡宝贝)是否存在,主次版本号是否可以被当前虚拟机运行、常量类型的tag标志等等。这个阶段的验证时基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行储存,后面三个验证阶段全是基于方法区的储存结构进行的,不再直接进行字节流操作。 元数据验证此过程包含验证是否有父类、父类是否允许被继承啊,各种修饰符是否冲突啊等等。 字节码验证主要目的时通过数据流和控制流分析确定程序语义是合法的、符合逻辑的。此过程保证任意时刻的操作数栈的数据类型与指令代码序列都能配合工作,保证跳转指令不会跳转到方法体以外的字节码指令上,保证类型转化是正常的,保证父类和子类之间的字段不冲突等等。 由于数据流验证非常复杂,为了减缓消耗的时间,自JDK1.6开始,方法体的Code属性的属性表中增加了一项为“StackMapTable”的属性,这项属性描述了方法体中所有的基本块。在字节码验证期间,就不需要根据程序推到这些状态的合法性,只需要检验StackMapTable属性中的记录是否合法即可。大大节省了字节码验证的时间。 符号引用验证此阶段发生在虚拟机将符号引用转化成直接引用的时候,这个转化动作将在连接的第三个阶段解析的时候发生。需要验证是否可以通过字符串的全限定名找到这个类,指定的类中是否符合方法的字段描述符以及简单名称所描述的方法和字段,类、方法、字段的访问性等等。 准备准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。此时给静态变量设置初始值是零值,并不是代码中设置的具体值,具体值还需要在putstatic指令执行时才会初始代码中设置的值。除非此static变量被final修饰了们就会在此时直接设置代码中的值。 解析解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程。 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存分布无关,引用的目标并不一定已经加载到内存中。 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。 除了invokedynamic指令以外,虚拟机实现可以对第一次解析的结果进行缓存。invokedynamic指令是可动态语言支持相关的指令,所以无法做到缓存。 初始化类初始化时类加载过程的最后一步。前面的操作除了自定义的类加载器之外,都是虚拟机主导的操作,初始化阶段,开始整整执行类中定义的Java代码了。 初始化阶段时执行类构造器<client>()方法的过程。<client>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。<client>()方法不需要显示的构造父类的构造函数,已经自己构造好了,并且父类的静态代码块是先于子类的静态代码块的。并且<client>()方法执行时带锁的,不同线程执行这个方法可能会出现线程阻塞的现象。 类加载器虚拟机设计团队把类加载阶段中“通过一个类的全限定名来获取此类的二进制字节流”这个动作放到Java虚拟机外部去实现了,实现这个动作的代码块叫做类加载器。 类与类加载器对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性。如果说某个类相等,那么这两个类一定是在同一个类加载器下加载完成的。这里的相等可以使用Class的equals方法、isAssignableFrom()方法、isInstance()方法验证,也可以使用instanceof关键字做对象所属关系的判断。例如全限定名都是com.pjjlt.MyTest。一个用虚拟机自己的类加载器加载,一个用用户自定义的类加载器加载,那么这两个类就不相等,分别产生的对象实例用instanceof关键字只能作用域自己的类上才会是true。 双亲委派机制那么问题来了,我要用自定义的类加载器加载一个Object放到内存中,那岂不是整个Java的基础功能全废了。其实不然,新建的Object类也会和原生的那个Object类是被一样对待的。这就涉及了双亲委派机制。 对于虚拟机的角度来说,只有虚拟机的类加载器和用户自定义的类记载器。对于用户来说有启动类加载器(Bootstrap ClassLoader)、拓展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)这么几种,而且他们是一种组合关系来复用父加载器。 双亲委派机制工作原理:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有它反馈自己无法加载的时候,才会交给子加载器加载。 这也解释了为什么你写的Object加载器创造出来的类和原生的是同一款了,因为人家就没有被你自己写的类加载器所加载,而是某父层的加载器加载了。
记一次线上OOM异常解决过程
背景事情是这个样子的,本部门维护了一个在线报表查询服务(简称为report),近一段时间,经常有用户向运营小伙伴反馈,report经常发生页面打不开,一段时间后自己恢复的问题。虽然不是交易系统,但给用户造成的困扰也很严重,浪费了大量资源,所以最近打算抽出时间精力,集中解决下。 过程在这个现象再一次发生时,登陆到服务器发现JVM一直在进行fullGC,但始终回收不到内存,同时日志中报出了java.lang.OutOfMemoryError: Java heap space无奈之下只能先重启服务保障正常访问。随后,通过修改Tomcat启动脚本,在JVM启动参数中添加OOM dump: vi catalina.sh #java jvm config#JAVA_OPTS="-Xms1024m -Xmx4096m -Xss1024K -XX:PermSize=512m -XX:MaxPermSize=2048m"JAVA_OPTS="-Xms1024m -Xmx4096m -Xss1024K -XX:PermSize=512m -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/tomcat8999/temp"一段时间后,再一次出现问题时登陆服务器发现OOM日志已经被dump出来了,24428就是当前JVM的进程号 接下来把日志拷贝到本机,启动Eclipse Memory Analyzer,加载 java_pid24428.hprof,如图: 打开Histogram查看各个类的对象情况 除去char[],String等java基础对象外,没发现占用大量空间的自定义类,再看下DominatorTree, 这个视图列出了内存中最大的对象 这里我们发现了一个异常现象, @0x6c3391470 pool-1-thread-3 这个线程占用了整个内存的97.5%,展开后问题定位到ReportQueryController这个类,这就好办了。其实从Leak Suspects Report也可以印证这一点: 然后我们看stack详情: OK!到这里问题已经准备定位到了,ReportQueryController.writeDownFile方法是提供报表数据下载的方法,问题就出在下载环节,接下来就是查找代码漏洞了。 // 输出报表内容@Overridepublic void processRow(List<String> row) throws Exception { String value = "\n" + format.format(row.toArray()); write(out, value, charset); atomicTotal.incrementAndGet();}...if (atomicTotal.get() > DOWNLOAD_MAX) { throw new RuntimeException(DOWNLOAD_MAX_ERROR);}问题就出在这段代码里,程序本来想限制最大下载行数DOWNLOAD_MAX=100000行,所以定义了一个自增Integer,每读取一行数据则+1,最后判断这个AtomicInteger的值是否达到DOWNLOAD_MAX,进而决定是否抛出异常。可问题是,如果读出的数据量远远超过DOWNLOAD_MAX,就有可能在判断atomicTotal.get() > DOWNLOAD_MAX前把内存占满。而且我们看此时AtomicInteger的值已经是11066861 ...
每日五分钟玩转JVM对象的内存布局
概览一个对象根据不同情况可以被划分成两种情况,当对象是一个非数组对象的时候,对象头,实例数据,对齐填充在内存中三分天下,而数组对象中在对象头中多了一个用于描述数组对象长度的部分 对象头对象头分为两部分,第一部分称之为"Mark Word",第二部分是用于获取该对象类型的类型指针,如果是数组对象还包括记录数组长度的数据。 在不同的操作系统中,这些区域所占的内存也不同,在32位的系统中,MarkWord占用32bit的空间(也就是4字节)。类型指针和数组长度数据一样合作占用32bit的空间。 在64位的操作系统中,MarkWord占用64bit的空间,类型指针在不开启指针压缩(CompressedOOPs)的情况下是64bit(8 byte),而在开启指针压缩的情况下,仅剩32bit(4 byte) Mark Word这一部分存储的是对象自身的运行时数据,这一块儿内容的数据结构并不固定,它会根据对象的状态复用自己的存储空间, 这是摘自markOop.hpp文件中的片段,其中表示了对象的以下五种状态: 标志位偏向锁标识位状态010无锁011偏向锁00无轻量级锁10无重量级锁11无GC Mark我们接下来接着去看MarkWord的结构: 在这里我们可以看到,初始化的时候只是定义了无锁和偏向锁状态的结构(上半部分是没有开启COOPs-指针压缩的结构,下半部分是开启了指针压缩的结构), 当处于轻量级锁、重量级锁时,记录的对象指针,根据JVM的说明,此时认为指针仍然是64位,最低两位假定为0;当处于偏向锁时,记录的为获得偏向锁的线程指针,该指针也是64位; 更多的内容我们就不再这里扩展了,根据反馈的情况,我会在后面并发编程中单开一篇来聊聊锁的进化之路。 类型指针这个东西有时候会用到去确定该对象属于哪个类的实例,也有用不到的时候,这个要根据不同的虚拟机对于对象的定位实现算法的选择来进行(比如HotSpot JVM就使用该类型指针去获取该对象类型数据) 实例数据实例数据是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,这里的字段内容不仅仅包括当前类的字段,也包括他的父类中所定义的字段。 这部分的存储规则遵循虚拟机分配策略参数和字段在Java源码中的定义顺序,HotSpot JVM默认的分配策略是long/double, int,short/char,byte/boolean,oops(普通对象指针,Ordinary Object Pointers)也可以理解为reference,关于指针压缩我们下节去说。 这里需要注意,在父类中定义的变量会出现在子类前,但是我们可以通过将CompactFileds参数设置为true,将子类中较小的变量插入到父类大变量的空隙中。 对齐填充这部分内容并不是必须存在的,因为Hot Spot JVM中规定了对象的大小必须是8字节的整数倍,在C/C++中类似的功能被称之为内存对齐,内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。 内存对齐遵循两个规则: 假设第一个成员的起始地址为0,每个成员的起始地址(startpos)必须是其数据类型所占空间大小的整数倍结构体的最终大小必须是其成员(基础数据类型成员)里最大成员所占大小的整数倍。这里也就不难理解为什么JVM规定对象的大小必须是8字节的整数倍了,因为在64位系统下(不开启指针压缩),对象中存在很多占用8 byte的数据类型。但是同时也存在一些4 byte的数据类型,这时我们的Padding就起到了作用,去补充不满8 byte的部分,凑齐8的整数倍。 公众号
程序员楼下闲聊某次jvm崩溃排查
大望路某写字楼下。猿A:上家公司的时候,我们组那个项目,每天半夜会跑个大批量数据处理的定时任务,然后程序经常崩溃。我:哦?那怎么处理的猿A:当时的架构有点水,说让调整“伊甸园”和“from-to”的比例……崩溃和这个就没关系我:少年,你成功引起了我的注意。来来来,请你喝饮料,好好聊聊当时的情况。业务场景“猿A”是我的同事兼死党,和他详聊后大概明白了当时的场景。 据我理解,那个定时任务,会从hive里拿出超多的数据(据说2亿左右),按具体业务做数据整合和处理,最终推送到es(elasticsearch)中。(hive什么的我没搞过,但不妨碍要讨论的东西) 业务处理部分,使用了线程池FixedThreadPool。 模拟解决过程问题定位猿A:那时候怀疑是内存OOM导致的jvm崩溃,进而怀疑大量对象GC回收不了,于是打了GC日志。我:嗯,没用hs_err_pid_xxx.log分析吗?猿A:当时小,还不会这个技能……死党“猿A”当时的解决过程比较粗暴,有了怀疑就直接在启动参数增加了-XX:+PrintGC。此命令会打印GC日志,姑且认为生产环境使用GC是CMS,写个demo模拟当时的场景。 public class CMSGCLogs { //启动参数:-Xmx10m -Xms10m -Xmn4M -XX:+PrintGC -XX:+UseConcMarkSweepGC public static void main(String[] args) throws InterruptedException { // 线程数设置为1,起名`T-1` ExecutorService es = Executors.newFixedThreadPool(1, new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r,"T-1"); } }); boolean flag = true; while (flag){ es.execute(()->{ try { byte[] bytes = new byte[1024*1000*1]; //模拟从hive中读取了大量数据(1M) TimeUnit.MILLISECONDS.sleep(50L); //模拟写入es过程 } catch (Exception e) { e.printStackTrace(); } }); } es.shutdown(); }}先背一段线程池的处理过程。 ...
HotSpot虚拟机对象揭秘
本博客旨在介绍HotSpot虚拟机在Java堆中对象的创建,内存布局和访问定位的过程。 1.对象的创建 下图是对象的创建流程: 1.1类加载检查 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过。如果没有,那必须先执行相应的类加载过程。 1.2分配内存 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。 1.2.1内存分配方式指针碰撞:如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离;空闲列表:如果Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录那些内存块是可用的,在分配的时候找到一块足够大的空间划分给对象实例,并更新列表上的记录。1.2.2HotSpot如何确定内存的分配方式? 选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial,ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。 1.2.2内存分配并发问题 对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的。可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。 解决方案: 一种是对分配内存空间的动作进行同步处理,实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定;1.3初始化零值 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 1.4设置对象头 接下来,虚拟机要对对象的对象头进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息。 1.5执行init方法 执行new指令之后会接着执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 2.对象的内存布局 在HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域: 对象头(Header)实例数据(Instance Data)对齐填充(Padding)2.1对象头 HotSpot虚拟机的对象头包括两部分信息。 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。 对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 2.2实例数据 实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。 2.3对齐填充 此部分并不是必然存在的 它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数,无需对齐。所以只有当对象实例数据没有对齐时,就需要通过对齐填充来补全。 3.对象的访问定位 建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。 3.1使用句柄 如果使用句柄访问的话,那么Java堆中将会抛出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。 3.2直接指针 如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何访问类型数据的相关信息,而reference中存储的直接就是对象地址。 3.3比较 这两种对象访问方式各有优势。 使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中实例数据指针,而reference本身不需要修改。 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销。HotSpot使用的就是此访问方式。 4.参考深入理解Java虚拟机(第2版)JavaGuide
JVM垃圾回收
1.要回收的内存区域Java虚拟机的内存模型分为五个部分,分别是:程序计数器、Java虚拟机栈、本地方法栈、堆、方法区。程序计数器、Java虚拟机栈、本地方法栈都是线程私有的,也就是每条线程都拥有这三块区域,而且会随着线程的创建而创建,线程的结束而销毁,因此这三个区域不需要垃圾回收。堆和方法区所有线程共享,并且都在JVM启动时创建,一直得运行到JVM停止时。因此它们没办法根据线程的创建而创建、线程的结束而释放。堆中存放JVM运行期间的所有对象,虽然每个对象的内存大小在加载该对象所属类的时候就确定了,但究竟创建多少个对象只有在程序运行期间才能确定。 因此,堆和方法区的内存回收具有不确定性,因此垃圾回收器要负责这两个区域的垃圾回收。 2.堆内存回收2.1堆内存回收判定方式在对堆进行对象回收之前,首先要判断哪些是无效对象。一个对象不被任何对象或变量引用,那么就是无效对象,需要被回收。一般有两种判别方式: 引用计数法 每个对象都有一个计数器,当这个对象被一个变量或另一个对象引用一次,该计数器加一;若该引用失效则计数器减一。当计数器为0时,就认为该对象是无效对象。 可达性分析法 所有和GC Roots直接或间接关联的对象都是有效对象,和GC Roots没有关联的对象就是无效对象。 GC Roots是指: 1.Java虚拟机栈所引用的对象(栈帧中局部变量表中引用类型的变量所引用的对象) 2.本地方法栈所引用的对象 3.方法区中静态属性引用的对象 4.方法区中常量所引用的对象 PS:注意!GC Roots并不包括堆中对象所引用的对象!这样就不会出现循环引用。两者对比: ,引用计数法虽然简单,但存在一个严重的问题,它无法解决循环引用的问题。 因此,目前主流语言均使用可达性分析方法来判断对象是否有效。 2.2堆内存回收过程判断该对象是否覆盖了finalize()方法 若已覆盖该方法,并该对象的finalize()方法还没有被执行过,那么就会将finalize()扔到F-Queue队列中;若未覆盖该方法,则直接释放对象内存。执行F-Queue队列中的finalize()方法:虚拟机会以较低的优先级执行这些finalize()方法们,也不会确保所有的finalize()方法都会执行结束。如果finalize()方法中出现耗时操作,虚拟机就直接停止执行,将该对象清除。对象重生或死亡:如果在执行finalize()方法时,将this赋给了某一个引用,那么该对象就重生了。如果没有,那么就会被垃圾收集器清除。注意: 强烈不建议使用finalize()函数进行任何操作!如果需要释放资源,请使用try-finally。 因为finalize()不确定性大,开销大,无法保证顺利执行。 3.方法区内存回收由于方法区中存放生命周期较长的类信息、常量、静态变量,因此方法区就像是堆的老年代,每次垃圾收集的只有少量的垃圾被清除掉。方法区中主要清除两种垃圾: 废弃常量 废弃的类3.1如何判断废弃常量清除废弃的常量和清除对象类似,只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。 3.2如何判断废弃的类清除废弃类的条件较为苛刻: 该类的所有对象都已被清除该类的java.lang.Class对象没有被任何对象或变量引用。只要一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区的时候创建,在方法区中该类被删除时清除。加载该类的ClassLoader已经被回收4.垃圾回收算法4.1标记-清除算法“标记-清除”算法是最基础的收集算法。算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。“标记-清除”算法的不足主要有两个:效率问题:标记和清除这两个过程的效率都不高空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次 4.2复制算法(新生代回收算法)“复制”算法是为了解决“标记-清除”的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等的复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。现在的商用虚拟机(包括HotSpot)都是采用这种收集算法来回收新生代新生代中98%的对象都是"朝生夕死"的,所以并不需要按照1 : 1的比例来划分内存空间,而是将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。HotSpot默认Eden与Survivor的大小比例是8 : 1,也就是说Eden:Survivor From : Survivor To = 8:1:1。所以每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。HotSpot实现的复制算法流程如下: 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次出发Minor gc的时候,会扫描Eden区和From区,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden区和From区清空。当后续Eden区又发生Minor gc的时候,会对Eden区和To区进行垃圾回收,存活的对象复制到From区,并将Eden区和To区清空部分对象会在From区域和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还存活,就存入老年代。4.3标记整理算法(老年代回收算法)复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。针对老年代的特点,提出了一种称之为“标记-整理算法”。标记过程仍与“标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。 4.4分代收集算法将内存划分为老年代和新生代。老年代中存放寿命较长的对象,新生代中存放“朝生夕死”的对象。然后在不同的区域使用不同的垃圾收集算法。 5.垃圾收集器垃圾收集器分为新生代垃圾收集器,老年代垃圾收集器,通用垃圾收集器。重点掌握CMS垃圾收集器和G1垃圾收集器。 1. CMS垃圾收集器CMS收集器是一款追求停顿时间的老年代收集器,它在垃圾收集时使得用户线程和GC线程并行执行,因此在垃圾收集过程中用户也不会感受到明显的卡顿。但用户线程和GC线程之间不停地切换会有额外的开销,因此垃圾回收总时间就会被延长。垃圾回收过程 1.初始标记停止一切用户线程,仅使用一条初始标记线程对所有与GC ROOTS直接关联的对象进行标记。速度很快。 2.并发标记使用多条并发标记线程并行执行,并与用户线程并发执行。此过程进行可达性分析,标记出所有废弃的对象。速度很慢。 3.重新标记停止一切用户线程,并使用多条重新标记线程并行执行,将刚才并发标记过程中新出现的废弃对象标记出来。这个过程的运行时间介于初始标记和并发标记之间。 4.并发清除只使用一条并发清除线程,和用户线程们并发执行,清除刚才标记的对象。这个过程非常耗时。 CMS缺点 1.吞吐量低由于CMS在垃圾收集过程使用用户线程和GC线程并行执行,从而线程切换会有额外开销,因此CPU吞吐量就不如在垃圾收集过程中停止一切用户线程的方式来的高。 2.无法处理浮动垃圾,导致频繁Full GC由于垃圾清除过程中,用户线程和GC线程并发执行,也就是用户线程仍在执行,那么在执行过程中会产生垃圾,这些垃圾称为“浮动垃圾”。 如果CMS在垃圾清理过程中,用户线程需要在老年代中分配内存时发现空间不足时,就需要再次发起Full GC,而此时CMS正在进行清除工作,因此此时只能由Serial Old临时对老年代进行一次Full GC。 3.使用“标记-清除”算法产生碎片空间由于CMS使用了“标记-清除”算法, 因此清除之后会产生大量的碎片空间,不利于空间利用率。不过CMS提供了应对策略: 开启-XX:+UseCMSCompactAtFullCollection 开启该参数后,每次FullGC完成后都会进行一次内存压缩整理,将零散在各处的对象整理到一块儿。但每次都整理效率不高,因此提供了以下参数。设置参数-XX:CMSFullGCsBeforeCompaction 本参数告诉CMS,经过了N次Full GC过后再进行一次内存整理。 2.G1垃圾收集器G1的特点1.追求停顿时间2.多线程GC3.面向服务端应用4.标记-整理和复制算法合并。与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。5.可对整个堆进行垃圾回收6.可预测停顿时间:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,G1的内存模型G1垃圾收集器没有新生代和老年代的概念了,而是将堆划分为一块块独立的Region。当要进行垃圾收集时,首先估计每个Region中的垃圾数量,每次都从垃圾回收价值最大的Region开始回收,因此可以获得最大的回收效率。Remembered Set一个对象和它内部所引用的对象可能不在同一个Region中,那么当垃圾回收时,是否需要扫描整个堆内存才能完整地进行一次可达性分析?当然不是,每个Region都有一个Remembered Set,用于记录本区域中所有对象引用的对象所在的区域,从而在进行可达性分析时,只要在GC ROOTs中再加上Remembered Set即可防止对所有堆内存的遍历。G1垃圾收集过程1.初始标记:仅标记与GC ROOTS直接关联的对象,停止所有用户线程,只启动一条初始标记线程,这个过程很快。2.并发标记:进行全面的可达性分析,找出存活的对象,开启一条并发标记线程与用户线程并行执行。这个过程比较长。3.最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,这一阶段需要停顿线程,但是可并行执行。4.筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。 ...
每日五分钟玩转JVMJVM简介
聊聊JVMJVM,一个熟悉又陌生的名词,从认识Java的第一天起,我们就会听到这个名字,在参加工作的前一两年,面试的时候还会经常被问到JDK,JRE,JVM这三者的区别。 JVM可以说和我们是老朋友了,但是在工作中的应用场景也许不如那些框架,但是在关键时候还是得靠它去搞定问题,俗话说得好,知己知彼,方能百战不殆,JVM作为前往高级工程师的一道坎,从这篇文章开始,我们会去逐步的分析,讲解,攻克这座大山。 什么是JVMJVM(Java Virtual Machine),翻译成中文就是Java虚拟机,总所周知,Java语言有一个非常鲜明的特性,也是前期Java 发展的口号之一 Write Once Run Everywhere"一次编写,到处运行",相信作为一名Java开发人员,我们对这句话都不会陌生,这句话的底气就来源于我们的JVM。 其中的原理就是,我们所编写的源程序java文件,被编译成了JVM可以识别的字节码文件(以class为后缀的文件),到处运行所依赖的其实就是为不同的平台实现了不同的虚拟机。 java的程序运行在JVM上,而非直接运行在CPU上。 JVM的学习要点首先,我们需要知道的是,作为一个虚拟机,必定是拥有自己的内存机制,所以我们必须对他的内存机制有所了解,其中包括了内存结构,垃圾回收机制等一些内容。 其次,我们需要知道,我们写的源代码被翻译成了字节码文件(因其后缀名为class,所以也会被称为类文件),对于这个文件的结构,我们必须有所了解,在知道了该类的结构下,对于一些并发和多线程的知识才能做到洞若观火,知其然知其所以然。 该字节码文件在进入虚拟机到执行之前,会经历一系列的过程,我们对于这个过程的机制一般称为类加载机制,当这个类被加载到JVM中,在运行的过程中,会有一系列的指令去帮助程序到达最终的目的。 其实,上面说了那么多,都是在帮助我们对于java文件的一个执行过程发生的事情有一个更深的了解,在遇到问题时,我们可以胸有成竹,刨根问底的去解决问题,但是在日常工作中,我们不会甚至肯定不会去看我们写完的字节码文件,我们关心的是如何使用工具去调优,使最低的成本发挥最高的价值,通过对JVM的调优使我们的程序的鲁棒性得到提升。 关于JVM的一个补充虽然,JVM叫做Java Virtual Machine,但是需要注意,随着Java的发展壮大,有越来越多的语言加入到JVM生态中,比如我们耳熟能详的Groovy,Scala,Kotlin等等,他们都是依托于JVM平台的,编译产生的文件也都是后缀为class的字节码文件。 写在末尾当亲爱的读者大人看到这篇文章的时候,说明Vi的技术博客的每日五分钟,玩转JVM已经开启更新,之前的Spring Boot系列暂告一段落(后续会不定期更新),同时最开始接触写作时写的Java基础系列也会迎来一次回炉重造,敬请期待。 很庆幸能够遇到你们,谢谢你们一直以来的支持和陪伴 :) 公众号
必看java后端亮剑诛仙最全知识点
原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。你可能有所感悟。零散的资料读了很多,但是很难有提升。到处是干货,但是并没什么用,简单来说就是缺乏系统化。另外,噪音太多,雷同的框架一大把,我不至于全都要去学了吧。 这里,我大体根据基础、Java基础、Java进阶给分了下类,挑的也都是最常用最重要的工具。 这篇文章耗费了我大量的精力,你要是觉得好,请不要吝啬你的赞。如果你认同,可以关注我的微信公众号xjjdog,里面讲的就是这些内容。我会尝试更加系统化。 最新的内容会在github持续更新,添加新的精选相关文章。地址: https://github.com/sayhiai/javaok基础知识数据结构基本的数据结构是非常重要的,无论接触什么编程语言,这些基本数据结构都是首先要掌握的。具体的实现,就体现在java的集合类中。这些数据结构,就是这些复杂工具的具体原始形态,要烂记于心。 培训机构一般没有时间普及基础知识,通过算法和数据结构,“通常”能够一眼看出是否是经过培训。 常用算法算法是某些大厂的门槛。毫无疑问,某些参加过ACM的应届生,能够秒杀大多数工作多年的码农。算法能够培养逻辑思维能力和动手能力,在刚参加工作的前几年,是非常大的加分项。但随着工作年限的增加,它的比重在能力体系中的比重,会慢慢降低。 算法的学习方式就是通过不断的练习与重复。不精此道的同学,永远不要试图解决一个没见过的问题。一些问题的最优解,可能耗费了某个博士毕生的精力,你需要的就是理解记忆以及举一反三。最快的进阶途径就是刷leetcode。 对于普通研发,排序算法和时间复杂度是必须要掌握的,也是工作和面试中最常用的。时间充裕,也可涉猎动态规划、背包等较高阶的算法知识,就是下图的左列。 书籍《算法导论》 《编程之美》 《数学之美》 数据库基础 MySQLMySQL是应用最广的关系型数据库。除了了解基本的使用和建模,一些稍底层的知识也是必要的。 MySQL有存储引擎的区别。InnoDB和MyISAM是最常用的,优缺点应该明晓。ACID是关系型数据库的基本属性,需要了解背后的事务隔离级别。脏读、幻读问题的产生原因也要了解。 为了加快查询速度,索引是数据库中非常重要的一个结构,B+树是最常用的索引结构。因字符集的问题,乱码问题也是经常被提及的。 专业的DBA通常能帮你解决一些规范和性能问题,但并不总是有DBA,很多事情需要后端自己动手。 书籍《MySQL技术内幕——InnoDB存储引擎》 《高性能MySQL》 《高可用MySQL》 网络基础网络通信是互联网时代最有魅力的一个特点,可以说我们的工作和生活,每时每刻都在和它打交道。 连接的三次握手和四次挥手,至今还有很多人非常模糊。造成的后果就是对网络连接处于的状态不慎了解,程序在性能和健壮性上大打折扣。 HTTP是使用最广泛的协议,通常都会要求对其有较深入的了解。对于Java来说,熟悉Netty开发是入门网络开发的捷径。 爬虫是网络开发中另外一个极具魅力的点,但建议使用python而不是java去做。 书籍《HTTP权威指南》 《TCP/IP详解 卷一》 操作系统 Linux科班出身的都学过《计算机组成机构》这门课,这非常重要,但很枯燥。结合Linux理解会直观的多。鉴于目前大多数服务器环境都是Linux,提前接触能够相辅相成。 需要搞清楚CPU、内存、网络、I/O设备之间的交互和速度差别。对于计算密集型应用,就需要关注程序执行的效率;对于I/O密集型,要关注进程(线程)之间的切换以及I/O设备的优化以及调度。这部分知识是开发一些高性能高可靠中间件的前提,无法绕过。 对于Linux,首先应该掌握的就是日常运维,包括常用命令的使用和软件安装配置。正则也是必须要掌握的一个知识点。 脚本编程对后端来说是一个非常大的加分项。它不仅能增加开发效率,也能在一些突发问题上使你游刃有余。 书籍《UNIX环境高级编程(第3版)》 《鸟哥的Linux私房菜》 《Linux内核设计与实现》 《Linux命令行大全》 相关文章《Linux上,最常用的一批命令解析(10年精选)》 Java基础JVMJava程序员的最爱和噩梦。以oracle版本为准,各个jvm版本之间有差别。JVM的知识包含两方面。一个是存储级别的,一个是执行级别的。 以存储为例,又分为堆内的和堆外的两种,各有千秋。垃圾回收器就是针对堆内内存设计的,目前最常用的有CMS和G1。JVM有非常丰富的配置参数来控制这个过程。在字节码层面,会有锁升级以及内存屏障一类的知识,并通过JIT编译来增加执行速度。 JVM还有一个内存模型JMM,用来协调多线程的并发访问。JVM的spec非常庞大,但面试经常提及。 另外,jdk还提供了一系列工具来窥探这些信息。包含jstat,jmap,jstack,jvisualvm等,都是最常用的。 书籍《深入理解Java虚拟机》 JDK现在,终于到了java程序员的核心了:JDK,一套依据jvm规范实现的一套API。我们平常的工作,就是组合这些API,来控制程序的行为。 jdk的代码非常庞大,内容也非常繁杂。最重要的大体包括:集合、多线程、NIO、反射、文件操作、Lambda语法等。这部分内容加上下面的SSM,基本上就是大多数小伙伴玩耍的地方。 假如说数据结构和算法是理论,这里就是支撑理论的实现。Java玩的好不好,就是说这里。 书籍《Effective Java 中文版》 《数据结构与算法分析:Java语言描述》 SSM你可能会用SSM开发项目,觉得编程无非就这些东西。设计模式烂记于心,IOC、AOP手到擒来。这里集中了大部分同行,有些可能到此为止就Ok了,因为有些同学接下来的重点是项目管理,而不是技术。 SSM最擅长的是Web开发。目前的表现形式逐渐多样化,随着前后端分离的盛行,Restful这种有着明确语义的模式逐渐流行。 书籍《Head First 设计模式》 《Spring揭秘》 《SpringBoot揭秘》 《MyBatis技术内幕》 《深入剖析Tomcat》 ...
JVMSANDBOX从阿里精准测试走出的开源贡献奖
阿里妹导读:稳定性是历年双11的技术质量保障核心。从 2016 年开始淘宝技术质量部潜心修行,创新地研发了一套实时无侵入的字节码增强框架,于是「JVM-SANDBOX」诞生了,并且顺手在 MTSC 大会上拿了开源贡献奖,今天,我们来瞅瞅这个拿奖的项目。 在近日举行的中国移动互联网测试开发大会(简称MTSC大会),来自淘系技术质量开源项目「JVM-SANDBOX」以及淘系同学参与维护的「 ATX」 包揽了 MTSC 2019 年度开源贡献奖,表彰过去一年在测试领域开源项目中的突出贡献。其中,「JVM-SANDBOX」致力于为服务端稳定性领域提供实时无侵入的字节码增强框架。 一、JVM-Sandbox的诞生功能回归、业务/系统监控、问题排查定位、强弱依赖、故障演练等是阿里 10 年双十一沉淀积累下来的稳定性专项,也是历年双十一质量保障的核心要素。要有效、轻量级地实现这些稳定性专项,都会触及到一块底层技术—— java 字节码增强。如果每个专项都能自己实现一套字节码增强逻辑,实现的门槛高、投入和维护成本高,且不同专项间相互影响造成不可预知的风险。如何屏蔽字节码增强技术的高门槛,降低成本,同时又能支持上层多个专项功能的快速实现和动态管理,成为淘宝技术质量部的目标。从 2016 年开始我们潜心修行,创新地研发了一套实时无侵入的字节码增强框架,于是 「JVM-SANDBOX」 诞生了。 对上面提到的专项进行抽象分析: 故障演练:在运行前,抛出异常或增加运行时间,即:干预方法的执行顺序和改变返回值;强弱依赖梳理:系统运行时,实时记录系统的对外调用情况,即:感知方法的入参和返回值;录制回放:运行时,记录方法的入参和返回值,回放时,不真实对外调用,而是直接返回录制时的返回值。即:感知方法入参和返回值,干预方法执行顺序,改变返回值;精准回归:获取每个请求的行调用链路,根据行调用链路进行场景去重,根据代码改动的情况和行调用链路确定需要回归范围,即:运行时行链路感知。不难发现,要解决这些问题本质就是如何完成 java 方法的环绕管控和运行时行链路的获取,即 AOP 框架的解决方案。目前常用 AOP 框架的解决方案有两种:proxy 和埋点。proxy 的优点在于已实现了统一的 API,减少了重复投入,但是不能实时生效,需要系统编译重启。埋点的优点在于动态生效灵活度高,但是没有统一 API。 要快速解决上边的三个问题,我们需要的 AOP 解决方案必须具备两个特性: 动态可插拔,即实现埋点方式的统一的 API;无侵入性,即解决 JVM 类隔离的问题。基于以上需求,我们研发了 JVM-Sandbox。 二、实现方式JVM-Sandbox 由纯 Java 编码完成,基于 JVMTI 技术规范,为观察和改变代码运行结果提供了即插即用模块接口的容器,提供两个核心功能:实时无侵入 AOP 框架和动态可插拔的模块管理容器。 2.1 JVM-Sandbox的核心功能 使用埋点技术提供统一的 API,来实现无需重启的 AOP 解决方案;使用容器完成 JVM 类隔离,来解决侵入性问题;提供容器管理机制,来完成各种容器的管理。2.2 JVM—Sandbox的核心事件模型 BEFORE、RETURN 和 THROWS 三个环节事件的正常流转和干预流转。 2.3 整体架构 ...
深入浅出JVM1Java-虚拟机
Java 虚拟机地位
JVM-栈stack溢出案例
介绍当启动一个新线程时,JVM就会给这个线程分配一个Java栈(这个栈的内存大小由-Xss参数来设置)。 一个Java栈的基本单位是帧,每一次函数调用就会生成栈帧,占用一定的栈空间。当函数本身需要的内存过大,或者函数调用函数(依赖调用或者递归调用)太深,超过了-Xss设置的内存大小,就会抛出StackOverflowError异常。 -Xss:默认值 1M,控制每个线程占用的内存,这个参数决定了函数调用的最大深度。如果设置的太小可能会很容易出现 StackOverflowError 异常。 JDK 5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。 示例代码public class StackOverflow { private static int deep = 1; /** * 通过无限递归来模拟栈溢出 */ private static void recursion() { deep++; recursion(); } public static void main(String[] args) { try { recursion(); } catch (Throwable e) { // catch 捕获的是 Throwable,而不是 Exception。因为 StackOverflowError 不属于 Exception 的子类。 System.out.println("Stack deep : " + deep); e.printStackTrace(); } // 不让进程结束,便于使用分析工具来查看内存情况 try { Thread.sleep(24 * 60 * 60 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } }}执行结果 ...
JVM-堆heap溢出案例
一、说明当虚拟机申请不到内存空间的时候,会报堆内存溢出: OutOfMemoryError:java heap space。 常见的原因:http://outofmemory.cn/c/java-...我测试到时候,运行在 16G 内存的机器上。JVM 堆内存 默认为物理内存的1/4,即 16 * 1/4 = 4G JDK 8的 JVM 在 JDK 7 的基础上从堆内存中移除了永久代(Perm Generation),替换为了堆内存之外的元空间(Metaspace),元空间是堆外直接内存,不受堆内存的限制,只受物理内存的限制,可以提供更大的空间。二、原因及解决办法OutOfMemoryError 异常的常见原因: 加载的数据过大。如:加载的文件或者图片过大、一次从数据库取出过多数据代码存在死循环或循环产生过多的对象解决方法 增加jvm的内存大小,使用 -Xmx 和 -Xms 来设置检查代码中是否有死循环或递归调用。检查是否有大循环重复产生新对象实体。检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。检查List、Map等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。三、代码示例/**java堆溢出实例 * 原理:java的堆是用来存放对象实例的,所以我们只要做到以下三点就可以使堆溢出: * 1、限制堆的大小,不可扩展 * 2、不断新建对象 * 3、保持对象存活不被回收 * 对应的,我们需要: * 1、改变JVM的启动参数,将堆的最小值和最大值设成一样,这样就可以避免堆自动扩展(其实不一样也可以) * 2、不断产生对象 * 3、使用一个List来保存对象,保持对象存活 * * JVM配置参数: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * */public class HeapOom { public static void main(String[] args) { // 此list实例会存放在堆内存中 List<byte[]> list = new ArrayList<>(); int i = 0; boolean flag = true; while (flag) { try { i++; // 每次增加一个1M大小的数组对象 list.add(new byte[1024 * 1024]); } catch (Throwable e) { // catch 捕获的是 Throwable,而不是 Exception。因为 OutOfMemoryError 不属于 Exception 的子类。 e.printStackTrace(); flag = false; // 记录次数 System.out.println("count=" + i); } } // 不让进程结束,便于使用分析工具来查看内存情况 try { Thread.sleep(24 * 60 * 60 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } }}使用的 java 1.8.0_171 版本: ...
JVM如何加载一个类的过程双亲委派模型中有哪些方法
类加载过程:加载、验证(验证阶段作用是保证Class文件的字节流包含的信息符合JVM规范,不会给JVM造成危害)、准备(准备阶段为变量分配内存并设置类变量的初始化)、解析(解析过程是将常量池内的符号引用替换成直接引用)、初始化。 双亲委派模型中方法:双亲委派是指如果一个类收到了类加载的请求,不会自己先尝试加载,先找父类加载器去完成。当顶层启动类加载器表示无法加载这个类的时候,子类才会尝试自己去加载。当回到最开的发起者加载器还无法加载时,并不会向下找,而是抛出ClassNotFound异常。 方法:启动(Bootstrap)类加载器,标准扩展(Extension)类加载器,应用程序类加载器(Application ),上下文(Custom)类加载器。意义是防止内存中出现多份同样的字节码 。
深入浅出JVM序
本系列主要是让一个刚入门的 java 开发者,也能愉快的从零开始成为一个真正的 jvm 大神。 大纲java 虚拟机的定义、总体架构、常用配置垃圾回收算法、各类垃圾回收器java 虚拟机对多线程的支持java 虚拟机的 class 文件结构java 虚拟机的执行系统待定感谢您的耐心阅读,如果您发现文章中有一些没表述清楚的,或者是不对的地方,请给我留言,您的鼓励是作者写作最大的动力。 作 者 : @mousycoder 原文出处 : http://mousycoder.com/2019/06...
Spark统一内存管理
Spark1.6 以后,增加统一内存管理机制内存管理模块包括堆内内存(On-heap Memory),堆外内存(Off-heap Memory)两大区域。 1.堆内内存Executor Memory:主要用于存放 Shuffle、Join、Sort、Aggregation 等计算过程中的临时数据Storage Memory:主要用于存储 spark 的 cache 数据,例如RDD的缓存、unroll数据User Memory:主要用于存储 RDD 转换操作所需要的数据,例如 RDD 依赖等信息Reserved Memory:系统预留内存,会用来存储Spark内部对象 private val RESERVED_SYSTEM_MEMORY_BYTES = 300 * 1024 * 1024val reservedMemory = conf.getLong("spark.testing.reservedMemory", if (conf.contains("spark.testing")) 0 else RESERVED_SYSTEM_MEMORY_BYTES)systemMemory: val systemMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)Runtime.getRuntime.maxMemory就是JVM运行时的堆内存,在Java程序中通过-Xmx -Xms配置,spark中通过spark.executor.memory 或 --executor-memory 配置的。useableMemory:spark可用内存 val usableMemory = systemMemory - reservedMemory补充: val minSystemMemory = (reservedMemory * 1.5).ceil.toLongexecution Memory不得小于reservedMemory 的1.5倍。 2.堆外内存Spark 1.6 开始引入了Off-heap memory,调用Java的Unsafe类API申请堆外的内存资源,这种方式不进行Java内存管理,可避免频繁GC,但需要自己实现内存申请和释放的逻辑。 3.堆内内存动态调整初始化:程序提交时,execution和storage各占0.5(通过spark.memory.storageFraction配置) ...
Java虚拟机规范Java虚拟机的结构
Java虚拟机的结构本文档指定了一个抽象机器,它没有描述Java虚拟机的任何特定实现。 要正确实现Java虚拟机,你只需要能够读取类文件格式并正确执行其中指定的操作,不属于Java虚拟机规范的实现细节会不必要地限制实现者的创造力。例如,运行时数据区的内存布局、使用的垃圾收集算法、Java虚拟机指令的任何内部优化(例如,将它们转换为机器代码)由实现者自行决定。 本规范中对Unicode的所有引用都是针对Unicode标准版本11.0.0给出的,可从http://www.unicode.org/获取。 class文件格式由Java虚拟机执行的编译代码使用独立于硬件和操作系统的二进制格式表示,通常(但不一定)存储在文件中,称为class文件格式,类文件格式精确地定义了类或接口的表示,包括可能在特定于平台的目标文件格式中被认为是理所当然的字节排序等细节。 第4章“class文件格式”详细介绍了class文件格式。 数据类型与Java编程语言一样,Java虚拟机也可以使用两种类型:原始类型和引用类型。相应地,有两种值可以存储在变量中,作为参数传递,由方法返回,并对其进行操作:原始值和引用值。 Java虚拟机期望几乎所有类型检查都在运行时之前完成,通常由编译器完成,而不必由Java虚拟机本身完成,原始类型的值不需要被标记或以其他方式检查以在运行时确定它们的类型,或者与引用类型的值区分开来。相反,Java虚拟机的指令集使用对特定类型的值进行操作的指令来区分其操作数类型,例如,iadd、ladd、fadd和dadd都是Java虚拟机指令,它们添加两个数值并产生数值结果,但每个指令都专门分别用于其操作数类型:int、long、float和double。 Java虚拟机包含对对象的显式支持,对象是动态分配的类实例或数组,对对象的引用被视为具有Java虚拟机类型引用,类型引用的值可以被认为是指向对象的指针,一个对象可能存在多个引用,对象总是通过类型引用的值操作、传递和测试。 原始类型和值Java虚拟机支持的原始数据类型是数字类型、布尔类型和returnAddress类型。 数字类型由整数类型和浮点类型组成。 整数类型是: byte,其值为8位有符号二进制补码整数,其默认值为零。short,其值为16位有符号二进制补码整数,其默认值为零。int,其值为32位带符号的二进制补码整数,其默认值为零。long,其值为64位带符号的二进制补码整数,其默认值为零。char,其值为16位无符号整数,表示基本多语言平面中的Unicode代码点,使用UTF-16编码,其默认值为空代码点('u0000')。浮点类型是: float,其值是浮点值集的元素,或者,如果支持,则为浮点扩展指数值集,其默认值为正零。double,其值是双精度值集的元素,或者,如果支持,则为双精度扩展指数值集,其默认值为正零。布尔类型的值对真值true和false进行编码,默认值为false。 Java®虚拟机规范的第一版没有将布尔值视为Java虚拟机类型,但是,布尔值在Java虚拟机中的支持有限,Java®虚拟机规范的第二版通过将布尔值视为一种类型来澄清该问题。 returnAddress类型的值是指向Java虚拟机指令的操作码的指针,在原始类型中,只有returnAddress类型与Java编程语言类型没有直接关联。 整数类型和值Java虚拟机的整数类型的值是: 对于byte,从-128到127(-2^7到2^7 - 1),包括在内。对于short,从-32768到32767(-2^15到2^15 - 1),包括在内。对于int,从-2147483648到2147483647(-2^31到2^31 - 1),包括在内。对于long,从-9223372036854775808到9223372036854775807(-2^63到2^63 - 1),包括在内。对于char,从0到65535,包括在内。
修炼内功JVM-虚拟机视角的方法调用
本文已收录【修炼内功】跃迁之路 『我们写的Java方法在被编译为class文件后是如何被虚拟机执行的?对于重写或者重载的方法,是在编译阶段就确定具体方法的么?如果不是,虚拟机在运行时又是如何确定具体方法的?』 方法调用不等于方法执行,一切方法调用在class文件中都只是常量池中的符号引用,这需要在类加载的解析阶段甚至到运行期间才能将符号引用转为直接引用,确定目标方法进行执行 在编译过程中编译器并不知道目标方法的具体内存地址,因此编译器会暂时使用符号引用来表示该目标方法编译代码 public class MethodDescriptor { public void printHello() { System.out.println("Hello"); } public void printHello(String name) { System.out.println("Hello " + name); } public static void main(String[] args) { MethodDescriptor md = new MethodDescriptor(); md.printHello(); md.printHello("manerfan"); }}查看其字节码 main方法中调用两次不同的printHello方法,对应class文件中均为invokevirtual指令,分别调用常量池中的#12及#14,查看常量池 #12及#14对应两个Methodref方法引用,这两个方法引用均为符号引用(使用方法描述符)而并非直接引用 虚拟机识别方法的关键在于类名、方法名及方法描述符(method descriptor),方法描述符由方法的参数类型及返回类型构成 方法名及方法描述符在编译阶段便可以确定,但对于实际类名,一些场景下(如类继承)只有在运行时才可知 方法调用指令目前Java虚拟机里提供了5中方法调用的字节码指令 invokestatic: 调用静态方法invokespecial: 调用实例构造器<init>方法、私有方法及父类方法invokevirtual: 调用虚方法(会在运行时确定具体的方法对象)invokeinterface: 调用接口方法(会在运行时确定一个实现此接口的对象)invokedynamic: 先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法invokestatic及invokespecial调用的方法(静态方法、构造方法、私有方法、父类方法),均可以在类加载的解析阶段确定唯一的调用版本,从而将符号引用直接解析为该方法的直接引用,这些方法称之为非虚方法 而invokevirtual及invokeinterface调用的方法(final方法除外,下文提到),在解析阶段并不能唯一确定,只有在运行时才能拿到实际的执行类从而确定唯一的调用版本,此时才可以将符号引用转为直接引用,这些方法称之为虚方法 invokedynamic比较特殊,单独分析 简单示意,如下代码 public interface MethodBase { String getName();}public class BaseMethod implements MethodBase { @Override public String getName() { return "manerfan"; } public void print() { System.out.println(getName()); }}public class MethodImpl extends BaseMethod { @Override public String getName() { return "maner-fan"; } @Override public void print() { System.out.println("Hello " + getName()); }; public String getSuperName() { return super.getName(); } public static String getDefaultName() { return "default"; }}public class MethodDescriptor { public static void print(BaseMethod baseMethod) { baseMethod.print(); } public static String getName(MethodBase methodBase) { return methodBase.getName(); } public static void main(String[] args) { MethodImpl.getDefaultName(); MethodImpl ml = new MethodImpl(); ml.getSuperName(); getName(ml); print(ml); }}查看MethodDescriptor的字节码 ...
修炼内功JVM-虚拟机栈及字节码基础
本文已收录【修炼内功】跃迁之路 在浅谈虚拟机内存模型一文中有简单介绍过,虚拟机栈是线程私有的,每个方法在执行的同时都会创建一个栈帧,方法执行时栈帧入栈,方法结束时栈帧出栈,虚拟机中栈帧的入栈顺序就是方法的调用顺序 写了很多文字,但都不尽如意,十分惭愧的是,删了写~写了删~ 无论怎样写都不能很好的将自己的想法描述出来,这里推荐一篇思路清奇的文章 探究Java虚拟机栈附上一张自己理解的虚拟机栈结构图,以致敬意 深入理解Java虚拟机The Java Virtual Machine Instruction Set
什么会导致Java应用程序的CPU使用率飙升
问题无限循环的while会导致CPU使用率飙升吗?经常使用Young GC会导致CPU占用率飙升吗?具有大量线程的应用程序的CPU使用率是否较高?CPU使用率高的应用程序的线程数是多少?处于BLOCKED状态的线程会导致CPU使用率飙升吗?分时操作系统中的CPU是消耗us还是sy?思路1.如何计算CPU使用率?CPU%= 1 - idleTime / sysTime * 100 idleTime:CPU空闲的时间sysTime:CPU处于用户模式和内核模式的时间总和2.与CPU使用率有关的是什么?人们常说,计算密集型程序的CPU密集程度更高。 那么,JAVA应用程序中的哪些操作更加CPU密集? 以下列出了常见的CPU密集型操作: 频繁的GC; 如果访问量很高,可能会导致频繁的GC甚至FGC。当调用量很大时,内存分配将如此之快以至于GC线程将连续执行,这将导致CPU飙升。序列化和反序列化。稍后将给出一个示例:当程序执行xml解析时,调用量会增加,从而导致CPU变满。序列化和反序列化;正则表达式。 我遇到了正则表达式使CPU充满的情况; 原因可能是Java正则表达式使用的引擎实现是NFA自动机,它将在字符匹配期间执行回溯。我写了一篇文章“ 正则表达式中的隐藏陷阱 ”来详细解释原因。线程上下文切换; 有许多已启动的线程,这些线程的状态在Blocked(锁定等待,IO等待等)和Running之间发生变化。当锁争用激烈时,这种情况很容易发生。有些线程正在执行非阻塞操作,例如while (true)语句。如果在程序中计算需要很长时间,则可以使线程休眠。3. CPU是否与进程和线程相关?现在,分时操作系统使用循环方式为进程调度分配时间片。如果进程正在等待或阻塞,那么它将不会使用CPU资源。线程称为轻量级进程,并共享进程资源。因此,线程调度在CPU中也是分时的。但在Java中,我们使用JVM进行线程调度。因此,通常,线程调度有两种模式:时间共享调度和抢占式调度。 答案1. while的无限循环会导致CPU使用率飙升吗?是。 首先,无限循环将调用CPU寄存器进行计数,此操作将占用CPU资源。那么,如果线程始终处于无限循环状态,CPU是否会切换线程? 除非操作系统时间片到期,否则无限循环不会放弃占用的CPU资源,并且无限循环将继续向系统请求时间片,直到系统没有空闲时间来执行任何其他操作。 stackoverflow中也提出了这个问题:为什么无意的无限循环增加了CPU的使用? https://stackoverflow.com/questions/2846165/why-does-an-infinite-loop-of-the-unintended-kind-increase-the-cpu-use 2.频繁的Young GC会导致CPU占用率飙升吗?是。 Young GC本身就是JVM用于垃圾收集的操作,它需要计算内存和调用寄存器。因此,频繁的Young GC必须占用CPU资源。 让我们来看一个现实世界的案例。for循环从数据库中查询数据集合,然后再次封装新的数据集合。如果内存不足以存储,JVM将回收不再使用的数据。因此,如果所需的存储空间很大,您可能会收到CPU使用率警报。 3.具有大量线程的应用程序的CPU使用率是否较高?不时。 如果通过jstack检查系统线程状态时线程总数很大,但处于Runnable和Running状态的线程数不多,则CPU使用率不一定很高。 我遇到过这样一种情况:系统线程的数量是1000+,其中超过900个线程处于BLOCKED和WAITING状态。该线程占用很少的CPU。 但是大多数情况下,如果线程数很大,那么常见的原因是大量线程处于BLOCKED和WAITING状态。 4.对于CPU占用率高的应用程序,线程数是否较大?不是。 高CPU使用率的关键因素是计算密集型操作。如果一个线程中有大量计算,则CPU使用率也可能很高。这也是数据脚本任务需要在大规模集群上运行的原因。 5.处于BLOCKED状态的线程是否会导致CPU占用率飙升?不会。 CPU使用率的飙升更多是由于上下文切换或过多的可运行状态线程。处于阻塞状态的线程不一定会导致CPU使用率上升。 6.如果分时操作系统中CPU的值us或sy值很高,这意味着什么?您可以使用命令查找CPU的值us和sy值top,如以下示例所示: us:用户空间占用CPU的百分比。简单来说,高我们是由程序引起的。通过分析线程堆栈很容易找到有问题的线程。sy:内核空间占用CPU的百分比。当sy为高时,如果它是由程序引起的,那么它基本上是由于线程上下文切换。经验如何找出CPU使用率高的原因?下面简要描述分析过程。 如果发现应用程序服务器的CPU使用率很高,请首先检查线程数,JVM,系统负载等参数,然后使用这些参数来证明问题的原因。其次,使用jstack打印堆栈信息并使用工具分析线程使用情况(建议使用fastThread,一个在线线程分析工具)。 以下是一个真实案例: 一天晚上,我突然收到一条消息,说CPU使用率达到了100%。所以立即,我倾倒了用jstack打印的堆栈信息。 进一步检查日志: onsumer_ODC_L_nn_jmq919_1543834242875 - priority:10 - threadid:0x00007fbf7011e000 - nativeid:0x2f093 - state:RUNNABLEstackTrace:java.lang.Thread.State:RUNNABLEat java.lang.Object.hashCode(Native Method)at java.util.HashMap.hash(HashMap.java:362)at java.util.HashMap.getEntry(HashMap.java:462)at java.util.HashMap.containsKey(HashMap.java:449)at com.project.order.odc.util.XmlSerializableTool.deSerializeXML(XMLSerializableTool.java:100)at com.project.plugin.service.message.resolver.impl.OrderFinishMessageResolver.parseMessage(OrderFinishMessageResolver.java:55)at com.project.plugin.service.message.resolver.impl.OrderFinishMessageResolver.parseMessage(OrderFinishMessageResolver.java:21)at com.project.plugin.service.message.resolver.impl.AbstractResolver.resolve(AbstractResolver.java:28)at com.project.plugin.service.jmq.AbstractListener.onMessage(AbstractListener.java:44)现在通过这个日志找到了问题:用于反序列化MQ消息实体的方法导致CPU使用率飙升。 ...
JAVA可视化监控程序JVisualVM的使用
0x01.监控本地JAVA进程JVisualVM是Oracle程序,安装JDK默认在bin目录下打开JVisualVM默认会显示本机JAVA进程1.Tab简介概述:显示线程基本状态,线程号,JVM参数以及一些系统属性监视:显示CPU,堆/metaspace,类加载信息,线程信息 执行垃圾回收:点击会进程垃圾回收堆dump:类似jmap dump,图形界面类似MAT 可以看到该类的实例数双击类可以看到实例,字段,以及该对象的引用线程:将当前JAVA进程的全部线程信息显示 线程dump:类似于jstack打印出的文件抽样器 CPU:可以查看热点方法,那些方法时间长内存:类似每秒钟执行一次jstat,可以实时查看内存插件:Visual GC: 查看GC情况,请见插件安装0x02.使用JMX监控远程JAVA进程//to do 0x03.插件安装 选择顶部Tab页,选择工具,插件直接勾选无法下载,需要网址与JDK版本一致 点击设置右键Java VisualVM 插件中心去插件中心查看符合自己版本的地址替换插件源网址修改完后可用修改
程序员笔记如何编写高性能的Java代码
一、并发无法创建新的本机线程...... 问题1:Java的中创建一个线程消耗多少内存? 每个线程有独自的栈内存,共享堆内存 问题2:一台机器可以创建多少线程? CPU,内存,操作系统,JVM,应用服务器 我们编写一段示例代码,来验证下线程池与非线程池的区别: //线程池和非线程池的区别<font></font>public class ThreadPool {<font></font> <font></font> public static int times = 100;//100,1000,10000<font></font> <font></font> public static ArrayBlockingQueue arrayWorkQueue = new ArrayBlockingQueue(1000);<font></font> public static ExecutorService threadPool = new ThreadPoolExecutor(5, //corePoolSize线程池中核心线程数<font></font> 10,<font></font> 60,<font></font> TimeUnit.SECONDS,<font></font> arrayWorkQueue,<font></font> new ThreadPoolExecutor.DiscardOldestPolicy()<font></font> );<font></font> <font></font> public static void useThreadPool() {<font></font> Long start = System.currentTimeMillis();<font></font> for (int i = 0; i < times; i++) {<font></font> threadPool.execute(new Runnable() {<font></font> public void run() {<font></font> System.out.println("说点什么吧...");<font></font> }<font></font> });<font></font> }<font></font> threadPool.shutdown();<font></font> while (true) {<font></font> if (threadPool.isTerminated()) {<font></font> Long end = System.currentTimeMillis();<font></font> System.out.println(end - start);<font></font> break;<font></font> }<font></font> }<font></font> }<font></font> <font></font> public static void createNewThread() {<font></font> Long start = System.currentTimeMillis();<font></font> for (int i = 0; i < times; i++) {<font></font> <font></font> new Thread() {<font></font> public void run() {<font></font> System.out.println("说点什么吧...");<font></font> }<font></font> }.start();<font></font> }<font></font> Long end = System.currentTimeMillis();<font></font> System.out.println(end - start);<font></font> }<font></font> <font></font> public static void main(String args[]) {<font></font> createNewThread();<font></font> //useThreadPool();<font></font> }<font></font> }启动不同数量的线程,然后比较线程池和非线程池的执行结果: ...
JVM垃圾回收算法总结
垃圾回收算法有很多种,目前商业虚拟机常用的是分代回收算法,但最初并不是用这个算法的我们来看一下垃圾收集算法的背景知识 标记-清除算法最基础的垃圾回收算法,顾名思义,整个回收过程分两步:1.逐个标记2.统一回收该算法可以算是后来所有垃圾回收算法的基石(后续所有算法都有标记和清除这两步,只不过策略上有了一些优化)这里值得一说的是这个标记 虚拟机是如何判断一个对象是“活”还是“死”?因此又引出两种标记算法:1.引用计数算法引用计数算法非常简单且高效,当一个对象被引用一次则+1不再被引用则-1,当计数为0就是不可能在被使用的对象了,但是这种算法存在一个致命的缺陷:两个对象相互引用对方呢?所以,这种算法肯定不能用,pass掉2.可达性分析算法目前的标记算法主流实现都是用的可达性分析算法。就是以一个叫GC Roots的对象为起点,通过引用链向下搜索,如果一个对象通过引用链无法与GC Roots对象链接,就视为可回收对象,上面说的那种相互引用的情况自然也解决了。扩展:即使是可达性分析中不可达的对象也并不是非死不可,只是暂处‘缓刑’,真正宣告一个对象死亡至少还要经历两次标记过程:当被判定不可达之后那么他被第一次标记并进行筛选,若对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过就‘放生’,如果被判定需要执行finalize()方法就会被放到一个叫F-Queue的队列中进行第二次标记对象被再次被引用就会放生,否则就会被回收。 说到这里敏锐的小伙伴可能以及察觉到了,上面都在说引用所以引用的定义就显得尤为关键了JDK1.2后Java对引用的概念进行了扩充,将引用分为:强引用、软引用、弱引用、虚引用四种 强引用:用处很大,无论如何都不会被GC回收软引用:有一定用处但不大,内存实在不够才会在内存溢出之前回收掉弱引用:比软引用强度更弱一些,GC会让它多活一轮,下一轮就回收虚引用:必回收,唯一作用就是被GC回收时会收到一个系统通知复制算法前面说的标记-清除算法其实两个过程效率都很低,并且回收之后内存被‘抠出很多洞’内存碎片化严重,此时如果过来了一个较大的对象,找不到一整块连续的内存空间就不得不提前触发另外一次GC回收。而复制算法则选择将内存一分为二每次只使用其中一半,满了之后将存活的对象整齐复制到另一块干净的内存上,将剩下的碎片一次性擦除,简单高效。但是也存在一个很大的缺陷,那就是可用内存变为原来的一半了。 分代收集算法事实上后来IBM公司经过研究发现,98%的对象都是‘朝生夕死’,所以并不需要1:1的划分内存,即我们现在常用的分代收集算法:根据对象的存活周期将内存划分为两块,分别为新生代和老年代,然后对各代采用不同的回收算法,在新生代中大部分是‘朝生夕死’的对象,继续将新生代8:2划分为Eden区和survival区,其中survival区1:1分成s0和s1两块,采用之前说的复制算法,减少内存碎片的产生。新生代满了会进行一次minor GC ,minor GC 存活的对象转移到survival区,survival区满了就会将survival区进行回收,存活的survival区对象复制到另外一块survival区中,并且survival区对象每存活一轮年龄+1当到达一定年龄就会前往老年代。 扩展:JVM何时会进行全局GC 01.手动调用System.GC 但也不是立即调用02.老年代空间不足03.永生代空间不足04.计算得知新生代前往老年代平均值大于老年代剩余空间
JVM-类加载的过程
类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。 类加载的过程:加载->链接->初始化 加载:简单的说,类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的对象实例加载需要借助类加载器,在 Java 虚拟机中,类加载器使用了双亲委派模型,即接收到加载请求时,会先将请求转发给父类加载器。 双亲委派模型:每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载 链接:链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备和解析三个阶段。即将类合并至 Java 虚拟机中,使之能够执行的过程。 (1)验证验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。格式验证:验证是否符合class文件规范语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法视频被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否通过富豪引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)(2)准备为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值(3)解析将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)初始化:是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。只有当初始化完成之后,类才正式成为可执行的状态。以盖房子为例,只有当房子装修过后,Tony 才能真正地住进去。类的初始化仅会被执行一次,这个特性被用来实现单例的延迟初始化。 那么,类的初始化何时会被触发呢?JVM 规范枚举了下述多种触发情况: 1.当虚拟机启动时,初始化用户指定的主类;2.当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;3.当遇到调用静态方法的指令时,初始化该静态方法所在的类;4.当遇到访问静态字段的指令时,初始化该静态字段所在的类;5.子类的初始化会触发父类的初始化;6.如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;7.使用反射 API 对某个类进行反射调用时,初始化这个类;8.当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。
方法区到底是个什么鬼
一、方法区与永久代这两个是非常容易混淆的概念,永久代的对象放在方法区中,就会想当然地认为,方法区就等同于持久代的内存区域。事实上两者是这样的关系: 《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 同时大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。换句话说:方法区是一种规范,永久代是Hotspot针对这一规范的一种实现。而永久代本身也在迭代中: 在Java 6中,方法区中包含的数据,除了JIT编译生成的代码存放在native memory的CodeCache区域,其他都存放在永久代;在Java 7中,Symbol的存储从PermGen移动到了native memory,并且把静态变量从instanceKlass末尾(位于PermGen内)移动到了java.lang.Class对象的末尾(位于普通Java heap内);在Java 8中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间(Metaspace),‑XX:MaxPermSize 参数失去了意义,取而代之的是-XX:MaxMetaspaceSize。对于Java8, HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在。那么取代永久代的就是元空间。它与永久代有什么不同的? 存储位置不同,永久代是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存; 存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。 二、方法区里存着什么?既然永久代是方法区的一种实现,那么在Hotspot下,方法区就等于永久代,也被称为非堆。那方法区里都存着什么呢?先抛结论: 静态变量 + 常量 + 类信息(构造方法/接口定义) + 运行时常量池存在方法区中 。 类信息与类常量池方法区里的class文件信息包括:魔数,版本号,常量池,类,父类和接口数组,字段,方法等信息,其实类里面又包括字段和方法的信息。在Class文件结构中,最头的4个字节用于存储魔数Magic Number,用于确定一个文件是否能被JVM接受,再接着4个字节用于存储版本号,前2个字节存储次版本号,后2个存储主版本号,再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个U2类型的数据(constant_pool_count)存储常量池容量计数值,参见下图。 因此class文件信息和class文件常量池的关系如下图: 图中还包含一个运行时常量池,这个稍后会介绍。 class文件常量池中存储了哪些内部呢? 我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References); 每个class文件都有一个class常量池。 动态常量池运行时常量池是方法区的一部分,是一块内存区域。Class 文件常量池将在类加载后进入方法区的运行时常量池中存放。一个类加载到 JVM 中后对应一个运行时常量池,运行时常量池相对于 Class 文件常量池来说具备动态性,Class 文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用。可以说运行时常量池就是用来索引和查找字段和方法名称和描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息,这涉及到方法的调用和字段获取。 静态常量池和动态常量池的关系以及区别静态常量池存储的是当class文件被java虚拟机加载进来后存放在方法区的一些字面量和符号引用,字面量包括字符串,基本类型的常量,符号引用其实引用的就是常量池里面的字符串,但符号引用不是直接存储字符串,而是存储字符串在常量池里的索引。 动态常量池是当class文件被加载完成后,java虚拟机会将静态常量池里的内容转移到动态常量池里,在静态常量池的符号引用有一部分是会被转变为直接引用的,比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用,而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。 本节总结方法区里存储着class文件信息和动态常量池,class文件的信息包括类信息和静态常量池。可以将类的信息是对class文件内容的一个框架,里面具体的内容通过常量池来存储。 动态常量池里的内容除了是静态常量池里的内容外,还将静态常量池里的符号引用转变为直接引用,而且动态常量池里的内容是能动态添加的。例如调用String的intern方法就能将string的值添加到String常量池中,这里String常量池是包含在动态常量池里的,但在jdk1.8后,将String常量池放到了堆中。 三、jvm中的常量池在Java的内存分配中,总共3种常量池: 字符串常量池(String Constant Pool):字符串常量池在Java内存区域的哪个位置?在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中; 在JDK7.0版本,字符串常量池被移到了堆中了。至于为什么移到堆内,大概是由于方法区的内存空间太小了。 字符串常量池是什么?在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。 在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降; 在JDK7.0中,StringTable的长度可以通过参数指定: -XX:StringTableSize=66666 字符串常量池里放的是什么?在JDK6.0及之前版本中,String Pool里放的都是字符串常量; 在JDK7.0中,由于String#intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用。关于String在内存中的存储和String#intern()方法的说明,可以参考我的另外一篇博客: 需要说明的是:字符串常量池中的字符串只存在一份! ...
从单例模式到HappensBefore
目录 双重检测锁的演变过程利用HappensBefore分析并发问题无volatile的双重检测锁双重检测锁的演变过程synchronized修饰方法的单例模式双重检测锁的最初形态是通过在方法声明的部分加上synchronized进行同步,保证同一时间调用方法的线程只有一个,从而保证new Singlton()的线程安全: public class Singleton { private static Singleton instance; private Singleton() { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}这样做的好处是代码简单、并且JVM保证new Singlton()这行代码线程安全。但是付出的代价有点高昂: 所有的线程的每一次调用都是同步调用,性能开销很大,而且new Singlton()只会执行一次,不需要每一次都进行同步。 既然只需要在new Singlton()时进行同步,那么把synchronized的同步范围缩小呢? 线程不安全的双重检测锁public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }}把synchronized同步的范围缩小以后,貌似是解决了每次调用都需要进行同步而导致的性能开销的问题。但是有引入了新的问题:线程不安全,返回的对象可能还没有初始化。 ...
总要先爬出坑的JEE架构
本博客 猫叔的博客,转载请申明出处先来看看官网对它的定义。Java平台企业版(Java EE)是社区驱动的企业软件的标准。Java EE是使用Java Community Process开发的,其中包括来自行业专家,商业和开源组织,Java用户组以及无数个人的贡献。每个版本都集成了符合行业需求的新功能,提高了应用程序的可移植性并提高了开发人员的工作效率 如今,Java EE的提供了丰富的企业软件平台,并与超过 20个兼容的Java EE实现可供选择。Java EE 8,你值得了解,起码官网还提示了你它还在更新新的功能。说到JEE,做web项目的朋友其实都有所了解,它将企业级软件架构分为三个层级,web层、业务逻辑层和数据存储层。先看看图,旧时代的辉煌!先介绍一下:WEB容器:给处于其中的应用程序组件(JSP,SERVLET)提供一个环境,使JSP,SERVLET直接跟容器中的环境变量接口交互,不必关注其它系统问题。主要由WEB服务器来实现。例如:TOMCAT,WEBLOGIC,WEBSPHERE等。该容器提供的接口严格遵守J2EE规范中的WEB APPLICATION 标准。我们把遵守以上标准的WEB服务器就叫做J2EE中的WEB容器。同时,JEE 平台将不同的模块化组件聚合后运行在通用的应用服务器上,例WebLogi,WebSphere , JBoss 等,这也包含 Tomcat Tomcat 仅仅是实现了 JEE Web 规范的 Web 容器。EJB容器:Enterprise java bean 容器。更具有行业领域特色。他提供给运行在其中的组件EJB各种管理功能。只要满足J2EE规范的EJB放入该容器,马上就会被容器进行高效率的管理。并且可以通过现成的接口来获得系统级别的服务。例如邮件服务、事务管理。WEB容器和EJB容器在原理上是大体相同的,更多的区别是被隔离的外界环境。WEB容器更多的是跟基于HTTP的请求打交道。而EJB容器不是。它是更多的跟数据库、其它服务打交道。但他们都是把与外界的交互实现从而减轻应用程序的负担。例如SERVLET不用关心HTTP的细节,直接引用环境变量session,request,response就行、EJB不用关心数据库连接速度、各种事务控制,直接由容器来完成。可以看到每个层次的职责如下:Web层:负责与用户交互或者对外提供接口业务逻辑层:为了实现业务逻辑而设计的流程处理和计算处理模块数据存取层:将业务逻辑层处理的结果持久化以待后续查询,并维护领域模型中对象的生命周期。值得一提的是,JEE平台是典型的二八原则的应用场景,它将 80%通用的与业务无关的逻辑和流程封装在应用服务器的模块化组件里,通过配置的模式提供给应用程序访问,应用程序实现 20%专用逻辑,并通过配置的形式来访问应用服务器提供的模块化组件。事实上,应用服务器提供的对象关系映射服务、数据持久服务、事务服务、安全服务、消息服务等通过简单的配置即可在应用程序中使用。JEE 时代的架构已经对企业级应用的整体架构进行了逻辑分层,包括上面提到的 Web 层、业务逻 和数据存取层,分别对应上图中的 Web 容器、 JB 容器和数据存取 ORM 组件与数据持久层 (数据库) 不同的层级有自己的职责,并从功能类型上划分层级,每个层级的职责单一。在分层架构下需要对项目管理过程中的团队进行职责划分,井建立团队交流机制。根据康威定律,设计系统的组织时,最终产生的设计等价于组织的沟通结构 ,通俗来讲,团队的交流机制应该与架构分层交互机制相对应。由于在架构上把整体的单体系统分成具有不同职责的层级,对应的项目管理倾向于把大的团队分成不同的职能团队,主要包括:用户 交互 UI 团队、后台业务逻辑处理团队、 数据存取 ORM 团队与 DBA 团队等,每个团队只对自己的职责负责,并对使用方提供组件服务质量保证。让我们在看看另一个经典,职能团队划分。JEE通过对单体架构的分层,结合职能划分,开始通过架构在一定程度上进行逻辑拆分,让各个专业的人能更加高效的做他们应该做的事情。但是,每个层次的多个业务逻辑的实现会被放在同一应用项目中,并且运行在同一个服务器上。尽管大多数公司会使用规范来约束不同业务逻辑的隔离性来解祸,但是久而久之,随着复杂业务逻辑的选代增加及开发人员的不断流动,新的程序员为了节省时间和赶进度,非法使用了其他组件的服务,业务组件之间、 组件之间、数据存取之间的稿合性必然增加,最后导致组件与组件之间难以划清界限,完全祸合在一起,将来的新功能迭代、增加和维护将难上加难。(反正你如果是入职接手一个老项目,那你一般都会很头疼)就当时而言,尽管 JEE 支持 Web容器和 EJB 容器的分离部署,大多数项目仍然部署在同 个应用服务器上井跑在一JVM 进程中。说说你和JEE的那些事吧!公众号:Java猫说学习交流群:728698035现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。
不可错过的CMS学习笔记
引子带着问题去学习一个东西,才会有目标感,我先把一直以来自己对CMS的一些疑惑罗列了下,希望这篇学习笔记能解决掉这些疑惑,希望也能对你有所帮助。CMS出现的初衷、背景和目的?CMS的适用场景?CMS的trade-off是什么?优势、劣势和代价CMS会回收哪个区域的对象?CMS的GC Roots包括那些对象?CMS的过程?CMS和Full gc是不是一回事?CMS何时触发?CMS的日志如何分析?CMS的调优如何做?CMS扫描那些对象?CMS和CMS collector的区别?CMS的推荐参数设置?为什么ParNew可以和CMS配合使用,而Parallel Scanvenge不可以?一、基础知识CMS收集器:Mostly-Concurrent收集器,也称并发标记清除收集器(Concurrent Mark-Sweep GC,CMS收集器),它管理新生代的方式与Parallel收集器和Serial收集器相同,而在老年代则是尽可能得并发执行,每个垃圾收集器周期只有2次短停顿。我之前对CMS的理解,以为它是针对老年代的收集器。今天查阅了《Java性能优化权威指南》和《Java性能权威指南》两本书,确认之前的理解是错误的。CMS的初衷和目的:为了消除Throught收集器和Serial收集器在Full GC周期中的长时间停顿。CMS的适用场景:如果你的应用需要更快的响应,不希望有长时间的停顿,同时你的CPU资源也比较丰富,就适合适用CMS收集器。二、CMS的过程CMS的正常过程这里我们首先看下CMS并发收集周期正常完成的几个状态。(STW)初始标记:这个阶段是标记从GcRoots直接可达的老年代对象、新生代引用的老年代对象,就是下图中灰色的点。这个过程是单线程的(JDK7之前单线程,JDK8之后并行,可以通过参数CMSParallelInitialMarkEnabled调整)。并发标记:由上一个阶段标记过的对象,开始tracing过程,标记所有可达的对象,这个阶段垃圾回收线程和应用线程同时运行,如上图中的灰色的点。在并发标记过程中,应用线程还在跑,因此会导致有些对象会从新生代晋升到老年代、有些老年代的对象引用会被改变、有些对象会直接分配到老年代,这些受到影响的老年代对象所在的card会被标记为dirty,用于重新标记阶段扫描。这个阶段过程中,老年代对象的card被标记为dirty的可能原因,就是下图中绿色的线:预清理:预清理,也是用于标记老年代存活的对象,目的是为了让重新标记阶段的STW尽可能短。这个阶段的目标是在并发标记阶段被应用线程影响到的老年代对象,包括:(1)老年代中card为dirty的对象;(2)幸存区(from和to)中引用的老年代对象。因此,这个阶段也需要扫描新生代+老年代。【PS:会不会扫描Eden区的对象,我看源代码猜测是没有,还需要继续求证】可中断的预清理:这个阶段的目标跟“预清理”阶段相同,也是为了减轻重新标记阶段的工作量。可中断预清理的价值:在进入重新标记阶段之前尽量等到一个Minor GC,尽量缩短重新标记阶段的停顿时间。另外可中断预清理会在Eden达到50%的时候开始,这时候离下一次minor gc还有半程的时间,这个还有另一个意义,即避免短时间内连着的两个停顿,如下图资料所示:在预清理步骤后,如果满足下面两个条件,就不会开启可中断的预清理,直接进入重新标记阶段:Eden的使用空间大于“CMSScheduleRemarkEdenSizeThreshold”,这个参数的默认值是2M;Eden的使用率大于等于“CMSScheduleRemarkEdenPenetration”,这个参数的默认值是50%。如果不满足上面两个条件,则进入可中断的预清理,可中断预清理可能会执行多次,那么退出这个阶段的出口有两个(源码参见下图):* 设置了CMSMaxAbortablePrecleanLoops,并且执行的次数超过了这个值,这个参数的默认值是0;* CMSMaxAbortablePrecleanTime,执行可中断预清理的时间超过了这个值,这个参数的默认值是5000毫秒。 如果是因为这个原因退出,gc日志打印如下:有可能可中断预清理过程中一直没等到Minor gc,这时候进入重新标记阶段的话,新生代还有很多活着的对象,就回导致STW变长,因此CMS还提供了CMSScavengeBeforeRemark参数,可以在进入重新标记之前强制进行依次Minor gc。(STW)重新标记:重新扫描堆中的对象,进行可达性分析,标记活着的对象。这个阶段扫描的目标是:新生代的对象 + Gc Roots + 前面被标记为dirty的card对应的老年代对象。如果预清理的工作没做好,这一步扫描新生代的时候就会花很多时间,导致这个阶段的停顿时间过长。这个过程是多线程的。并发清除:用户线程被重新激活,同时将那些未被标记为存活的对象标记为不可达;并发重置:CMS内部重置回收器状态,准备进入下一个并发回收周期。CMS的异常情况上面描述的是CMS的并发周期正常完成的情况,但是还有几种CMS并发周期失败的情况:并发模式失败(Concurrent mode failure):CMS的目标就是在回收老年代对象的时候不要停止全部应用线程,在并发周期执行期间,用户的线程依然在运行,如果这时候如果应用线程向老年代请求分配的空间超过预留的空间(担保失败),就回触发concurrent mode failure,然后CMS的并发周期就会被一次Full GC代替——停止全部应用进行垃圾收集,并进行空间压缩。如果我们设置了UseCMSInitiatingOccupancyOnly和CMSInitiatingOccupancyFraction参数,其中CMSInitiatingOccupancyFraction的值是70,那预留空间就是老年代的30%。晋升失败:新生代做minor gc的时候,需要CMS的担保机制确认老年代是否有足够的空间容纳要晋升的对象,担保机制发现不够,则报concurrent mode failure,如果担保机制判断是够的,但是实际上由于碎片问题导致无法分配,就会报晋升失败。永久代空间(或Java8的元空间)耗尽,默认情况下,CMS不会对永久代进行收集,一旦永久代空间耗尽,就回触发Full GC。三、CMS的调优针对停顿时间过长的调优首先需要判断是哪个阶段的停顿导致的,然后再针对具体的原因进行调优。使用CMS收集器的JVM可能引发停顿的情况有:(1)Minor gc的停顿;(2)并发周期里初始标记的停顿;(3)并发周期里重新标记的停顿;(4)Serial-Old收集老年代的停顿;(5)Full GC的停顿。其中并发模式失败会导致第(4)种情况,晋升失败和永久代空间耗尽会导致第(5)种情况。针对并发模式失败的调优想办法增大老年代的空间,增加整个堆的大小,或者减少年轻代的大小以更高的频率执行后台的回收线程,即提高CMS并发周期发生的频率。设置UseCMSInitiatingOccupancyOnly和CMSInitiatingOccupancyFraction参数,调低CMSInitiatingOccupancyFraction的值,但是也不能调得太低,太低了会导致过多的无效的并发周期,会导致消耗CPU时间和更多的无效的停顿。通常来讲,这个过程需要几个迭代,但是还是有一定的套路,参见《Java性能权威指南》中给出的建议,摘抄如下:> 对特定的应用程序,该标志的更优值可以根据 GC 日志中 CMS 周期首次启动失败时的值得到。具体方法是,在垃圾回收日志中寻找并发模式失效,找到后再反向查找 CMS 周期最近的启动记录,然后根据日志来计算这时候的老年代空间占用值,然后设置一个比该值更小的值。增多回收线程的个数CMS默认的垃圾收集线程数是*(CPU个数 + 3)/4*,这个公式的含义是:当CPU个数大于4个的时候,垃圾回收后台线程至少占用25%的CPU资源。举个例子:如果CPU核数是1-4个,那么会有1个CPU用于垃圾收集,如果CPU核数是5-8个,那么久会有2个CPU用于垃圾收集。针对永久代的调优如果永久代需要垃圾回收(或元空间扩容),就会触发Full GC。默认情况下,CMS不会处理永久代中的垃圾,可以通过开启CMSPermGenSweepingEnabled配置来开启永久代中的垃圾回收,开启后会有一组后台线程针对永久代做收集,需要注意的是,触发永久代进行垃圾收集的指标跟触发老年代进行垃圾收集的指标是独立的,老年代的阈值可以通过CMSInitiatingPermOccupancyFraction参数设置,这个参数的默认值是80%。开启对永久代的垃圾收集只是其中的一步,还需要开启另一个参数——CMSClassUnloadingEnabled,使得在垃圾收集的时候可以卸载不用的类。四、CMS的trade-off是什么?优势低延迟的收集器:几乎没有长时间的停顿,应用程序只在Minor gc以及后台线程扫描老年代的时候发生极其短暂的停顿。劣势更高的CPU使用:必须有足够的CPU资源用于运行后台的垃圾收集线程,在应用程序线程运行的同时扫描堆的使用情况。【PS:现在服务器的CPU资源基本不是问题,这个点可以忽略】CMS收集器对老年代收集的时候,不再进行任何压缩和整理的工作,意味着老年代随着应用的运行会变得碎片化;碎片过多会影响大对象的分配,虽然老年代还有很大的剩余空间,但是没有连续的空间来分配大对象,这时候就会触发Full GC。CMS提供了两个参数来解决这个问题:(1)UseCMSCompactAtFullCollection,在要进行Full GC的时候进行内存碎片整理;(2)CMSFullGCsBeforeCompaction,每隔多少次不压缩的Full GC后,执行一次带压缩的Full GC。会出现浮动垃圾;在并发清理阶段,用户线程仍然在运行,必须预留出空间给用户线程使用,因此CMS比其他回收器需要更大的堆空间。五、几个问题的解答为什么ParNew可以和CMS配合使用,而Parallel Scanvenge不可以?答:这个跟Hotspot VM的历史有关,Parallel Scanvenge是不在“分代框架”下开发的,而ParNew、CMS都是在分代框架下开发的。CMS中minor gc和major gc是顺序发生的吗?答:不是的,可以交叉发生,即在并发周期执行过程中,是可以发生Minor gc的,这个找个gc日志就可以观察到。CMS的并发收集周期合适触发?由下图可以看出,CMS 并发周期触发的条件有两个:阈值检查机制:老年代的使用空间达到某个阈值,JVM的默认值是92%(jdk1.5之前是68%,jdk1.6之后是92%),或者可以通过CMSInitiatingOccupancyFraction和UseCMSInitiatingOccupancyOnly两个参数来设置;这个参数的设置需要看应用场景,设置得太小,会导致CMS频繁发生,设置得太大,会导致过多的并发模式失败。例如动态检查机制:JVM会根据最近的回收历史,估算下一次老年代被耗尽的时间,快到这个时间的时候就启动一个并发周期。设置UseCMSInitiatingOccupancyOnly这个参数可以将这个特性关闭。CMS的并发收集周期会扫描哪些对象?会回收哪些对象?答:CMS的并发周期只会回收老年代的对象,但是在标记老年代的存活对象时,可能有些对象会被年轻代的对象引用,因此需要扫描整个堆的对象。CMS的gc roots包括哪些对象?答:首先,在JVM垃圾收集中Gc Roots的概念如何理解(参见R大对GC roots的概念的解释);第二,CMS的并发收集周期中,如何判断老年代的对象是活着?我们前面提到了,在CMS的并发周期中,仅仅扫描Gc Roots直达的对象会有遗漏,还需要扫描新生代的对象。如下图中的蓝色字体所示,CMS中的年轻代和老年代是分别收集的,因此在判断年轻代的对象存活的时候,需要把老年代当作自己的GcRoots,这时候并不需要扫描老年代的全部对象,而是使用了card table数据结构,如果一个老年代对象引用了年轻代的对象,则card中的值会被设置为特殊的数值;反过来判断老年代对象存活的时候,也需要把年轻代当作自己的Gc Roots,这个过程我们在第三节已经论述过了。如果我的应用决定使用CMS收集器,推荐的JVM参数是什么?我自己的应用使用的参数如下,是根据PerfMa的xxfox生成的,大家也可以使用这个产品调优自己的JVM参数:-Xmx4096M -Xms4096M -Xmn1536M -XX:MaxMetaspaceSize=512M -XX:MetaspaceSize=512M -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses -XX:+CMSClassUnloadingEnabled -XX:+ParallelRefProcEnabled -XX:+CMSScavengeBeforeRemark -XX:ErrorFile=/home/admin/logs/xelephant/hs_err_pid%p.log -Xloggc:/home/admin/logs/xelephant/gc.log -XX:HeapDumpPath=/home/admin/logs/xelephant -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+HeapDumpOnOutOfMemoryErrorCMS相关的参数总结(需要注意的是,这里我没有考虑太多JDK版本的问题,JDK1.7和JDK1.8这些参数的配置,有些默认值可能不一样,具体使用的时候还需要根据具体的版本来确认怎么设置)| 编号 | 参数名称 | 解释 || — | — | — || 1 | UseConcMarkSweepGC | 启用CMS收集器 || 2 | UseCMSInitiatingOccupancyOnly | 关闭CMS的动态检查机制,只通过预设的阈值来判断是否启动并发收集周期 || 3 | CMSInitiatingOccupancyFraction | 老年代空间占用到多少的时候启动并发收集周期,跟UseCMSInitiatingOccupancyOnly一起使用 || 4 | ExplicitGCInvokesConcurrentAndUnloadsClasses | 将System.gc()触发的Full GC转换为一次CMS并发收集,并且在这个收集周期中卸载 Perm(Metaspace)区域中不需要的类 || 5 | CMSClassUnloadingEnabled | 在CMS收集周期中,是否卸载类 || 6 | ParallelRefProcEnabled | 是否开启并发引用处理 || 7 | CMSScavengeBeforeRemark | 如果开启这个参数,会在进入重新标记阶段之前强制触发一次minor gc |参考资料从实际案例聊聊Java应用的GC优化理解CMS垃圾回收日志图解CMS垃圾回收机制,你值得拥有为什么CMS虽然是老年代的gc,但仍要扫描新生代的?R大对GC roots的概念的解释Introduce to CMS Collector《深入理解Java虚拟机》《Java性能权威指南》Oracle的GC调优手册what-is-the-threshold-for-cms-old-gc-to-be-triggeredFrequently Asked Questions about Garbage Collection in the Hotspot Java VirtualMachineJava SE HotSpot at a Glancexxfox:PerfMa的参数调优神器详解CMS垃圾回收机制ParNew和PSYoungGen和DefNew是一个东西么?Java SE的内存管理白皮书Garbage Collection in Elasticsearch and the G1GCA Heap of Trouble毕玄的文章:为什么不建议JVM源码分析之SystemGC完全解读读者讨论关于CMS收集器的回收范围,下面这张图是有误导的,从官方文档上看来,CMS收集器包括年轻代和老年代的收集,只不过对年轻代的收集的策略和ParNew相同,这个可以从参考资料16的第11页看到。concurrent mode failure和promotion failed触发的Full GC有啥不同?(这个问题是我、阿飞、蒋晓峰一起讨论的结果)答:concurrent mode failure触发的"Full GC"不是我们常说的Full GC——正常的Full GC其实是整个gc过程包括ygc和cms gc。也就是说,这个问题本身是有问题的,concurrent mode failure的时候触发的并不是我们常说的Full GC。然后再去讨论一个遗漏的知识点:CMS gc的并发周期有两种模式:foreground和background。concurrent mode failure触发的是foreground模式,会暂停整个应用,会将一些并行的阶段省掉做一次老年代收集,行为跟Serial-Old的一样,至于在这个过程中是否需要压缩,则需要看三个条件:(1)我们设置了UseCMSCompactAtFullCollection和CMSFullGCsBeforeCompaction,前者设置为true,后者默认是0,前者表示是在Full GC的时候执行压缩,后者表示是每隔多少个进行压缩,默认是0的话就是每次Full GC都压缩;(2)用户调用了System.gc(),而且DisableExplicitGC没有开启;(3)young gen报告接下来如果做增量收集会失败。promotion failed触发的是我们常说的的Full GC,对年轻代和老年代都会回收,并进行整理。promotion failed和concurrent mode failure的触发原因有啥不同?promotion failed是说,担保机制确定老年代是否有足够的空间容纳新来的对象,如果担保机制说有,但是真正分配的时候发现由于碎片导致找不到连续的空间而失败;concurrent mode failure是指并发周期还没执行完,用户线程就来请求比预留空间更大的空间了,即后台线程的收集没有赶上应用线程的分配速度。什么情况下才选择使用CMS收集器呢?我之前的观念是:小于8G的都用CMS,大于8G的选择G1。蒋晓峰跟我讨论了下这个观念,提出了一些别的想法,我觉得也有道理,记录在这里:除了看吞吐量和延时,还需要看具体的应用,比方说ES,Lucene和G1是不兼容的,因此默认的收集器就是CMS,具体见可参考资料17和18。小于3G的堆,如果不是对延迟有特别高的需求,不建议使用CMS,主要是由于CMS的几个缺点导致的:(1)并发周期的触发比例不好设置;(2)抢占CPU时间;(3)担保判断导致YGC变慢;(4)碎片问题,更详细的讨论参见资料19。本文作者:杜琪阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...
聊聊G1 GC的String Deduplication
序本文主要研究一下G1 GC的String Deduplication-XX:+UseStringDeduplicationjdk8u20给G1 GC带来了String Deduplication特性来将相同的字符串指向同一份数据,来减少重复字符串的内存开销该特性默认是关闭的,可以使用-XX:+UseStringDeduplication来开启(前提是使用-XX:+UseG1GC)具体的实现大致是JVM会记录char[]的weak reference及hash value,当找到一个hash code相同的String时,就会挨个char进行比较,当所有都match,那么其中一个String就会修改指针指向另一个String的char[],这样前者的char[]就可以被回收实例实验代码 @Test public void testG1StringDeduplication() throws InterruptedException { List<String> data = IntStream.rangeClosed(1,10000000) .mapToObj(i -> “number is " + ( i % 2 == 0 ? “odd” : “even”)) .collect(Collectors.toList()); System.gc(); long bytes = RamUsageEstimator.sizeOfAll(data); System.out.println(“string list size in MB:” + bytes*1.0/1024/1024); System.out.println(“used heap size in MB:” + ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed()*1.0/1024/1024); System.out.println(“used non heap size in MB:” + ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage().getUsed()*1.0/1024/1024); }关闭StringDeduplication-XX:+UseG1GC -XX:-UseStringDeduplication输出如下:string list size in MB:586.8727111816406used heap size in MB:831.772346496582used non heap size in MB:6.448394775390625整个jvm heap占用了约831MB,其中string list占用了约586MB开启StringDeduplication-XX:+UseG1GC -XX:+UseStringDeduplication输出如下:string list size in MB:296.83294677734375used heap size in MB:645.0970153808594used non heap size in MB:6.376350402832031整个jvm heap占用了约645MB,其中string list占用了约296MB小结jdk8u20给G1 GC带来了String Deduplication特性来将相同的字符串指向同一份数据,来减少重复字符串的内存开销该特性默认是关闭的,可以使用-XX:+UseStringDeduplication来开启(前提是使用-XX:+UseG1GC)在有大量重复string的前提下,使用G1 GC开启String Deduplication确实能够节省一定的内存,可以节约20%左右的内存,不过这个是理想的情况,因为普通应用里头的string重复的可能不多docJava 8 String deduplication vs. String.intern()JEP 192: String Deduplication in G1String Deduplication – A new feature in Java 8 Update 20Java 8 Update 20: String DeduplicationDuplicate Strings: How to Get Rid of Them and Save MemoryString deduplication feature (from Java 8 update 20)G1 GC: Reducing Memory Consumption by 20% ...
聊聊jvm的StringTable及SymbolTable
序本文主要研究一下jvm的StringTable及SymbolTableStringTable及SymbolTableJDK的变动在java7的时候将字符串常量池移到java heap,字符串常量池被限制在整个应用的堆内存中,在运行时调用String.intern()增加字符串常量不会使永久代OOM了。使用-XX:StringTableSize可以设置StringTableSize,默认是65536java8的时候去除PermGen,将其中的方法区移到non-heap中的Metaspace,因而SymbolTable也跟随Metaspace移到了non-heap中SymbolTablesymbolic references in Runtime Constant Pool一个完整的类加载过程必须经历加载(Loading)、连接(Linking)、初始化(Initialization)这三个步骤其中类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,然后将其转换为一个与目标类型对应的java.lang.Class对象实例;连接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证(Verification)、准备(Preparation)、解析(Resolution)三个阶段;初始化阶段将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作在连接(Linking)步骤里头的解析(Resolution)阶段,需要将常量池中所有的符号引用(classes、interfaces、fields、methods referenced in the constant pool)转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法)SymbolTable这个词在传统编程语言的实现里头比较常用(This data structure serves many of the purposes of the symbol table of a conventional programming language implementation),而在jvm里头对应的是Runtime Constant Pool中的symbolic references(Runtime Constant Pool除了symbolic references还包含了static constants),它是在类加载的时候(Resolution in Linking)根据class元数据中的constant pool table创建的,因而称为Runtime Constant Pool;这部分属于metaspcae,在native memory中查看StringTable/ # jcmd 1 VM.stringtable1:StringTable statistics:Number of buckets : 65536 = 524288 bytes, each 8Number of entries : 23407 = 374512 bytes, each 16Number of literals : 23407 = 2153344 bytes, avg 91.996Total footprsize_t : = 3052144 bytesAverage bucket size : 0.357Variance of bucket size : 0.360Std. dev. of bucket size: 0.600Maximum bucket size : 5使用jcmd pid VM.stringtable可以在运行时查看查看SymbolTable/ # jcmd 1 VM.symboltable1:SymbolTable statistics:Number of buckets : 32768 = 262144 bytes, each 8Number of entries : 128885 = 2062160 bytes, each 16Number of literals : 128885 = 7160912 bytes, avg 55.560Total footprsize_t : = 9485216 bytesAverage bucket size : 3.933Variance of bucket size : 3.982Std. dev. of bucket size: 1.996Maximum bucket size : 14使用jcmd pid VM.symboltable可以在运行时查看同时查看StringTable及SymbolTable-XX:+PrintStringTableStatisticsSymbolTable statistics:Number of buckets : 32768 = 262144 bytes, each 8Number of entries : 129215 = 2067440 bytes, each 16Number of literals : 129215 = 7173248 bytes, avg 55.514Total footprsize_t : = 9502832 bytesAverage bucket size : 3.943Variance of bucket size : 3.990Std. dev. of bucket size: 1.998Maximum bucket size : 14StringTable statistics:Number of buckets : 65536 = 524288 bytes, each 8Number of entries : 23470 = 375520 bytes, each 16Number of literals : 23470 = 2157736 bytes, avg 91.936Total footprsize_t : = 3057544 bytesAverage bucket size : 0.358Variance of bucket size : 0.361Std. dev. of bucket size: 0.601Maximum bucket size : 5启动时添加-XX:+PrintStringTableStatistics参数,在jvm进程退出时会输出SymbolTable statistics及StringTable statisticsjcmd pid VM.native_memory/ # jcmd 1 VM.native_memory scale=MB1:Native Memory Tracking:Total: reserved=1857MB, committed=112MB- Java Heap (reserved=502MB, committed=32MB) (mmap: reserved=502MB, committed=32MB)- Class (reserved=1065MB, committed=47MB) (classes #8386) ( instance classes #7843, array classes #543) (malloc=1MB #21250) (mmap: reserved=1064MB, committed=45MB) ( Metadata: ) ( reserved=40MB, committed=40MB) ( used=39MB) ( free=1MB) ( waste=0MB =0.00%) ( Class space:) ( reserved=1024MB, committed=6MB) ( used=5MB) ( free=0MB) ( waste=0MB =0.00%)- Thread (reserved=29MB, committed=3MB) (thread #29) (stack: reserved=29MB, committed=2MB)- Code (reserved=243MB, committed=15MB) (malloc=1MB #4744) (mmap: reserved=242MB, committed=14MB)- GC (reserved=2MB, committed=0MB) (mmap: reserved=2MB, committed=0MB)- Internal (reserved=1MB, committed=1MB) (malloc=1MB #2172)- Symbol (reserved=10MB, committed=10MB) (malloc=7MB #223735) (arena=3MB #1)- Native Memory Tracking (reserved=4MB, committed=4MB) (tracking overhead=4MB)使用jcmd pid VM.native_memory输出的Symbol部分包含了StringTable(interned String)及SymbolTable小结在java7的时候将字符串常量池则移到java heap,字符串常量池被限制在整个应用的堆内存中,在运行时调用String.intern()增加字符串常量不会使永久代OOM了。使用-XX:StringTableSize可以设置StringTableSize,默认是65536;java8的时候去除PermGen,将其中的方法区移到non-heap中的Metaspace,因而SymbolTable也跟随Metaspace移到了non-heap中StringTable位于heap中(java7+),而SymbolTable则在native memory中;使用jcmd pid VM.stringtable可以在运行时查看StringTable;使用jcmd pid VM.symboltable可以在运行时查看SymbolTable在启动时添加-XX:+PrintStringTableStatistics参数,在jvm进程退出时会输出SymbolTable statistics及StringTable statistics;使用jcmd pid VM.native_memory输出的Symbol部分包含了heap中StringTable(interned String)及non heap中的SymbolTabledocJava的类加载机制Chapter 5. Loading, Linking, and Initializing5.1. The Run-Time Constant PoolUnderstanding String Table Size in HotSpot聊聊jvm的PermGen与MetaspaceWill Java’s interned strings be GCed?Garbage collection behaviour for String.intern()10 Things Every Java Programmer Should Know about StringDifference between String literal and New String object in JavaUnderstand JVM Loading, JVM Linking, and JVM Initialization ...
聊聊java String的intern
序本文主要研究一下java String的internString.intern()java.base/java/lang/String.javapublic final class String implements java.io.Serializable, Comparable<String>, CharSequence, Constable, ConstantDesc { //…… /** * Returns a canonical representation for the string object. * <p> * A pool of strings, initially empty, is maintained privately by the * class {@code String}. * <p> * When the intern method is invoked, if the pool already contains a * string equal to this {@code String} object as determined by * the {@link #equals(Object)} method, then the string from the pool is * returned. Otherwise, this {@code String} object is added to the * pool and a reference to this {@code String} object is returned. * <p> * It follows that for any two strings {@code s} and {@code t}, * {@code s.intern() == t.intern()} is {@code true} * if and only if {@code s.equals(t)} is {@code true}. * <p> * All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the * <cite>The Java™ Language Specification</cite>. * * @return a string that has the same contents as this string, but is * guaranteed to be from a pool of unique strings. * @jls 3.10.5 String Literals / public native String intern(); //…… } 当调用intern方法时,如果常量池已经包含一个equals此String对象的字符串,则返回池中的字符串当调用intern方法时,如果常量池没有一个equals此String对象的字符串,将此String对象添加到池中,并返回此String对象的引用(即intern方法返回指向heap中的此String对象引用)所有literal strings及string-valued constant expressions都是interned的实例基于jdk12StringExistInPoolBeforeInternpublic class StringExistInPoolBeforeIntern { public static void main(String[] args){ String stringObject = new String(“tomcat”); //NOTE 在intern之前,string table已经有了tomcat,因而intern返回tomcat,不会指向stringObject stringObject.intern(); String stringLiteral = “tomcat”; System.out.println(stringObject == stringLiteral); //false }}tomcat这个literal string是interned过的,常量池没有tomcat,因而添加到常量池,常量池有个tomcat;另外由于stringObject是new的,所以heap中也有一个tomcat,而此时它指向heap中的tomcatstringObject.intern()返回的是heap中常量池的tomcat;stringLiteral是tomcat这个literal string,由于常量池已经有该值,因而stringLiteral指向的是heap中常量池的tomcat此时stringObject指向的是heap中的tomcat,而stringLiteral是heap中常量池的tomcat,因而二者不等,返回falseStringNotExistInPoolBeforeInternpublic class StringNotExistInPoolBeforeIntern { public static void main(String[] args){ String stringObject = new String(“tom”) + new String(“cat”); //NOTE 在intern之前,string table没有tomcat,因而intern指向stringObject stringObject.intern(); String stringLiteral = “tomcat”; System.out.println(stringObject == stringLiteral); //true }}tom及cat这两个literal string是interned过的,常量池没有tom及cat,因而添加到常量池,常量池有tom、cat;另外由于stringObject是new出来的,是tom及cat二者concat,因而heap中有一个tomcatstringObject的intern方法执行的时候,由于常量池中没有tomcat,因而添加到常量池,intern()返回的是指向heap中的tomcat的引用;stringLiteral是tomcat这个literal string,由于stringObject.intern()已经将tomcat添加到常量池了并指向heap中的tomcat的引用,所以stringLiteral返回的是指向heap中的tomcat的引用由于stringLiteral返回的是指向heap中的tomcat的引用,其实就是stringObject,因而二者相等,返回truejavap基于jdk12StringExistInPoolBeforeInternjavac src/main/java/com/example/javac/StringExistInPoolBeforeIntern.javajavap -v src/main/java/com/example/javac/StringExistInPoolBeforeIntern.class Last modified 2019年4月6日; size 683 bytes MD5 checksum 207635ffd7560f1df24b98607e2ca7db Compiled from “StringExistInPoolBeforeIntern.java"public class com.example.javac.StringExistInPoolBeforeIntern minor version: 0 major version: 56 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #8 // com/example/javac/StringExistInPoolBeforeIntern super_class: #9 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1Constant pool: #1 = Methodref #9.#21 // java/lang/Object."<init>”:()V #2 = Class #22 // java/lang/String #3 = String #23 // tomcat #4 = Methodref #2.#24 // java/lang/String."<init>":(Ljava/lang/String;)V #5 = Methodref #2.#25 // java/lang/String.intern:()Ljava/lang/String; #6 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream; #7 = Methodref #18.#28 // java/io/PrintStream.println:(Z)V #8 = Class #29 // com/example/javac/StringExistInPoolBeforeIntern #9 = Class #30 // java/lang/Object #10 = Utf8 <init> #11 = Utf8 ()V #12 = Utf8 Code #13 = Utf8 LineNumberTable #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 StackMapTable #17 = Class #31 // “[Ljava/lang/String;” #18 = Class #32 // java/io/PrintStream #19 = Utf8 SourceFile #20 = Utf8 StringExistInPoolBeforeIntern.java #21 = NameAndType #10:#11 // “<init>”:()V #22 = Utf8 java/lang/String #23 = Utf8 tomcat #24 = NameAndType #10:#33 // “<init>”:(Ljava/lang/String;)V #25 = NameAndType #34:#35 // intern:()Ljava/lang/String; #26 = Class #36 // java/lang/System #27 = NameAndType #37:#38 // out:Ljava/io/PrintStream; #28 = NameAndType #39:#40 // println:(Z)V #29 = Utf8 com/example/javac/StringExistInPoolBeforeIntern #30 = Utf8 java/lang/Object #31 = Utf8 [Ljava/lang/String; #32 = Utf8 java/io/PrintStream #33 = Utf8 (Ljava/lang/String;)V #34 = Utf8 intern #35 = Utf8 ()Ljava/lang/String; #36 = Utf8 java/lang/System #37 = Utf8 out #38 = Utf8 Ljava/io/PrintStream; #39 = Utf8 println #40 = Utf8 (Z)V{ public com.example.javac.StringExistInPoolBeforeIntern(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #2 // class java/lang/String 3: dup 4: ldc #3 // String tomcat 6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 9: astore_1 10: aload_1 11: invokevirtual #5 // Method java/lang/String.intern:()Ljava/lang/String; 14: pop 15: ldc #3 // String tomcat 17: astore_2 18: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 21: aload_1 22: aload_2 23: if_acmpne 30 26: iconst_1 27: goto 31 30: iconst_0 31: invokevirtual #7 // Method java/io/PrintStream.println:(Z)V 34: return LineNumberTable: line 11: 0 line 13: 10 line 14: 15 line 15: 18 line 16: 34 StackMapTable: number_of_entries = 2 frame_type = 255 / full_frame / offset_delta = 30 locals = [ class “[Ljava/lang/String;”, class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream ] frame_type = 255 / full_frame / offset_delta = 0 locals = [ class “[Ljava/lang/String;”, class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream, int ]}SourceFile: “StringExistInPoolBeforeIntern.java"可以看到常量池有个tomcatStringNotExistInPoolBeforeInternjavac src/main/java/com/example/javac/StringNotExistInPoolBeforeIntern.javajavap -v src/main/java/com/example/javac/StringNotExistInPoolBeforeIntern.class Last modified 2019年4月6日; size 1187 bytes MD5 checksum 6d173f303b61b8f5826e54bb6ed5157c Compiled from “StringNotExistInPoolBeforeIntern.java"public class com.example.javac.StringNotExistInPoolBeforeIntern minor version: 0 major version: 56 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #11 // com/example/javac/StringNotExistInPoolBeforeIntern super_class: #12 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 3Constant pool: #1 = Methodref #12.#24 // java/lang/Object."<init>”:()V #2 = Class #25 // java/lang/String #3 = String #26 // tom #4 = Methodref #2.#27 // java/lang/String."<init>”:(Ljava/lang/String;)V #5 = String #28 // cat #6 = InvokeDynamic #0:#32 // #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; #7 = Methodref #2.#33 // java/lang/String.intern:()Ljava/lang/String; #8 = String #34 // tomcat #9 = Fieldref #35.#36 // java/lang/System.out:Ljava/io/PrintStream; #10 = Methodref #21.#37 // java/io/PrintStream.println:(Z)V #11 = Class #38 // com/example/javac/StringNotExistInPoolBeforeIntern #12 = Class #39 // java/lang/Object #13 = Utf8 <init> #14 = Utf8 ()V #15 = Utf8 Code #16 = Utf8 LineNumberTable #17 = Utf8 main #18 = Utf8 ([Ljava/lang/String;)V #19 = Utf8 StackMapTable #20 = Class #40 // “[Ljava/lang/String;” #21 = Class #41 // java/io/PrintStream #22 = Utf8 SourceFile #23 = Utf8 StringNotExistInPoolBeforeIntern.java #24 = NameAndType #13:#14 // “<init>”:()V #25 = Utf8 java/lang/String #26 = Utf8 tom #27 = NameAndType #13:#42 // “<init>”:(Ljava/lang/String;)V #28 = Utf8 cat #29 = Utf8 BootstrapMethods #30 = MethodHandle 6:#43 // REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; #31 = String #44 // \u0001\u0001 #32 = NameAndType #45:#46 // makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; #33 = NameAndType #47:#48 // intern:()Ljava/lang/String; #34 = Utf8 tomcat #35 = Class #49 // java/lang/System #36 = NameAndType #50:#51 // out:Ljava/io/PrintStream; #37 = NameAndType #52:#53 // println:(Z)V #38 = Utf8 com/example/javac/StringNotExistInPoolBeforeIntern #39 = Utf8 java/lang/Object #40 = Utf8 [Ljava/lang/String; #41 = Utf8 java/io/PrintStream #42 = Utf8 (Ljava/lang/String;)V #43 = Methodref #54.#55 // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; #44 = Utf8 \u0001\u0001 #45 = Utf8 makeConcatWithConstants #46 = Utf8 (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; #47 = Utf8 intern #48 = Utf8 ()Ljava/lang/String; #49 = Utf8 java/lang/System #50 = Utf8 out #51 = Utf8 Ljava/io/PrintStream; #52 = Utf8 println #53 = Utf8 (Z)V #54 = Class #56 // java/lang/invoke/StringConcatFactory #55 = NameAndType #45:#60 // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; #56 = Utf8 java/lang/invoke/StringConcatFactory #57 = Class #62 // java/lang/invoke/MethodHandles$Lookup #58 = Utf8 Lookup #59 = Utf8 InnerClasses #60 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; #61 = Class #63 // java/lang/invoke/MethodHandles #62 = Utf8 java/lang/invoke/MethodHandles$Lookup #63 = Utf8 java/lang/invoke/MethodHandles{ public com.example.javac.StringNotExistInPoolBeforeIntern(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=3, args_size=1 0: new #2 // class java/lang/String 3: dup 4: ldc #3 // String tom 6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 9: new #2 // class java/lang/String 12: dup 13: ldc #5 // String cat 15: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 18: invokedynamic #6, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; 23: astore_1 24: aload_1 25: invokevirtual #7 // Method java/lang/String.intern:()Ljava/lang/String; 28: pop 29: ldc #8 // String tomcat 31: astore_2 32: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 35: aload_1 36: aload_2 37: if_acmpne 44 40: iconst_1 41: goto 45 44: iconst_0 45: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V 48: return LineNumberTable: line 11: 0 line 13: 24 line 14: 29 line 15: 32 line 16: 48 StackMapTable: number_of_entries = 2 frame_type = 255 / full_frame / offset_delta = 44 locals = [ class “[Ljava/lang/String;”, class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream ] frame_type = 255 / full_frame */ offset_delta = 0 locals = [ class “[Ljava/lang/String;”, class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream, int ]}SourceFile: “StringNotExistInPoolBeforeIntern.java"InnerClasses: public static final #58= #57 of #61; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandlesBootstrapMethods: 0: #30 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; Method arguments: #31 \u0001\u0001可以看到常量池有tom、cat、tomcat小结当调用intern方法时,如果常量池已经包含一个equals此String对象的字符串,则返回池中的字符串当调用intern方法时,如果常量池没有一个equals此String对象的字符串,将此String对象添加到池中,并返回此String对象的引用(即intern方法返回指向heap中的此String对象引用)所有literal strings及string-valued constant expressions都是interned的doc浅谈String的internWhy does String.intern() return different results under JDK8 and JDK9?How to Initialize and Compare Strings in Java?Difference between String literal and New String object in Java聊聊jvm的PermGen与MetaspaceGuide to Java String PoolString Literal Vs String Object in Java ...
聊聊netty的maxDirectMemory
序本文主要研究一下netty的maxDirectMemoryPlatformDependentnetty-common-4.1.33.Final-sources.jar!/io/netty/util/internal/PlatformDependent.javapublic final class PlatformDependent { private static final InternalLogger logger = InternalLoggerFactory.getInstance(PlatformDependent.class); private static final Pattern MAX_DIRECT_MEMORY_SIZE_ARG_PATTERN = Pattern.compile( “\s*-XX:MaxDirectMemorySize\s*=\s*([0-9]+)\s*([kKmMgG]?)\s*$”); private static final boolean IS_WINDOWS = isWindows0(); private static final boolean IS_OSX = isOsx0(); private static final boolean MAYBE_SUPER_USER; private static final boolean CAN_ENABLE_TCP_NODELAY_BY_DEFAULT = !isAndroid(); private static final Throwable UNSAFE_UNAVAILABILITY_CAUSE = unsafeUnavailabilityCause0(); private static final boolean DIRECT_BUFFER_PREFERRED; private static final long MAX_DIRECT_MEMORY = maxDirectMemory0(); //…… static { if (javaVersion() >= 7) { RANDOM_PROVIDER = new ThreadLocalRandomProvider() { @Override public Random current() { return java.util.concurrent.ThreadLocalRandom.current(); } }; } else { RANDOM_PROVIDER = new ThreadLocalRandomProvider() { @Override public Random current() { return ThreadLocalRandom.current(); } }; } // Here is how the system property is used: // // * < 0 - Don’t use cleaner, and inherit max direct memory from java. In this case the // “practical max direct memory” would be 2 * max memory as defined by the JDK. // * == 0 - Use cleaner, Netty will not enforce max memory, and instead will defer to JDK. // * > 0 - Don’t use cleaner. This will limit Netty’s total direct memory // (note: that JDK’s direct memory limit is independent of this). long maxDirectMemory = SystemPropertyUtil.getLong(“io.netty.maxDirectMemory”, -1); if (maxDirectMemory == 0 || !hasUnsafe() || !PlatformDependent0.hasDirectBufferNoCleanerConstructor()) { USE_DIRECT_BUFFER_NO_CLEANER = false; DIRECT_MEMORY_COUNTER = null; } else { USE_DIRECT_BUFFER_NO_CLEANER = true; if (maxDirectMemory < 0) { maxDirectMemory = MAX_DIRECT_MEMORY; if (maxDirectMemory <= 0) { DIRECT_MEMORY_COUNTER = null; } else { DIRECT_MEMORY_COUNTER = new AtomicLong(); } } else { DIRECT_MEMORY_COUNTER = new AtomicLong(); } } logger.debug("-Dio.netty.maxDirectMemory: {} bytes", maxDirectMemory); DIRECT_MEMORY_LIMIT = maxDirectMemory >= 1 ? maxDirectMemory : MAX_DIRECT_MEMORY; int tryAllocateUninitializedArray = SystemPropertyUtil.getInt(“io.netty.uninitializedArrayAllocationThreshold”, 1024); UNINITIALIZED_ARRAY_ALLOCATION_THRESHOLD = javaVersion() >= 9 && PlatformDependent0.hasAllocateArrayMethod() ? tryAllocateUninitializedArray : -1; logger.debug("-Dio.netty.uninitializedArrayAllocationThreshold: {}", UNINITIALIZED_ARRAY_ALLOCATION_THRESHOLD); MAYBE_SUPER_USER = maybeSuperUser0(); if (!isAndroid()) { // only direct to method if we are not running on android. // See https://github.com/netty/netty/issues/2604 if (javaVersion() >= 9) { CLEANER = CleanerJava9.isSupported() ? new CleanerJava9() : NOOP; } else { CLEANER = CleanerJava6.isSupported() ? new CleanerJava6() : NOOP; } } else { CLEANER = NOOP; } // We should always prefer direct buffers by default if we can use a Cleaner to release direct buffers. DIRECT_BUFFER_PREFERRED = CLEANER != NOOP && !SystemPropertyUtil.getBoolean(“io.netty.noPreferDirect”, false); if (logger.isDebugEnabled()) { logger.debug("-Dio.netty.noPreferDirect: {}", !DIRECT_BUFFER_PREFERRED); } /* * We do not want to log this message if unsafe is explicitly disabled. Do not remove the explicit no unsafe * guard. / if (CLEANER == NOOP && !PlatformDependent0.isExplicitNoUnsafe()) { logger.info( “Your platform does not provide complete low-level API for accessing direct buffers reliably. " + “Unless explicitly requested, heap buffer will always be preferred to avoid potential system " + “instability.”); } } private static long maxDirectMemory0() { long maxDirectMemory = 0; ClassLoader systemClassLoader = null; try { systemClassLoader = getSystemClassLoader(); // When using IBM J9 / Eclipse OpenJ9 we should not use VM.maxDirectMemory() as it not reflects the // correct value. // See: // - https://github.com/netty/netty/issues/7654 String vmName = SystemPropertyUtil.get(“java.vm.name”, “”).toLowerCase(); if (!vmName.startsWith(“ibm j9”) && // https://github.com/eclipse/openj9/blob/openj9-0.8.0/runtime/include/vendor_version.h#L53 !vmName.startsWith(“eclipse openj9”)) { // Try to get from sun.misc.VM.maxDirectMemory() which should be most accurate. Class<?> vmClass = Class.forName(“sun.misc.VM”, true, systemClassLoader); Method m = vmClass.getDeclaredMethod(“maxDirectMemory”); maxDirectMemory = ((Number) m.invoke(null)).longValue(); } } catch (Throwable ignored) { // Ignore } if (maxDirectMemory > 0) { return maxDirectMemory; } try { // Now try to get the JVM option (-XX:MaxDirectMemorySize) and parse it. // Note that we are using reflection because Android doesn’t have these classes. Class<?> mgmtFactoryClass = Class.forName( “java.lang.management.ManagementFactory”, true, systemClassLoader); Class<?> runtimeClass = Class.forName( “java.lang.management.RuntimeMXBean”, true, systemClassLoader); Object runtime = mgmtFactoryClass.getDeclaredMethod(“getRuntimeMXBean”).invoke(null); @SuppressWarnings(“unchecked”) List<String> vmArgs = (List<String>) runtimeClass.getDeclaredMethod(“getInputArguments”).invoke(runtime); for (int i = vmArgs.size() - 1; i >= 0; i –) { Matcher m = MAX_DIRECT_MEMORY_SIZE_ARG_PATTERN.matcher(vmArgs.get(i)); if (!m.matches()) { continue; } maxDirectMemory = Long.parseLong(m.group(1)); switch (m.group(2).charAt(0)) { case ‘k’: case ‘K’: maxDirectMemory = 1024; break; case ’m’: case ‘M’: maxDirectMemory = 1024 * 1024; break; case ‘g’: case ‘G’: maxDirectMemory = 1024 * 1024 * 1024; break; } break; } } catch (Throwable ignored) { // Ignore } if (maxDirectMemory <= 0) { maxDirectMemory = Runtime.getRuntime().maxMemory(); logger.debug(“maxDirectMemory: {} bytes (maybe)”, maxDirectMemory); } else { logger.debug(“maxDirectMemory: {} bytes”, maxDirectMemory); } return maxDirectMemory; } / * Returns the maximum memory reserved for direct buffer allocation. / public static long maxDirectMemory() { return DIRECT_MEMORY_LIMIT; } //……}netty的PlatformDependent有个静态属性MAX_DIRECT_MEMORY,它是根据maxDirectMemory0方法来计算的maxDirectMemory0方法会根据jvm的类型来做不同处理,如果是IBM J9 / Eclipse OpenJ9的话,就不能使用VM.maxDirectMemory()来获取,正常hotspot则采用VM.maxDirectMemory()来获取(VM.maxDirectMemory是读取-XX:MaxDirectMemorySize配置,如果有设置且大于0则使用该值,如果没有设置该参数则默认值为0,则默认是取的Runtime.getRuntime().maxMemory())static代码块里头设置了DIRECT_MEMORY_LIMIT;它首先从系统属性读取io.netty.maxDirectMemory到maxDirectMemory,如果maxDirectMemory值小于0,则设置maxDirectMemory为MAX_DIRECT_MEMORY;DIRECT_MEMORY_LIMIT = maxDirectMemory >= 1 ? maxDirectMemory : MAX_DIRECT_MEMORY;maxDirectMemory方法直接返回DIRECT_MEMORY_LIMITByteBuffer.allocateDirectjava.base/java/nio/ByteBuffer.javapublic abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>{ //…… / * Allocates a new direct byte buffer. * * <p> The new buffer’s position will be zero, its limit will be its * capacity, its mark will be undefined, each of its elements will be * initialized to zero, and its byte order will be * {@link ByteOrder#BIG_ENDIAN BIG_ENDIAN}. Whether or not it has a * {@link #hasArray backing array} is unspecified. * * @param capacity * The new buffer’s capacity, in bytes * * @return The new byte buffer * * @throws IllegalArgumentException * If the {@code capacity} is a negative integer / public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); } //……}ByteBuffer.allocateDirect方法实际是创建了DirectByteBufferDirectByteBufferjava.base/java/nio/DirectByteBuffer.javaclass DirectByteBuffer extends MappedByteBuffer implements DirectBuffer { //…… // Primary constructor // DirectByteBuffer(int cap) { // package-private super(-1, 0, cap, cap); boolean pa = VM.isDirectMemoryPageAligned(); int ps = Bits.pageSize(); long size = Math.max(1L, (long)cap + (pa ? ps : 0)); Bits.reserveMemory(size, cap); long base = 0; try { base = UNSAFE.allocateMemory(size); } catch (OutOfMemoryError x) { Bits.unreserveMemory(size, cap); throw x; } UNSAFE.setMemory(base, size, (byte) 0); if (pa && (base % ps != 0)) { // Round up to page boundary address = base + ps - (base & (ps - 1)); } else { address = base; } cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; } //……}DirectByteBuffer的构造器里头会调用Bits.reserveMemory,出现OutOfMemoryError,则调用Bits.unreserveMemory(size, cap),然后抛出OutOfMemoryErrorBits.reserveMemoryjava.base/java/nio/Bits.java/ * Access to bits, native and otherwise. */class Bits { // package-private private Bits() { } // – Direct memory management – // A user-settable upper limit on the maximum amount of allocatable // direct buffer memory. This value may be changed during VM // initialization if it is launched with “-XX:MaxDirectMemorySize=<size>”. private static volatile long MAX_MEMORY = VM.maxDirectMemory(); private static final AtomicLong RESERVED_MEMORY = new AtomicLong(); private static final AtomicLong TOTAL_CAPACITY = new AtomicLong(); private static final AtomicLong COUNT = new AtomicLong(); private static volatile boolean MEMORY_LIMIT_SET; // max. number of sleeps during try-reserving with exponentially // increasing delay before throwing OutOfMemoryError: // 1, 2, 4, 8, 16, 32, 64, 128, 256 (total 511 ms ~ 0.5 s) // which means that OOME will be thrown after 0.5 s of trying private static final int MAX_SLEEPS = 9; //…… // These methods should be called whenever direct memory is allocated or // freed. They allow the user to control the amount of direct memory // which a process may access. All sizes are specified in bytes. static void reserveMemory(long size, int cap) { if (!MEMORY_LIMIT_SET && VM.initLevel() >= 1) { MAX_MEMORY = VM.maxDirectMemory(); MEMORY_LIMIT_SET = true; } // optimist! if (tryReserveMemory(size, cap)) { return; } final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess(); boolean interrupted = false; try { // Retry allocation until success or there are no more // references (including Cleaners that might free direct // buffer memory) to process and allocation still fails. boolean refprocActive; do { try { refprocActive = jlra.waitForReferenceProcessing(); } catch (InterruptedException e) { // Defer interrupts and keep trying. interrupted = true; refprocActive = true; } if (tryReserveMemory(size, cap)) { return; } } while (refprocActive); // trigger VM’s Reference processing System.gc(); // A retry loop with exponential back-off delays. // Sometimes it would suffice to give up once reference // processing is complete. But if there are many threads // competing for memory, this gives more opportunities for // any given thread to make progress. In particular, this // seems to be enough for a stress test like // DirectBufferAllocTest to (usually) succeed, while // without it that test likely fails. Since failure here // ends in OOME, there’s no need to hurry. long sleepTime = 1; int sleeps = 0; while (true) { if (tryReserveMemory(size, cap)) { return; } if (sleeps >= MAX_SLEEPS) { break; } try { if (!jlra.waitForReferenceProcessing()) { Thread.sleep(sleepTime); sleepTime <<= 1; sleeps++; } } catch (InterruptedException e) { interrupted = true; } } // no luck throw new OutOfMemoryError(“Direct buffer memory”); } finally { if (interrupted) { // don’t swallow interrupts Thread.currentThread().interrupt(); } } } private static boolean tryReserveMemory(long size, int cap) { // -XX:MaxDirectMemorySize limits the total capacity rather than the // actual memory usage, which will differ when buffers are page // aligned. long totalCap; while (cap <= MAX_MEMORY - (totalCap = TOTAL_CAPACITY.get())) { if (TOTAL_CAPACITY.compareAndSet(totalCap, totalCap + cap)) { RESERVED_MEMORY.addAndGet(size); COUNT.incrementAndGet(); return true; } } return false; } //……}Bits.reserveMemory方法会先调用tryReserveMemory尝试分配direct memory,不成功则继续往下执行do while(refprocActive)refprocActive这段循环是不断尝试allocation直到分配成功,或者直到没有引用来处理且分配失败如果refprocActive循环没有分配成功,则调用System.gc(),然后进入最后一段循环尝试分配;最后这段循环如果分配成功则返回,分配不成功且sleeps大于等于MAX_SLEEPS,则跳出循环,最后抛出OutOfMemoryError(“Direct buffer memory”)异常小结netty的PlatformDependent有个静态属性MAX_DIRECT_MEMORY,它是根据maxDirectMemory0方法来计算的;maxDirectMemory0方法会根据jvm的类型来做不同处理,如果是IBM J9 / Eclipse OpenJ9的话,就不能使用VM.maxDirectMemory()来获取,正常hotspot则采用VM.maxDirectMemory()来获取(VM.maxDirectMemory是读取-XX:MaxDirectMemorySize配置,如果有设置且大于0则使用该值,如果没有设置该参数则默认值为0,则默认是取的Runtime.getRuntime().maxMemory())static代码块里头设置了DIRECT_MEMORY_LIMIT;它首先从系统属性读取io.netty.maxDirectMemory到maxDirectMemory,如果maxDirectMemory值小于0,则设置maxDirectMemory为MAX_DIRECT_MEMORY;DIRECT_MEMORY_LIMIT = maxDirectMemory >= 1 ? maxDirectMemory : MAX_DIRECT_MEMORY;maxDirectMemory方法直接返回DIRECT_MEMORY_LIMITByteBuffer.allocateDirect方法实际是创建了DirectByteBuffer;DirectByteBuffer的构造器里头会调用Bits.reserveMemory,出现OutOfMemoryError,则调用Bits.unreserveMemory(size, cap),然后抛出OutOfMemoryError;Bits.reserveMemory方法会先调用tryReserveMemory尝试分配direct memory,不成功则继续往下执行do while(refprocActive);refprocActive这段循环是不断尝试allocation直到分配成功,或者直到没有引用来处理且分配失败;如果refprocActive循环没有分配成功,则调用System.gc(),然后进入最后一段循环尝试分配;最后这段循环如果分配成功则返回,分配不成功且sleeps大于等于MAX_SLEEPS,则跳出循环,最后抛出OutOfMemoryError(“Direct buffer memory”)异常doc聊聊jvm的-XX:MaxDirectMemorySizeIn Netty 4, do I need to set option -XX:MaxDirectMemorySize?Netty之Java堆外内存扫盲贴直播一次问题排查过程Change default value of io.netty.maxDirectMemory ? #6349LEAK: ByteBuf.release() was not called before it’s garbage-collected #422 ...
JVM虚拟机笔记之对象存活判定算法和垃圾收集算法(三)
程序计数器.虚拟机栈.本地方法栈随线程而生随线程而灭,栈帧分配多少内存在类结构确定后就确定了。垃圾回收针对的是Java堆和方法区。一:对象已死吗在垃圾收集器进行回收前,第一件事就是确定这些对象哪些还存活,哪些已经死去。1.引用计数算法在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器减1;其中计数器为0的对象是不可能再被使用的已死对象。当两个对象相互引用时,这两个对象就不会被回收 引用计数算法,不被主流虚拟机采用,主要原因是它很难解决对象之间相互循环引用的问题。2 可达性分析算法通过一系列的称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所经过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(在图论中称为对象不可达)时,这个对象就是不可用的。如图object5,6,7虽然有关联,但是到gc roots是不可达的所以是可回收对象。在java中,可作为GC Roots的对象包括:虚拟机栈(栈帧中的本地变量表)中引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象本地方法栈中JNI引用的对象3 引用的分类强引用:是指在程序代码中直接存在的引用,譬如引用new操作符创建的对象。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。软引用:还有用但是并非必需的引用,早系统将要发生内存溢出异常之前会把这些对象列进回收范围中进行二次回收,若还是没有足够的内存,才会抛出内存溢出异常。弱引用:非必需的对象,只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论内存是否够用都将回收这些对象。虚引用:一个对象是否有虚引用的存在完全不会对他的生存时间构成影响,也无法通过虚引用来取得一个对象实例。4 宣告一个对象死亡的过程要真正宣告一个对象死亡,至少要经历两次标记过程:若对象在进行可达性分析后发现没有与GC Roots相连接的引用链,会被 第一次标记 并且进行一次筛选。筛选的条件是此对象是否有必要执行finalize()方法(如当对象没有重写finalize()方法或者finalize()方法已经被虚拟机调用过则认为没有必要执行)。如果有必要执行则将该对象放置在F-Queue队列中,并在稍后由一个由虚拟机自己建立的、低优先级的Finalizer线程去执行它;稍后GC将对F-Queue中的对象进行第二次标记,如果对象还是没有被引用,则会被回收。但是作者不建议通过finalize()方法“拯救”对象,因为它运行代价高、不确定性大、无法保证各个对象的调用顺序。5 回收方法区永久代(方法区)的垃圾收集主要回收两部分内容:废弃常量和无用的类废弃常量:假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说,就是没有任何String对象引用常量池中的“abc”常量,也没有其他 地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。无用的类:同时满足下面3个条件的类(实例、类加载器被回收,java.lang.Class对象没有被引用)。①该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。 ②加载该类的ClassLoader已经被回收。 ③该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。二:垃圾收集算法1.标记清除算法分为两个阶段:标记和清除标记:首先标记所有需要回收的对象(标记过程在上文宣告一个对象死亡过程中提及)清除:在标记完成后统一回收所有被标记的对象不足地方:①效率问题,标记和清除过程效率都不高;②空间问题,垃圾回收后较多不不连续的内存碎片,导致分配较大对象时找不到足够的连续内存而不得不提前触发又一次垃圾回收动作.2.复制算法(新生代)将可用内存按容量分为两个块,每次只用其中之一。当这一块内存用完之后,将还存活的对象复制到另一边去,然后清除所有已经使用过的部分。优点:每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。缺点:代价是将内存缩小为了原来的一半,未免太高了一点。解决方法:新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。在HotSpot里,考虑到大部分对象存活时间很短将内存分为Eden和两块Survivor,默认比例为8:1:1。代价是存在部分内存空间浪费,适合在新生代使用。备注:不保证每次不多于10%的对象存活,survivor空间不够时,依赖其他内存分配担保(老年代)3 标记-整理算法(老年代算法)(Mark-Compact)对象存活率太高情况下,进行较多复制操作效率将会变低,应对诸如所以对象都存活情况下,所有老年代不采用复制算法.标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。4.分代收集算法当前商用虚拟机都采用了这种算法,根据对象的存活周期将内存划分为几块,一般是把Java堆分为新生代和老生代,根据各个年代采用适当的收集算法。新生代一般采用复制算法(Copying)。老生代一搬采用 标记-清理(Mark-Sweep) 或者标记-整理(Mark-Compact) 进行回收。
聊聊jvm的Stack Memory
序本文主要研究一下jvm的Stack MemoryVirtual Machine Stack每个jvm线程都有一个私有的Virtual Machine Stack,它在线程同时被创建该stack主要用于存储frames,即所谓的stack frames每个方法在执行的时候都会创建一个stack frame,用于存储方法的局部变量、返回值、Operand stack等Stack MemoryStack Memory是执行线程时所使用的内存,他们包含了方法用到的一些短生命周期的values及heap中objects的引用Stack Memory是按照LIFO (Last-In-First-Out)的顺序被引用的,每当一个方法被调用,都会在stack memory中创建一块区域用于保存原始类型的值及heap中objects的引用;当方法执行结束时,这块区域就被释放可以被下一个方法使用;相对于heap memory来说,stack memory是非常小的一块。通过-Xss或者-XX:ThreadStackSize可以指定stack memory的大小当一个线程所需的stack size超过了设定的大小是,jvm会抛出StackOverflowError;而当系统没有足够内存可以供jvm创建一个新线程时,jvm会抛出OutOfMemoryErrorStackOverflowError实例 /** * -Xss144k */ @Test public void testStackOverflow(){ recursiveInvoke(); } public void recursiveInvoke(){ String uuid = UUID.randomUUID().toString(); recursiveInvoke(); }异常输出如下:java.lang.StackOverflowError at java.base/sun.security.provider.ByteArrayAccess.b2iBig64(ByteArrayAccess.java:263) at java.base/sun.security.provider.SHA.implCompressCheck(SHA.java:139) at java.base/sun.security.provider.SHA.implCompress(SHA.java:128) at java.base/sun.security.provider.SHA.implDigest(SHA.java:109) at java.base/sun.security.provider.DigestBase.engineDigest(DigestBase.java:210) at java.base/sun.security.provider.DigestBase.engineDigest(DigestBase.java:189) at java.base/java.security.MessageDigest$Delegate.engineDigest(MessageDigest.java:629) at java.base/java.security.MessageDigest.digest(MessageDigest.java:385) at java.base/sun.security.provider.SecureRandom.engineNextBytes(SecureRandom.java:245) at java.base/sun.security.provider.NativePRNG$RandomIO.implNextBytes(NativePRNG.java:535) at java.base/sun.security.provider.NativePRNG$NonBlocking.engineNextBytes(NativePRNG.java:318) at java.base/java.security.SecureRandom.nextBytes(SecureRandom.java:741) at java.base/java.util.UUID.randomUUID(UUID.java:150) at com.example.VMStackTest.recursiveInvoke(VMStackTest.java:21)Xss越大,每个线程的大小就越大,占用的内存越多,能创建的线程就越少;Xss越小,则递归的深度越小,容易出现栈溢出 java.lang.StackOverflowError。减少局部变量的声明,可以节省栈帧大小,增加调用深度。OOMKilled实例代码实例 public void createThreadAsManyAsPossible(){ try{ while (true) { new Thread(new Runnable() { public void run() { try { TimeUnit.HOURS.sleep(1); } catch (InterruptedException e) { } } }).start(); } }catch (Throwable e){ LOGGER.error(e.getMessage(),e); } }docker命令启动docker run -v /tmp:/tmp –memory=256m -p 8080:8080 -e JAVA_OPTS=’-server -Xmx128m -Xss1m -XX:ParallelGCThreads=4 -XX:ConcGCThreads=4 -XX:+UnlockDiagnosticVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp’ -e PROFILE=‘default’ \demodocker inspect containerId[ { “Id”: “93ec46e93290f8dc70901680c251dd9faef4a5a283cb6020ec8640fed4001026”, “Created”: “2019-04-01T02:22:41.007466012Z”, //…… “State”: { “Status”: “exited”, “Running”: false, “Paused”: false, “Restarting”: false, “OOMKilled”: true, “Dead”: false, “Pid”: 0, “ExitCode”: 137, “Error”: “”, “StartedAt”: “2019-04-01T02:22:41.284627718Z”, “FinishedAt”: “2019-04-01T02:22:59.192763475Z” }, “Image”: “sha256:bbaf9474dd259b8c6afe39cbbccce62bb695b52fda79d8ce3a727be89fcef5c6”, //……]可以看到State里头的OOMKilled为trueOutOfMemoryError实例代码实例 public void createThreadAsManyAsPossible(){ try{ while (true) { new Thread(new Runnable() { public void run() { try { TimeUnit.HOURS.sleep(1); } catch (InterruptedException e) { } } }).start(); } }catch (Throwable e){ LOGGER.error(e.getMessage(),e); } }运行参数-server-Xmx128m-Xss1m-XX:ParallelGCThreads=4-XX:ConcGCThreads=4-XX:+UnlockDiagnosticVMOptions-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/tmp-Djava.io.tmpdir=/tmp不使用docker,直接在本机执行,可以看到如下报错异常输出java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached at java.base/java.lang.Thread.start0(Native Method) ~[na:na] at java.base/java.lang.Thread.start(Thread.java:804) ~[na:na]查看thread信息查看thread stack size/ # jcmd 1 VM.native_memory scale=MB1:Native Memory Tracking:Total: reserved=1321MB, committed=204MB- Java Heap (reserved=128MB, committed=121MB) (mmap: reserved=128MB, committed=121MB)- Class (reserved=1065MB, committed=46MB) (classes #8234) ( instance classes #7694, array classes #540) (malloc=1MB #19759) (mmap: reserved=1064MB, committed=44MB) ( Metadata: ) ( reserved=40MB, committed=39MB) ( used=38MB) ( free=1MB) ( waste=0MB =0.00%) ( Class space:) ( reserved=1024MB, committed=6MB) ( used=5MB) ( free=0MB) ( waste=0MB =0.00%)- Thread (reserved=36MB, committed=2MB) (thread #36) (stack: reserved=36MB, committed=2MB)- Code (reserved=65MB, committed=11MB) (malloc=1MB #4218) (mmap: reserved=65MB, committed=10MB)- GC (reserved=9MB, committed=7MB) (malloc=5MB #6043) (mmap: reserved=4MB, committed=2MB)- Internal (reserved=4MB, committed=4MB) (malloc=2MB #3454) (mmap: reserved=2MB, committed=2MB)- Symbol (reserved=10MB, committed=10MB) (malloc=7MB #220126) (arena=3MB #1)- Native Memory Tracking (reserved=4MB, committed=4MB) (tracking overhead=4MB)使用jcmd pid VM.native_memory scale=MB可以查看thread stack的信息,具体在Thread部分,这里显示一共有36个线程,stack的reserved大小为36M查看线程快照/ # jcmd 1 Thread.print1:2019-04-01 10:43:13Full thread dump OpenJDK 64-Bit Server VM (12+33 mixed mode):Threads class SMR info:_java_thread_list=0x0000562b1182fe30, length=28, elements={0x0000562b0ffa5000, 0x0000562b0ffaa000, 0x0000562b0ffdc000, 0x0000562b0ffde800,0x0000562b0ffe1000, 0x0000562b1003d800, 0x0000562b1016b000, 0x0000562b1017f800,0x0000562b1141f800, 0x0000562b10a74000, 0x0000562b10b64000, 0x0000562b10fee800,0x0000562b125d1000, 0x0000562b1040d800, 0x0000562b11983800, 0x0000562b11982000,0x0000562b10cc7800, 0x0000562b13630800, 0x0000562b13632000, 0x0000562b136f0800,0x0000562b136e0800, 0x0000562b136e1800, 0x0000562b13621800, 0x0000562b1360c000,0x0000562b136cd000, 0x0000562b12115000, 0x0000562b11f9e000, 0x0000562b11ff8800}“Reference Handler” #2 daemon prio=10 os_prio=0 cpu=2.69ms elapsed=31.15s tid=0x0000562b0ffa5000 nid=0xb waiting on condition [0x00007f5cbb576000] java.lang.Thread.State: RUNNABLE at java.lang.ref.Reference.waitForReferencePendingList(java.base@12/Native Method) at java.lang.ref.Reference.processPendingReferences(java.base@12/Reference.java:241) at java.lang.ref.Reference$ReferenceHandler.run(java.base@12/Reference.java:213)“Finalizer” #3 daemon prio=8 os_prio=0 cpu=0.25ms elapsed=31.15s tid=0x0000562b0ffaa000 nid=0xc in Object.wait() [0x00007f5cbb475000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(java.base@12/Native Method) - waiting on <0x00000000f800a840> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(java.base@12/ReferenceQueue.java:155) - locked <0x00000000f800a840> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(java.base@12/ReferenceQueue.java:176) at java.lang.ref.Finalizer$FinalizerThread.run(java.base@12/Finalizer.java:170)“Signal Dispatcher” #4 daemon prio=9 os_prio=0 cpu=0.79ms elapsed=31.14s tid=0x0000562b0ffdc000 nid=0xd runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE"C2 CompilerThread0" #5 daemon prio=9 os_prio=0 cpu=4680.36ms elapsed=31.14s tid=0x0000562b0ffde800 nid=0xe waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE No compile task"C1 CompilerThread0" #6 daemon prio=9 os_prio=0 cpu=2329.95ms elapsed=31.14s tid=0x0000562b0ffe1000 nid=0xf waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE No compile task"Sweeper thread" #7 daemon prio=9 os_prio=0 cpu=71.45ms elapsed=31.13s tid=0x0000562b1003d800 nid=0x10 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE"Service Thread" #8 daemon prio=9 os_prio=0 cpu=47.53ms elapsed=31.06s tid=0x0000562b1016b000 nid=0x11 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE"Common-Cleaner" #9 daemon prio=8 os_prio=0 cpu=3.92ms elapsed=31.05s tid=0x0000562b1017f800 nid=0x13 in Object.wait() [0x00007f5cbad6c000] java.lang.Thread.State: TIMED_WAITING (on object monitor) at java.lang.Object.wait(java.base@12/Native Method) - waiting on <0x00000000f80c1880> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(java.base@12/ReferenceQueue.java:155) - locked <0x00000000f80c1880> (a java.lang.ref.ReferenceQueue$Lock) at jdk.internal.ref.CleanerImpl.run(java.base@12/CleanerImpl.java:148) at java.lang.Thread.run(java.base@12/Thread.java:835) at jdk.internal.misc.InnocuousThread.run(java.base@12/InnocuousThread.java:134)“Catalina-utility-1” #13 prio=1 os_prio=0 cpu=9.33ms elapsed=19.61s tid=0x0000562b1141f800 nid=0x19 waiting on condition [0x00007f5cb93ac000] java.lang.Thread.State: WAITING (parking) at jdk.internal.misc.Unsafe.park(java.base@12/Native Method) - parking to wait for <0x00000000ffac03b8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(java.base@12/LockSupport.java:194) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@12/AbstractQueuedSynchronizer.java:2081) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(java.base@12/ScheduledThreadPoolExecutor.java:1177) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(java.base@12/ScheduledThreadPoolExecutor.java:899) at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@12/ThreadPoolExecutor.java:1054) at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@12/ThreadPoolExecutor.java:1114) at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@12/ThreadPoolExecutor.java:628) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(java.base@12/Thread.java:835)“Catalina-utility-2” #14 prio=1 os_prio=0 cpu=2.97ms elapsed=19.61s tid=0x0000562b10a74000 nid=0x1a waiting on condition [0x00007f5cb827d000] java.lang.Thread.State: TIMED_WAITING (parking) at jdk.internal.misc.Unsafe.park(java.base@12/Native Method) - parking to wait for <0x00000000ffac03b8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.parkNanos(java.base@12/LockSupport.java:235) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(java.base@12/AbstractQueuedSynchronizer.java:2123) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(java.base@12/ScheduledThreadPoolExecutor.java:1182) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(java.base@12/ScheduledThreadPoolExecutor.java:899) at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@12/ThreadPoolExecutor.java:1054) at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@12/ThreadPoolExecutor.java:1114) at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@12/ThreadPoolExecutor.java:628) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(java.base@12/Thread.java:835)“container-0” #15 prio=5 os_prio=0 cpu=0.16ms elapsed=19.60s tid=0x0000562b10b64000 nid=0x1b waiting on condition [0x00007f5cb817c000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(java.base@12/Native Method) at org.apache.catalina.core.StandardServer.await(StandardServer.java:568) at org.springframework.boot.web.embedded.tomcat.TomcatWebServer$1.run(TomcatWebServer.java:181)“Thread-1” #16 daemon prio=9 os_prio=0 cpu=0.26ms elapsed=19.38s tid=0x0000562b10fee800 nid=0x1c waiting on condition [0x00007f5cb7f25000] java.lang.Thread.State: WAITING (parking) at jdk.internal.misc.Unsafe.park(java.base@12/Native Method) - parking to wait for <0x00000000fe2f54f0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(java.base@12/LockSupport.java:194) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@12/AbstractQueuedSynchronizer.java:2081) at java.util.concurrent.LinkedBlockingQueue.take(java.base@12/LinkedBlockingQueue.java:433) at org.LatencyUtils.PauseDetector$PauseDetectorThread.run(PauseDetector.java:85)“SimplePauseDetectorThread_0” #17 daemon prio=9 os_prio=0 cpu=20.21ms elapsed=19.37s tid=0x0000562b125d1000 nid=0x1d waiting on condition [0x00007f5cb7e24000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(java.base@12/Native Method) at java.lang.Thread.sleep(java.base@12/Thread.java:340) at java.util.concurrent.TimeUnit.sleep(java.base@12/TimeUnit.java:446) at org.LatencyUtils.TimeServices.sleepNanos(TimeServices.java:62) at org.LatencyUtils.SimplePauseDetector$SimplePauseDetectorThread.run(SimplePauseDetector.java:116)“NioBlockingSelector.BlockPoller-1” #18 daemon prio=5 os_prio=0 cpu=2.27ms elapsed=17.43s tid=0x0000562b1040d800 nid=0x23 runnable [0x00007f5cba443000] java.lang.Thread.State: RUNNABLE at sun.nio.ch.EPoll.wait(java.base@12/Native Method) at sun.nio.ch.EPollSelectorImpl.doSelect(java.base@12/EPollSelectorImpl.java:120) at sun.nio.ch.SelectorImpl.lockAndDoSelect(java.base@12/SelectorImpl.java:124) - locked <0x00000000fb4b21c0> (a sun.nio.ch.Util$2) - locked <0x00000000fb4b1dc8> (a sun.nio.ch.EPollSelectorImpl) at sun.nio.ch.SelectorImpl.select(java.base@12/SelectorImpl.java:136) at org.apache.tomcat.util.net.NioBlockingSelector$BlockPoller.run(NioBlockingSelector.java:304)“http-nio-8080-exec-1” #19 daemon prio=5 os_prio=0 cpu=0.13ms elapsed=17.42s tid=0x0000562b11983800 nid=0x24 waiting on condition [0x00007f5cb7d23000] java.lang.Thread.State: WAITING (parking) at jdk.internal.misc.Unsafe.park(java.base@12/Native Method) - parking to wait for <0x00000000fb50ba60> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(java.base@12/LockSupport.java:194) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@12/AbstractQueuedSynchronizer.java:2081) at java.util.concurrent.LinkedBlockingQueue.take(java.base@12/LinkedBlockingQueue.java:433) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:107) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:33) at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@12/ThreadPoolExecutor.java:1054) at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@12/ThreadPoolExecutor.java:1114) at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@12/ThreadPoolExecutor.java:628) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(java.base@12/Thread.java:835)“http-nio-8080-exec-2” #20 daemon prio=5 os_prio=0 cpu=0.09ms elapsed=17.42s tid=0x0000562b11982000 nid=0x25 waiting on condition [0x00007f5cb807b000] java.lang.Thread.State: WAITING (parking) at jdk.internal.misc.Unsafe.park(java.base@12/Native Method) - parking to wait for <0x00000000fb50ba60> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(java.base@12/LockSupport.java:194) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@12/AbstractQueuedSynchronizer.java:2081) at java.util.concurrent.LinkedBlockingQueue.take(java.base@12/LinkedBlockingQueue.java:433) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:107) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:33) at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@12/ThreadPoolExecutor.java:1054) at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@12/ThreadPoolExecutor.java:1114) at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@12/ThreadPoolExecutor.java:628) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(java.base@12/Thread.java:835)“http-nio-8080-exec-3” #21 daemon prio=5 os_prio=0 cpu=0.35ms elapsed=17.42s tid=0x0000562b10cc7800 nid=0x26 waiting on condition [0x00007f5cb77e5000] java.lang.Thread.State: WAITING (parking) at jdk.internal.misc.Unsafe.park(java.base@12/Native Method) - parking to wait for <0x00000000fb50ba60> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(java.base@12/LockSupport.java:194) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@12/AbstractQueuedSynchronizer.java:2081) at java.util.concurrent.LinkedBlockingQueue.take(java.base@12/LinkedBlockingQueue.java:433) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:107) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:33) at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@12/ThreadPoolExecutor.java:1054) at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@12/ThreadPoolExecutor.java:1114) at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@12/ThreadPoolExecutor.java:628) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(java.base@12/Thread.java:835)“http-nio-8080-exec-4” #22 daemon prio=5 os_prio=0 cpu=0.10ms elapsed=17.42s tid=0x0000562b13630800 nid=0x27 waiting on condition [0x00007f5cb75ff000] java.lang.Thread.State: WAITING (parking) at jdk.internal.misc.Unsafe.park(java.base@12/Native Method) - parking to wait for <0x00000000fb50ba60> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(java.base@12/LockSupport.java:194) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@12/AbstractQueuedSynchronizer.java:2081) at java.util.concurrent.LinkedBlockingQueue.take(java.base@12/LinkedBlockingQueue.java:433) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:107) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:33) at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@12/ThreadPoolExecutor.java:1054) at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@12/ThreadPoolExecutor.java:1114) at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@12/ThreadPoolExecutor.java:628) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(java.base@12/Thread.java:835)“http-nio-8080-exec-5” #23 daemon prio=5 os_prio=0 cpu=0.06ms elapsed=17.41s tid=0x0000562b13632000 nid=0x28 waiting on condition [0x00007f5cb74fe000] java.lang.Thread.State: WAITING (parking) at jdk.internal.misc.Unsafe.park(java.base@12/Native Method) - parking to wait for <0x00000000fb50ba60> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(java.base@12/LockSupport.java:194) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@12/AbstractQueuedSynchronizer.java:2081) at java.util.concurrent.LinkedBlockingQueue.take(java.base@12/LinkedBlockingQueue.java:433) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:107) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:33) at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@12/ThreadPoolExecutor.java:1054) at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@12/ThreadPoolExecutor.java:1114) at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@12/ThreadPoolExecutor.java:628) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(java.base@12/Thread.java:835)“http-nio-8080-exec-6” #24 daemon prio=5 os_prio=0 cpu=0.14ms elapsed=17.41s tid=0x0000562b136f0800 nid=0x29 waiting on condition [0x00007f5cb73fd000] java.lang.Thread.State: WAITING (parking) at jdk.internal.misc.Unsafe.park(java.base@12/Native Method) - parking to wait for <0x00000000fb50ba60> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(java.base@12/LockSupport.java:194) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@12/AbstractQueuedSynchronizer.java:2081) at java.util.concurrent.LinkedBlockingQueue.take(java.base@12/LinkedBlockingQueue.java:433) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:107) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:33) at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@12/ThreadPoolExecutor.java:1054) at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@12/ThreadPoolExecutor.java:1114) at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@12/ThreadPoolExecutor.java:628) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(java.base@12/Thread.java:835)“http-nio-8080-exec-7” #25 daemon prio=5 os_prio=0 cpu=0.12ms elapsed=17.41s tid=0x0000562b136e0800 nid=0x2a waiting on condition [0x00007f5cb72fc000] java.lang.Thread.State: WAITING (parking) at jdk.internal.misc.Unsafe.park(java.base@12/Native Method) - parking to wait for <0x00000000fb50ba60> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(java.base@12/LockSupport.java:194) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@12/AbstractQueuedSynchronizer.java:2081) at java.util.concurrent.LinkedBlockingQueue.take(java.base@12/LinkedBlockingQueue.java:433) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:107) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:33) at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@12/ThreadPoolExecutor.java:1054) at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@12/ThreadPoolExecutor.java:1114) at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@12/ThreadPoolExecutor.java:628) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(java.base@12/Thread.java:835)“http-nio-8080-exec-8” #26 daemon prio=5 os_prio=0 cpu=0.09ms elapsed=17.41s tid=0x0000562b136e1800 nid=0x2b waiting on condition [0x00007f5cb71fb000] java.lang.Thread.State: WAITING (parking) at jdk.internal.misc.Unsafe.park(java.base@12/Native Method) - parking to wait for <0x00000000fb50ba60> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(java.base@12/LockSupport.java:194) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@12/AbstractQueuedSynchronizer.java:2081) at java.util.concurrent.LinkedBlockingQueue.take(java.base@12/LinkedBlockingQueue.java:433) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:107) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:33) at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@12/ThreadPoolExecutor.java:1054) at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@12/ThreadPoolExecutor.java:1114) at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@12/ThreadPoolExecutor.java:628) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(java.base@12/Thread.java:835)“http-nio-8080-exec-9” #27 daemon prio=5 os_prio=0 cpu=0.14ms elapsed=17.41s tid=0x0000562b13621800 nid=0x2c waiting on condition [0x00007f5cb70fa000] java.lang.Thread.State: WAITING (parking) at jdk.internal.misc.Unsafe.park(java.base@12/Native Method) - parking to wait for <0x00000000fb50ba60> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(java.base@12/LockSupport.java:194) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@12/AbstractQueuedSynchronizer.java:2081) at java.util.concurrent.LinkedBlockingQueue.take(java.base@12/LinkedBlockingQueue.java:433) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:107) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:33) at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@12/ThreadPoolExecutor.java:1054) at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@12/ThreadPoolExecutor.java:1114) at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@12/ThreadPoolExecutor.java:628) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(java.base@12/Thread.java:835)“http-nio-8080-exec-10” #28 daemon prio=5 os_prio=0 cpu=0.14ms elapsed=17.41s tid=0x0000562b1360c000 nid=0x2d waiting on condition [0x00007f5cb6ff9000] java.lang.Thread.State: WAITING (parking) at jdk.internal.misc.Unsafe.park(java.base@12/Native Method) - parking to wait for <0x00000000fb50ba60> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(java.base@12/LockSupport.java:194) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@12/AbstractQueuedSynchronizer.java:2081) at java.util.concurrent.LinkedBlockingQueue.take(java.base@12/LinkedBlockingQueue.java:433) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:107) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:33) at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@12/ThreadPoolExecutor.java:1054) at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@12/ThreadPoolExecutor.java:1114) at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@12/ThreadPoolExecutor.java:628) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(java.base@12/Thread.java:835)“http-nio-8080-ClientPoller-0” #29 daemon prio=5 os_prio=0 cpu=2.85ms elapsed=17.40s tid=0x0000562b136cd000 nid=0x2e runnable [0x00007f5cb6ef8000] java.lang.Thread.State: RUNNABLE at sun.nio.ch.EPoll.wait(java.base@12/Native Method) at sun.nio.ch.EPollSelectorImpl.doSelect(java.base@12/EPollSelectorImpl.java:120) at sun.nio.ch.SelectorImpl.lockAndDoSelect(java.base@12/SelectorImpl.java:124) - locked <0x00000000fb53c5f8> (a sun.nio.ch.Util$2) - locked <0x00000000fb53c460> (a sun.nio.ch.EPollSelectorImpl) at sun.nio.ch.SelectorImpl.select(java.base@12/SelectorImpl.java:136) at org.apache.tomcat.util.net.NioEndpoint$Poller.run(NioEndpoint.java:743) at java.lang.Thread.run(java.base@12/Thread.java:835)“http-nio-8080-Acceptor-0” #30 daemon prio=5 os_prio=0 cpu=0.45ms elapsed=17.40s tid=0x0000562b12115000 nid=0x2f runnable [0x00007f5cb6df7000] java.lang.Thread.State: RUNNABLE at sun.nio.ch.ServerSocketChannelImpl.accept0(java.base@12/Native Method) at sun.nio.ch.ServerSocketChannelImpl.accept(java.base@12/ServerSocketChannelImpl.java:525) at sun.nio.ch.ServerSocketChannelImpl.accept(java.base@12/ServerSocketChannelImpl.java:277) at org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept(NioEndpoint.java:448) at org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept(NioEndpoint.java:70) at org.apache.tomcat.util.net.Acceptor.run(Acceptor.java:95) at java.lang.Thread.run(java.base@12/Thread.java:835)“DestroyJavaVM” #32 prio=5 os_prio=0 cpu=4915.44ms elapsed=17.32s tid=0x0000562b11f9e000 nid=0x6 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE"Attach Listener" #33 daemon prio=9 os_prio=0 cpu=0.60ms elapsed=15.49s tid=0x0000562b11ff8800 nid=0x3c waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE"VM Thread" os_prio=0 cpu=170.49ms elapsed=31.16s tid=0x0000562b0ff90000 nid=0xa runnable"Shenandoah GC Threads#0" os_prio=0 cpu=57.15ms elapsed=31.46s tid=0x0000562b0fe25800 nid=0x7 runnable"Shenandoah GC Threads#1" os_prio=0 cpu=40.46ms elapsed=29.33s tid=0x0000562b111b0000 nid=0x15 runnable"Shenandoah GC Threads#2" os_prio=0 cpu=56.93ms elapsed=29.32s tid=0x0000562b111ae000 nid=0x16 runnable"Shenandoah GC Threads#3" os_prio=0 cpu=47.89ms elapsed=29.30s tid=0x0000562b11562800 nid=0x17 runnable"VM Periodic Task Thread" os_prio=0 cpu=59.59ms elapsed=31.06s tid=0x0000562b1016e000 nid=0x12 waiting on conditionJNI global refs: 14, weak refs: 0使用jcmd pid Thread.print可以打印线程快照查看spring boot运行线程信息/ # curl -i localhost:8080/actuator/metrics/jvm.threads.daemonHTTP/1.1 200Content-Disposition: inline;filename=f.txtContent-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8Transfer-Encoding: chunkedDate: Mon, 01 Apr 2019 02:49:19 GMT{“name”:“jvm.threads.daemon”,“description”:“The current number of live daemon threads”,“baseUnit”:“threads”,“measurements”:[{“statistic”:“VALUE”,“value”:20.0}],“availableTags”:[]}/ # curl -i localhost:8080/actuator/metrics/jvm.threads.peakHTTP/1.1 200Content-Disposition: inline;filename=f.txtContent-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8Transfer-Encoding: chunkedDate: Mon, 01 Apr 2019 02:47:32 GMT{“name”:“jvm.threads.peak”,“description”:“The peak live thread count since the Java virtual machine started or peak was reset”,“baseUnit”:“threads”,“measurements”:[{“statistic”:“VALUE”,“value”:24.0}],“availableTags”:[]}/ # curl -i localhost:8080/actuator/metrics/jvm.threads.liveHTTP/1.1 200Content-Disposition: inline;filename=f.txtContent-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8Transfer-Encoding: chunkedDate: Mon, 01 Apr 2019 02:47:52 GMT{“name”:“jvm.threads.live”,“description”:“The current number of live threads including both daemon and non-daemon threads”,“baseUnit”:“threads”,“measurements”:[{“statistic”:“VALUE”,“value”:24.0}],“availableTags”:[]}/ # curl -i localhost:8080/actuator/metrics/tomcat.threads.currentHTTP/1.1 200Content-Disposition: inline;filename=f.txtContent-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8Transfer-Encoding: chunkedDate: Mon, 01 Apr 2019 02:48:31 GMT{“name”:“tomcat.threads.current”,“description”:null,“baseUnit”:“threads”,“measurements”:[{“statistic”:“VALUE”,“value”:10.0}],“availableTags”:[{“tag”:“name”,“values”:[“http-nio-8080”]}]}/ # curl -i localhost:8080/actuator/metrics/tomcat.threads.busyHTTP/1.1 200Content-Disposition: inline;filename=f.txtContent-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8Transfer-Encoding: chunkedDate: Mon, 01 Apr 2019 02:48:58 GMT{“name”:“tomcat.threads.busy”,“description”:null,“baseUnit”:“threads”,“measurements”:[{“statistic”:“VALUE”,“value”:1.0}],“availableTags”:[{“tag”:“name”,“values”:[“http-nio-8080”]}]}/ # curl -i localhost:8080/actuator/metrics/tomcat.threads.config.maxHTTP/1.1 200Content-Disposition: inline;filename=f.txtContent-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8Transfer-Encoding: chunkedDate: Mon, 01 Apr 2019 02:50:15 GMT{“name”:“tomcat.threads.config.max”,“description”:null,“baseUnit”:“threads”,“measurements”:[{“statistic”:“VALUE”,“value”:200.0}],“availableTags”:[{“tag”:“name”,“values”:[“http-nio-8080”]}]}springboot的actuator默认可以查看jvm.threads开头(jvm.threads.daemon、jvm.threads.peak、jvm.threads.live)以及tomcat.threads开头(tomcat.threads.current、tomcat.threads.busy、tomcat.threads.config.max)的metrics;另外使用/actuator/threaddump也可以打印线程快照;不过actuator的metrics没有统计也没有dump出来gc等其他线程,比如C2 CompilerThread0、C1 CompilerThread0、Sweeper thread、Service Thread、VM Thread、Shenandoah GC Threads#0、Shenandoah GC Threads#1、Shenandoah GC Threads#2、Shenandoah GC Threads#3、VM Periodic Task Thread等小结每个jvm线程都有一个私有的Virtual Machine Stack,它在线程同时被创建;该stack主要用于存储frames,即所谓的stack frames;每个方法在执行的时候都会创建一个stack frame,用于存储方法的局部变量、返回值、Operand stack等Stack Memory是执行线程时所使用的内存,他们包含了方法用到的一些短生命周期的values及heap中objects的引用;Stack Memory是按照LIFO (Last-In-First-Out)的顺序被引用的,每当一个方法被调用,都会在stack memory中创建一块区域用于保存原始类型的值及heap中objects的引用;当方法执行结束时,这块区域就被释放可以被下一个方法使用;相对于heap memory来说,stack memory是非常小的一块。通过-Xss或者-XX:ThreadStackSize可以指定stack memory的大小;当一个线程所需的stack size超过了设定的大小是,jvm会抛出StackOverflowError;而当系统没有足够内存可以供jvm创建一个新线程时,jvm会抛出OutOfMemoryError查询线程信息使用jcmd pid VM.native_memory scale=MB可以查看thread stack的信息,具体在Thread部分使用jcmd pid Thread.print可以打印线程快照springboot的actuator默认可以查看jvm.threads开头(jvm.threads.daemon、jvm.threads.peak、jvm.threads.live)以及tomcat.threads开头(tomcat.threads.current、tomcat.threads.busy、tomcat.threads.config.max)的metrics使用/actuator/threaddump也可以打印线程快照(不过actuator的metrics没有统计也没有dump出来gc等其他线程)docJava线程与Xss2.5.2. Java Virtual Machine StacksJVMInternalsJVM memory modelJava Heap Space vs Stack – Memory Allocation in JavaHow does Java (JVM) allocate stack for each threadJVM Stacks and Stack FramesA Visual Look at JVM Stacks and FramesStack Memory and Heap Space in JavaJava: What is the limit to the number of threads you can create?Troubleshoot OutOfMemoryError: Unable to Create New Native ThreadJDK 8: Thread Stack Size TuningJVM Tuning: Heapsize, Stacksize and Garbage Collection Fundamental ...
聊聊jvm的CompressedClassSpace
序本文主要研究一下jvm的CompressedClassSpaceCompressedClassSpacejava8移除了permanent generation,然后class metadata存储在native memory中,其大小默认是不受限的,可以通过-XX:MaxMetaspaceSize来限制如果开启了-XX:+UseCompressedOops及-XX:+UseCompressedClassesPointers(默认是开启),则UseCompressedOops会使用32-bit的offset来代表java object的引用,而UseCompressedClassPointers则使用32-bit的offset来代表64-bit进程中的class pointer;可以使用CompressedClassSpaceSize来设置这块的空间大小如果开启了指针压缩,则CompressedClassSpace分配在MaxMetaspaceSize里头,即MaxMetaspaceSize=Compressed Class Space Size + Metaspace area (excluding the Compressed Class Space) Size查看CompressedClassSpace大小jcmd pid GC.heap_info/ # jcmd 1 GC.heap_info1:Shenandoah Heap 524288K total, 144896K committed, 77232K used 2048 x 256K regionsStatus: not cancelledReserved region: - [0x00000000e0000000, 0x0000000100000000) Metaspace used 45675K, capacity 46867K, committed 47104K, reserved 1091584K class space used 5406K, capacity 5838K, committed 5888K, reserved 1048576K可以看到整个metaspace使用了45675K,其中class space使用了5406K,而Metaspace area (excluding the Compressed Class Space)使用了45675K-5406K=40269K;整个metaspace的reserved大小为1091584K,其中class space的reserved大小为1048576Kjcmd pid VM.native_memory/ # jcmd 1 VM.native_memory1:Native Memory Tracking:Total: reserved=2224403KB, committed=238187KB- Java Heap (reserved=524288KB, committed=144896KB) (mmap: reserved=524288KB, committed=144896KB)- Class (reserved=1092940KB, committed=48460KB) (classes #8563) ( instance classes #7988, array classes #575) (malloc=1356KB #20589) (mmap: reserved=1091584KB, committed=47104KB) ( Metadata: ) ( reserved=43008KB, committed=41216KB) ( used=40286KB) ( free=930KB) ( waste=0KB =0.00%) ( Class space:) ( reserved=1048576KB, committed=5888KB) ( used=5407KB) ( free=481KB) ( waste=0KB =0.00%)- Thread (reserved=37130KB, committed=2846KB) (thread #36) (stack: reserved=36961KB, committed=2676KB) (malloc=127KB #189) (arena=42KB #70)- Code (reserved=529360KB, committed=15420KB) (malloc=968KB #4745) (mmap: reserved=528392KB, committed=14452KB)- GC (reserved=21844KB, committed=7724KB) (malloc=5460KB #9644) (mmap: reserved=16384KB, committed=2264KB)- Compiler (reserved=165KB, committed=165KB) (malloc=34KB #455) (arena=131KB #5)- Internal (reserved=3758KB, committed=3758KB) (malloc=1710KB #6582) (mmap: reserved=2048KB, committed=2048KB)- Other (reserved=32KB, committed=32KB) (malloc=32KB #3)- Symbol (reserved=10277KB, committed=10277KB) (malloc=7456KB #225421) (arena=2821KB #1)- Native Memory Tracking (reserved=4235KB, committed=4235KB) (malloc=10KB #126) (tracking overhead=4225KB)- Arena Chunk (reserved=176KB, committed=176KB) (malloc=176KB)- Logging (reserved=7KB, committed=7KB) (malloc=7KB #264)- Arguments (reserved=18KB, committed=18KB) (malloc=18KB #500)- Module (reserved=165KB, committed=165KB) (malloc=165KB #1708)- Safepoint (reserved=4KB, committed=4KB) (mmap: reserved=4KB, committed=4KB)- Unknown (reserved=4KB, committed=4KB) (mmap: reserved=4KB, committed=4KB)可以看到class部分,reserved大小为1092940KB,其中Metadata的reserved大小为43008KB,Class space的reserved大小为1048576KB;其中Metadata使用了40286KB,而Class space使用了5407KBjmx查看 @GetMapping("/meta") public Object getMetaspaceSize(){ return ManagementFactory.getPlatformMXBeans(MemoryPoolMXBean.class) .stream() .filter(e -> MemoryType.NON_HEAP == e.getType()) .filter(e -> e.getName().equals(“Metaspace”) || e.getName().equals(“Compressed Class Space”)) .map(e -> “name:"+e.getName()+",info:"+e.getUsage()) .collect(Collectors.toList()); }输出如下:/ # curl -i localhost:8080/memory/metaHTTP/1.1 200Content-Type: application/json;charset=UTF-8Transfer-Encoding: chunkedDate: Sun, 31 Mar 2019 03:06:55 GMT[“name:Metaspace,info:init = 0(0K) used = 46236784(45153K) committed = 47710208(46592K) max = -1(-1K)”,“name:Compressed Class Space,info:init = 0(0K) used = 5482736(5354K) committed = 6029312(5888K) max = 1073741824(1048576K)"]这里可以看到Metaspace总共使用了45153K,其中Compressed Class Space部分使用了5354K,而Metaspace area (excluding the Compressed Class Space)使用了45153K-5354K=39799K;而这里显示的Metaspace的max为-1,其中Compressed Class Space部分max值为1048576K即1Gspring boot应用查看/ # curl -i “http://localhost:8080/actuator/metrics/jvm.memory.used?tag=area:nonheap"HTTP/1.1 200Content-Disposition: inline;filename=f.txtContent-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8Transfer-Encoding: chunkedDate: Sun, 31 Mar 2019 02:52:51 GMT{“name”:“jvm.memory.used”,“description”:“The amount of used memory”,“baseUnit”:“bytes”,“measurements”:[{“statistic”:“VALUE”,“value”:6.4449464E7}],“availableTags”:[{“tag”:“id”,“values”:[“CodeHeap ’non-profiled nmethods’”,“CodeHeap ‘profiled nmethods’”,“Compressed Class Space”,“Metaspace”,“CodeHeap ’non-nmethods’”]}]}/ # curl -i “http://localhost:8080/actuator/metrics/jvm.memory.used?tag=area:nonheap&tag=id:Metaspace"HTTP/1.1 200Content-Disposition: inline;filename=f.txtContent-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8Transfer-Encoding: chunkedDate: Sun, 31 Mar 2019 02:54:56 GMT{“name”:“jvm.memory.used”,“description”:“The amount of used memory”,“baseUnit”:“bytes”,“measurements”:[{“statistic”:“VALUE”,“value”:4.7468312E7}],“availableTags”:[]}/ # curl -i “http://localhost:8080/actuator/metrics/jvm.memory.used?tag=area:nonheap&tag=id:Compressed%20Class%20Space"HTTP/1.1 200Content-Disposition: inline;filename=f.txtContent-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8Transfer-Encoding: chunkedDate: Sun, 31 Mar 2019 02:55:18 GMT{“name”:“jvm.memory.used”,“description”:“The amount of used memory”,“baseUnit”:“bytes”,“measurements”:[{“statistic”:“VALUE”,“value”:5609952.0}],“availableTags”:[]}springboot使用micrometer,通过/actuator/metrics接口提供相关指标查询功能,其中Metaspace及Compressed Class Space在jvm.memory.used这个metric中它是基于MemoryPoolMXBean来实现的,具体详见micrometer-core-1.1.3-sources.jar!/io/micrometer/core/instrument/binder/jvm/JvmMemoryMetrics.java小结java8移除了permanent generation,然后class metadata存储在native memory中,其大小默认是不受限的,可以通过-XX:MaxMetaspaceSize来限制;如果开启了-XX:+UseCompressedOops及-XX:+UseCompressedClassesPointers(默认是开启),则UseCompressedOops会使用32-bit的offset来代表java object的引用,而UseCompressedClassPointers则使用32-bit的offset来代表64-bit进程中的class pointer;可以使用CompressedClassSpaceSize来设置这块的空间大小开启了指针压缩,则CompressedClassSpace分配在MaxMetaspaceSize里头,即MaxMetaspaceSize=Compressed Class Space Size + Metaspace area (excluding the Compressed Class Space) Size查看CompressedClassSpace的内存使用情况有好几种方法:jcmd pid GC.heap_info(Metaspace为总的部分,包含了class space,而Metaspace area (excluding the Compressed Class Space)需要自己计算即total-class space)jcmd pid VM.native_memory(class为总的部分,包含了Metaspace area (excluding the Compressed Class Space)及Class Space)使用JMX来获取NON_HEAP类型中的name为Metaspace及Compressed Class Space的MemoryPoolMXBean可以得到Metaspace及Compressed Class Space的使用情况(JMX得到的Metaspace为总的部分,而Metaspace area (excluding the Compressed Class Space)需要自己计算即total-class space)如果是springboot应用,它使用micrometer,通过/actuator/metrics接口提供相关指标查询功能,其中Metaspace及Compressed Class Space在jvm.memory.used这个metric中docClass Metadata7.19.3 -XX:CompressedClassSpaceSizeJDK 8: UseCompressedClassPointers vs. UseCompressedOopsJVM源码分析之Metaspace解密聊聊jvm的PermGen与Metaspace ...
聊聊jvm的code cache
序本文主要研究一下jvm的code cacheCode CacheJVM生成的native code存放的内存空间称之为Code Cache;JIT编译、JNI等都会编译代码到native code,其中JIT生成的native code占用了Code Cache的绝大部分空间相关参数Codecache Size Options-XX:InitialCodeCacheSize用于设置初始CodeCache大小-XX:ReservedCodeCacheSize用于设置Reserved code cache的最大大小,通常默认是240M-XX:CodeCacheExpansionSize用于设置code cache的expansion size,通常默认是64KCodecache Flush Options-XX:+UseCodeCacheFlushing是否在code cache满的时候先尝试清理一下,如果还是不够用再关闭编译,默认为falseCompilation Policy Options-XX:CompileThreshold方法触发编译时的调用次数,默认是10000-XX:OnStackReplacePercentage方法中循环执行部分代码的执行次数触发OSR编译时的阈值,默认是140Compilation Limit Options-XX:MaxInlineLevel针对嵌套调用的最大内联深度,默认为9-XX:MaxInlineSize方法可以被内联的最大bytecode大小,默认为35-XX:MinInliningThreshold方法可以被内联的最小调用次数,默认为250-XX:+InlineSynchronizedMethods是否允许内联synchronized methods,默认为trueDiagnostic Options-XX:+PrintFlagsFinal(默认没有启用)用于查看所有可设置的参数及最终值(JDK 6 update 21开始才可以用),默认是不包括diagnostic或experimental系的。如果要在-XX:+PrintFlagsFinal的输出里看到这两种参数的信息,分别需要显式指定-XX:+UnlockDiagnosticVMOptions / -XX:+UnlockExperimentalVMOptions(-XX:+PrintCommandLineFlags 这个参数的作用是显示出VM初始化完毕后所有跟最初的默认值不同的参数及它们的值)-XX:+PrintCodeCache(默认没有启用)-XX:+PrintCodeCache用于jvm关闭时输出code cache的使用情况-XX:+PrintCodeCacheOnCompilation(默认没有启用)用于在方法每次被编译时输出code cache的使用情况查看Code Cache的使用情况-XX:+PrintCodeCacheCodeHeap ’non-profiled nmethods’: size=120032Kb used=2154Kb max_used=2160Kb free=117877Kb bounds [0x00000001178ea000, 0x0000000117b5a000, 0x000000011ee22000]CodeHeap ‘profiled nmethods’: size=120028Kb used=10849Kb max_used=11005Kb free=109178Kb bounds [0x00000001103b3000, 0x0000000110e73000, 0x00000001178ea000]CodeHeap ’non-nmethods’: size=5700Kb used=1177Kb max_used=1239Kb free=4522Kb bounds [0x000000010fe22000, 0x0000000110092000, 0x00000001103b3000] total_blobs=5638 nmethods=4183 adapters=435 compilation: enabled stopped_count=0, restarted_count=0 full_count=0jvm启动参数加上-XX:+PrintCodeCache,可以在jvm关闭时输出code cache的使用情况这里分了non-profiled nmethods、profiled nmethods、non-nmethods三部分来展示其中size就是限制的最大大小,used表示实际使用量,max_used就是使用大小的high water mark,free由size-used得来jcmd pid Compiler.codecache/ # jcmd 1 Compiler.codecache1:CodeHeap ’non-profiled nmethods’: size=120036Kb used=1582Kb max_used=1582Kb free=118453Kb bounds [0x00007f1e42226000, 0x00007f1e42496000, 0x00007f1e4975f000]CodeHeap ‘profiled nmethods’: size=120032Kb used=9621Kb max_used=9621Kb free=110410Kb bounds [0x00007f1e3acee000, 0x00007f1e3b65e000, 0x00007f1e42226000]CodeHeap ’non-nmethods’: size=5692Kb used=1150Kb max_used=1198Kb free=4541Kb bounds [0x00007f1e3a75f000, 0x00007f1e3a9cf000, 0x00007f1e3acee000] total_blobs=5610 nmethods=4369 adapters=412 compilation: enabled stopped_count=0, restarted_count=0 full_count=0使用jcmd的Compiler.codecache也可以查看code cache的使用情况,输出跟-XX:+PrintCodeCache相同jcmd pid VM.native_memory/ # jcmd 1 VM.native_memory1:Native Memory Tracking:Total: reserved=1928023KB, committed=231182KB- Java Heap (reserved=511488KB, committed=140288KB) (mmap: reserved=511488KB, committed=140288KB)- Class (reserved=1090832KB, committed=46608KB) (classes #8218) ( instance classes #7678, array classes #540) (malloc=1296KB #19778) (mmap: reserved=1089536KB, committed=45312KB) ( Metadata: ) ( reserved=40960KB, committed=39680KB) ( used=38821KB) ( free=859KB) ( waste=0KB =0.00%) ( Class space:) ( reserved=1048576KB, committed=5632KB) ( used=5190KB) ( free=442KB) ( waste=0KB =0.00%)- Thread (reserved=37130KB, committed=2806KB) (thread #36) (stack: reserved=36961KB, committed=2636KB) (malloc=127KB #189) (arena=42KB #70)- Code (reserved=248651KB, committed=15351KB) (malloc=963KB #4600) (mmap: reserved=247688KB, committed=14388KB)- GC (reserved=21403KB, committed=7611KB) (malloc=5419KB #9458) (mmap: reserved=15984KB, committed=2192KB)- Compiler (reserved=150KB, committed=150KB) (malloc=20KB #447) (arena=131KB #5)- Internal (reserved=3744KB, committed=3744KB) (malloc=1696KB #6416) (mmap: reserved=2048KB, committed=2048KB)- Other (reserved=24KB, committed=24KB) (malloc=24KB #2)- Symbol (reserved=10094KB, committed=10094KB) (malloc=7305KB #219914) (arena=2789KB #1)- Native Memory Tracking (reserved=4130KB, committed=4130KB) (malloc=12KB #158) (tracking overhead=4119KB)- Arena Chunk (reserved=177KB, committed=177KB) (malloc=177KB)- Logging (reserved=7KB, committed=7KB) (malloc=7KB #264)- Arguments (reserved=18KB, committed=18KB) (malloc=18KB #500)- Module (reserved=165KB, committed=165KB) (malloc=165KB #1699)- Safepoint (reserved=4KB, committed=4KB) (mmap: reserved=4KB, committed=4KB)- Unknown (reserved=4KB, committed=4KB) (mmap: reserved=4KB, committed=4KB)使用jcmd的VM.native_memory也可以查看code cache的使用情况(Code部分),Compiler部分为Memory tracking used by the compiler when generating code使用MemoryPoolMXBean查看 @Test public void testGetCodeCacheUsage(){ ManagementFactory.getPlatformMXBeans(MemoryPoolMXBean.class) .stream() .filter(e -> MemoryType.NON_HEAP == e.getType()) .filter(e -> e.getName().startsWith(“CodeHeap”)) .forEach(e -> { LOGGER.info(“name:{},info:{}",e.getName(),e.getUsage()); }); }MemoryPoolMXBean包含了HEAP及NON_HEAP,其中code cache属于NON_HEAP,其输出如下:12:21:10.728 [main] INFO com.example.CodeCacheTest - name:CodeHeap ’non-nmethods’,info:init = 2555904(2496K) used = 1117696(1091K) committed = 2555904(2496K) max = 5836800(5700K)12:21:10.743 [main] INFO com.example.CodeCacheTest - name:CodeHeap ‘profiled nmethods’,info:init = 2555904(2496K) used = 1543808(1507K) committed = 2555904(2496K) max = 122908672(120028K)12:21:10.743 [main] INFO com.example.CodeCacheTest - name:CodeHeap ’non-profiled nmethods’,info:init = 2555904(2496K) used = 319616(312K) committed = 2555904(2496K) max = 122912768(120032K)spring boot应用查看/ # curl -i “http://localhost:8080/actuator/metrics/jvm.memory.used?tag=area:nonheap"HTTP/1.1 200Content-Disposition: inline;filename=f.txtContent-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8Transfer-Encoding: chunkedDate: Sat, 30 Mar 2019 04:26:39 GMT{“name”:“jvm.memory.used”,“description”:“The amount of used memory”,“baseUnit”:“bytes”,“measurements”:[{“statistic”:“VALUE”,“value”:6.5295408E7}],“availableTags”:[{“tag”:“id”,“values”:[“CodeHeap ’non-profiled nmethods’”,“CodeHeap ‘profiled nmethods’”,“Compressed Class Space”,“Metaspace”,“CodeHeap ’non-nmethods’”]}]}/ # curl -i “http://localhost:8080/actuator/metrics/jvm.memory.used?tag=area:nonheap&tag=id:CodeHeap%20%27non-profiled%20nmethods%27"HTTP/1.1 200Content-Disposition: inline;filename=f.txtContent-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8Transfer-Encoding: chunkedDate: Sat, 30 Mar 2019 04:24:58 GMT{“name”:“jvm.memory.used”,“description”:“The amount of used memory”,“baseUnit”:“bytes”,“measurements”:[{“statistic”:“VALUE”,“value”:1592448.0}],“availableTags”:[]}springboot使用micrometer,通过/actuator/metrics接口提供相关指标查询功能,其中code cache在jvm.memory.used这个metric中它是基于MemoryPoolMXBean来实现的,具体详见micrometer-core-1.1.3-sources.jar!/io/micrometer/core/instrument/binder/jvm/JvmMemoryMetrics.java小结JVM生成的native code存放的内存空间称之为Code Cache;JIT编译、JNI等都会编译代码到native code,其中JIT生成的native code占用了Code Cache的绝大部分空间-XX:ReservedCodeCacheSize用于设置Reserved code cache的最大大小,通常默认是240M;对于有些应用来说240M可能太大,code cache可能都填不满,相当于unconstrained,此时JIT就会继续编译任何它认为可以编译的code查看Code Cache的内存使用情况有好几种方法:jvm启动参数加上-XX:+PrintCodeCache,可以在jvm关闭时输出code cache的使用情况使用jcmd的Compiler.codecache,其输出跟-XX:+PrintCodeCache相同;使用jcmd的VM.native_memory也可以查看code cache的使用情况(Code部分)使用JMX来获取NON_HEAP类型中的name为CodeHeap开头的MemoryPoolMXBean可以得到code cache的使用情况如果是springboot应用,它使用micrometer,通过/actuator/metrics接口提供相关指标查询功能,其中code cache在jvm.memory.used这个metric中doc15 Codecache TuningJVM的编译策略What are ReservedCodeCacheSize and InitialCodeCacheSize?Why does the JVM have a maximum inline depth?Code Cache满导致应用性能降低 ...
聊聊jvm的-XX:MaxDirectMemorySize
序本文主要研究一下jvm的-XX:MaxDirectMemorySize-XX:MaxDirectMemorySize-XX:MaxDirectMemorySize=size用于设置New I/O(java.nio) direct-buffer allocations的最大大小,size的单位可以使用k/K、m/M、g/G;如果没有设置该参数则默认值为0,意味着JVM自己自动给NIO direct-buffer allocations选择最大大小System.initPhase1java.base/java/lang/System.javapublic final class System { /* Register the natives via the static initializer. * * VM will invoke the initializeSystemClass method to complete * the initialization for this class separated from clinit. * Note that to use properties set by the VM, see the constraints * described in the initializeSystemClass method. / private static native void registerNatives(); static { registerNatives(); } /* Don’t let anyone instantiate this class / private System() { } /* * Initialize the system class. Called after thread initialization. / private static void initPhase1() { // VM might invoke JNU_NewStringPlatform() to set those encoding // sensitive properties (user.home, user.name, boot.class.path, etc.) // during “props” initialization. // The charset is initialized in System.c and does not depend on the Properties. Map<String, String> tempProps = SystemProps.initProperties(); VersionProps.init(tempProps); // There are certain system configurations that may be controlled by // VM options such as the maximum amount of direct memory and // Integer cache size used to support the object identity semantics // of autoboxing. Typically, the library will obtain these values // from the properties set by the VM. If the properties are for // internal implementation use only, these properties should be // masked from the system properties. // // Save a private copy of the system properties object that // can only be accessed by the internal implementation. VM.saveProperties(tempProps); props = createProperties(tempProps); StaticProperty.javaHome(); // Load StaticProperty to cache the property values lineSeparator = props.getProperty(“line.separator”); FileInputStream fdIn = new FileInputStream(FileDescriptor.in); FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out); FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err); setIn0(new BufferedInputStream(fdIn)); setOut0(newPrintStream(fdOut, props.getProperty(“sun.stdout.encoding”))); setErr0(newPrintStream(fdErr, props.getProperty(“sun.stderr.encoding”))); // Setup Java signal handlers for HUP, TERM, and INT (where available). Terminator.setup(); // Initialize any miscellaneous operating system settings that need to be // set for the class libraries. Currently this is no-op everywhere except // for Windows where the process-wide error mode is set before the java.io // classes are used. VM.initializeOSEnvironment(); // The main thread is not added to its thread group in the same // way as other threads; we must do it ourselves here. Thread current = Thread.currentThread(); current.getThreadGroup().add(current); // register shared secrets setJavaLangAccess(); // Subsystems that are invoked during initialization can invoke // VM.isBooted() in order to avoid doing things that should // wait until the VM is fully initialized. The initialization level // is incremented from 0 to 1 here to indicate the first phase of // initialization has completed. // IMPORTANT: Ensure that this remains the last initialization action! VM.initLevel(1); } //……}System的initPhase1方法会调用VM.saveProperties(tempProps)方法来保存一份系统配置供内部实现使用;其中tempProps为SystemProps.initProperties()jvm.cpphotspot/share/prims/jvm.cpp // Convert the -XX:MaxDirectMemorySize= command line flag // to the sun.nio.MaxDirectMemorySize property. // Do this after setting user properties to prevent people // from setting the value with a -D option, as requested. // Leave empty if not supplied if (!FLAG_IS_DEFAULT(MaxDirectMemorySize)) { char as_chars[256]; jio_snprintf(as_chars, sizeof(as_chars), JULONG_FORMAT, MaxDirectMemorySize); Handle key_str = java_lang_String::create_from_platform_dependent_str(“sun.nio.MaxDirectMemorySize”, CHECK_NULL); Handle value_str = java_lang_String::create_from_platform_dependent_str(as_chars, CHECK_NULL); result_h->obj_at_put(ndx * 2, key_str()); result_h->obj_at_put(ndx * 2 + 1, value_str()); ndx++; }jvm.cpp里头有一段代码用于把-XX:MaxDirectMemorySize命令参数转换为key为sun.nio.MaxDirectMemorySize的属性VM.savePropertiesjava.base/jdk/internal/misc/VM.javapublic class VM { // the init level when the VM is fully initialized private static final int JAVA_LANG_SYSTEM_INITED = 1; private static final int MODULE_SYSTEM_INITED = 2; private static final int SYSTEM_LOADER_INITIALIZING = 3; private static final int SYSTEM_BOOTED = 4; private static final int SYSTEM_SHUTDOWN = 5; // 0, 1, 2, … private static volatile int initLevel; private static final Object lock = new Object(); //…… // A user-settable upper limit on the maximum amount of allocatable direct // buffer memory. This value may be changed during VM initialization if // “java” is launched with “-XX:MaxDirectMemorySize=<size>”. // // The initial value of this field is arbitrary; during JRE initialization // it will be reset to the value specified on the command line, if any, // otherwise to Runtime.getRuntime().maxMemory(). // private static long directMemory = 64 * 1024 * 1024; // Returns the maximum amount of allocatable direct buffer memory. // The directMemory variable is initialized during system initialization // in the saveAndRemoveProperties method. // public static long maxDirectMemory() { return directMemory; } //…… // Save a private copy of the system properties and remove // the system properties that are not intended for public access. // // This method can only be invoked during system initialization. public static void saveProperties(Map<String, String> props) { if (initLevel() != 0) throw new IllegalStateException(“Wrong init level”); // only main thread is running at this time, so savedProps and // its content will be correctly published to threads started later if (savedProps == null) { savedProps = props; } // Set the maximum amount of direct memory. This value is controlled // by the vm option -XX:MaxDirectMemorySize=<size>. // The maximum amount of allocatable direct buffer memory (in bytes) // from the system property sun.nio.MaxDirectMemorySize set by the VM. // If not set or set to -1, the max memory will be used // The system property will be removed. String s = props.get(“sun.nio.MaxDirectMemorySize”); if (s == null || s.isEmpty() || s.equals("-1")) { // -XX:MaxDirectMemorySize not given, take default directMemory = Runtime.getRuntime().maxMemory(); } else { long l = Long.parseLong(s); if (l > -1) directMemory = l; } // Check if direct buffers should be page aligned s = props.get(“sun.nio.PageAlignDirectMemory”); if (“true”.equals(s)) pageAlignDirectMemory = true; } //……}VM的saveProperties方法读取sun.nio.MaxDirectMemorySize属性,如果为null或者是空或者是-1,那么则设置为Runtime.getRuntime().maxMemory();如果有设置MaxDirectMemorySize且值大于-1,那么使用该值作为directMemory的值;而VM的maxDirectMemory方法则返回的是directMemory的值获取maxDirectMemory的值实例 public BufferPoolMXBean getDirectBufferPoolMBean(){ return ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class) .stream() .filter(e -> e.getName().equals(“direct”)) .findFirst() .orElseThrow(); } public JavaNioAccess.BufferPool getNioBufferPool(){ return SharedSecrets.getJavaNioAccess().getDirectBufferPool(); } /* * -XX:MaxDirectMemorySize=60M / @Test public void testGetMaxDirectMemory(){ ByteBuffer.allocateDirect(2510241024); System.out.println(Runtime.getRuntime().maxMemory()/1024.0/1024.0); System.out.println(VM.maxDirectMemory()/ 1024.0 / 1024.0); System.out.println(getDirectBufferPoolMBean().getTotalCapacity() / 1024.0 / 1024.0); System.out.println(getNioBufferPool().getTotalCapacity() / 1024.0 / 1024.0); }输出输出结果如下:4096.060.025.025.0由于java9模块化之后,VM从原来的sun.misc.VM变更到java.base模块下的jdk.internal.misc.VM;上面代码默认是unamed module,要使用jdk.internal.misc.VM就需要使用–add-exports java.base/jdk.internal.misc=ALL-UNNAMED将其导出到UNNAMED,这样才可以运行同理java9模块化之后,SharedSecrets从原来的sun.misc.SharedSecrets变更到java.base模块下的jdk.internal.access.SharedSecrets;要使用–add-exports java.base/jdk.internal.access=ALL-UNNAMED将其导出到UNNAMED,这样才可以运行从输出结果可以看出,Runtime.getRuntime().maxMemory()输出的值是正确的,而BufferPoolMXBean及JavaNioAccess.BufferPool的getTotalCapacity返回的都是directBuffer大小,而非max值使用API查看directBuffer使用情况实例 /** * -XX:MaxDirectMemorySize=60M / @Test public void testGetDirectMemoryUsage(){ ByteBuffer.allocateDirect(3010241024); System.out.println(getDirectBufferPoolMBean().getMemoryUsed()/ 1024.0 / 1024.0); System.out.println(getNioBufferPool().getMemoryUsed() / 1024.0 / 1024.0); }输出输出结果如下:30.030.0可以看到BufferPoolMXBean及JavaNioAccess.BufferPool的getMemoryUsed可以返回directBuffer大小OOMjava.lang.OutOfMemoryError: Direct buffer memory at java.base/java.nio.Bits.reserveMemory(Bits.java:175) at java.base/java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:118) at java.base/java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:317)如果上面的ByteBuffer.allocateDirect改为分配超过60M,则运行抛出OutOfMemoryError使用NMT查看directBuffer使用情况jcmd 3088 VM.native_memory scale=MB3088:Native Memory Tracking:Total: reserved=5641MB, committed=399MB- Java Heap (reserved=4096MB, committed=258MB) (mmap: reserved=4096MB, committed=258MB)- Class (reserved=1032MB, committed=6MB) (classes #1609) ( instance classes #1460, array classes #149) (mmap: reserved=1032MB, committed=5MB) ( Metadata: ) ( reserved=8MB, committed=5MB) ( used=3MB) ( free=2MB) ( waste=0MB =0.00%) ( Class space:) ( reserved=1024MB, committed=1MB) ( used=0MB) ( free=0MB) ( waste=0MB =0.00%)- Thread (reserved=18MB, committed=18MB) (thread #18) (stack: reserved=18MB, committed=18MB)- Code (reserved=242MB, committed=7MB) (mmap: reserved=242MB, committed=7MB)- GC (reserved=203MB, committed=60MB) (malloc=18MB #2443) (mmap: reserved=185MB, committed=43MB)- Internal (reserved=1MB, committed=1MB) (malloc=1MB #1257)- Other (reserved=30MB, committed=30MB) (malloc=30MB #2)- Symbol (reserved=1MB, committed=1MB) (malloc=1MB #13745)- Shared class space (reserved=17MB, committed=17MB) (mmap: reserved=17MB, committed=17MB)从Other部分可以看到其值跟ByteBuffer.allocateDirect使用的值一致,改变ByteBuffer.allocateDirect的值再重新查看,可以发现Other部分跟着改变;因而初步断定Other部分应该是可以反映direct memory的使用大小小结-XX:MaxDirectMemorySize=size用于设置New I/O(java.nio) direct-buffer allocations的最大大小,size的单位可以使用k/K、m/M、g/G;如果没有设置该参数则默认值为0,意味着JVM自己自动给NIO direct-buffer allocations选择最大大小;从代码java.base/jdk/internal/misc/VM.java中可以看到默认是取的Runtime.getRuntime().maxMemory()使用jdk.internal.misc.VM.maxDirectMemory()可以获取maxDirectMemory的值;由于java9模块化之后,VM从原来的sun.misc.VM变更到java.base模块下的jdk.internal.misc.VM;上面代码默认是unamed module,要使用jdk.internal.misc.VM就需要使用–add-exports java.base/jdk.internal.misc=ALL-UNNAMED将其导出到UNNAMED,这样才可以运行BufferPoolMXBean及JavaNioAccess.BufferPool(通过SharedSecrets获取)的getMemoryUsed可以获取direct memory的大小;其中java9模块化之后,SharedSecrets从原来的sun.misc.SharedSecrets变更到java.base模块下的jdk.internal.access.SharedSecrets;要使用–add-exports java.base/jdk.internal.access=ALL-UNNAMED将其导出到UNNAMED,这样才可以运行另外使用NMT也可以查看direct memory的使用情况,其包含在了Other部分docDefault HotSpot Maximum Direct Memory SizeJVM源码分析之堆外内存完全解读Java堆外内存增长问题排查CaseDirectByteBuffer堆外内存溢出问题排查Default for XX:MaxDirectMemorySizejava.lang.OutOfMemoryError: Direct buffer memory (with tcnative) #6813VM.java MaxDirectMemorySizeSummary of understanding “-XX:MaxDirectMemorySize” setting.native-mem-tracking.mdJava Platform, Standard Edition Tools Reference ...
JVM虚拟机笔记之对象探究及内存溢出
1 对象的创建1.虚拟机遇到new指令时首先去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查引用代表的类是否已被加载、解析和初始化过。如果没有,则执行类加载过程(第7章 虚拟机类加载机制)。2.加载检查通过后,分配内存(内存在类加载完成后便可完全确定)。3.内存分配完成后,虚拟机对对象进行必要的设置,如对象是哪个类的实例、如何找到类的元数据信息.对象哈希码.对象的GC分代年龄等(都放在对象的对象头中)。内存分配方式有:指针碰撞和空闲列表。使用哪种方式取决于堆是否规整(又由垃圾回收决定)。内存分配的线程安全问题:1.CAS配上失败重试的方式保证原子性;2.每个线程在Java堆中预先分配一小块内存,本地线程分配缓冲(TLAB)。分配完成后内存空间初始化为0值(不包括对象头)。从虚拟机角度看,一个新的对象产生了,但从java程序视角看,对象创建才刚刚开始,因为<init>方法还没有执行,,所有字段为零。执行new指令之后会接着执行<init>方法(构造方法),进行初始化,这样一个真正可用的对象才算完成产生。2 对象的内存布局对象在内存中存储的布局可以分为3块区域:对象头、实例数据、对齐填充①对象头含两部分(Header)存储对象自身的 运行时数据,如哈希码、GC分代年龄.锁信息(锁标志,线程持有的锁,偏向线程ID)等。长度在32位和64位的虚拟机中,分别为32bit、 64bit,官方称它为“Mark Word”。类型指针,对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。备注:如果对象是java数组,对象头中还必须有一块记录数据长度的数据②实例数据(InstanceData)对象真正存储的有用信息,也是程序中定义的各种类型的字段内容,包括父类继承的都记录下来,存储顺序受到虚拟机分配策略参数和字段在Java源码中的顺序的影响。③对齐填充(Padding)由于HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍,通俗的说,就是对象大小必须是8字节的整数倍。对象头正好是8字节的倍数。当实例数据部分没有对齐时,需要通过对齐填充来补全。3 对象的访问定位Java程序通过栈上的reference数据来操作堆上的具体对象。不同虚拟机实现的对象访问方式会有所不同,目前主流的访问方式有两种:使用句柄和直接指针。使用句柄:堆中划分出一块内存作为句柄池,reference存储对象的句柄地址 是间接访问,优点是reference中存储的是稳定的句柄地址,对象移动(垃圾会收时)时只会改变句柄中的实例数据指针。使用直接指针 是直接访问,节省了一次指针定位的开销,优点就是速度快。(HotSpot)
聊聊openjdk的jhsdb工具
序本文主要研究一下openjdk的jhsdb工具sa-jdi.jarexport JAVA_HOME="/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home"chmod +x $JAVA_HOME/lib/sa-jdi.jarjava -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDBjava -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.CLHSDB在java9之前,JAVA_HOME/lib目录下有个sa-jdi.jar,可以通过如上命令启动HSDB(图形界面)及CLHSDB(命令行)sa-jdi.jar中的sa的全称为Serviceability Agent,它之前是sun公司提供的一个用于协助调试HotSpot的组件,而HSDB便是使用Serviceability Agent来实现的HSDB就是HotSpot Debugger的简称,由于Serviceability Agent在使用的时候会先attach进程,然后暂停进程进行snapshot,最后deattach进程(进程恢复运行),所以在使用HSDB时要注意jhsdb/ # jhsdb clhsdb command line debugger debugd debug server hsdb ui debugger jstack –help to get more information jmap –help to get more information jinfo –help to get more information jsnap –help to get more informationjhsdb是java9引入的,可以在JAVA_HOME/bin目录下找到jhsdb;它取代了jdk9之前的JAVA_HOME/lib/sa-jdi.jarjhsdb有clhsdb、debugd、hsdb、jstack、jmap、jinfo、jsnap这些mode可以使用其中hsdb为ui debugger,就是jdk9之前的sun.jvm.hotspot.HSDB;而clhsdb即为jdk9之前的sun.jvm.hotspot.CLHSDBjhsdb jstack/ # jhsdb jstack –help –locks to print java.util.concurrent locks –mixed to print both java and native frames (mixed mode) –exe executable image name –core path to coredump –pid pid of process to attach–pid用于指定JVM的进程ID;–exe用于指定可执行文件;–core用于指定core dump文件异常jhsdb jstack –mixed –pid 1//……Caused by: sun.jvm.hotspot.debugger.DebuggerException: get_thread_regs failed for a lwp at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.getThreadIntegerRegisterSet0(Native Method) at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$1GetThreadIntegerRegisterSetTask.doit(LinuxDebuggerLocal.java:534) at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.run(LinuxDebuggerLocal.java:151)如果出现这个异常表示是采用jdk版本的问题,可以尝试一下其他jdk编译版本debugger/ # jhsdb jstack –locks –pid 1Attaching to process ID 1, please wait…Debugger attached successfully.Server compiler detected.JVM version is 12+33Deadlock Detection:No deadlocks found.“DestroyJavaVM” #32 prio=5 tid=0x000055c3b5be0800 nid=0x6 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE JavaThread state: _thread_blockedLocked ownable synchronizers: - None"http-nio-8080-Acceptor-0" #30 daemon prio=5 tid=0x000055c3b5d71800 nid=0x2f runnable [0x00007fa0d13de000] java.lang.Thread.State: RUNNABLE JavaThread state: _thread_in_native - sun.nio.ch.ServerSocketChannelImpl.accept0(java.io.FileDescriptor, java.io.FileDescriptor, java.net.InetSocketAddress[]) @bci=0 (Interpreted frame) - sun.nio.ch.ServerSocketChannelImpl.accept(java.io.FileDescriptor, java.io.FileDescriptor, java.net.InetSocketAddress[]) @bci=4, line=525 (Interpreted frame) - sun.nio.ch.ServerSocketChannelImpl.accept() @bci=41, line=277 (Interpreted frame) - org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept() @bci=4, line=448 (Interpreted frame) - org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept() @bci=1, line=70 (Interpreted frame) - org.apache.tomcat.util.net.Acceptor.run() @bci=98, line=95 (Interpreted frame) - java.lang.Thread.run() @bci=11, line=835 (Interpreted frame)Locked ownable synchronizers: - <0x00000000e3aab6e0>, (a java/util/concurrent/locks/ReentrantLock$NonfairSync)“http-nio-8080-ClientPoller-0” #29 daemon prio=5 tid=0x000055c3b5c20000 nid=0x2e runnable [0x00007fa0d14df000] java.lang.Thread.State: RUNNABLE JavaThread state: _thread_in_native - sun.nio.ch.EPoll.wait(int, long, int, int) @bci=0 (Interpreted frame) - sun.nio.ch.EPollSelectorImpl.doSelect(java.util.function.Consumer, long) @bci=96, line=120 (Interpreted frame) - sun.nio.ch.SelectorImpl.lockAndDoSelect(java.util.function.Consumer, long) @bci=42, line=124 (Interpreted frame) - locked <0x00000000e392ece8> (a sun.nio.ch.EPollSelectorImpl) - locked <0x00000000e392ee38> (a sun.nio.ch.Util$2) - sun.nio.ch.SelectorImpl.select(long) @bci=31, line=136 (Interpreted frame) - org.apache.tomcat.util.net.NioEndpoint$Poller.run() @bci=55, line=743 (Interpreted frame) - java.lang.Thread.run() @bci=11, line=835 (Interpreted frame)Locked ownable synchronizers: - None"http-nio-8080-exec-10" #28 daemon prio=5 tid=0x000055c3b48d6000 nid=0x2d waiting on condition [0x00007fa0d15e0000] java.lang.Thread.State: WAITING (parking) JavaThread state: _thread_blocked - jdk.internal.misc.Unsafe.park(boolean, long) @bci=0 (Interpreted frame) - parking to wait for <0x00000000e3901670> (a java/util/concurrent/locks/AbstractQueuedSynchronizer$ConditionObject) - java.util.concurrent.locks.LockSupport.park(java.lang.Object) @bci=14, line=194 (Interpreted frame) - java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await() @bci=42, line=2081 (Interpreted frame) - java.util.concurrent.LinkedBlockingQueue.take() @bci=27, line=433 (Interpreted frame) - org.apache.tomcat.util.threads.TaskQueue.take() @bci=36, line=107 (Interpreted frame) - org.apache.tomcat.util.threads.TaskQueue.take() @bci=1, line=33 (Interpreted frame) - java.util.concurrent.ThreadPoolExecutor.getTask() @bci=147, line=1054 (Interpreted frame) - java.util.concurrent.ThreadPoolExecutor.runWorker(java.util.concurrent.ThreadPoolExecutor$Worker) @bci=26, line=1114 (Interpreted frame) - java.util.concurrent.ThreadPoolExecutor$Worker.run() @bci=5, line=628 (Interpreted frame) - org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run() @bci=4, line=61 (Interpreted frame) - java.lang.Thread.run() @bci=11, line=835 (Interpreted frame) //……/ # jhsdb jstack –mixed –pid 1Attaching to process ID 1, please wait…Debugger attached successfully.Server compiler detected.JVM version is 12+33Deadlock Detection:No deadlocks found.—————– 47 —————–“http-nio-8080-Acceptor-0” #30 daemon prio=5 tid=0x000055c3b5d71800 nid=0x2f runnable [0x00007fa0d13de000] java.lang.Thread.State: RUNNABLE JavaThread state: _thread_in_native0x00007fa0ee0923ad ????????—————– 46 —————–“http-nio-8080-ClientPoller-0” #29 daemon prio=5 tid=0x000055c3b5c20000 nid=0x2e runnable [0x00007fa0d14df000] java.lang.Thread.State: RUNNABLE JavaThread state: _thread_in_native0x00007fa0ee05f3d0 epoll_pwait + 0x1d0x00007fa0daa97810 * sun.nio.ch.EPoll.wait(int, long, int, int) bci:0 (Interpreted frame)0x00007fa0daa91680 * sun.nio.ch.EPollSelectorImpl.doSelect(java.util.function.Consumer, long) bci:96 line:120 (Interpreted frame)0x00007fa0db85f57c * sun.nio.ch.SelectorImpl.lockAndDoSelect(java.util.function.Consumer, long) bci:42 line:124 (Compiled frame)* sun.nio.ch.SelectorImpl.select(long) bci:31 line:136 (Compiled frame)* org.apache.tomcat.util.net.NioEndpoint$Poller.run() bci:55 line:743 (Interpreted frame)0x00007fa0daa91c88 * java.lang.Thread.run() bci:11 line:835 (Interpreted frame)0x00007fa0daa88849 <StubRoutines>0x00007fa0ed122952 _ZN9JavaCalls11call_helperEP9JavaValueRK12methodHandleP17JavaCallArgumentsP6Thread + 0x3c20x00007fa0ed1208d0 _ZN9JavaCalls12call_virtualEP9JavaValue6HandleP5KlassP6SymbolS6_P6Thread + 0x2000x00007fa0ed1ccfc5 _ZL12thread_entryP10JavaThreadP6Thread + 0x750x00007fa0ed74f3a3 _ZN10JavaThread17thread_main_innerEv + 0x1030x00007fa0ed74c3f5 _ZN6Thread8call_runEv + 0x750x00007fa0ed4a477e _ZL19thread_native_entryP6Thread + 0xee//……–locks或者–mixed花费的时间可能比较长(几分钟,可能要将近6分钟),因而进程暂停的时间也可能比较长,在使用这两个选项时要注意jhsdb jmapjmap -heap pid/ # jmap -heap 1Error: -heap option usedCannot connect to core dump or remote debug server. Use jhsdb jmap insteadjdk9及以上版本使用jmap -heap pid命令查看当前heap使用情况时,发现报错,提示需要使用jhsdb jmap来替代jhsdb jmap pid/ # jhsdb jmap 1sh: jhsdb: not found发现jlink的时候没有添加jdk.hotspot.agent这个module,添加了这个module之后可以发现JAVA_HOME/bin目录下就有了jhsdbPTRACE_ATTACH failed/ # jhsdb jmap 1You have to set –pid or –exe. <no option> to print same info as Solaris pmap –heap to print java heap summary –binaryheap to dump java heap in hprof binary format –dumpfile name of the dump file –histo to print histogram of java object heap –clstats to print class loader statistics –finalizerinfo to print information on objects awaiting finalization –exe executable image name –core path to coredump –pid pid of process to attach/ # jhsdb jmap –heap –pid 1Attaching to process ID 1, please wait…ERROR: ptrace(PTRACE_ATTACH, ..) failed for 1: Operation not permittedError attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can’t attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 1: Operation not permittedsun.jvm.hotspot.debugger.DebuggerException: sun.jvm.hotspot.debugger.DebuggerException: Can’t attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 1: Operation not permitted at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.execute(LinuxDebuggerLocal.java:176) at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.attach(LinuxDebuggerLocal.java:336) at jdk.hotspot.agent/sun.jvm.hotspot.HotSpotAgent.attachDebugger(HotSpotAgent.java:672) at jdk.hotspot.agent/sun.jvm.hotspot.HotSpotAgent.setupDebuggerLinux(HotSpotAgent.java:612) at jdk.hotspot.agent/sun.jvm.hotspot.HotSpotAgent.setupDebugger(HotSpotAgent.java:338) at jdk.hotspot.agent/sun.jvm.hotspot.HotSpotAgent.go(HotSpotAgent.java:305) at jdk.hotspot.agent/sun.jvm.hotspot.HotSpotAgent.attach(HotSpotAgent.java:141) at jdk.hotspot.agent/sun.jvm.hotspot.tools.Tool.start(Tool.java:185) at jdk.hotspot.agent/sun.jvm.hotspot.tools.Tool.execute(Tool.java:118) at jdk.hotspot.agent/sun.jvm.hotspot.tools.JMap.main(JMap.java:176) at jdk.hotspot.agent/sun.jvm.hotspot.SALauncher.runJMAP(SALauncher.java:326) at jdk.hotspot.agent/sun.jvm.hotspot.SALauncher.main(SALauncher.java:455)Caused by: sun.jvm.hotspot.debugger.DebuggerException: Can’t attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 1: Operation not permitted at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.attach0(Native Method) at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$1AttachTask.doit(LinuxDebuggerLocal.java:326) at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.run(LinuxDebuggerLocal.java:151)发现PTRACE_ATTACH被docker禁用了,需要在运行容器时启用PTRACE_ATTACHdocker启用SYS_PTRACEdocker run –cap-add=SYS_PTRACE之后就可以正常使用jhsdb如下:/ # jhsdb jmap –heap –pid 1Attaching to process ID 1, please wait…Debugger attached successfully.Server compiler detected.JVM version is 12+33using thread-local object allocation.Shenandoah GC with 4 thread(s)Heap Configuration: MinHeapFreeRatio = 40 MaxHeapFreeRatio = 70 MaxHeapSize = 523763712 (499.5MB) NewSize = 1363144 (1.2999954223632812MB) MaxNewSize = 17592186044415 MB OldSize = 5452592 (5.1999969482421875MB) NewRatio = 2 SurvivorRatio = 8 MetaspaceSize = 21807104 (20.796875MB) CompressedClassSpaceSize = 1073741824 (1024.0MB) MaxMetaspaceSize = 17592186044415 MB ShenandoahRegionSize = 262144 (0.25MB)Heap Usage:Shenandoah Heap: regions = 1997 capacity = 523501568 (499.25MB) used = 70470552 (67.2059555053711MB) committed = 144441344 (137.75MB)jhsdb jinfo/ # jhsdb jinfo –help –flags to print VM flags –sysprops to print Java System properties <no option> to print both of the above –exe executable image name –core path to coredump –pid pid of process to attach使用jhsdb显示jinfo的sysprops如下:/ # jhsdb jinfo –sysprops –pid 1Attaching to process ID 1, please wait…Debugger attached successfully.Server compiler detected.JVM version is 12+33awt.toolkit = sun.awt.X11.XToolkitjava.specification.version = 12sun.jnu.encoding = UTF-8//……这个命令其实跟jinfo -sysprops 1是等价的jhsdb jsnap/ # jhsdb jsnap –pid 1Attaching to process ID 1, please wait…Debugger attached successfully.Server compiler detected.JVM version is 12+33java.threads.started=27 event(s)java.threads.live=24java.threads.livePeak=24java.threads.daemon=20java.cls.loadedClasses=8250 event(s)java.cls.unloadedClasses=1 event(s)java.cls.sharedLoadedClasses=0 event(s)java.cls.sharedUnloadedClasses=0 event(s)java.ci.totalTime=18236958158 tick(s)java.property.java.vm.specification.version=12java.property.java.vm.specification.name=Java Virtual Machine Specificationjava.property.java.vm.specification.vendor=Oracle Corporationjava.property.java.vm.version=12+33java.property.java.vm.name=OpenJDK 64-Bit Server VMjava.property.java.vm.vendor=Azul Systems, Inc.java.property.java.vm.info=mixed modejava.property.jdk.debug=release//……jhsdb jsnap的功能主要是由jdk.hotspot.agent模块中的sun.jvm.hotspot.tools.JSnap.java来提供的,它可以用于查看threads及class loading/unloading相关的event、JVM属性参数等,其中–all可以显示更多的JVM属性参数jhsdb与jcmdjhsdb: A New Tool for JDK 9这篇文章中列出了jhsdb与jcmd的等价命令,如下图:小结在java9之前,JAVA_HOME/lib目录下有个sa-jdi.jar,可以通过如上命令启动HSDB(图形界面)及CLHSDB(命令行);sa-jdi.jar中的sa的全称为Serviceability Agent,它之前是sun公司提供的一个用于协助调试HotSpot的组件,而HSDB便是使用Serviceability Agent来实现的;HSDB就是HotSpot Debugger的简称,由于Serviceability Agent在使用的时候会先attach进程,然后暂停进程进行snapshot,最后deattach进程(进程恢复运行),所以在使用HSDB时要注意jhsdb是java9引入的,可以在JAVA_HOME/bin目录下找到jhsdb;它取代了jdk9之前的JAVA_HOME/lib/sa-jdi.jar;jhsdb有clhsdb、debugd、hsdb、jstack、jmap、jinfo、jsnap这些mode可以使用;其中hsdb为ui debugger,就是jdk9之前的sun.jvm.hotspot.HSDB;而clhsdb即为jdk9之前的sun.jvm.hotspot.CLHSDBjhsdb在jdk.hotspot.agent这个模块中;对于jhsdb jstack的–locks或者–mixed命令花费的时间可能比较长(几分钟,可能要将近6分钟),因而进程暂停的时间也可能比较长,在使用这两个选项时要注意;对于jdk9及以后的版本不再使用jmap -heap命令来查询heap内存情况,需要用jhsdb jmap –heap –pid来替代;使用jhsdb jmap需要在运行容器时启用PTRACE_ATTACH才可以docJVM信息查看jhsdbjdk.hotspot.agent jhsdbjhsdb: A New Tool for JDK 9jcmd: One JDK Command-Line Tool to Rule Them AllJVM in Docker and PTRACE_ATTACHServiceability in HotSpotThe HotSpot™ Serviceability Agent: An out-of-process high level debugger for a Java™ virtual machine ...
聊聊openjdk的jvm.cfg文件
序本文主要研究一下openjdk的jvm.cfg文件jdk8/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/jvm.cfg# List of JVMs that can be used as an option to java, javac, etc.# Order is important – first in this list is the default JVM.# NOTE that this both this file and its format are UNSUPPORTED and# WILL GO AWAY in a future release.## You may also select a JVM in an arbitrary location with the# “-XXaltjvm=<jvm_dir>” option, but that too is unsupported# and may not be available in a future release.#-server KNOWN-client IGNORE对于jdk8该文件在JAVA_HOME/jre/lib/目录下;其注释显示该配置文件用于配置java、javac能够使用的option,其中配置顺序非常重要,第一个为default JVM(不管其是KNOWN还是IGNORE;IGNORE仅仅是禁用VM option;ERROR则报错显示not supported);可以看到这里-server是默认的,而-client则被忽略jdk9-12/Library/Java/JavaVirtualMachines/jdk-12.jdk/Contents/Home/lib/jvm.cfg-server KNOWN-client IGNORE对于jdk9、10、11、12该文件在JAVA_HOME/lib/目录下设置-client为default对于jdk12,把-client移到前面-client IGNORE-server KNOWN执行java -version显示如下java -versionError: missing client' JVM at /Library/Java/JavaVirtualMachines/jdk-12.jdk/Contents/Home/lib/client/libjvm.dylib’.Please install or use the JRE or JDK that contains these missing components.可以看到对于mac的jdk12,把client设置为default则报错禁用client-server KNOWN-client ERROR执行java -client -version显示如下java -client -versionError: client VM not supported可以看到设置-client为ERROR,则报错显示not supported删除jvm.cfgjava -versionError: could not open `/Library/Java/JavaVirtualMachines/jdk-12.jdk/Contents/Home/lib/jvm.cfg’可以看到删除jvm.cfg,执行java -version则会报错小结jvm.cfg文件用于配置java、javac能够使用的option,其中配置顺序非常重要,第一个为default JVM(不管其是KNOWN还是IGNORE;IGNORE仅仅是禁用VM option;ERROR则报错显示not supported)对于jdk8该文件在JAVA_HOME/jre/lib/目录下;对于jdk9、10、11、12该文件在JAVA_HOME/lib/目录下;删除jvm.cfg文件的话,则执行java命令会报错可以看到对于mac来说其open jdk的jvm.cfg中-server位于-client前面,-server是default JVM,而-client则被忽略docThe Java HotSpot VM Architecture OverviewWhat is the purpose of jvm.cfg file in relation to Java?Running the JVM in server modeServer-Class Machine DetectionChange Default Java VM to ClientOpenJDK default options to always use the server VM ...
聊聊HotSpot VM的Native Memory Tracking
序本文主要研究一下HotSpot VM的Native Memory TrackingNative Memory Trackingjava8给HotSpot VM引入了Native Memory Tracking (NMT)特性,可以用于追踪JVM的内部内存使用使用开启-XX:NativeMemoryTracking=summary使用-XX:NativeMemoryTracking=summary可以用于开启NMT,其中该值默认为off,可以设置summary、detail来开启;开启的话,大概会增加5%-10%的性能消耗查看/ # jcmd 1 VM.native_memory summary/ # jcmd 1 VM.native_memory summary scale=MB使用jcmd pid VM.native_memory可以查看,后面可以加summary或者detail,如果是开启summary的,就只能使用summary;其中scale参数可以指定展示的单位,可以为KB或者MB或者GB创建baseline/ # jcmd 1 VM.native_memory baseline1:Baseline succeeded创建baseline之后可以用summary.diff来对比查看diff/ # jcmd 1 VM.native_memory summary.diff使用summary.diff来查看跟baseline对比的统计信息shutdown时输出-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics使用上述命令可以在jvm shutdown的时候输出整体的native memory统计关闭/ # jcmd 1 VM.native_memory shutdown1:Native memory tracking has been turned off使用jcmd pid VM.native_memory shutdown可以用于关闭NMT;注意使用jcmd关闭之后貌似没有对应jcmd命令来开启实例/ # jcmd 1 VM.native_memory summary scale=MB1:Native Memory Tracking:Total: reserved=2175MB, committed=682MB- Java Heap (reserved=501MB, committed=463MB) (mmap: reserved=501MB, committed=463MB)- Class (reserved=1070MB, committed=50MB) (classes #8801) ( instance classes #8204, array classes #597) (malloc=2MB #24660) (mmap: reserved=1068MB, committed=49MB) ( Metadata: ) ( reserved=44MB, committed=43MB) ( used=42MB) ( free=1MB) ( waste=0MB =0.00%) ( Class space:) ( reserved=1024MB, committed=6MB) ( used=5MB) ( free=0MB) ( waste=0MB =0.00%)- Thread (reserved=228MB, committed=27MB) (thread #226) (stack: reserved=227MB, committed=26MB) (malloc=1MB #1139)- Code (reserved=243MB, committed=17MB) (malloc=1MB #5509) (mmap: reserved=242MB, committed=16MB)- GC (reserved=23MB, committed=15MB) (malloc=8MB #11446) (mmap: reserved=16MB, committed=7MB)- Compiler (reserved=26MB, committed=26MB) (malloc=2MB #1951) (arena=24MB #13)- Internal (reserved=5MB, committed=5MB) (malloc=3MB #9745) (mmap: reserved=2MB, committed=2MB)- Other (reserved=2MB, committed=2MB) (malloc=2MB #202)- Symbol (reserved=10MB, committed=10MB) (malloc=8MB #233939) (arena=3MB #1)- Native Memory Tracking (reserved=5MB, committed=5MB) (tracking overhead=5MB)- Arena Chunk (reserved=63MB, committed=63MB) (malloc=63MB)可以看到整个memory主要包含了Java Heap、Class、Thread、Code、GC、Compiler、Internal、Other、Symbol、Native Memory Tracking、Arena Chunk这几部分;其中reserved表示应用可用的内存大小,committed表示应用正在使用的内存大小Java Heap部分表示heap内存目前占用了463MB;Class部分表示已经加载的classes个数为8801,其metadata占用了50MB;Thread部分表示目前有225个线程,占用了27MB;Code部分表示JIT生成的或者缓存的instructions占用了17MB;GC部分表示目前已经占用了15MB的内存空间用于帮助GC;Code部分表示compiler生成code的时候占用了26MB;Internal部分表示命令行解析、JVMTI等占用了5MB;Other部分表示尚未归类的占用了2MB;Symbol部分表示诸如string table及constant pool等symbol占用了10MB;Native Memory Tracking部分表示该功能自身占用了5MB;Arena Chunk部分表示arena chunk占用了63MB一个arena表示使用malloc分配的一个memory chunk,这些chunks可以被其他subsystems做为临时内存使用,比如pre-thread的内存分配,它的内存释放是成bulk的小结java8给HotSpot VM引入了Native Memory Tracking (NMT)特性,可以用于追踪JVM的内部内存使用使用-XX:NativeMemoryTracking=summary可以用于开启NMT,其中该值默认为off,可以设置summary、detail来开启;开启的话,大概会增加5%-10%的性能消耗;使用-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics可以在jvm shutdown的时候输出整体的native memory统计;其他的可以使用jcmd pid VM.native_memory相关命令进行查看、diff、shutdown等整个memory主要包含了Java Heap、Class、Thread、Code、GC、Compiler、Internal、Other、Symbol、Native Memory Tracking、Arena Chunk这几部分;其中reserved表示应用可用的内存大小,committed表示应用正在使用的内存大小docNative Memory TrackingNative Memory Tracking diagnostic-toolsNMT Memory CategoriesMemory footprint of the JVMNative Memory Tracking in JVM ...
JVM虚拟机笔记之运行时数据区域
本文参照深入了解Java虚拟机-周志明,纯粹做做笔记,写写自己觉得较为重要的内容方便理解Java虚拟机运行时数据区如下:本地方法栈:本地方法栈和Java虚拟机栈作用类似,区别是虚拟机栈为Java方法(也就是字节码)服务,而本地方法栈为虚拟机使用到的native方法服务。异常情况也与虚拟机栈一致。Java堆:虚拟机中管理的内存最大的一块,所有线程共享,虚拟机启动时创建。此内存区域唯一目的是存放对象实例,几乎所有对象实例及数组都要在堆上分配内存。(随着JIT编译器发展和逃逸技术,可能有些实例不在堆分配)。垃圾收集:Java堆是垃圾回收的主要区域,从内存回收的角度来看,由于现在收集器基本采用分代收集算法,所以堆还可以细分为: 1.新生代和2.老年代再细致一点还可以分为1.有Eden空间.2.From Survivor空间.3.To Survior空间等。从内存分配的角度来看,线程共享的堆可能会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer.TLAB). 划分目的:为了更好地回收内存,或者更快得分配内存。Java堆可以处在物理上不连续的内存空间,只要逻辑连续即可,堆也是可扩展的(通过 -Xmx -Xms控制)。方法区(元空间,Metaspace):与堆相同,也是线程共享的数据区域,用于存储已被虚拟机加载的类信息丶常量(final)丶静态变量丶即时编译器编译后的代码等数据。 垃圾收集:垃圾收集行为在此区域较少出现,回收目标主要是针对常量池的回收和对类型的卸载,但是对类型卸载很苛刻,对此区域未完全回收则出现内存泄漏。运行时常量池:属于方法区的一部分。class文件除了有类的版本,方法,字段,接口等描述信息外,还有常量池用于存放编译器生成的各种字面量和符号引用,类加载后进入方法区的常量池存放。 运行时常量池还具备动态性,并非只有编译器才能产生,运行期也可以产生,比如String的intern方法
Java虚拟机规范(介绍)
介绍一点历史Java®编程语言是一种通用的、并发的、面向对象的语言,它的语法类似于C和C++,但它省略了许多使C和C++复杂、混乱和不安全的特性。最初开发Java平台是为了解决为联网的消费者设备构建软件的问题,它旨在支持多种主机架构,并允许安全交付软件组件,为了满足这些要求,编译后的代码必须能够在网络上传输,在任何客户端上运行,并向客户端保证运行安全。万维网的普及使这些属性更加有趣,Web浏览器使数百万人能够以简单的方式上网并访问富媒体内容,最后有一种介质,无论你使用的是什么机器,还是连接到快速网络或慢速调制解调器,你所看到和听到的内容基本相同。网络爱好者很快发现网络HTML文档格式支持的内容太有限了,HTML扩展(例如表单)只突出了这些限制,同时明确表示没有浏览器可以包含用户想要的所有功能,可扩展性就是答案。HotJava浏览器首先展示了Java编程语言和平台的有趣属性,使在HTML页面中嵌入程序成为可能,程序与它们出现的HTML页面一起透明地下载到浏览器中,在被浏览器接受之前,会仔细检查程序以确保它们是安全的。与HTML页面一样,编译的程序与网络和主机无关,程序的行为方式相同,无论它们来自何处,或者它们被装入和运行的机器类型。包含Java平台的Web浏览器不再局限于预定的一组功能,包含动态内容的网页的访问者可以确保他们的机器不会被该内容损坏,程序员可以编写程序一次,它将在任何提供Java运行时环境的机器上运行。Java虚拟机Java虚拟机是Java平台的基石,它是该技术的组成部分,负责其硬件和操作系统的独立性,其编译的代码很小以及保护用户免受恶意程序攻击的能力。Java虚拟机是一种抽象计算机,像真正的计算机一样,它有一个指令集并在运行时操作各种内存区域,使用虚拟机实现编程语言是相当普遍的,最著名的虚拟机可能是UCSD Pascal的P-Code机器。在Sun Microsystems,Inc.完成的Java虚拟机的第一个原型实现模拟了由类似于当代个人数字助理(PDA)的手持设备托管的软件中的Java虚拟机指令集。Oracle当前的实现模拟在移动、桌面和服务器设备上的Java虚拟机,但Java虚拟机不承担任何特定的实现技术、主机硬件或主机操作系统,它本身并没有被解释,但也可以通过将其指令集编译为硅CPU来实现,它也可以用微代码实现或直接用硅实现。Java虚拟机不知道Java编程语言,只知道特定的二进制格式,即class文件格式,class文件包含Java虚拟机指令(或字节码)和符号表,以及其他辅助信息。出于安全考虑,Java虚拟机对class文件中的代码施加了强大的语法和结构约束,但是,任何具有可以用有效class文件表示的功能的语言都可以由Java虚拟机托管,由通用的、与机器无关的平台吸引,其他语言的实现者可以将Java虚拟机作为其语言的交付工具。此处指定的Java虚拟机与Java SE 11平台兼容,并支持Java语言规范Java SE 11 Edition中指定的Java编程语言。规范的组织第2章概述了Java虚拟机架构。第3章介绍用Java编程语言编写的代码编译成Java虚拟机指令集。第4章说明了class文件格式,这是一种独立于硬件和操作系统的二进制格式,用于表示已编译的类和接口。第5章说明了Java虚拟机的启动以及类和接口的加载、链接和初始化。第6章说明了Java虚拟机的指令集,按字母顺序显示操作码助记符。第7章给出了一个由操作码值索引的Java虚拟机操作码助记符表。在Java®虚拟机规范的第二版中,第2章概述了Java编程语言,该语言旨在支持Java虚拟机的规范,但它本身并不是规范的一部分。在Java虚拟机规范Java SE 11 Edition中,有关Java编程语言的信息,请参阅Java语言规范Java SE 11 Edition。在Java®虚拟机规范的第二版中,第8章详细介绍了解释Java虚拟机线程与共享主内存交互的低级别操作,在Java虚拟机规范Java SE 11 Edition中,读者可以参考Java语言规范的第17章,Java SE 11 Edition,了解有关线程和锁的信息,第17章反映了JSR 133专家组制作的Java内存模型和线程规范。符号在整个规范中,引用了从Java SE Platform API中提取的类和接口,每当使用单个标识符N引用类或接口(除了在示例中声明的那些之外),预期的引用是包java.lang中名为N的类或接口,使用java.lang以外的包中的类或接口的完全限定名。每当引用在包java或其任何子包中声明的类或接口时,预期的引用都是由引导类加载器加载的类或接口。每当引用名为java的包的子包时,预期的引用就是由引导类加载器确定的子包。
Java虚拟机规范(目录)
Java虚拟机规范Java SE 11 Edition介绍Java虚拟机介绍Java虚拟机的结构class文件格式数据类型原始类型和值引用类型和值运行时数据区帧对象的表示浮点算术特殊方法异常指令集汇总类库公有设计,私有实现编译Java虚拟机格式示例使用常量、局部变量和控制结构算术访问运行时常量池更多控制示例接收参数调用方法使用类实例数组编译开关操作数堆栈上的操作抛出和处理异常编译finally同步注解模块class文件格式ClassFile结构名称描述符常量池字段方法属性格式检查Java虚拟机代码的约束class文件的验证Java虚拟机的局限性加载、链接和初始化运行时常量池Java虚拟机启动创建和加载链接初始化绑定原生方法实现Java虚拟机退出Java虚拟机指令集假设:“必须”的含义保留的操作码虚拟机错误指令描述的格式指令操作码助记符
Java动态追踪技术探究
引子在遥远的希艾斯星球爪哇国塞沃城中,两名年轻的程序员正在为一件事情苦恼,程序出问题了,一时看不出问题出在哪里,于是有了以下对话:“Debug一下吧。”“线上机器,没开Debug端口。”“看日志,看看请求值和返回值分别是什么?”“那段代码没打印日志。”“改代码,加日志,重新发布一次。”“怀疑是线程池的问题,重启会破坏现场。”长达几十秒的沉默之后:“据说,排查问题的最高境界,就是只通过Review代码来发现问题。”比几十秒长几十倍的沉默之后:“我轮询了那段代码一十七遍之后,终于得出一个结论。”“结论是?”“我还没到达只通过Review代码就能发现问题的至高境界。”从JSP说起对于大多数Java程序员来说,早期的时候,都会接触到一个叫做JSP(Java Server Pages)的技术。虽然这种技术,在前后端代码分离、前后端逻辑分离、前后端组织架构分离的今天来看,已经过时了,但是其中还是有一些有意思的东西,值得拿出来说一说。当时刚刚处于Java入门时期的我们,大多数精力似乎都放在了JSP的页面展示效果上了:“这个表格显示的行数不对”“原来是for循环写的有问题,改一下,刷新页面再试一遍”“嗯,好了,表格显示没问题了,但是,登录人的姓名没取到啊,是不是Sesstion获取有问题?”“有可能,我再改一下,一会儿再刷新试试”……在一遍一遍修改代码刷新浏览器页面重试的时候,我们自己也许并没有注意到一件很酷的事情:我们修改完代码,居然只是简单地刷新一遍浏览器页面,修改就生效了,整个过程并没有重启JVM。按照我们的常识,Java程序一般都是在启动时加载类文件,如果都像JSP这样修改完代码,不用重启就生效的话,那文章开头的问题就可以解决了啊:Java文件中加一段日志打印的代码,不重启就生效,既不破坏现场,又可以定位问题。忍不住试一试:修改、编译、替换class文件。额,不行,新改的代码并没有生效。那为什么偏偏JSP可以呢?让我们先来看看JSP的运行原理。当我们打开浏览器,请求访问一个JSP文件的时候,整个过程是这样的:JSP文件修改过后,之所以能及时生效,是因为Web容器(Tomcat)会检查请求的JSP文件是否被更改过。如果发生过更改,那么就将JSP文件重新解析翻译成一个新的Sevlet类,并加载到JVM中。之后的请求,都会由这个新的Servet来处理。这里有个问题,根据Java的类加载机制,在同一个ClassLoader中,类是不允许重复的。为了绕开这个限制,Web容器每次都会创建一个新的ClassLoader实例,来加载新编译的Servlet类。之后的请求都会由这个新的Servlet来处理,这样就实现了新旧JSP的切换。HTTP服务是无状态的,所以JSP的场景基本上都是一次性消费,这种通过创建新的ClassLoader来“替换”class的做法行得通,但是对于其他应用,比如Spring框架,即便这样做了,对象多数是单例,对于内存中已经创建好的对象,我们无法通过这种创建新的ClassLoader实例的方法来修改对象行为。我就是想不重启应用加个日志打印,就这么难吗?Java对象行为既然JSP的办法行不通,那我们来看看还有没有其他的办法。仔细想想,我们会发现,文章开头的问题本质上是动态改变内存中已存在对象的行为的问题。所以,我们得先弄清楚JVM中和对象行为有关的地方在哪里,有没有更改的可能性。我们都知道,对象使用两种东西来描述事物:行为和属性。举个例子:public class Person{ private int age; private String name; public void speak(String str) { System.out.println(str); } public Person(int age, String name) { this.age = age; this.name = name; }}上面Person类中age和name是属性,speak是行为。对象是类的事例,每个对象的属性都属于对象本身,但是每个对象的行为却是公共的。举个例子,比如我们现在基于Person类创建了两个对象,personA和personB:Person personA = new Person(43, “lixunhuan”);personA.speak(“我是李寻欢”);Person personB = new Person(23, “afei”);personB.speak(“我是阿飞”);personA和personB有各自的姓名和年龄,但是有共同的行为:speak。想象一下,如果我们是Java语言的设计者,我们会怎么存储对象的行为和属性呢?“很简单,属性跟着对象走,每个对象都存一份。行为是公共的东西,抽离出来,单独放到一个地方。”“咦?抽离出公共的部分,跟代码复用好像啊。”“大道至简,很多东西本来都是殊途同归。”也就是说,第一步我们首先得找到存储对象行为的这个公共的地方。一番搜索之后,我们发现这样一段描述:Method area is created on virtual machine startup, shared among all Java virtual machine threads and it is logically part of heap area. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors.Java的对象行为(方法、函数)是存储在方法区的。“方法区中的数据从哪来?”“方法区中的数据是类加载时从class文件中提取出来的。”“class文件从哪来?”“从Java或者其他符合JVM规范的源代码中编译而来。”“源代码从哪来?”“废话,当然是手写!”“倒着推,手写没问题,编译没问题,至于加载……有没有办法加载一个已经加载过的类呢?如果有的话,我们就能修改字节码中目标方法所在的区域,然后重新加载这个类,这样方法区中的对象行为(方法)就被改变了,而且不改变对象的属性,也不影响已经存在对象的状态,那么就可以搞定这个问题了。可是,这岂不是违背了JVM的类加载原理?毕竟我们不想改变ClassLoader。”“少年,可以去看看java.lang.instrument.Instrumentation。”java.lang.instrument.Instrumentation看完文档之后,我们发现这么两个接口:redefineClasses和retransformClasses。一个是重新定义class,一个是修改class。这两个大同小异,看reDefineClasses的说明:This method is used to replace the definition of a class without reference to the existing class file bytes, as one might do when recompiling from source for fix-and-continue debugging. Where the existing class file bytes are to be transformed (for example in bytecode instrumentation) retransformClasses should be used.都是替换已经存在的class文件,redefineClasses是自己提供字节码文件替换掉已存在的class文件,retransformClasses是在已存在的字节码文件上修改后再替换之。当然,运行时直接替换类很不安全。比如新的class文件引用了一个不存在的类,或者把某个类的一个field给删除了等等,这些情况都会引发异常。所以如文档中所言,instrument存在诸多的限制:The redefinition may change method bodies, the constant pool and attributes. The redefinition must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. These restrictions maybe be lifted in future versions. The class file bytes are not checked, verified and installed until after the transformations have been applied, if the resultant bytes are in error this method will throw an exception.我们能做的基本上也就是简单修改方法内的一些行为,这对于我们开头的问题,打印一段日志来说,已经足够了。当然,我们除了通过reTransform来打印日志,还能做很多其他非常有用的事情,这个下文会进行介绍。那怎么得到我们需要的class文件呢?一个最简单的方法,是把修改后的Java文件重新编译一遍得到class文件,然后调用redefineClasses替换。但是对于没有(或者拿不到,或者不方便修改)源码的文件我们应该怎么办呢?其实对于JVM来说,不管是Java也好,Scala也好,任何一种符合JVM规范的语言的源代码,都可以编译成class文件。JVM的操作对象是class文件,而不是源码。所以,从这种意义上来讲,我们可以说“JVM跟语言无关”。既然如此,不管有没有源码,其实我们只需要修改class文件就行了。直接操作字节码Java是软件开发人员能读懂的语言,class字节码是JVM能读懂的语言,class字节码最终会被JVM解释成机器能读懂的语言。无论哪种语言,都是人创造的。所以,理论上(实际上也确实如此)人能读懂上述任何一种语言,既然能读懂,自然能修改。只要我们愿意,我们完全可以跳过Java编译器,直接写字节码文件,只不过这并不符合时代的发展罢了,毕竟高级语言设计之始就是为我们人类所服务,其开发效率也比机器语言高很多。对于人类来说,字节码文件的可读性远远没有Java代码高。尽管如此,还是有一些杰出的程序员们创造出了可以用来直接编辑字节码的框架,提供接口可以让我们方便地操作字节码文件,进行注入修改类的方法,动态创造一个新的类等等操作。其中最著名的框架应该就是ASM了,cglib、Spring等框架中对于字节码的操作就建立在ASM之上。我们都知道,Spring的AOP是基于动态代理实现的,Spring会在运行时动态创建代理类,代理类中引用被代理类,在被代理的方法执行前后进行一些神秘的操作。那么,Spring是怎么在运行时创建代理类的呢?动态代理的美妙之处,就在于我们不必手动为每个需要被代理的类写代理类代码,Spring在运行时会根据需要动态地创造出一个类,这里创造的过程并非通过字符串写Java文件,然后编译成class文件,然后加载。Spring会直接“创造”一个class文件,然后加载,创造class文件的工具,就是ASM了。到这里,我们知道了用ASM框架直接操作class文件,在类中加一段打印日志的代码,然后调用retransformClasses就可以了。BTrace截止到目前,我们都是停留在理论描述的层面。那么如何进行实现呢?先来看几个问题:在我们的工程中,谁来做这个寻找字节码,修改字节码,然后reTransform的动作呢?我们并非先知,不可能知道未来有没有可能遇到文章开头的这种问题。考虑到性价比,我们也不可能在每个工程中都开发一段专门做这些修改字节码、重新加载字节码的代码。如果JVM不在本地,在远程呢?如果连ASM都不会用呢?能不能更通用一些,更“傻瓜”一些。幸运的是,因为有BTrace的存在,我们不必自己写一套这样的工具了。什么是BTrace呢?BTrace已经开源,项目描述极其简短:A safe, dynamic tracing tool for the Java platform.BTrace是基于Java语言的一个安全的、可提供动态追踪服务的工具。BTrace基于ASM、Java Attach Api、Instruments开发,为用户提供了很多注解。依靠这些注解,我们可以编写BTrace脚本(简单的Java代码)达到我们想要的效果,而不必深陷于ASM对字节码的操作中不可自拔。看BTrace官方提供的一个简单例子:拦截所有java.io包中所有类中以read开头的方法,打印类名、方法名和参数名。当程序IO负载比较高的时候,就可以从输出的信息中看到是哪些类所引起,是不是很方便?package com.sun.btrace.samples;import com.sun.btrace.annotations.;import com.sun.btrace.AnyType;import static com.sun.btrace.BTraceUtils.;/** * This sample demonstrates regular expression * probe matching and getting input arguments * as an array - so that any overload variant * can be traced in “one place”. This example * traces any “readXX” method on any class in * java.io package. Probed class, method and arg * array is printed in the action. /@BTrace public class ArgArray { @OnMethod( clazz="/java\.io\../", method="/read./" ) public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args) { println(pcn); println(pmn); printArray(args); }}再来看另一个例子:每隔2秒打印截止到当前创建过的线程数。package com.sun.btrace.samples;import com.sun.btrace.annotations.;import static com.sun.btrace.BTraceUtils.*;import com.sun.btrace.annotations.Export;/** * This sample creates a jvmstat counter and * increments it everytime Thread.start() is * called. This thread count may be accessed * from outside the process. The @Export annotated * fields are mapped to jvmstat counters. The counter * name is “btrace.” + <className> + “.” + <fieldName> */ @BTrace public class ThreadCounter { // create a jvmstat counter using @Export @Export private static long count; @OnMethod( clazz=“java.lang.Thread”, method=“start” ) public static void onnewThread(@Self Thread t) { // updating counter is easy. Just assign to // the static field! count++; } @OnTimer(2000) public static void ontimer() { // we can access counter as “count” as well // as from jvmstat counter directly. println(count); // or equivalently … println(Counters.perfLong(“btrace.com.sun.btrace.samples.ThreadCounter.count”)); }}看了上面的用法是不是有所启发?忍不住冒出来许多想法。比如查看HashMap什么时候会触发rehash,以及此时容器中有多少元素等等。有了BTrace,文章开头的问题可以得到完美的解决。至于BTrace具体有哪些功能,脚本怎么写,这些Git上BTrace工程中有大量的说明和举例,网上介绍BTrace用法的文章更是恒河沙数,这里就不再赘述了。我们明白了原理,又有好用的工具支持,剩下的就是发挥我们的创造力了,只需在合适的场景下合理地进行使用即可。既然BTrace能解决上面我们提到的所有问题,那么BTrace的架构是怎样的呢?BTrace主要有下面几个模块:BTrace脚本:利用BTrace定义的注解,我们可以很方便地根据需要进行脚本的开发。Compiler:将BTrace脚本编译成BTrace class文件。Client:将class文件发送到Agent。Agent:基于Java的Attach Api,Agent可以动态附着到一个运行的JVM上,然后开启一个BTrace Server,接收client发过来的BTrace脚本;解析脚本,然后根据脚本中的规则找到要修改的类;修改字节码后,调用Java Instrument的reTransform接口,完成对对象行为的修改并使之生效。整个BTrace的架构大致如下:BTrace最终借Instruments实现class的替换。如上文所说,出于安全考虑,Instruments在使用上存在诸多的限制,BTrace也不例外。BTrace对JVM来说是“只读的”,因此BTrace脚本的限制如下:不允许创建对象不允许创建数组不允许抛异常不允许catch异常不允许随意调用其他对象或者类的方法,只允许调用com.sun.btrace.BTraceUtils中提供的静态方法(一些数据处理和信息输出工具)不允许改变类的属性不允许有成员变量和方法,只允许存在static public void方法不允许有内部类、嵌套类不允许有同步方法和同步块不允许有循环不允许随意继承其他类(当然,java.lang.Object除外)不允许实现接口不允许使用assert不允许使用Class对象如此多的限制,其实可以理解。BTrace要做的是,虽然修改了字节码,但是除了输出需要的信息外,对整个程序的正常运行并没有影响。ArthasBTrace脚本在使用上有一定的学习成本,如果能把一些常用的功能封装起来,对外直接提供简单的命令即可操作的话,那就再好不过了。阿里的工程师们早已想到这一点,就在去年(2018年9月份),阿里巴巴开源了自己的Java诊断工具——Arthas。Arthas提供简单的命令行操作,功能强大。究其背后的技术原理,和本文中提到的大致无二。Arthas的文档很全面,想详细了解的话可以戳这里。本文旨在说明Java动态追踪技术的来龙去脉,掌握技术背后的原理之后,只要愿意,各位读者也可以开发出自己的“冰封王座”出来。尾声:三生万物现在,让我们试着站在更高的地方“俯瞰”这些问题。Java的Instruments给运行时的动态追踪留下了希望,Attach API则给运行时动态追踪提供了“出入口”,ASM则大大方便了“人类”操作Java字节码的操作。基于Instruments和Attach API前辈们创造出了诸如JProfiler、Jvisualvm、BTrace、Arthas这样的工具。以ASM为基础发展出了cglib、动态代理,继而是应用广泛的Spring AOP。Java是静态语言,运行时不允许改变数据结构。然而,Java 5引入Instruments,Java 6引入Attach API之后,事情开始变得不一样了。虽然存在诸多限制,然而,在前辈们的努力下,仅仅是利用预留的近似于“只读”的这一点点狭小的空间,仍然创造出了各种大放异彩的技术,极大地提高了软件开发人员定位问题的效率。计算机应该是人类有史以来最伟大的发明之一,从电磁感应磁生电,到高低电压模拟0和1的比特,再到二进制表示出几种基本类型,再到基本类型表示出无穷的对象,最后无穷的对象组合交互模拟现实生活乃至整个宇宙。两千五百年前,《道德经》有言:“道生一,一生二,二生三,三生万物。”两千五百年后,计算机的发展过程也大抵如此吧。作者简介高扬,2017年加入美团打车,负责美团打车结算系统的开发。 ...
95%的技术面试必考的JVM知识点都在这,另附加分思路!
概述:知识点汇总jvm的知识点汇总共6个大方向:内存模型、类加载机制、GC垃圾回收是比较重点的内容。性能调优部分偏重实际应用,重点突出实践能力。编译器优化和执行模式部分偏重理论基础,主要掌握知识点。各个部分的内容如下:1>内存模型部分:程序计数器、方法区、堆、栈、本地方法栈的作用,保存哪些数据;2>类加载部分:双亲委派的加载机制以及常用类加载器分别加载哪种类型的类;3>GC部分:分代回收的思想和依据,以及不同垃圾回收算法实现的思路、适合的场景;4>性能调优部分:常用的jvm优化参数的作用,参数调优的依据,要了解常用的jvm分析工具能分析哪类问题,以及使用方法;5>执行模式部分:解释、编译、混合模式的优缺点,了解java7提供的分层编译技术。需要知道JIT即时编译技术和OSR也就是栈上替换,知道C1、C2编译器针对的场景,其中C2针对server模式,优化更激进。在新技术方面可以了解一下java10提供的由java实现的graal编译器。6>编译优化部分:前端编译器javac的编译过程、AST抽象语法树、编译期优化和运行期优化。编译优化的常用技术,包括公共子表达式的消除、方法内联、逃逸分析、栈上分配、同步消除等。明白了这些才能写出对编译器友好的代码。jvm的内容相对来说比较集中,但是对知识深度的掌握要求较高,建议面试前重点加强。一、jvm内存相关考点1.详解-jvm内存模型 jvm内存模型主要指运行时的数据区,包括5个部分。栈也叫方法栈,是线程私有的,线程在执行每个方法时都会同时创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息。调用方法时执行入栈,方法返回时执行出栈。本地方法栈与栈类似,也是用来保存线程执行方法时的信息,不同的是,执行java方法使用栈,而执行native方法使用本地方法栈。程序计数器保存着当前线程所执行的字节码位置,每个线程工作时都有一个独立的计数器。程序计数器为执行java方法服务,执行native方法时,程序计数器为空。栈、本地方法栈、程序计数器这三个部分都是线程独占的。堆是jvm管理的内存中最大的一块,堆被所有线程共享,目的是为了存放对象实例,几乎所有的对象实例都在这里分配。当堆内存没有可用的空间时,会抛出OOM异常。根据对象存活的周期不同,jvm把堆内存进行分代管理,由垃圾回收器来进行对象的回收管理。方法区也是各个线程共享的内存区域,又叫非堆区。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,jdk1.7中的永久代和1.8中的metaspace都是方法区的一种实现。面试回答此知识点相关问题时,要答出两个要点:一个是各部分的功能,另一个是哪些线程共享,哪些独占。2.详解-jmm内存可见性 jmm是java内存模型,与刚才讲到的jvm内存模型是两回事,jmm的主要目标是定义程序中变量的访问规则,如图所示,所有的共享变量都存储在主内存中共享。每个线程有自己的工作内存,工作内存中保存的是主内存中变量的副本,线程对变量的读写等操作必须在自己 的工作内存中进行,而不能直接读写主内存中的变量。在多线程进行数据交互时,例如线程a给一个共享变量赋值后,由线程b来读取这个值,a修改完变量是修改在自己的工作区内存中,b是不可见的,只有从a的工作区写回到主内存,b再从主内存读取到自己的工作区才能进行进一步的操作。由于指令重排序的存在,这个写-读的顺序有可能被打乱。因此jmm需要提供原子性、可见性、有序性的保证。3、详解-jmm保证 主要介绍下jmm如何保证原子性、可见性,有序性。jmm保证对除long和double外的基础数据类型的读写操作是原子性的。另外关键字Synchronized也可以提供原子性保证。Synchronized的原子性是通过java的两个高级的字节码指令monitorenter和monitorexit来保证的。jmm可见性的保证,一个是通过Synchronized,另外一个就是volatile。volatile强制变量的赋值会同步刷新回主内存,强制变量的读取会从主内存重新加载,保证不同的线程总是能够看到该变量的最新值。jmm对有序性的保证,主要通过volatile和一系列happens-before原则。volatile的另一个作用就是阻止指令重排序,这样就可以保证变量读写的有序性。happens-before原则包括一系列规则,如程序顺序原则,即一个线程内必须保证语义串行性;锁规则,即对同一个锁的解锁一定发生在再次加锁之前;此外还包括happens-before原则的传递性、线程启动、中断、终止规则等。二、类加载机制相关考点1.详解类加载机制 类的加载指的是将编译好的class类文件中的字节码读入到内存中,将其放在方法区内并创建对应的Class对象。类的加载分为加载、链接、初始化,其中链接又包括验证、准备、解析三步。看到图中上半部分深绿色,我们逐个分析:加载是文件到内存的过程。通过类的完全限定名查找此类字节码文件,并利用字节码文件创建一个Class对象验证是对类文件内容验证。目的在于确保Class文件符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种:文件格式验证,元数据验证,字节码验证,符号引用验证。准备阶段是进行内存分配。为类变量也就是类中由static修饰的变量分配内存,并且设置初始值,这里要注意,初始值是0或者null,而不是代码中设置的具体值,代码中设置的值是在初始化阶段完成的。另外这里也不包含用final修饰的静态变量,因为final在编译的时候就会分配了。解析主要是解析字段、接口、方法。主要是将常量池中的符号引用替换为直接引用的过程。直接引用就是直接指向目标的指针、相对偏移量等。最后是初始化:主要完成静态块执行与静态变量的赋值。这是类加载最后阶段,若被加载类的父类没有初始化,则先对父类进行初始化。只有对类主动使用时,才会进行初始化,初始化的触发条件包括创建类的实例的时候、访问类的静态方法或者静态变量的时候、Class.forName()反射类的时候、或者某个子类被初始化的时候。类的生命周期,就是从类的加载到类实例的创建与使用,再到类对象不再被使用时可以被GC卸载回收。这里要注意一点,由java虚拟机自带的三种类加载器加载的类在虚拟机的整个生命周期中是不会被卸载的,只有用户自定义的类加载器所加载的类才可以被卸载。2.详解类加载器 java自带的三种类加载器分别是:bootstrap启动类加载器、扩展类加载器和应用加载器也叫系统加载器。图右边的桔黄色文字表示各类加载器对应的加载目录。启动类加载器加载java home中lib目录下的类,扩展加载器负责加载ext目录下的类,应用加载器加载classpath指定目录下的类。除此之外,可以自定义类加载器。java的类加载使用双亲委派模式,即一个类加载器在加载类时,先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类加载器,就继续向上委托,直到顶层的启动类加载器,如图中蓝色向上的箭头。如果父类加载器能够完成类加载,就成功返回,如果父类加载器无法完成加载,那么子加载器才会尝试自己去加载。这种双亲委派模式的好处,一个可以避免类的重复加载,另外也避免了java的核心API被篡改。三、其他知识梳理1.详解分代回收 前面提到过,java的堆内存被分代管理,分代管理主要是为了方便垃圾回收,这样做基于2个事实,第一、大部分对象很快就不再使用,第二,还有一部分不会立即无用,但也不会持续很长时间。虚拟机中划分为年轻代、老年代、和永久代。1>年轻代:主要用来存放新创建的对象,年轻代分为eden区和两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象会在两个Survivor区交替保存,达到一定次数的对象会晋升到老年代。2>老年代:用来存放从年轻代晋升而来的,存活时间较长的对象。3>永久代:主要保存类信息等内容,这里的永久代是指对象划分方式,不是专指1.7的permGen,或者1.8之后的metaspace。根据年轻代与老年代的特点,jvm提供了不同的垃圾回收算法。垃圾回收算法按类型可以分为引用计数法、复制法和标记清除法。其中引用计数法是通过对象被引用的次数来确定对象是否被使用,缺点是无法解决循环引用的问题。复制算法需要from和to两块相同大小的内存空间,对象分配时只在from块中进行,回收时把存活对象复制到to块中,并清空from块,然后交换两块的分工,即把from块作为to块,把to块作为from块。缺点是内存使用率较低。标记清除算法分为标记对象和清除不在使用的对象两个阶段,标记清除算法的缺点是会产生内存碎片。jvm中提供的年轻代回收算法Serial、ParNew、Parallel Scavenge都是复制算法,而CMS、G1、zgc都属于标记清除算法。本篇文章,对这几个算法就不展开了,具体可见《32个Java面试必考点》总结:面试考察点及加分项1.jvm相关的面试考察点首先,需要jvm的内存模型和java的内存模型;其次,要了解的类的加载过程,了解双亲委派机制;第三,要理解内存的可见性与java内存模型对原子性、可见性、有序性的保证机制;第四,要了解常用的gc算法的特点、执行过程,和适用场景,例如g1适合对最大延迟有要求的场合,zgc适用于64为系统的大内存服务中;第五,要了解常用的jvm参数,明白对不同参数的调整会有怎样的影响,适用什么样的场景。例如垃圾回收的并发数、偏向锁设置等2.相关加分项 如果想要面试官对你留下更好的印象的话,注意这些加分项:首先,如果在编译器优化方面有深入的了解的话,会让面试官觉得你对技术的深度比较有追求。例如知道在编程时如何合理利用栈上分配降低gc压力、如何编写适合内联优化等代码等。其次,如果你能有线上实际问题的排查经验或思路那就更好了,面试官都喜欢动手能力强的同学。例如解决过线上经常full gc问题,排查过内存泄露问题等。第三,如果能有针对特定场景的jvm优化实践或者优化思路,也会有意想不到的效果。例如针对高并发低延迟的场景,如何调整gc参数尽量降低gc停顿时间,针对队列处理机如何尽可能提高吞吐率等;第四,如果对最新的jvm技术趋势有所了解,也会给面试官留下比较深刻的印象。例如了解zgc高效的实现原理,了解Graalvm的特点等。总之,掌握以上具体的JVM考点,才能在面试时应答自如。希望读完此篇文章的你,都能在金三银四的招聘季做好准备,拿到心仪的Offer。以上内容摘取自《32个Java面试必考点》第03讲:深入浅出JVM,点此查看更多
Java类加载器详解
Java虚拟机中的类加载有三大步骤:,链接,初始化.其中加载是指查找字节流(也就是由Java编译器生成的class文件)并据此创建类的过程,这中间我们需要借助类加载器来查找字节流.Java虚拟机默认类加载器Java虚拟机提供了3种类加载器,启动(Bootstrap)类加载器、扩展(Extension)类加载器、应用(Application)类加载器.除了启动类加载器外,其他的类加载器都是java.lang.ClassLoader的子类.启动类加载器由C++语言实现,没有对应的Java对象,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中.扩展类加载器是指sun.misc.Launcher$ExtClassLoader类,由Java语言实现,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,他的父类加载器是null.应用类加载器是指sun.misc.Launcher$AppClassLoader类,他负责加载应用程序路径下的类,这里路径指java -classpath或-D java.class.path 指定的路径,他的父类加载器是扩展类加载器.注意这里面的父子类加载器并不是继承的关系,只是ClassLoader类中的parent属性.我们来看Launcher类中创建扩展类加载器的代码:public ExtClassLoader(File[] var1) throws IOException { super(getExtURLs(var1), (ClassLoader)null, Launcher.factory); SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this); }这里设置了其父加载器为null.双亲委派机制Java虚拟机在加载类时默认采用的是双亲委派机制,即当一个类加载器接收到加载请求时,会将请求转发到父类加载器,如果父类加载器在路径下没有找到该类,才会交给子类加载器去加载.我们来看ClassLoader中laodClass方法:protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先判断类是否已加载过,加载过就直接返回 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { //有父类加载器,调用父加载器的loadClass c = parent.loadClass(name, false); } else { //调用Bootstrap Classloader c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { long t1 = System.nanoTime(); //到自己指定类加载路径下查找是否有class字节码 c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }通过这种层级我们可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子类加载器再加载一次。其次也考虑到安全因素,比如我们自己写一个java.lang.String的类,通过双亲委派机制传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载我们新写的java.lang.String,而直接返回已加载过的String.class,这样保证生成的对象是同一种类型.自定义类加载器除了jvm自身提供的类加载器,我们还可以自定义类加载器,我们先写一个Person类public class Person { private int age; private String name; //省略getter/setter方法}我们先看他是由哪个类加载器加载的.public class TestJava { public static void main(String[] args) throws Exception { Person person = new Person(); System.out.println(“person是由” + person.getClass().getClassLoader() + “加载的”); }}运行结果如下:我们把Person.class放置在其他目录下再运行会发生什么,在上面的loadClass方法中其实已经有了答案,会抛出ClassNotFoundException,因为在指定路径下查找不到字节码.我们现在写一个自定义的类加载器,让他能够去加载person类,很简单,我们只需要继承ClassLoader并重写findClass方法,这里面写查找字节码的逻辑.public class PersonCustomClassLoader extends ClassLoader { private String classPath; public PersonCustomClassLoader(String classPath) { this.classPath = classPath; } private byte[] loadByte(String name) throws Exception { name = name.replaceAll("\.", “/”); FileInputStream fis = new FileInputStream(classPath + “/” + name + “.class”); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadByte(name); return defineClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } }}我们来测试一下:public class TestJava { public static void main(String[] args) throws Exception { PersonCustomClassLoader classLoader = new PersonCustomClassLoader("/home/shenxinjian"); Class<?> pClass = classLoader.loadClass(“me.shenxinjian.algorithm.Person”); System.out.println(“person是由” + pClass.getClassLoader() + “类加载器加载的”); }}测试结果如下:编写自定义类加载器的意义当class文件不在classPath路径下,如上面那种情况,默认系统类加载器无法找到该class文件,在这种情况下我们需要实现一个自定义的classLoader来加载特定路径下的class文件来生成class对象。当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中,这种情况下也需要编写自定义的ClassLoader并实现相应的逻辑 ...
JVM(六)为什么新生代有两个Survivor分区?
本文会使用排除法的手段,来讲解新生代的区域划分,从而让读者能够更清晰的理解分代回收器的原理,在开始之前我们先来整体认识一下分代收集器。分代收集器会把内存空间分为:老生代和新生代两个区域,而新生代又会分为:Eden 区和两个 Survivor区(From Survivor、To Survivor),来看内存空间分布图,如下:(图片来自 fancydeepin)可以看出 Eden 和 Survivor 分区的默认比例是 8:1:1,这个值可以通过:–XX:SurvivorRatio 设定,默认值: –XX:SurvivorRatio=8。顺便说一下,新生代和老生代默认情况下的内存占比是 1:2,该值可以通过:-XX:NewRatio 来设定。为什么 Survivor 分区不能是 0 个?如果 Survivor 是 0 的话,也就是说新生代只有一个 Eden 分区,每次垃圾回收之后,存活的对象都会进入老生代,这样老生代的内存空间很快就被占满了,从而触发最耗时的 Full GC ,显然这样的收集器的效率是我们完全不能接受的。为什么 Survivor 分区不能是 1 个?如果 Survivor 分区是 1 个的话,假设我们把两个区域分为 1:1,那么任何时候都有一半的内存空间是闲置的,显然空间利用率太低不是最佳的方案。但如果设置内存空间的比例是 8:2 ,只是看起来似乎“很好”,假设新生代的内存为 100 MB( Survivor 大小为 20 MB ),现在有 70 MB 对象进行垃圾回收之后,剩余活跃的对象为 15 MB 进入 Survivor 区,这个时候新生代可用的内存空间只剩了 5 MB,这样很快又要进行垃圾回收操作,显然这种垃圾回收器最大的问题就在于,需要频繁进行垃圾回收。为什么 Survivor 分区是 2 个?如果 Survivor 分区有 2 个分区,我们就可以把 Eden、From Survivor、To Survivor 分区内存比例设置为 8:1:1 ,那么任何时候新生代内存的利用率都 90% ,这样空间利用率基本是符合预期的。再者就是虚拟机的大部分对象都符合“朝生夕死”的特性,所以每次新对象的产生都在空间占比比较大的 Eden 区,垃圾回收之后再把存活的对象方法存入 Survivor 区,如果是 Survivor 区存活的对象,那么“年龄”就 +1 ,当年龄增长到 15 (可通过 -XX:+MaxTenuringThreshold 设定)对象就升级到老生代。总结根据上面的分析可以得知,当新生代的 Survivor 分区为 2 个的时候,不论是空间利用率还是程序运行的效率都是最优的,所以这也是为什么 Survivor 分区是 2 个的原因了。 ...
JVM运行时数据区域
参考资料Java Virtual Machine Specification | 2.5. Run-Time Data AreasJava Virtual Machine Specification | 5.3. Creation and Loading极客时间 | 深入拆解 Java 虚拟机 | 01 | Java代码是怎么运行的?
关于a+b共创建了几个对象的问题
首先,我们先看下以下代码的输出情况String s = “a” + “b”;System.out.println(s == “ab”); // true将这段代码反编译后得到如下结果 public static void main(java.lang.String[]); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // String ab 2: astore_1 3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 6: aload_1 7: ldc #2 // String ab 9: if_acmpne 16 12: iconst_1 13: goto 17 16: iconst_0 17: invokevirtual #4 // Method java/io/PrintStream.println:(Z)V 20: return重点是这句0: ldc #2 // String ab,可以发现 从一开始"a"+“b"就是一个整体,这是因为jvm在编译时就去掉其中的加号,直接将其编译成一个相连的结果存入了常量池,而不是等到运行时再去进行相加,所以运行s==“ab"结果为true;结论:String s = “a” + “b”;只创建了一个对象,就是ab,存于常量池中。 ...
JVM执行方法调用(一)- 重载与重写
原文回顾Java语言中的重载与重写,并且看看JVM是怎么处理它们的。重载Overload定义:在同一个类中有多个方法,它们的名字相同,但是参数类型不同。或者,父子类中,子类有一个方法与父类非私有方法名字相同,但是参数类型不同。那么子类的这个方法对父类方法构成重载。JVM是怎么处理重载的?其实是编译阶段编译器就已经决定好调用哪一个重载方法。看下面代码:class Overload { void invoke(Object obj, Object… args) { } void invoke(String s, Object obj, Object… args) { } void test1() { // 调用第二个 invoke 方法 invoke(null, 1); } void test2() { // 调用第二个 invoke 方法 invoke(null, 1, 2); } void test3() { // 只有手动绕开可变长参数的语法糖,才能调用第一个invoke方法 invoke(null, new Object[]{1}); }}上面的注释告诉了我们结果,那么怎么才能证明上面的注释呢?我们利用javap观察字节码可以知道。$ javac Overload.java$ javap -c Overload.javaCompiled from “Overload.java"class Overload { … void invoke(java.lang.Object, java.lang.Object…); Code: 0: return void invoke(java.lang.String, java.lang.Object, java.lang.Object…); Code: 0: return void test1(); Code: … 10: invokevirtual #4 // Method invoke:(Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)V 13: return void test2(); Code: … 17: invokevirtual #4 // Method invoke:(Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)V 20: return void test3(); Code: … 13: invokevirtual #5 // Method invoke:(Ljava/lang/Object;[Ljava/lang/Object;)V 16: return}这里面有很多JVM指令,你暂且不用关心,我们看test1、test2、test3方法调用的是哪个方法: void test1(); Code: … 10: invokevirtual #4 // Method invoke:(Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)V 13: returninvoke是方法名,(Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)V则是方法描述符。这里翻译过来就是void invoke(String, Object, Object[]),Java的可变长参数实际上就是数组,所以等同于void invoke(String, Object, Object…)。同理,test2调用的是void invoke(String, Object, Object…),test3调用的是void invoke(Object, Object…)。关于方法描述符的详参JVM Spec - 4.3.2. Field Descriptors和JVM Spec - 4.3.3. Method Descriptors。所以重载方法的选择是在编译过程中就已经决定的,下面是编译器的匹配步骤:不允许自动拆装箱,不允许可变长参数,尝试匹配如果没有匹配到,则允许自动拆装箱,不允许可变长参数,尝试匹配如果没有匹配到,则允许自动拆装箱,允许可变长参数,尝试匹配注意:编译器是根据实参类型来匹配,实参类型和实际类型不是一个概念如果在一个步骤里匹配到了多个方法,则根据形参类型来找最贴切的。在上面的例子中第一个invoke的参数是Object, Object…,第二个invoke的参数是String, Object, Object…,两个方法的第一个参数String是Object的子类,因此更为贴切,所以invoke(null, 1, 2)会匹配到第二个invoke方法上。重写OverrideJava语言中的定义:子类方法有一个方法与父类方法的名字相同且参数类型相同。父类方法的返回值可以替换掉子类方法的返回值。也就是说父类方法的返回值类型:要么和子类方法返回值类型一样。要么是子类方法返回值类型的父类。两者都是非私有、非静态方法。(更多详细信息可参考Java Language Spec - 8.4.8. Inheritance, Overriding, and Hiding,这里除了有更精确详细的重写的定义,同时包含了范型方法的重写定义。)但是JVM中对于重写的定义则有点不同:子类方法的名字与方法描述符与父类方法相同。两者都是非私有、非静态方法。(更多详细信息可参考JVM Spec - 5.4.5. Overriding)注意上面提到的方法描述符,前面讲过方法描述符包含了参数类型及返回值,JVM要求这两个必须完全相同才可以,但是Java语言说的是参数类型相同但是返回值类型可以不同。Java编译器通过创建Bridge Method来解决这个问题,看下面代码:class A { Object f() { return null; }}class C extends A { Integer f() { return null; }}然后用javap查看编译结果:$ javac Override.java$ javap -v C.classclass C extends A…{ java.lang.Integer f(); descriptor: ()Ljava/lang/Integer; flags: Code: stack=1, locals=1, args_size=1 0: aconst_null 1: areturn … java.lang.Object f(); descriptor: ()Ljava/lang/Object; flags: ACC_BRIDGE, ACC_SYNTHETIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokevirtual #2 // Method f:()Ljava/lang/Integer; 4: areturn LineNumberTable: line 7: 0}可以看到编译器替我们创建了一个Object f()的Bridge Method,它调用的是Integer f(),这样就构成了JVM所定义的重写。思维导图参考文档极客时间 - 深入拆解 Java 虚拟机 - 04 | JVM是如何执行方法调用的?(上)JVM Spec - 4.3.2. Field DescriptorsJVM Spec - 4.3.3. Method DescriptorsJava Language Spec - 8.4.8. Inheritance, Overriding, and HidingJava Language Spec - 8.4.9. OverloadingJVM Spec - 5.4.5. OverridingEffects of Type Erasure and Bridge Methods广告 ...