-
调优思路 JVM是Java Virtual Machine(Java虚拟机)的缩写,Java代码在不同平台上运行时不需要重新编译,Java语言使用JVM屏蔽了与具体平台相关的硬件指令差异,使得Java语言编译程序只需生成在JVM上运行的字节码,实现在多种平台上不加修改地运行。JVM包括即时编译(JIT)、内存管理(垃圾回收GC技术)和Runtime技术,其中GC调优是性能调优中应用最为广泛。本章调优思路主要针对GC展开说明: 首先优选尽可能高的JDK版本,高版本有更新的特性和优化,对Java程序性能有好处; 其次根据实际业务场景和硬件资源给JVM选择合理的堆空间; 最后要选择合理的GC算法。 同时,Java自带很多工具,对程序运行的检测和性能分析都很有帮助,利用这些工具可以辅助Java性能调优。 主要优化参数 优化项 优化项简介 默认值 生效范围 鲲鹏916 鲲鹏920 -Xmx 设置JVM最大可用堆内存大小。 根据系统资源计算默认值 Java进程重启生效 YY -Xms 设置初始堆大小,一般和Xmx保持一致。 根据系统资源计算默认值 Java进程重启生效 YY -Xmn 设置年轻代堆大小。 根据系统资源计算默认值 Java进程重启生效 YY -Xss 设置每个线程的堆大小。 JDK 1.5以后每个线程堆栈大小默认为1MB,1.5以前为256KB。 Java进程重启生效 YY 二、 介绍 jstat是JDK自带的一个JVM统计监控工具,利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括堆大小和应用程序GC状况的监控。 安装方式 完整安装JDK后自带jstat工具,无需单独安装,一般位于Java的bin目录下。 使用方式 jstat是个非常强大的命令,可选项多,可以详细查看堆内各个部分的使用量,以及加载类的数量。使用时,需加上查看进程的进程pid和所选参数。 命令格式:jstat [参数] 一级参数主要功能如下: 参数 说明 jstat -class 显示加载class的数量,及所占空间等信息。 jstat -compiler 显示虚拟机实时编译的数量等信息。 jstat -gc 显示GC的信息,查看GC的次数及时间。 jstat -gccapacity 显示虚拟机内存中三代对象的使用和占用大小。 jstat -gcutil 显示GC的统计信息。 jstat -gcnew 显示年轻代对象的信息。 jstat -gcnewcapacity 显示年轻代对象信息及其内存占用情况。 jstat -gcold 显示老年代对象的信息。 jstat -gcpermcapacity 显示永久代对象的信息及其占用量。 jstat -printcompilation 显示当前虚拟机的执行信息。 GC优化中,利用jstat -gc 较多,其输出参数及含义如下: 参数 说明 S0C 年轻代中第一个survivor区的容量(字节) S1C 年轻代中第二个survivor区的容量(字节) S0U 年轻代中第一个survivor区目前已使用空间(字节) S1U 年轻代中第二个survivor区目前已使用空间(字节) EC 年轻代中Eden区的容量 (字节) EU 年轻代中Eden区目前已使用空间(字节) OC 老年代的容量(字节) OU 老年代目前已使用空间(字节) PC Perm(永久代)的容量(字节) PU Perm(永久代)目前已使用空间(字节) YGC 从应用程序启动到采样时年轻代中gc次数 YGCT 从应用程序启动到采样时年轻代中gc所用时间(s) FGC 从应用程序启动到采样时老年代(全gc)gc次数 FGCT 从应用程序启动到采样时老年代(全gc)gc所用时间(s) GCT 从应用程序启动到采样时gc用的总时间(s) 输出格式: jstat -gc 2342 sh-4.4# jstat -gc 159 1000 S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 6144.0 13824.0 6128.8 0.0 702464.0 590176.1 497664.0 468611.6 139032.0 131745.2 15664.0 14404.5 151478 1673.004 1057 34 三、 介绍 jmap是JDK自带的堆信息查看和调试工具,可以将堆信息导出到文件分析,可以查看堆空间分配等信息,是Java性能调优常用工具之一。 安装方式 完整安装JDK后自带jmap工具,无需单独安装,一般位于Java的bin目录下。 使用方式 命令格式:jmap [参数] 常用参数如下: 参数 说明 举例 -dump:[live,]format=b,file=/you/path/filename.hprof 输出JVM的堆对象内容到指定文件。推荐以.hprof后缀命名文件,可以用MAT工具直接分析。 live选项是可选的,如果选live,那么只输出活的对象到文件。 jmap -dump:format=b,file=/opt/log/java12123.hprof 12123 -finalizerinfo 输出正等候回收的对象的信息。 jmap -finalizerinfo 12123 -heap 输出当前Java进程堆的概要统计信息,如GC算法,heap的配置空间等。 jmap -heap 12123 -histo[:live] 输出当前class的实例数目、内存占用、类全名信息。 jmap -histo:live 12123 | head -n 20 -permstat 打印classload和jvm heap中perm代的信息,包含每个classloader的名字、活泼性、地址、父classloader和加载的class数量等信息。 jmap -permstat 12123 jmap -heap 举例说明: using parallel threads in the new generation. ##新生代采用的是并行线程处理方式 using thread-local object allocation. Concurrent Mark-Sweep GC ##同步并行垃圾回收 Heap Configuration: ##堆配置情况 MinHeapFreeRatio = 40 ##最小堆使用比例 MaxHeapFreeRatio = 70 ##最大堆可用比例 MaxHeapSize = 2147483648 (2048.0MB) ##最大堆空间大小 NewSize = 268435456 (256.0MB) ##新生代分配大小 MaxNewSize = 268435456 (256.0MB) ##最大新生代可分配大小 OldSize = 5439488 (5.1875MB) ##老年代大小 NewRatio = 2 ##新生代比例 SurvivorRatio = 8 ##新生代与survivor的比例 PermSize = 134217728 (128.0MB) ##perm区大小 MaxPermSize = 134217728 (128.0MB) ##最大可分配perm区大小 Heap Usage: ##堆使用情况 New Generation (Eden + 1 Survivor Space): ##新生代(Eden区 + survivor空间) capacity = 241631232 (230.4375MB) ##Eden区容量 used = 77776272 (74.17323303222656MB) ##已经使用大小 free = 163854960 (156.26426696777344MB) ##剩余容量 32.188004570534986% used ##使用比例 Eden Space: ##Eden区 capacity = 214827008 (204.875MB) ##Eden区容量 used = 74442288 (70.99369812011719MB) ##Eden区使用 free = 140384720 (133.8813018798828MB) ##Eden区当前剩余容量 34.65220164496263% used ##Eden区使用情况 From Space: ##survivor1区 capacity = 26804224 (25.5625MB) ##survivor1区容量 used = 3333984 (3.179534912109375MB) ##survivor1区已使用情况 free = 23470240 (22.382965087890625MB) ##survivor1区剩余容量 12.43827838477995% used ##survivor1区使用比例 To Space: ##survivor2 区 capacity = 26804224 (25.5625MB) ##survivor2区容量 used = 0 (0.0MB) ##survivor2区已使用情况 free = 26804224 (25.5625MB) ##survivor2区剩余容量 0.0% used ## survivor2区使用比例 concurrent mark-sweep generation: ##老年代使用情况 capacity = 1879048192 (1792.0MB) ##老年代容量 used = 30847928 (29.41887664794922MB) ##老年代已使用容量 free = 1848200264 (1762.5811233520508MB) ##老年代剩余容量 1.6416783843721663% used ##老年代使用比例 Perm Generation: ##perm区使用情况 capacity = 134217728 (128.0MB) ##perm区容量 used = 47303016 (45.111671447753906MB) ##perm区已使用容量 free = 86914712 (82.8883285522461MB) ##perm区剩余容量 35.24349331855774% used ##perm区使用比例
-
常用的JVM配置参数:-Xms2g:初始化堆大小为 2g;-Xmx2g:堆最大内存为 2g;-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;-XX:+UseG1GC:设置使用G1垃圾回收器-XX:+PrintGC:开启打印 gc 信息;-XX:+PrintGCDetails:打印 gc 详细信息。-XX:+PrintHeapAtGC: 表示可以看到每次GC前后堆内存布局-XX:UseTLAB:设置使用TLAB-XX:+PrintTLAB: 表示可以看到TLAB的使用情况。 TLAB的全称是Thread Local Allocation Buffer 即线程本地分配缓 存区,这是一个线程专用的内存分配区域。-verbose:gc(-verbose:class可以输出类加载的信息)-Xss:表示可以设置虚拟机栈的大小为128k-Xoss:表示设置本地方法栈的大小为128k。不过HotSpot并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说这个参数是无效的-XX:+TraceClassLoading: 表示查看类的加载信息-XX:+TraceClassUnLoading: 表示查看类的卸载信息-XX:+HeapDumpOnOutOfMemoryError: 表示可以让虚拟机在出现内存溢出异常时Dump出当前的堆内存转储快照-XX:HeapDumpPath:表示可以让虚拟机在出现内存溢出异常时Dump出当前的堆内存转储快照存储地址XX:OnOutOfMemoryError:当系统发生OOM错误时,虚拟机在错误发生时运行一段第三方脚本, 比如, 当OOM发生时,重置系统 -=c:\reset.bat-XX:-UseGCOverheadLimit:取消outofmemory警告-XX:PretenureSizeThreshold: 表示对象大于3145728(3M)时直接进入老年代分配,这里只能以字节作为单位-XX:MaxTenuringThreshold: 表示对象年龄大于1,自动进入老年代,如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代被回收的概率。-XX:CompileThreshold: 表示一个方法被调用1000次之后,会被认为是热点代码,并触发即时编译-XX:+UseSpining:开启自旋锁-XX:PreBlockSpin:更改自旋锁的自旋次数,使用这个参数必须先开启自旋锁-XX:MaxGCPauseMillis:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低响应时间或者收集频率等,此值建议使用并行收集器时,一直打开开启逃逸分析(JDK8中,逃逸分析默认开启。)-XX:+DoEscapeAnalysis关闭逃逸分析-XX:-DoEscapeAnalysis逃逸分析结果展示-XX:+PrintEscapeAnalysis(JDK8中,同步消除默认开启。)-XX:+EliminateLocks
-
Class字节码编译后被Java虚拟机所执行的代码使用了一种平台中立(不依赖于特定硬件及操作系统的)的二进制格式来表示,并且经常(但并非绝对)以文件的形式存储,因此这种格式被称为Class文件格式。Class文件格式中精确地定义了类与接口的表示形式,包括在平台相关的目标文件格式中一些细节上的惯例,正如概念所说,Java为了能够实现平台无关性,制定了一套自己的二进制格式,并经常以文件的方式存储,称为Class文件。这样在不同平台上,只要都安装了Java虚拟机,具备Java运行环境[JRE],那么都可以运行相同的Class文件。上图描述了Java程序运行的一个全过程,也可以看出Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序可以运行在这个平台上。由Java源文件编译生成字节码文件,这个过程非常复杂,学过《编译原理》的朋友都知道必须经过词法分析、语法分析、语义分析、中间代码生成、代码优化等;同样的,Java源文件到字节码的生成也想要经历这些步骤。Javac编译器的最后任务就是调用con.sun.tools.javac.jvm.Gen类将这课语法树编译为Java字节码文件。其实,所谓的编译字节码,无非就是将符合Java语法规范的Java代码转化为符合JVM规范的字节码文件。JVM的架构模型是基于栈的,大部分都需要通过栈来完成。字节码结构比较特殊,其内部不包含任何的分隔符,无法人工区分段落(字节码文件本身就是给机器读的),所以无论是字节顺序、数量都是有严格规定的,所有16位、32位、64位长度的数据都将构造成2个、4个、8个-----8位字节单位来表示,多字节数据项总是按照Big-endian顺序(高位字节在地址的最低位,地位字节在地址的最高位)来进行存储。参考《Java虚拟机规范 Java SE7版》的描述,每一个字节码其实都对应着全局唯一的一个类或者接口的定义信息。字节码文件才用的是一种类似于C语言结构体的伪结构来描述字节码文件格式。字节码文件中对应的“基本类型”u1,u2,u4,u8分别表示无符号1、2、4、8个字节。Class文件----总体格式值得一提的是,一个有效的class字节码文件的前4个字节为0xCAFEBABE,都是固定的,被称为“魔术”,即magic。它就是JVM用于校验所读取的目标文件是否是一个有效且合法的字节码文件。由此可见,JVM并不是通过判断文件后缀名的方式来校验,以防止人为手动修改。
-
JVM栈空间每个Java虚拟机线程都有自己的Java虚拟机栈。Java虚拟机栈用来存放栈帧,而栈帧主要包括了:局部变量表、操作数栈、动态链接。Java虚拟机栈允许被实现为固定大小或者可动态扩展的内存大小。Java虚拟机使用局部变量表来完成方法调用时的参数传递。局部变量表的长度在编译期已经决定了并存储于类和接口的二进制表示中,一个局部变量可以保存一个类型为boolean、byte、char、short、float、reference和returnAddress的数据,两个局部变量可以保存一个类型为long和double的数据。 Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果。 每个栈帧中都包含一个指向运行时常量区的引用支持当前方法的动态链接。在Class文件中,方法调用和访问成员变量都是通过符号引用来表示的,动态链接的作用就是将符号引用转化为实际方法的直接引用或者访问变量的运行是内存位置的正确偏移量。总的来说,Java虚拟机栈是用来存放局部变量和过程结果的地方。Java虚拟机栈可能发生如下异常情况: 如果Java虚拟机栈被实现为固定大小内存,线程请求分配的栈容量超过Java虚拟机栈允许的最大容量时,Java虚拟机将会抛出一个StackOverflowError异常。如果Java虚拟机栈被实现为动态扩展内存大小,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。1.符号引用(Symbolic References): 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。2.直接引用:直接引用可以是(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)(3)一个能间接定位到目标的句柄直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
-
什么是JVMJVM是Java Virtual Machine(Java虚拟机)的缩写,它是一种用于计算设备的规范,通过在实际的计算机上仿真模拟各种计算机功能来实现。JVM是一个虚构出来的计算机,屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。JVM包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。每一种平台的解释器都是不一样的,但是实现的虚拟机是相同的,Java可以跨平台也就是这个原因。当一个程序从开始运行,虚拟机就开始实例化多个程序启动会存在多个虚拟机实例,程序退出或关闭虚拟机实例消亡,虚拟机之间数据是不能共享的。JVM允许一个应用并发执行多个线程(程序执行过程中的一个线程实体)。当线程结束时,会释放线程的所有资源。JVM内存区域主要分为线程私有区域,线程共享区域,以及直接内存。JVM作为Java程序的运行环境,具有跨平台性、自动内存管理、安全性等优点,同时也存在一些缺点。在选择使用JVM时,开发人员应该充分考虑其需求和限制,并根据实际情况进行权衡。JVM 的运行原理JVM(Java Virtual Machine)的运行原理主要涉及以下几个方面:指令集和解释器:JVM包含一套字节码指令集,用于执行Java程序的各种操作。Java源文件经过编译器编译成字节码文件,这些字节码文件被JVM中的解释器逐条解释并转换为底层操作系统能够执行的机器码。解释器还负责将每一条指令翻译成不同平台上的机器码,从而实现Java的跨平台性。虚拟机栈和堆:JVM包括一个虚拟机栈,每个线程都有一个对应的栈。每个栈帧都包含局部变量、操作数栈、动态链接和方法出口信息。JVM堆是用于存储对象的内存区域,由所有线程共享。堆内存用于动态分配内存,垃圾回收器会自动回收不再使用的对象。类加载器和存储器:JVM通过类加载器将字节码文件加载到内存中,并创建对应的类。类加载器负责解析类中的符号引用,并将其转换为直接引用。JVM还包含一个方法区,用于存储已被加载的类信息、常量、静态变量等数据。垃圾回收器:JVM的垃圾回收器自动回收不再使用的对象,释放堆内存空间。垃圾回收器通过标记-清除、复制、标记-整理和分代收集等算法来回收内存,以避免内存泄漏和减少不必要的内存占用。链接和安全机制:JVM在运行过程中会对二进制字节码进行校验、初始化装载类中的静态变量以及解析类中调用的接口、类。JVM还提供了一系列安全机制,如沙箱运行环境、访问控制和安全异常处理等,以确保Java应用程序的安全性。线程管理和调度:JVM内部有两种线程:守护线程和非守护线程。主线程属于非守护线程,守护线程通常由JVM自己使用。JVM通过线程调度器对线程进行管理和调度,确保程序的正确执行。JVM通过指令集、解释器、虚拟机栈和堆、类加载器、垃圾回收器和安全机制等组件的协同工作,实现了Java程序的跨平台运行和内存管理。虽然JVM存在一些缺点,但其优点使得它在许多场景下成为了一个可靠的选择。JVM的优点JVM(Java Virtual Machine)为Java应用程序提供了一个虚拟机环境,使得Java应用程序可以在各种不同的硬件和操作系统上运行。虽然JVM增加了一层抽象,但这并不意味着它浪费性能。实际上,JVM的这种设计带来了很多好处。跨平台性:这是JVM最广为人知的优点。由于JVM屏蔽了底层硬件和操作系统的细节,Java应用程序只需编译为字节码,就可以在任何支持JVM的平台上运行,无需担心兼容性问题。自动内存管理:JVM提供了自动内存管理机制,包括自动内存分配、垃圾收集等。这大大减少了程序员需要处理的内存管理细节,降低了出错的可能性,也提高了开发效率。安全性:JVM通过类加载器、字节码校验、沙箱运行环境等机制,为Java应用程序提供了一个安全的环境,防止恶意代码的执行和攻击。性能优化:JVM通过即时编译(JIT)和热点优化等技术,将字节码转换为本地代码,提高了运行速度。此外,JVM还可以根据应用程序的运行情况动态调整内存大小,优化性能。多线程支持:JVM内置对多线程的支持,使得Java语言能够轻松地编写并发和并行程序。工具支持:由于JVM是一个标准,许多工具和框架都围绕着JVM进行设计和优化。例如,各种性能分析工具、调试器、远程调试等。易于集成:由于JVM是一个标准,各种第三方库和框架可以轻松地与JVM集成,为Java开发者提供丰富的功能和工具。虽然JVM增加了一层抽象,但它的设计使得Java应用程序在安全性、跨平台性、性能等方面有了很好的保障。因此,这层抽象是值得的,也为Java的成功打下了坚实的基础。JVM 的缺点JVM(Java Virtual Machine)作为Java程序的运行环境,具有许多优点,如跨平台性、自动内存管理、安全性等。然而,它也存在一些缺点,下面列举了一些JVM的缺点:内存消耗大:JVM启动和运行需要占用较大的内存,而且由于垃圾回收机制的存在,JVM的内存占用也较高。在一些资源有限的环境中,这可能导致问题。执行速度相对较慢:虽然JIT编译器可以提高Java程序的执行速度,但与本地代码相比,Java程序的执行速度仍然较慢。这一点在对实时性要求较高的应用程序中可能会成为问题。配置复杂性:由于JVM的各种配置选项和优化参数较多,使得调优和优化JVM变得复杂。不正确的配置可能导致性能下降或其他问题。产生碎片空间:随着时间的推移,频繁的垃圾回收操作可能导致内存空间产生碎片,这会影响到程序的性能和内存管理效率。对其他语言的支持有限:尽管JVM为Java提供了强大的支持,但对于其他语言(如C、C++等)的支持可能相对有限。这些语言可能需要使用JNI(Java Native Interface)等技术才能在JVM上运行。启动延迟:由于JVM需要加载类和库,这可能导致应用程序启动时存在一定的延迟。这在某些场景下可能会成为问题,例如在需要快速启动的应用程序中。对硬件和操作系统依赖性:JVM依赖于底层硬件和操作系统提供的支持。如果硬件或操作系统出现问题,可能会导致JVM运行异常。不易调试:与本地代码相比,Java代码在JVM上的调试可能更加困难。这可能需要使用专门的调试工具和技术来进行故障排除。需要注意的是,这些缺点并不意味着JVM不适合所有情况。在许多场景下,JVM的优点使其成为了一个可靠的选择。然而,在选择使用JVM时,开发人员应该充分考虑其需求和限制,并根据实际情况进行权衡。总结现在所有用Java开发的程序都有用到JVM,可以说在我们生活中无处不在了。Java的应用非常广泛,它是继C/C++之后的又一壮举。学好JVM能为我们的工作生活带来极大的便利
-
XX:+UseContainerSupport:启用容器支持,JVM 将自动检测并使用容器特定的内存限制。-XX:InitialRAMPercentage=68:JVM 初始堆大小为主机可用内存的百分之68。-XX:MaxRAMPercentage=68:JVM 最大堆大小为主机可用内存的百分之68。-XX:+UseG1GC:开启 G1 垃圾回收器。-XX:+UnlockExperimentalVMOptions:解锁实验性 VM 选项,以便使用实验性功能。-XX:G1NewSizePercent=60:设置新生代大小占堆大小的比例为60%。-XX:ParallelGCThreads=11:设置并行 GC 线程数为11。-XX:ConcGCThreads=4:设置并发 GC 线程数为4。-XX:MaxGCPauseMillis=160:设置最大 GC 暂停时间为160毫秒。-XX:MetaspaceSize=120m:设置元空间初始大小为120MB。-XX:MaxMetaspaceSize=350m:设置元空间最大*小为350MB。-XX:MaxDirectMemorySize=300m:设置直接内存最大*小为300MB。-XX:+HeapDumpOnOutOfMemoryError:在内存溢出时生成堆转储文件。-Dio.netty.eventLoopThreads=6:设置 Netty EventLoop 线程数为6。-Dio.netty.tryReflectionSetAccessible=true:允许 Netty 反射调用私有方法。-Dlog4j2.formatMsgNoLookups=true:关闭 Log4j2 参数查找。-Dspring.profiles.active=sandbox:启用 Spring Boot 的沙盒配置文件。–add-exports=java.base/jdk.internal.misc=ALL-UNNAMED:导出指定的包以供未命名模块使用。-jar:指定 JAR 包的路径和名称。综上所述,这些启动参数可以优化 JVM 的内存管理、垃圾回收、线程处理等方面的性能,提高应用程序的稳定性和响应速度。
-
导言随着技术的不断发展,软件开发行业也在日新月异地进步。在过去的几十年里,Java语言和Java虚拟机(JVM)在开发企业级应用方面扮演了重要角色。然而,随着硬件和软件的进步,以及JVM本身的改进,人们开始质疑在现代时代是否仍然有必要进行JVM调优。本文将探讨这个问题,并提供一些观点供读者参考。观点1. JVM的发展和优化首先,让我们回顾一下JVM的发展历程。JVM作为一种虚拟机,负责解释和执行Java字节码,并提供内存管理、垃圾回收和线程管理等功能。随着时间的推移,JVM不断演化和改进,以提供更好的性能和稳定性。例如,Java 8引入了元空间(Metaspace)来替代永久代(PermGen),从而减少了内存泄漏的风险。此外,JVM的垃圾回收器也得到了改进,以更高效地回收无用对象。2. 现代硬件和自动优化在过去,硬件资源相对有限,而且JVM的性能也不够高效。因此,进行JVM调优是提高应用性能的重要手段。然而,现代硬件已经取得了巨大的进步。计算机的处理能力、内存容量和硬盘速度都有了显著提升。同时,JVM本身也具备了自动优化的能力。现代的JVM实现通常能够根据应用程序的运行状况和硬件环境,自动调整参数和执行优化,以提供更好的性能。3. 应用场景和性能需求无论是在过去还是在现在,JVM调优的必要性主要取决于应用场景和性能需求。某些应用可能对性能要求极高,例如金融交易系统或大规模数据处理系统。对于这些应用,进行JVM调优仍然是必要的,以确保应用能够以最佳性能运行。然而,对于一些中小型应用或仅用于内部用途的应用,JVM的默认配置和自动优化可能已经足够满足需求,无需额外的调优。4. 资源成本和开发时间进行JVM调优需要投入一定的时间和资源。优化JVM参数、选择适当的垃圾回收器、调整堆大小等等,都需要经验和实验验证。在一些情况下,进行JVM调优可能并不划算。如果应用的性能需求可以通过其他方式满足,例如通过使用更高级的硬件或优化算法,那么将资源用于JVM调优可能并不划算。5. 监控和性能分析工具现代的监控和性能分析工具使我们能够更好地了解应用程序的行为和性能瓶颈。这些工具可以帮助我们定位问题并做出有针对性的优化。在许多情况下,通过使用这些工具,我们可以快速识别并解决性能问题,而无需进行复杂的JVM调优。结论在现代时代,JVM调优是否仍然必要主要取决于应用场景、性能需求以及可用资源。对于对性能要求极高的关键应用来说,JVM调优仍然是必要的,以确保最佳性能。然而,对于一些中小型应用或对性能要求不高的应用,JVM的默认配置和自动优化通常已经足够满足需求,无需额外的调优。同时,现代的监控和性能分析工具使我们能够更好地识别和解决性能问题,从而减少了对JVM调优的需求。因此,在决定是否进行JVM调优时,我们应该基于具体情况进行评估,并考虑应用的性能需求、可用资源、开发时间和成本等因素。不同的应用有不同的需求,没有一种固定的答案适用于所有情况。通过合理的评估和权衡,我们可以选择最佳的方法来满足应用的性能需求。
-
1. JVM概述JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。 引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行 ——百度百科可以将JVM理解成一台机器,这个机器可以用来执行java程序。这样java代码就可以实现一次编写,到处运行了。在不同的机器上配置相应的JVM即可。 在机器上,JVM实际上也是一个程序。 2. JVM内存分配2.1 运行时数据区域根据《Java 虚拟机规范(Java SE 7 版)》的规定,Java虚拟机所有管理的内存将会包括以下几个运行时数据区域,如下图所示:简单说一下各个数据区域的作用: 线程私有的数据区域:线程私有区域都是与线程同生共死的,即生命周期与线程相同。 线程共享的数据区域:生命周期与JVM相同 程序计数器:每个线程都有的"小本本",线程不是连续不断工作的。再次运行时它就要看看“小本本”里的内容才知道自己该从什么地方继续做下去。 在JVM中的多线程是通过轮流切换的方式执行的,在任何时刻一个CPU(相当于多核CPU的一个核)只能运行一个线程。如果某个进程的某个线程占用CPU很长的时间,那么其他的线程会一直等下去吗?为了让用户不会有:”哇!我这个傻X计算机怎么这么卡啊!“的错觉。CPU就给了每个线程分配了一个CPU时间,当线程的CPU时间用完之后,他就会把CPU的计算资源让出来给其他线程,等到下次轮到它的时候它再执行。所以每个线程都需要一个私有的程序计数器来记录自己执行到哪儿了。它实际可以理解成一个记录程序执行的字节码的行号的指示器。 计数器只会占用内存中很小的一部分空间。Java虚拟机栈:程序员口中常说的“堆栈”大抵说的就是这个区域中的局部变量表。虚拟机栈是用来描述Java方法执行时的内存模型。这种描述是通过存储方法开始执行(入栈)到方法结束(出栈)过程中的局部变量表、操作数栈、动态链接及方法的出口等信息来是实现的。方法在执行时会创建一个栈帧,用来存储前面说的各种信息,方法完成,栈帧出栈。局部变量表存放了编译期间可知的各种基本数据类型和引用类型,以前上课的时候说的堆栈的时候,老师可能会画这样的一张图:说的就是这个局部变量表。局部变量表所需要的内存空间在编译期间就分配完成。以上可以对栈的作用做个小小的结论: 1. 只有在方法调用时,才为当前栈分配一个帧,然后将该帧压入栈。 2. 帧中存放了方法的局部变量表,当方法执行完之后,对应的帧则从栈中弹出。JVM规范中,对这个数据区域规定了两种异常状况: StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度 OutOfMemoryError:虚拟机栈动态扩展时无法申请都足够的内存空间 本地方法栈:与虚拟机栈相似,不过虚拟机栈是为JVM执行java方法,而本地方法栈则是为虚拟机使用到的Native方法服务。本地方法栈也可能会出现StackOverflowError和OutOfMemoryError。 Java堆:对大多数应用来说,这个区域是JVM所管理的最大的内存空间,也是GC重点关注的区域。该区域被线程共享,存在的目的就是为了存放对象实例,几乎所有的对象实例和数组都要在堆上分配。如果Java堆中没有空间可以用来实例化对象,而且也没法再申请新的内存时,该区域会抛出OutOfMemoryError。 方法区:线程共享区域,用于存储已经被虚拟机加载的类信息、常量、静态变量、即使编译器后的代码等数据。这个区域也有人称其为永久代。方法区无法满足内存分配需求时也会抛出OutOfMemoryError。 运行时常量池也是方法区中的一部分,class文件会包括类的版本,字段,方法,接口等描述信息,也会有个常量池,用于存放编译期生成的各种字面量和符号引用。这部分内容在类加载之后进入到方法区的运行时常量池中存放。该区域具有动态性,即常量不一定是编译期间就确定的,在运行期间也可以有新的常量产生,进入运行时常量池中。 3. 异常代码实战实战一下数据提供的代码,旨在对运行时数据区域内存分配和使用有更深的理解,当出现相关异常的时候能够快速地定位到异常区域和异常代码。 首先了解一下IDEA如何配置JVM启动时的参数。Run—>EditConfigurationsJVM相关配置参数说明可以通过在CMD中通过java -X查看 Java 堆溢出Java堆是用来存储对象实例和数组的,只要的不停的创建对象,且确保对象不会被回收,当对象的数量达到堆最大的容量限制后,就会产生内存溢出异常了。下面的JVM参数设置了Java堆内存的大小为20MB,不可扩展(将最大值-Xms和最小值-Xmx设为一样即可避免堆自动扩展)。通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照,以便我们后面的分析。 JVM参数配置-Xms20m -Xmx20m-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=E:\\heapdump代码如下:public class HeapOOM { static class OOMObject{} public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); //不停地创建对象,直到OOM while(true){ list.add(new OOMObject()); } }}程序运行结果如下: java.lang.OutOfMemoryError: Java heap spaceDumping heap to E:\\heapdump\java_pid8948.hprof ...Exception 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:261) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227) at java.util.ArrayList.add(ArrayList.java:458) at per.ling.JVMPractice.HeapOOM.main(HeapOOM.java:13)Heap dump file created [28866091 bytes in 0.206 secs]使用JDK自带的内存映像分析工具jvisualVM来分析程序dump出来的堆转储快照。装入文件后点击类,我们可以看到类名和它对应的实例个数及占用的内存空间。 打开该文件可以看到OOMObject这个类有810326个实例,占用内存13M左右。这里我们可以看到类实例相关的情况,查看概要我们还可以看到相关的线程及可能出现异常的代码块。如果是内存泄露的话,我们可能还需要观察一下相关的GC Roots。Java的内存溢出有很多中情况,刚兴趣可以搜一下,学习一波。 虚拟机栈溢出虚拟机栈可能抛出的异常: StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度OutOfMemoryError:虚拟机堆栈动态扩展时无法申请都足够的内存空间JVM参数如下: -Xss128k-XX:+HeapDumpOnStackOverflowErrow-XX:HeapDumpPath=E:\\heapdump代码如下:public class StackOOM { private static long stackLength = 0L; public static void main(String[] args) { try { stackLeak(); } catch (Throwable e){ System.out.println("The length of statck is " + stackLength); throw e; } } private static void stackLeak() { stackLength++; stackLeak(); }}控制台输出如下: The length of statck is 41351Exception in thread "main" java.lang.StackOverflowError at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21) at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21) at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21) at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21) at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21) at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21) at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21)小结:StackOverflowerError主要是针对运行时的栈而言,而OutOfMemoryError(内存溢出)针对的是整个内存区域。后者出现的原因主要是申请内存时没有更多的内存空间导致的,而导致这样的原因有很多。 详情可以参见: Java常见的几种内存溢出及解决方案转自:https://zhuanlan.zhihu.com/p/96243104
-
环境鲲鹏 920 128 核Bisheng JDK 17.0.1 / 17.0.4运行 BenchmarkSQL + ShardingSphere-JDBC网卡队列绑核 0-15,BenchmarkSQL 绑核 16-127。现象概率性出现运行 BenchmarkSQL us 较高(几乎压满 CPU 核),sy 较低。之前使用的是 GraalVM EE,本来以为是 GraalVM EE 才有的现象,后续测试发现使用 Bisheng JDK 17 也有相似现象。多次启动 BenchmarkSQL 进程,性能时好时坏。正常情况下,使用 BenchmarkSQL + ShardingSphere-JDBC 性能可以达到 200 万 tpmC 以上。最近启动进程时有较高概率发生性能异常,性能相比正常情况下降约 30%。Bisheng JDK 17 现象正常情况us 相对较低,网卡中断几乎跑满前 16 核。正常情况下 tpmC 在 200 万左右。异常现象最近测试发现,经常出现 us 较高,中断相比性能正常时更低。tpmC 在 160 万左右。性能下降至正常的 70% 左右。升级 Bisheng JDK 17.0.4 问题未解决async-profiler 采样对比正常情况与异常情况的采样,表面现象为计算开销增加,在代码路径上没有看出其他异常。 GraalVM EE 现象偶发现象:CPU us 特别高,sy 较低。退出压测进程重新运行后现象消失。6 节点测试中,该现象频繁出现GraalVM 发生该现象时对性能影响非常明显,tpmC 相比正常几乎减半。
-
这篇总结主要是基于我之前JVM系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢#更多详细内容可以查看我的专栏文章:深入理解JVM虚拟机https://blog.csdn.net/column/details/21960.htmlJVM介绍和源码首先JVM是一个虚拟机,当你安装了jre,它就包含了jvm环境。JVM有自己的内存结构,字节码执行引擎,因此class字节码才能在jvm上运行,除了Java以外,Scala,groovy等语言也可以编译成字节码而后在jvm中运行。JVM是用c开发的。JVM内存模型内存模型老生常谈了,主要就是线程共享的堆区,方法区,本地方法栈。还有线程私有的虚拟机栈和程序计数器。堆区存放所有对象,每个对象有一个地址,Java类jvm初始化时加载到方法区,而后会在堆区中生成一个Class对象,来负责这个类所有实例的实例化。栈区存放的是栈帧结构,栈帧是一段内存空间,包括参数列表,返回地址,局部变量表等,局部变量表由一堆slot组成,slot的大小固定,根据变量的数据类型决定需要用到几个slot。方法区存放类的元数据,将原来的字面量转换成引用,当然,方法区也提供常量池,常量池存放-128到127的数字类型的包装类。字符串常量池则会存放使用intern的字符串变量。JVM OOM和内存泄漏这里指的是oom和内存泄漏这类错误。oom一般分为三种,堆区内存溢出,栈区内存溢出以及方法区内存溢出。堆内存溢出主要原因是创建了太多对象,比如一个集合类死循环添加一个数,此时设置jvm参数使堆内存最大值为10m,一会就会报oom异常。栈内存溢出主要与栈空间和线程有关,因为栈是线程私有的,如果创建太多线程,内存值超过栈空间上限,也会报oom。方法区内存溢出主要是由于动态加载类的数量太多,或者是不断创建一个动态代理,用不了多久方法区内存也会溢出,会报oom,这里在1.7之前会报permgem oom,1.8则会报meta space oom,这是因为1.8中删除了堆中的永久代,转而使用元数据区。内存泄漏一般是因为对象被引用无法回收,比如一个集合中存着很多对象,可能你在外部代码把对象的引用置空了,但是由于对象还被集合给引用着,所以无法被回收,导致内存泄漏。测试也很简单,就在集合里添加对象,添加完以后把引用置空,循环操作,一会就会出现oom异常,原因是内存泄漏太多了,导致没有空间分配新的对象。常见调试工具命令行工具有jstack jstat jmap 等,jstack可以跟踪线程的调用堆栈,以便追踪错误原因。jstat可以检查jvm的内存使用情况,gc情况以及线程状态等。jmap用于把堆栈快照转储到文件系统,然后可以用其他工具去排查。visualvm是一款很不错的gui调试工具,可以远程登录主机以便访问其jvm的状态并进行监控。class文件结构class文件结构比较复杂,首先jvm定义了一个class文件的规则,并且让jvm按照这个规则去验证与读取。开头是一串魔数,然后接下来会有各种不同长度的数据,通过class的规则去读取这些数据,jvm就可以识别其内容,最后将其加载到方法区。JVM的类加载机制jvm的类加载顺序是bootstrap类加载器,extclassloader加载器,最后是appclassloader用户加载器,分别加载的是jdk/bin ,jdk/ext以及用户定义的类目录下的类(一般通过ide指定),一般核心类都由bootstrap和ext加载器来加载,appclassloader用于加载自己写的类。双亲委派模型,加载一个类时,首先获取当前类加载器,先找到最高层的类加载器bootstrap让他尝试加载,他如果加载不了再让ext加载器去加载,如果他也加载不了再让appclassloader去加载。这样的话,确保一个类型只会被加载一次,并且以高层类加载器为准,防止某些类与核心类重复,产生错误。defineclass findclass和loadclass类加载classloader中有两个方法loadclass和findclass,loadclass遵从双亲委派模型,先调用父类加载的loadclass,如果父类和自己都无法加载该类,则会去调用findclass方法,而findclass默认实现为空,如果要自定义类加载方式,则可以重写findclass方法。常见使用defineclass的情况是从网络或者文件读取字节码,然后通过defineclass将其定义成一个类,并且返回一个Class对象,说明此时类已经加载到方法区了。当然1.8以前实现方法区的是永久代,1.8以后则是元空间了。JVM虚拟机字节码执行引擎jvm通过字节码执行引擎来执行class代码,他是一个栈式执行引擎。这部分内容比较高深,在这里就不献丑了。编译期优化和运行期优化编译期优化主要有几种1 泛型的擦除,使得泛型在编译时变成了实际类型,也叫伪泛型。2 自动拆箱装箱,foreach循环自动变成迭代器实现的for循环。3 条件编译,比如if(true)直接可得。运行期优化主要有几种1 JIT即时编译Java既是编译语言也是解释语言,因为需要编译代码生成字节码,而后通过解释器解释执行。但是,有些代码由于经常被使用而成为热点代码,每次都编译太过费时费力,干脆直接把他编译成本地代码,这种方式叫做JIT即时编译处理,所以这部分代码可以直接在本地运行而不需要通过jvm的执行引擎。2 公共表达式擦除,就是一个式子在后面如果没有被修改,在后面调用时就会被直接替换成数值。3 数组边界擦除,方法内联,比较偏,意义不大。4 逃逸分析,用于分析一个对象的作用范围,如果只局限在方法中被访问,则说明不会逃逸出方法,这样的话他就是线程安全的,不需要进行并发加锁。1JVM的垃圾回收1 GC算法:停止复制,存活对象少时适用,缺点是需要两倍空间。标记清除,存活对象多时适用,但是容易产生随便。标记整理,存活对象少时适用,需要移动对象较多。2 GC分区,一般GC发生在堆区,堆区可分为年轻代,老年代,以前有永久代,现在没有了。年轻代分为eden和survior,新对象分配在eden,当年轻代满时触发minor gc,存活对象移至survivor区,然后两个区互换,等待下一场gc,当对象存活的阈值达到设定值时进入老年代,大对象也会直接进入老年代。老年代空间较大,当老年代空间不足以存放年轻代过来的对象时,开始进行full gc。同时整理年轻代和老年代。一般年轻代使用停止复制,老年代使用标记清除。3 垃圾收集器serial串行parallel并行它们都有年轻代与老年代的不同实现。然后是scanvage收集器,注重吞吐量,可以自己设置,不过不注重延迟。cms垃圾收集器,注重延迟的缩短和控制,并且收集线程和系统线程可以并发。cms收集步骤主要是,初次标记gc root,然后停顿进行并发标记,而后处理改变后的标记,最后停顿进行并发清除。g1收集器和cms的收集方式类似,但是g1将堆内存划分成了大小相同的小块区域,并且将垃圾集中到一个区域,存活对象集中到另一个区域,然后进行收集,防止产生碎片,同时使分配方式更灵活,它还支持根据对象变化预测停顿时间,从而更好地帮用户解决延迟等问题。
-
这篇总结主要是基于我之前JVM系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢#更多详细内容可以查看我的专栏文章:深入理解JVM虚拟机https://blog.csdn.net/column/details/21960.htmlJVM介绍和源码首先JVM是一个虚拟机,当你安装了jre,它就包含了jvm环境。JVM有自己的内存结构,字节码执行引擎,因此class字节码才能在jvm上运行,除了Java以外,Scala,groovy等语言也可以编译成字节码而后在jvm中运行。JVM是用c开发的。JVM内存模型内存模型老生常谈了,主要就是线程共享的堆区,方法区,本地方法栈。还有线程私有的虚拟机栈和程序计数器。堆区存放所有对象,每个对象有一个地址,Java类jvm初始化时加载到方法区,而后会在堆区中生成一个Class对象,来负责这个类所有实例的实例化。栈区存放的是栈帧结构,栈帧是一段内存空间,包括参数列表,返回地址,局部变量表等,局部变量表由一堆slot组成,slot的大小固定,根据变量的数据类型决定需要用到几个slot。方法区存放类的元数据,将原来的字面量转换成引用,当然,方法区也提供常量池,常量池存放-128到127的数字类型的包装类。字符串常量池则会存放使用intern的字符串变量。JVM OOM和内存泄漏这里指的是oom和内存泄漏这类错误。oom一般分为三种,堆区内存溢出,栈区内存溢出以及方法区内存溢出。堆内存溢出主要原因是创建了太多对象,比如一个集合类死循环添加一个数,此时设置jvm参数使堆内存最大值为10m,一会就会报oom异常。栈内存溢出主要与栈空间和线程有关,因为栈是线程私有的,如果创建太多线程,内存值超过栈空间上限,也会报oom。方法区内存溢出主要是由于动态加载类的数量太多,或者是不断创建一个动态代理,用不了多久方法区内存也会溢出,会报oom,这里在1.7之前会报permgem oom,1.8则会报meta space oom,这是因为1.8中删除了堆中的永久代,转而使用元数据区。内存泄漏一般是因为对象被引用无法回收,比如一个集合中存着很多对象,可能你在外部代码把对象的引用置空了,但是由于对象还被集合给引用着,所以无法被回收,导致内存泄漏。测试也很简单,就在集合里添加对象,添加完以后把引用置空,循环操作,一会就会出现oom异常,原因是内存泄漏太多了,导致没有空间分配新的对象。常见调试工具命令行工具有jstack jstat jmap 等,jstack可以跟踪线程的调用堆栈,以便追踪错误原因。jstat可以检查jvm的内存使用情况,gc情况以及线程状态等。jmap用于把堆栈快照转储到文件系统,然后可以用其他工具去排查。visualvm是一款很不错的gui调试工具,可以远程登录主机以便访问其jvm的状态并进行监控。class文件结构class文件结构比较复杂,首先jvm定义了一个class文件的规则,并且让jvm按照这个规则去验证与读取。开头是一串魔数,然后接下来会有各种不同长度的数据,通过class的规则去读取这些数据,jvm就可以识别其内容,最后将其加载到方法区。JVM的类加载机制jvm的类加载顺序是bootstrap类加载器,extclassloader加载器,最后是appclassloader用户加载器,分别加载的是jdk/bin ,jdk/ext以及用户定义的类目录下的类(一般通过ide指定),一般核心类都由bootstrap和ext加载器来加载,appclassloader用于加载自己写的类。双亲委派模型,加载一个类时,首先获取当前类加载器,先找到最高层的类加载器bootstrap让他尝试加载,他如果加载不了再让ext加载器去加载,如果他也加载不了再让appclassloader去加载。这样的话,确保一个类型只会被加载一次,并且以高层类加载器为准,防止某些类与核心类重复,产生错误。defineclass findclass和loadclass类加载classloader中有两个方法loadclass和findclass,loadclass遵从双亲委派模型,先调用父类加载的loadclass,如果父类和自己都无法加载该类,则会去调用findclass方法,而findclass默认实现为空,如果要自定义类加载方式,则可以重写findclass方法。常见使用defineclass的情况是从网络或者文件读取字节码,然后通过defineclass将其定义成一个类,并且返回一个Class对象,说明此时类已经加载到方法区了。当然1.8以前实现方法区的是永久代,1.8以后则是元空间了。JVM虚拟机字节码执行引擎jvm通过字节码执行引擎来执行class代码,他是一个栈式执行引擎。这部分内容比较高深,在这里就不献丑了。编译期优化和运行期优化编译期优化主要有几种1 泛型的擦除,使得泛型在编译时变成了实际类型,也叫伪泛型。2 自动拆箱装箱,foreach循环自动变成迭代器实现的for循环。3 条件编译,比如if(true)直接可得。运行期优化主要有几种1 JIT即时编译Java既是编译语言也是解释语言,因为需要编译代码生成字节码,而后通过解释器解释执行。但是,有些代码由于经常被使用而成为热点代码,每次都编译太过费时费力,干脆直接把他编译成本地代码,这种方式叫做JIT即时编译处理,所以这部分代码可以直接在本地运行而不需要通过jvm的执行引擎。2 公共表达式擦除,就是一个式子在后面如果没有被修改,在后面调用时就会被直接替换成数值。3 数组边界擦除,方法内联,比较偏,意义不大。4 逃逸分析,用于分析一个对象的作用范围,如果只局限在方法中被访问,则说明不会逃逸出方法,这样的话他就是线程安全的,不需要进行并发加锁。1JVM的垃圾回收1 GC算法:停止复制,存活对象少时适用,缺点是需要两倍空间。标记清除,存活对象多时适用,但是容易产生随便。标记整理,存活对象少时适用,需要移动对象较多。2 GC分区,一般GC发生在堆区,堆区可分为年轻代,老年代,以前有永久代,现在没有了。年轻代分为eden和survior,新对象分配在eden,当年轻代满时触发minor gc,存活对象移至survivor区,然后两个区互换,等待下一场gc,当对象存活的阈值达到设定值时进入老年代,大对象也会直接进入老年代。老年代空间较大,当老年代空间不足以存放年轻代过来的对象时,开始进行full gc。同时整理年轻代和老年代。一般年轻代使用停止复制,老年代使用标记清除。3 垃圾收集器serial串行parallel并行它们都有年轻代与老年代的不同实现。然后是scanvage收集器,注重吞吐量,可以自己设置,不过不注重延迟。cms垃圾收集器,注重延迟的缩短和控制,并且收集线程和系统线程可以并发。cms收集步骤主要是,初次标记gc root,然后停顿进行并发标记,而后处理改变后的标记,最后停顿进行并发清除。g1收集器和cms的收集方式类似,但是g1将堆内存划分成了大小相同的小块区域,并且将垃圾集中到一个区域,存活对象集中到另一个区域,然后进行收集,防止产生碎片,同时使分配方式更灵活,它还支持根据对象变化预测停顿时间,从而更好地帮用户解决延迟等问题。
-
这篇总结主要是基于我之前JVM系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢#更多详细内容可以查看我的专栏文章:深入理解JVM虚拟机https://blog.csdn.net/column/details/21960.htmlJVM介绍和源码首先JVM是一个虚拟机,当你安装了jre,它就包含了jvm环境。JVM有自己的内存结构,字节码执行引擎,因此class字节码才能在jvm上运行,除了Java以外,Scala,groovy等语言也可以编译成字节码而后在jvm中运行。JVM是用c开发的。JVM内存模型内存模型老生常谈了,主要就是线程共享的堆区,方法区,本地方法栈。还有线程私有的虚拟机栈和程序计数器。堆区存放所有对象,每个对象有一个地址,Java类jvm初始化时加载到方法区,而后会在堆区中生成一个Class对象,来负责这个类所有实例的实例化。栈区存放的是栈帧结构,栈帧是一段内存空间,包括参数列表,返回地址,局部变量表等,局部变量表由一堆slot组成,slot的大小固定,根据变量的数据类型决定需要用到几个slot。方法区存放类的元数据,将原来的字面量转换成引用,当然,方法区也提供常量池,常量池存放-128到127的数字类型的包装类。字符串常量池则会存放使用intern的字符串变量。JVM OOM和内存泄漏这里指的是oom和内存泄漏这类错误。oom一般分为三种,堆区内存溢出,栈区内存溢出以及方法区内存溢出。堆内存溢出主要原因是创建了太多对象,比如一个集合类死循环添加一个数,此时设置jvm参数使堆内存最大值为10m,一会就会报oom异常。栈内存溢出主要与栈空间和线程有关,因为栈是线程私有的,如果创建太多线程,内存值超过栈空间上限,也会报oom。方法区内存溢出主要是由于动态加载类的数量太多,或者是不断创建一个动态代理,用不了多久方法区内存也会溢出,会报oom,这里在1.7之前会报permgem oom,1.8则会报meta space oom,这是因为1.8中删除了堆中的永久代,转而使用元数据区。内存泄漏一般是因为对象被引用无法回收,比如一个集合中存着很多对象,可能你在外部代码把对象的引用置空了,但是由于对象还被集合给引用着,所以无法被回收,导致内存泄漏。测试也很简单,就在集合里添加对象,添加完以后把引用置空,循环操作,一会就会出现oom异常,原因是内存泄漏太多了,导致没有空间分配新的对象。常见调试工具命令行工具有jstack jstat jmap 等,jstack可以跟踪线程的调用堆栈,以便追踪错误原因。jstat可以检查jvm的内存使用情况,gc情况以及线程状态等。jmap用于把堆栈快照转储到文件系统,然后可以用其他工具去排查。visualvm是一款很不错的gui调试工具,可以远程登录主机以便访问其jvm的状态并进行监控。class文件结构class文件结构比较复杂,首先jvm定义了一个class文件的规则,并且让jvm按照这个规则去验证与读取。开头是一串魔数,然后接下来会有各种不同长度的数据,通过class的规则去读取这些数据,jvm就可以识别其内容,最后将其加载到方法区。JVM的类加载机制jvm的类加载顺序是bootstrap类加载器,extclassloader加载器,最后是appclassloader用户加载器,分别加载的是jdk/bin ,jdk/ext以及用户定义的类目录下的类(一般通过ide指定),一般核心类都由bootstrap和ext加载器来加载,appclassloader用于加载自己写的类。双亲委派模型,加载一个类时,首先获取当前类加载器,先找到最高层的类加载器bootstrap让他尝试加载,他如果加载不了再让ext加载器去加载,如果他也加载不了再让appclassloader去加载。这样的话,确保一个类型只会被加载一次,并且以高层类加载器为准,防止某些类与核心类重复,产生错误。defineclass findclass和loadclass类加载classloader中有两个方法loadclass和findclass,loadclass遵从双亲委派模型,先调用父类加载的loadclass,如果父类和自己都无法加载该类,则会去调用findclass方法,而findclass默认实现为空,如果要自定义类加载方式,则可以重写findclass方法。常见使用defineclass的情况是从网络或者文件读取字节码,然后通过defineclass将其定义成一个类,并且返回一个Class对象,说明此时类已经加载到方法区了。当然1.8以前实现方法区的是永久代,1.8以后则是元空间了。JVM虚拟机字节码执行引擎jvm通过字节码执行引擎来执行class代码,他是一个栈式执行引擎。这部分内容比较高深,在这里就不献丑了。编译期优化和运行期优化编译期优化主要有几种1 泛型的擦除,使得泛型在编译时变成了实际类型,也叫伪泛型。2 自动拆箱装箱,foreach循环自动变成迭代器实现的for循环。3 条件编译,比如if(true)直接可得。运行期优化主要有几种1 JIT即时编译Java既是编译语言也是解释语言,因为需要编译代码生成字节码,而后通过解释器解释执行。但是,有些代码由于经常被使用而成为热点代码,每次都编译太过费时费力,干脆直接把他编译成本地代码,这种方式叫做JIT即时编译处理,所以这部分代码可以直接在本地运行而不需要通过jvm的执行引擎。2 公共表达式擦除,就是一个式子在后面如果没有被修改,在后面调用时就会被直接替换成数值。3 数组边界擦除,方法内联,比较偏,意义不大。4 逃逸分析,用于分析一个对象的作用范围,如果只局限在方法中被访问,则说明不会逃逸出方法,这样的话他就是线程安全的,不需要进行并发加锁。1JVM的垃圾回收1 GC算法:停止复制,存活对象少时适用,缺点是需要两倍空间。标记清除,存活对象多时适用,但是容易产生随便。标记整理,存活对象少时适用,需要移动对象较多。2 GC分区,一般GC发生在堆区,堆区可分为年轻代,老年代,以前有永久代,现在没有了。年轻代分为eden和survior,新对象分配在eden,当年轻代满时触发minor gc,存活对象移至survivor区,然后两个区互换,等待下一场gc,当对象存活的阈值达到设定值时进入老年代,大对象也会直接进入老年代。老年代空间较大,当老年代空间不足以存放年轻代过来的对象时,开始进行full gc。同时整理年轻代和老年代。一般年轻代使用停止复制,老年代使用标记清除。3 垃圾收集器serial串行parallel并行它们都有年轻代与老年代的不同实现。然后是scanvage收集器,注重吞吐量,可以自己设置,不过不注重延迟。cms垃圾收集器,注重延迟的缩短和控制,并且收集线程和系统线程可以并发。cms收集步骤主要是,初次标记gc root,然后停顿进行并发标记,而后处理改变后的标记,最后停顿进行并发清除。g1收集器和cms的收集方式类似,但是g1将堆内存划分成了大小相同的小块区域,并且将垃圾集中到一个区域,存活对象集中到另一个区域,然后进行收集,防止产生碎片,同时使分配方式更灵活,它还支持根据对象变化预测停顿时间,从而更好地帮用户解决延迟等问题。
-
一、GC-垃圾回收:stop-the-world(stw): 他会在任何一种GC算法中发生。stw意味着jvm因为需要执行GC而停止了应用程序的执行。当stw发生时,出GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成。GC优化的很多时候,就是减少stw的发生。需要注意的是,jvm gc只回收堆和方法区内的对象,而栈区的数据,在超出作用域后会被jvm自动释放掉,所有其不再jvm gc的管理范围内。jvm -gc 如何判断对象可以被回收了?对象没有应用作用域发生未捕获异常程序在作用域正常执行完毕程序执行了System.exit();程序发生意外终止(被杀线程等)在java程序中不能显示的分配和注销缓存,因为这些事情jvm都帮我们做了,那就是GC.有些时候我们可以将相关对象设置成null来试图显示的清楚缓存,但是并不是设置成null就会一定被标记为可回收,有可能会发生逃逸。将对象设置成null至少没有什么坏处,但是使用System.gc()便不可取了,使用System.gc()的时候并不是马上执行GC操作,而是会等待一段时间,甚至不执行,而且System.gc()如果别执行,会出发Full GC,这费城影响性能。GC什么时候执行:eden区空间不够存放新对象的时候,执行minor gc。 升到年老代的对象大于老年代的剩余空间时执行full gc,或者小于的时候,被 HandlePromotionFailure 参数强制Full GC。 调优主要是减少Full GC 的触发次数,可以通过NewRatio 控制新生代转老年代的比例,通过MaxTurningThreshold 设置对象进入老年代的年龄阀值。按代的垃圾回收机制:新生代(Young generation):绝大多数的最新被创建的对象都会被分配到这里,由于大部分在创建后很快变得不可达,很多对象别创建在新生代,然后消失。对象从这个区域消失的过程,我们称之为 Minor GC老年代(old generation): 对象没有变得不可达,并且从新生代周期中存活了下来,会被拷贝到这里。其区域分配的空间要比新生代多。也正是由于其相对较大的空间,发生在老年代的GC次数要比新生代少得多。对象从老年代消失的过程称之为: Major GC, 或者 Full GC.持久代(Permanent generation):也称之为方法区,用于保存类常量以及字符串常量,注意,这个区域不是用于存储那些从老年代存活下来的对象,这个区域也可能发生GC, 发生在这个区域的GC事件也被算作Major GC,只不过在这个区域发生GC 的条件非诚严苛,必须符合以下三种条件:所有实例被回收加载该类的ClassLoader被回收Class 对象无法通过任何途径访问(包括反射)如果老年代要引用新生代的对象,会发生什么呢?为了解决这个问题,老年代中存在一个 card table ,它是一个512byte大小的块。所有老年代的对象指向新生代对象的引用都会被记录在这个表中。当针对新生代执行GC的时候,只需要查询 card table 来决定是否可以被回收,而不用查询整个老年代。这个 card table 由一个write barrier 来管理。write barrier给GC带来了很大的性能提升,虽然由此可能带来一些开销,但完全是值得的。默认的新生代和老年代所占空间的比例为1:2新生代空间的构成和逻辑:分为三个部分: 一个伊甸园空间(eden), 两个幸存者空间)(From Survivor, To Survivor)默认比例: Eden:From:to = 8:1:1每个空间执行顺序:绝大多数刚刚被创建的对象会存放在伊甸园EDEN空间在eden空间执行第一次gc(minor gc)后,存活的对象被移动到其中的一个幸存者区此后,每次Eden空间执行gc后,存活的对象都会被堆积在同一个幸存者空间。当一个幸存者空间饱和,还存在存活的对象会被移动到另一个幸存者空间,然后会清空已经饱和的那个幸存者空间在以上步骤中重复N次(N=MAXTenuringThreshold(年龄阀值设定,默认15))依然存活的对象,就会别移动到老年代从上面的步骤可以发现,两个幸存者空间,必须有一个是保持空的,如果两个幸存者空间都有数据,或者两个都是空的,那一定是你的系统出现了某种错误。我们需要重点记住的是,对象在刚刚被创建之后,是保存在Eden区的,哪些长期存活的对象会经由幸存者空间转到老年代空间。也有例外的情况,对于一些比较大的对象(需要分配连续比较大的空间)则直接进入到老年代,一般在幸存者空间不足的情况下发生。老年代空间的构成与逻辑:老年代空间的构成其实很简单,他不像新生代那样划分为几个区域,他只有一个区域,里面存储的对象并不像新生代空间绝大部分都是朝闻道,夕死矣。这里的对象几乎都是从Survivor空间中熬过来的,他们绝不会轻易狗带。因此FULL GC 发生的次数不会有minor gc那么频繁,并且做一次full gc的时间比minor gc要更长(约10倍)二、GC算法:1. 根搜索算法(可达性分析):从GCROOT开始,寻找对应的引用节点,找到这个节点后,继续寻找这个节点的引用节点。当所有的引用节点寻找完毕后,剩余的节点则被认为是没有被引用到的节点,及无用的节点。目前java中可以作为GCroot的对象有: 虚拟机栈中引用的对象(本地变量表),方法区中静态属性引用的对象,方法区中常量引用的对象,本地方法栈中引用的对象(native)2. 标记-清除算法:标记-清除算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,在扫描整个空间中未标记的对象进行直接回收。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活的对象比较多的情况下极为高效,但是由于标记-清除算法直接回收不存活的对象,并没有对存活的对象进行整理,因此会导致内存碎片。3. 复制算法:复制算法将内存划分为两个区间,使用此算法时,所有的动态分配的对象都只能分配在其中一个区间,而另一个区间是闲置的。复制算法采用从根集合扫描,将存活对象复制到空闲区间,当扫描完毕活动区间后,会将活动区间一次性全部回收,此时原本的空闲区间变成了活动区间,下次gc的时候会重复刚才的操作,以此循环。复制算法在存活对象较少的时候,极为高效,但是带来的成本是牺牲一半的内存空间用于对象的移动,所以复制算法使用的场景,必须是对象的存活率非常低才行。4. 标记-整理算法:标记-整理算法采用和标记-清除算法一样的方式进行对象的标记,清除,但是在回收不存活对象占用的空间后,会见给所有的存活的对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题,JVM 为了优化内存得回收,是用来分代回收的方式,对于新生代的内存回收,主要采用复制算法,而对于老年代的回收,大多采用标记整理算法。三、垃圾回收器需要注意的是,每一个回收器都存在stw的问题,只不过各个回收器在stw时间优化程度、算法的不同,可根据自身需求选择适合的回收器。1.Serial(-XX: + UseSerialGC)从名字可以看出,这是一个串行的垃圾回收器,这也是java虚拟机中最基本,历史最悠久的收集器,在jdk1.3之前是java虚拟机新生代收集器的唯一选择,目前也是ClientVM 下ServerVM4核4gb以下机器的默认垃圾回收器,Serial收集器并不是只能使用一个CPU进行收集,而是当jvm需要进行垃圾回收的时候,需暂停所有的用户线程,直到回收结束。使用算法: 复制算法。Serial收集器虽然是最老的,但是它对于限定单个CPU的环境来说,由于没有线程交互的开销,专心做垃圾收集,所以它在这种情况下是相对于其他收集器中最高效的。2. SerialOld(-XX: + UseSerialGC)SerialOld是Serial收集器的老年代收集器版本,它同样是一个单线程收集器,这个收集器目前主要用于Client模式下使用。如果在Server模式下,它主要还有两大用途:一个是在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用,另外一个就是作为CMS收集器的后备预案,如果CMS出现Concurrent Mode Failure,则SerialOld将作为后备收集器。使用算法:标记 - 整理算法3. ParNew(-XX: +UseParNewGC)ParNew其实就是Serial收集器的多线程版本。除了Serial收集器外,只有它能与CMS收集器配合工作。使用算法: 复制算法ParNew 是许多运行在Server模式下的JVM的首选的新生代收集器,但是在单cpu的情况下,他的效率远远低于Serial收集器,所以一定要注意使用场景。4. ParallelScavenge(-XX:+UseParallelGC)ParallelScavenge又被称为吞吐量优先收集器,和ParNew 收集器类似,是一个新生代收集器使用算法: 复制算法ParallelScavenge收集器的目的是打到一个可控的吞吐量,所谓吞吐量就是cpu用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。如果虚拟机总共运行了100分钟,其中垃圾回收用了1分钟,那么吞吐量就是99%所以这个收集器适合在后台运算而不需要很多交互的任务。接下来看看两个用于准备控制吞吐量的参数 1,-XX:MaxGCPauseMills(控制最大垃圾收集的时间) 设置一个大于0的毫秒数,收集器尽可能地保证内存回收不超过设定值。但是并不是设置地越小就越快。GC停顿时间缩短是以缩短吞吐量和新生代空间来换取的。 2,-XX:GCTimeRatio(设置吞吐量大小) 设置一个0-100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。5. ParallelOld(-XX: + UseParallelOldGC)ParallelOld 是并行收集器,和SerialOld 一样,是一个老年代收集器,是老年代吞吐量优先的一个收集器,这个收集器在JDK1.6之后才开始提供的,再次之前,ParallelScavenge只能选择SerialOld来作为其老年代的收集器,这严重拖累了ParallelScavenge的整体速度,而ParallelOld出现了之后,吞吐量有限收集器才名副其实使用算法: 标记-整理算法在注重吞吐量与CPU数量大于1 的情况下,都可以优先考虑ParallelScavenge + ParallelOld收集器6. CMS(-XX:UseConcMarkSweepGC)CMS是一个老年代收集器,全称Concurrent Low Pause Collector, 是JDK1.4以后开始引用的心GC收集器,在jdk5,jdk6中得到了进一步的改进。他是对于响应时间的重要性需求大于吞吐量要求的收集器,对于要求服务器响应速度高的情况下,使用CMS非常合适。CMS的一大特点,就是用两次短暂的暂定来代替串行或者并行标记整理算法时候的长暂停使用算法:标记-清理执行过程如下:初始标记(STW initial mark):在这个阶段,需要虚拟机停顿在正在执行的应用线程,官方叫法叫做STW,这个过程从根对象扫描直接关联的对象,并做标记,这个过程会很快完成。并发标记(Concurrent marking) :这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记,注意这里是并发标记,标识用户线程可以和GC线程一起并发执行,这个阶段不会暂停用户线程并发预清理(Concurrent precleaning):这个阶段仍然是并发的,jvm查找正在执行并发标记阶段时候进入老年代的对象(可能这是会有对象从新生代晋升到老年代,或被分配到老年代)通过重新扫描,减少在一个阶段重新标记的工作,因为下一个阶段会stw重新标记(stw remark): 这个阶段会再次暂停正在执行的应用线程,重新从根对象开始查找并标记并发阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致)并处理对象关联,这一次耗时回避“初始标记”更长,并且这个阶段可以并行标记。并发清理(Concurrent sweeping): 这个阶段是并发的,应用程序和GC清理线程可以一起并发执行并发重置(Concurrent reset):这个阶段仍然是并发的,重置CMS收集器的数据结构,等待下一次垃圾回收CMS:缺点:内存碎片;由于使用了标记-清理算法,导致内存空间中会产生内存碎片,不过CMS收集器做了一些小的优化,就是把未分配的空间汇总成一个列表,当有JVM需要分配内存空间的时候,会搜索这个列表找到符合条件的空间来存储这个对象,但是内存碎片的问题仍然存在,如果一个对象需要三块连续的空间来存储,因为内存碎片的问题,找不到这样的空间,就会导致full gc.需要更多的CPU资源:由于使用了并发处理,很多情况下都是GC线程和用户线程并发执行的,这样就需要占用更多的CPU资源,也是牺牲了一定吞吐量的原因。需要更大的堆空间:因为CMS标记阶段用用程序的线程还是执行的,那么就会有堆空间继续分配的问题,为了保障CMS在回收堆空间之前还有空间分配给新加入的对象,必须预留一部分空间,cms默认在老年代空间使用68%的时候启动垃圾回收,可以通过-XX:CMSinitiatingOccupancyFraction=n来设置这个阀值。7. garbageFirst(G1)G1收集器是jdk1.7提供的一个新的收集器,是当今收集器技术发展的最前沿成果之一。G1是一款面向服务端应用的垃圾收集器,Hotspot开发团队赋予它的使命是未来可以替换掉cms.G1具备以下特点:并行与并发: G1能充分利用多CPU,多核心环境下的硬件优势,使用多个CPU来缩短STW停顿的时间,部分其他收集器原本需要停顿java线程执行的G1动作,G1收集器仍然可以通过并发的方式让java程序继续执行。分代收集:与其他收集器一样,分代概念在G1中仍然得以保留,虽然G1可以不需要其他收集器配合就能单独管理整个GC堆,但他能够采取不同的方式去处理新创建的对象和已经存活了一段时间。熬过多个gc的旧对象已获得更好的收集效果空间整合:与CMS的标记-清除算法不同,G1收集器从整体上看是基于标记-整理算法实现的,从局部(两个region)上看是基于复制算法实现的,但无论如何,两种算法都意味着g1运行期间不会产生内存空间碎片,收集后能够提供规整的可用内存。这种特性有利于程序的长时间运行, 分配大对象时不会因为无法找到连续的内存空间而提前触发下一次GC可预测的停顿:这是G1相比cms的另一大优势,降低停顿时间是G1和cms的共同关注点,但是G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时java(RTSJ)的垃圾收集器的特征了。整理一下新生代和老年代的收集器。新生代收集器:Serial (-XX:+UseSerialGC)ParNew(-XX:+UseParNewGC)ParallelScavenge(-XX:+UseParallelGC)G1 收集器老年代收集器:SerialOld(-XX:+UseSerialOldGC)ParallelOld(-XX:+UseParallelOldGC)CMS(-XX:+UseConcMarkSweepGC)G1 收集器
-
这篇总结主要是基于我之前JVM系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢#更多详细内容可以查看我的专栏文章:深入理解JVM虚拟机https://blog.csdn.net/column/details/21960.htmlJVM介绍和源码首先JVM是一个虚拟机,当你安装了jre,它就包含了jvm环境。JVM有自己的内存结构,字节码执行引擎,因此class字节码才能在jvm上运行,除了Java以外,Scala,groovy等语言也可以编译成字节码而后在jvm中运行。JVM是用c开发的。JVM内存模型内存模型老生常谈了,主要就是线程共享的堆区,方法区,本地方法栈。还有线程私有的虚拟机栈和程序计数器。堆区存放所有对象,每个对象有一个地址,Java类jvm初始化时加载到方法区,而后会在堆区中生成一个Class对象,来负责这个类所有实例的实例化。栈区存放的是栈帧结构,栈帧是一段内存空间,包括参数列表,返回地址,局部变量表等,局部变量表由一堆slot组成,slot的大小固定,根据变量的数据类型决定需要用到几个slot。方法区存放类的元数据,将原来的字面量转换成引用,当然,方法区也提供常量池,常量池存放-128到127的数字类型的包装类。字符串常量池则会存放使用intern的字符串变量。JVM OOM和内存泄漏这里指的是oom和内存泄漏这类错误。oom一般分为三种,堆区内存溢出,栈区内存溢出以及方法区内存溢出。堆内存溢出主要原因是创建了太多对象,比如一个集合类死循环添加一个数,此时设置jvm参数使堆内存最大值为10m,一会就会报oom异常。栈内存溢出主要与栈空间和线程有关,因为栈是线程私有的,如果创建太多线程,内存值超过栈空间上限,也会报oom。方法区内存溢出主要是由于动态加载类的数量太多,或者是不断创建一个动态代理,用不了多久方法区内存也会溢出,会报oom,这里在1.7之前会报permgem oom,1.8则会报meta space oom,这是因为1.8中删除了堆中的永久代,转而使用元数据区。内存泄漏一般是因为对象被引用无法回收,比如一个集合中存着很多对象,可能你在外部代码把对象的引用置空了,但是由于对象还被集合给引用着,所以无法被回收,导致内存泄漏。测试也很简单,就在集合里添加对象,添加完以后把引用置空,循环操作,一会就会出现oom异常,原因是内存泄漏太多了,导致没有空间分配新的对象。常见调试工具命令行工具有jstack jstat jmap 等,jstack可以跟踪线程的调用堆栈,以便追踪错误原因。jstat可以检查jvm的内存使用情况,gc情况以及线程状态等。jmap用于把堆栈快照转储到文件系统,然后可以用其他工具去排查。visualvm是一款很不错的gui调试工具,可以远程登录主机以便访问其jvm的状态并进行监控。class文件结构class文件结构比较复杂,首先jvm定义了一个class文件的规则,并且让jvm按照这个规则去验证与读取。开头是一串魔数,然后接下来会有各种不同长度的数据,通过class的规则去读取这些数据,jvm就可以识别其内容,最后将其加载到方法区。JVM的类加载机制jvm的类加载顺序是bootstrap类加载器,extclassloader加载器,最后是appclassloader用户加载器,分别加载的是jdk/bin ,jdk/ext以及用户定义的类目录下的类(一般通过ide指定),一般核心类都由bootstrap和ext加载器来加载,appclassloader用于加载自己写的类。双亲委派模型,加载一个类时,首先获取当前类加载器,先找到最高层的类加载器bootstrap让他尝试加载,他如果加载不了再让ext加载器去加载,如果他也加载不了再让appclassloader去加载。这样的话,确保一个类型只会被加载一次,并且以高层类加载器为准,防止某些类与核心类重复,产生错误。defineclass findclass和loadclass类加载classloader中有两个方法loadclass和findclass,loadclass遵从双亲委派模型,先调用父类加载的loadclass,如果父类和自己都无法加载该类,则会去调用findclass方法,而findclass默认实现为空,如果要自定义类加载方式,则可以重写findclass方法。常见使用defineclass的情况是从网络或者文件读取字节码,然后通过defineclass将其定义成一个类,并且返回一个Class对象,说明此时类已经加载到方法区了。当然1.8以前实现方法区的是永久代,1.8以后则是元空间了。JVM虚拟机字节码执行引擎jvm通过字节码执行引擎来执行class代码,他是一个栈式执行引擎。这部分内容比较高深,在这里就不献丑了。编译期优化和运行期优化编译期优化主要有几种1 泛型的擦除,使得泛型在编译时变成了实际类型,也叫伪泛型。2 自动拆箱装箱,foreach循环自动变成迭代器实现的for循环。3 条件编译,比如if(true)直接可得。运行期优化主要有几种1 JIT即时编译Java既是编译语言也是解释语言,因为需要编译代码生成字节码,而后通过解释器解释执行。但是,有些代码由于经常被使用而成为热点代码,每次都编译太过费时费力,干脆直接把他编译成本地代码,这种方式叫做JIT即时编译处理,所以这部分代码可以直接在本地运行而不需要通过jvm的执行引擎。2 公共表达式擦除,就是一个式子在后面如果没有被修改,在后面调用时就会被直接替换成数值。3 数组边界擦除,方法内联,比较偏,意义不大。4 逃逸分析,用于分析一个对象的作用范围,如果只局限在方法中被访问,则说明不会逃逸出方法,这样的话他就是线程安全的,不需要进行并发加锁。1JVM的垃圾回收1 GC算法:停止复制,存活对象少时适用,缺点是需要两倍空间。标记清除,存活对象多时适用,但是容易产生随便。标记整理,存活对象少时适用,需要移动对象较多。2 GC分区,一般GC发生在堆区,堆区可分为年轻代,老年代,以前有永久代,现在没有了。年轻代分为eden和survior,新对象分配在eden,当年轻代满时触发minor gc,存活对象移至survivor区,然后两个区互换,等待下一场gc,当对象存活的阈值达到设定值时进入老年代,大对象也会直接进入老年代。老年代空间较大,当老年代空间不足以存放年轻代过来的对象时,开始进行full gc。同时整理年轻代和老年代。一般年轻代使用停止复制,老年代使用标记清除。3 垃圾收集器serial串行parallel并行它们都有年轻代与老年代的不同实现。然后是scanvage收集器,注重吞吐量,可以自己设置,不过不注重延迟。cms垃圾收集器,注重延迟的缩短和控制,并且收集线程和系统线程可以并发。cms收集步骤主要是,初次标记gc root,然后停顿进行并发标记,而后处理改变后的标记,最后停顿进行并发清除。g1收集器和cms的收集方式类似,但是g1将堆内存划分成了大小相同的小块区域,并且将垃圾集中到一个区域,存活对象集中到另一个区域,然后进行收集,防止产生碎片,同时使分配方式更灵活,它还支持根据对象变化预测停顿时间,从而更好地帮用户解决延迟等问题。
上滑加载中
推荐直播
-
华为AI技术发展与挑战:集成需求分析的实战指南
2024/11/26 周二 18:20-20:20
Alex 华为云学堂技术讲师
本期直播将综合讨论华为AI技术的发展现状,技术挑战,并深入探讨华为AI应用开发过程中的需求分析过程,从理论到实践帮助开发者快速掌握华为AI应用集成需求的框架和方法。
去报名 -
华为云DataArts+DWS助力企业数据治理一站式解决方案及应用实践
2024/11/27 周三 16:30-18:00
Walter.chi 华为云数据治理DTSE技术布道师
想知道数据治理项目中,数据主题域如何合理划分?数据标准及主数据标准如何制定?数仓分层模型如何合理规划?华为云DataArts+DWS助力企业数据治理项目一站式解决方案和应用实践告诉您答案!本期将从数据趋势、数据治理方案、数据治理规划及落地,案例分享四个方面来助力企业数据治理项目合理咨询规划及顺利实施。
去报名
热门标签