当前位置: 首页 > news >正文

wordpress 作品集网站佛山建设工程交易中心网站

wordpress 作品集网站,佛山建设工程交易中心网站,个性flash网站,微信怎么推广找客源JVM优化为什么要学习JVM优化#xff1a; 1#xff1a;深入地理解 Java 这门语言 我们常用的布尔型 Boolean#xff0c;我们都知道它有两个值#xff0c;true 和 false#xff0c;但你们知道其实在运行时#xff0c;Java 虚拟机是 没有布尔型 Boolean 这种类型的#x…JVM优化为什么要学习JVM优化 1深入地理解 Java 这门语言 我们常用的布尔型 Boolean我们都知道它有两个值true 和 false但你们知道其实在运行时Java 虚拟机是 没有布尔型 Boolean 这种类型的Boolean 型在虚拟机中使用整型的 1 和 0 表示一般所有的数据最底层都是1和0 2更好的解决线上排查问题 我们知道我们一个Java 应用部署在线上机器上肯定时不时会出现问题。除去网络、系统本身问题很多时候 Java 应用 出现问题基本就是 Java 虚拟机的内存出现了问题要么是内存溢出了要么是 GC 频繁导致响应慢等等那如何解决这些问题就是学习JVM优化的一个原因 JVM回顾对于一些概念来说可以大致的过一遍 什么是JVM JVM是Java Virtual MachineJava虚拟机的缩写JVM是一种用于计算设备的规范它是一个虚构出来的计算 机是通过在实际的计算机上仿真模拟各种计算机功能来实现的一般用来操作java的 主流虚拟机 JVM与操作系统 为什么要在程序和操作系统中间添加一个JVM Java 是一门抽象程度特别高的语言提供了自动内存管理等一系列的特性这些特性直接在操作系统上实现是不太 可能的或者难以实现就如你从0到有的完成一件事而不用其他封装好的东西所以就需要 JVM 进行一番转换 从图中可以看到有了 JVM 这个抽象层之后Java 就可以实现跨平台了实际上c也可以但是现在不行具体可以看看这个博客https://www.zhihu.com/question/386866683/answer/2524741732?utm_id0JVM 只需要保证能够正确执行 .class 文 件就可以运行在诸如 Linux、Windows、MacOS 等平台上了只要他们也有JVM 而 Java 跨平台的意义在于一次编译处处运行能够做到这一点 JVM 功不可没比如我们在 Maven 仓库下载同一 版本的 jar 包就可以到处运行不需要在每个平台上再编译一次 现在的一些 JVM 的扩展语言比如 Clojure、JRuby、Groovy 等编译到最后都是 .class 文件Java 语言的维护 者只需要控制好 JVM 这个解析器就可以将这些扩展语言无缝的运行在 JVM 之上了 应用程序、JVM、操作系统之间的关系 我们用一句话概括 JVM 与操作系统之间的关系JVM 上承开发语言下接操作系统它的中间接口就是字节码 JVM、JRE、JDK 的关系 JVM 是 Java 程序能够运行的核心但是需要注意JVM 自己什么也干不了你需要给它提供生产原料.class 文 件 且仅仅是 JVM是无法完成一次编译处处运行的它需要一个基本的类库比如怎么操作文件、怎么连接网络等而 Java 体系很慷慨会一次性将 JVM 运行所需的类库都传递给它JVM 标准加上实现的一大堆基础类库就组成 了 Java 的运行时环境也就是我们常说的 JREJava Runtime Environment 对于 JDK 来说就更庞大了一些除了 JREJDK 还提供了一些非常好用的小工具比如 javac、java、jar 等它 是 Java 开发的核心让外行也可以炼剑 我们也可以看下 JDK 的全拼Java Development Kit我非常怕 kit装备这个单词它就像一个无底洞预示着 你永无休止的对它进行研究可以多装备JVM、JRE、JDK 它们三者之间的关系可以用一个包含关系表示 Java虚拟机规范和 Java 语言规范的关系 左半部分是 Java 虚拟机规范其实就是为输入和执行字节码提供一个运行环境字节码最终变为01就如c也会变成01一样只是一般java需要编译变成可以识别的实际上class可以认为是变成c只是是另外一种形式因为jvm是c/c写的c/c代表都有或者其中一个一般表示都有右半部分是我们常说的 Java 语法 规范比如 switch、for、泛型、lambda 等相关的程序最终都会编译成字节码而连接左右两部分的桥梁依然是Java 的字节码 如果 .class 文件的规格是不变的这两部分是可以独立进行优化的但 Java 也会偶尔扩充一下 .class 文件的格式 增加一些字节码指令以便支持更多的特性 我们可以把 Java 虚拟机可以看作是一台抽象的计算机它有自己的指令集以及各种运行时内存区域若学习过《计算 机组成结构原理》或者相关的书籍那么可以发现有类似的情况 为了更好的进行理解可以认为计算机是一个大的元件我自己的理解如果有问题可以选择不看如果玩过我的世界这个游戏可以在网上搜索在我的世界里知道电脑的相关问题即也的确是一个大的文件在其中我们可以发现通过点击某些东西可以进行实现某些东西只是这些东西在现实中是由电脑键盘统称为键盘使得里面的某个东西会进行点击从而实现某些东西所以可以认为键盘与点击中间我们再次的创造一个可以识别你输入的东西从而进行某些点击既然你点击会实现某些东西那么自然可以实现键盘来使得某些点击我们可以你从键盘输入的东西称为二进制而中间的就是识别二进制的然后结果与二进制也自然有中间综上所述无论是c还是java都会有类似的中间的操作都是将他们进行变成二进制那么jvm由于是c写的那么对应的class自然可能底层是通过变成c然后变成二进制操作底层所以可以认为java是c变二进制也没有错但是我们大多数都会认为是jvm将class变成二进制的虽然jvm可能将他变成c我们可以先这样的认为以后也是如此在后面会根据理解进行改变的 当然对应的中间是如何实现的自然我并不知道这太底层了一般都可以分成很多部分也就是为什么有些知识会分开说明的原因就如二进制到实现某些东西的中间可能存在类似的java的栈堆等等来保存信息使得给结果使用并且可能触发什么使得关闭释放即该中间又何尝不是结果呢所以二进制和结果的中间一般也是属于结果的因为一般只有他会实现很多功能其他的基本只有一种如编译所以就看成中间了实际上主要的是该结果是结果即到头了所以才可以这样说明但是通常只会说明作用而不会说明如何实现需要非常多的知识且就算终其一生也未必能够学的明白世上知识何其之多掌握能力之内的知识足以 最后我们简单看一下一个 Java 程序的执行过程它到底是如何运行起来的 所以具体分开是有两步的首先是出现可以让c识别的class然后c读取将他变成c第二步在c中也是c读取c文件然后变成二进制所以对于c来说多出了出现class这一步然后变成二进制即class可以看成要变成我们写的c文件的意思虽然并不是这是因为更好的封装的从而简便代码才会有变成class这一步即java是4步c是2步 这里的 Java 程序是文本格式的比如下面这段 HelloWorld.java它遵循的就是 Java 语言规范其中我们调用了System.out 等模块也就是 JRE 里提供的类库 package com.test1;/****/ public class HelloWorld {public static void main(String[] args) {System.out.println(Hello World);} } 我们可以选择在idea中查看对应的class文件 难道class文件长这样吗答并不是他只是idea给我们总结的而已实际上并不是这样你也可以去磁盘中打开就知道了。但是一般是乱码而若要真正的看到对应的文件信息需要如下操作 使用 JDK 的工具 javac 进行编译后会产生 HelloWorld 的字节码我们一直在说 Java 字节码是沟通 JVM 与 Java 程序的桥梁下面使用 javap 来稍微看一下字节码到底长什么样子 点击如下 然后到该class文件所在的目录下在对应目录下哦这可是文件的命令操作自然不会操作类指向的即定位的所以不要在java后缀文件的对应目录下操作否则可能看不到结果或者说找不到类的错误信息出现虽然只是提示一般来说错误都可以说成是提示的在控制台里输入javap -v HelloWorld也可以javap -v HelloWorld.class即他默认加上.class的有的话则不加不加-v说明就是我们在idea中点击的class文件的内容即明面上的整体内容加上-v才基本上算是底层的class文件我们可以找到如下 0 getstatic #2 java/lang/System.out // getstatic 获取静态字段的值 3 ldc #3 Hello World // ldc 常量池中的常量值入栈 5 invokevirtual #4 java/io/PrintStream.println // invokevirtual 运行时方法绑定调用方法 8 return //void 函数返回但是要注意随着idea或者说jdk版本的不同可能并不会出现这些东西或者少些或者有些不同所以这里以上面的说明为主即可我修改并整理的 比如我这里就是这样 //可以找到如下 Code:stack2, locals1, args_size10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String Hello World5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: return //注意可能你运行一次并不会出现这些等下再次的运行即可因为可能他并没有加载好或者需要初始化或者有问题Java 虚拟机采用基于栈的架构其指令由操作码和操作数组成这些 字节码指令 就叫作 opcode其中getstatic、ldc、invokevirtual、return 等就是 opcode可以看到是比较容易理解的因为根据后面的说明就是一个完整的System.out.println(“Hello World”);的执行来源 JVM 就是靠解析这些 opcode 和操作数来完成程序的执行的当我们使用 Java 命令运行 .class 文件的时候实际上 就相当于启动了一个 JVM 进程一般关闭程序也就是关闭jvm进程相当于关闭class文件的执行但一般来说我们最好不要强制关闭即最好优雅的关闭比如手动执行System.exit(0)即可大多数代码最好这样特别是大型的代码量虽然Runtime.getRuntime().exit(0)一般也是优雅的但是一般我们不会认为他是的因为过程中并没有好的处理再101章博客有具体说明 然后 JVM 会翻译这些字节码它有两种执行方式常见的就是解释执行将 opcode 操作数翻译成机器代码直接执行另 外一种执行方式就是 JIT也就是我们常说的即时编译编译执行它会在一定条件下将字节码编译成机器码之后再等下执行你可能会有疑问解释执行难道不会将关联的代码进行分开吗答并不会的实际上在字节码中解释执行的意思是通过字节码来执行即如果是编译执行那么在都编译成二进制后才执行而解释执行则是先编译一段然后我执行该一段一路过去你可能会有疑问关联的代码解释执行会不会破坏呢答不会因为既然你变成了class那么对应的编译必然是整体的编译而不是一部分所以说不会破坏即解释执行可以看成是局部的编译执行且局部的编译执行的代码是互相没有关联的实际上就算是一个地址的执行他也算局部的编译执行因为他执行是等待某些数据而已所以并没有什么关联如后面说明的iconst_1他执行只是加载虽然后一步在代码上看起来与他有联系但是在字节码中是没有的所以他们都是一步一步的执行且没有问题 java虚拟机的内存管理 JVM整体架构 根据 JVM 规范JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分 JVM分为五大模块 类装载器子系统运行时数据区执行引擎本地方法接口 和 垃圾收集模块 JVM运行时内存也就是主要说明运行时数据区 Java 虚拟机有自动内存管理机制如果出现面的问题排查错误就必须要了解虚拟机是怎样使用内存的 Java7和Java8内存结构的不同主要体现在方法区的实现 方法区是java虚拟机规范中定义的一种概念上的区域不同的厂商可以对虚拟机进行不同的实现 我们通常使用的Java SE都是由Sun JDK和OpenJDK所提供这也是应用最广泛的版本而该版本使用的VM就是HotSpot VM通常情况下我们所讲的java虚拟机指的就是HotSpot的版本 JDK7 内存结构 JDK8 的内存结构主要说明这个 针对JDK8虚拟机内存详解以后也是这样一般jdk8之后的通常不会改变如果改变了可以去网上找资料但一般以jdk8为主 JDK7和JDK8变化小结上面说明的都是概念真正的存放位置看如下特别是堆和方法区在jdk8以前是属于一个地方下面框框看下图就知道了 直接内存一般代表电脑的内存可能并不代表全部物理内存既然电脑可以存在并利用内存如c可以直接操作内存虽然说是这样说但是实际上最终还是二进制的只是我们一般这样认为的因为最主流和比较底层那么我们也可以用一些内存来表示某些东西如元空间一般是c弄的因为java都是c搞出来的自然可以这样说 对于Java8HotSpots取消了永久代也就是方法区那里这就是一个名称而已一般方法区看成永久代所以你可以认为方法区就是永久代那么是不是就没有方法区了呢 当然不是方法区只是一个规范只不过它的实现变了即由别人实现了 在Java8中元空间Metaspace登上舞台方法区存在于元空间Metaspace同时元空间不再与堆连续这是好的减少关联或者其他原因后面会说明为什么而且是 存在于本地内存Native memory而不会向方法区一样与堆会操作连续或者太多的关联一般可以为GC带来好的操作也就是解决下面说明的永久代会为 GC 带来不必要的复杂度 方法区Java8之后的变化 移除了永久代PermGen替换为元空间Metaspace 永久代中的class metadata类元信息转移到了native memory本地内存而不是虚拟机 永久代中的interned Strings字符串常量池 和 class static variables类静态变量转移到了Java heap堆 永久代参数PermSize MaxPermSize- 元空间参数MetaspaceSize MaxMetaspaceSize Java8为什么要将永久代替换成Metaspace 字符串存在永久代中容易出现性能问题和内存溢出 类及方法的信息等比较难确定其大小因此对于永久代的大小指定比较困难太小容易出现永久代溢出太 大则容易导致老年代溢出 永久代会为 GC 带来不必要的复杂度并且回收效率偏低 Oracle 可能会将HotSpot 与 JRockit 合二为一而JRockit没有所谓的永久代 PC 程序计数器 什么是程序计数器 程序计数器Program Counter Register也叫PC寄存器后面都按照这个说明是一块较小的内存空间它可以看做是当前线程所执行 的字节码的行号指示器在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成 我们来看看这个代码 package com.test1;/****/ public class PC {public static void main(String[] args) {int x 1;int y 2;System.out.println(xy);} } 现在我们来换另外一种方式看他的字节码首先安装如下 然后重启idea一般来说安装只是放在一个地方需要重写加载但是可能会自动加载这主要看idea或者插件的某些东西了但最好重启一下然后点击如下在点击对应的类或者class类后然后点击如下基本才会出现他主要看class里的内容只是可以通过类可以定位到而已不是命令操作哦所以可以指向即定位到大多数操作类来看字节码信息的都是这样的即操作定位 出现如下 右边的就是字节码的主要信息了一般都表示main方法里面的所以并不是全部哦自己使用javap查看就知道了我们以后基本说明main里面的所以我们以main字节码信息为主了其他的大致忽略即可你看一下对应的HelloWorld就知道了就是之前给出的内容注意如果需要改变他我们通常只能改变源码但是直接改变并不会立即改变其class文件你可以选择执行一下即可这样一般都会改变 这里直接的给出进行分析 指令地址 操作指令 0 iconst_1 //数值 1相当于1 1 istore_1 //存储1x即就是int x 1很明显上面操作了iconst_1执行那么这里操作存储存储他他们都是分开执行的而不是代码中源码中的有联系的自然就是操作该1即x前缀一般代表执行后缀一般代表具体变量_分开的即1给x了即istore的1不是具体数字而是对应变量的编号或者说他整个就是一个编号只是他的名称会附带赋值操作而已通常代表第几个变量1代表第一个就是x一般不会与栈有关因为pc寄存器是先操作使得执行的而虚拟机栈后操作那么则是解释的问题即与其他字节码相关虽然其他的大致忽略这里了解即可 2 iconst_2 //同理相当于y 3 istore_2 //存储2y即int x 2注意对应的名称是可变的也就是说iconst_1代表对应的数值是可以改变自己修改就知道了比如将1变成3那么iconst_1就是iconst_3当然对应的istore_1一般是固定的因为是按照顺序来的在字节码main对应里面的其他信息后面可能会给出对应的顺序数据自己可以通过javap查看通常是局部变量表的序号所以他是1而iconst_3中3是没有保存的所以他会变化来进行对应 4 getstatic #2 java/lang/System.out : Ljava/io/PrintStream; //开始获取值获取的是他out的值 7 iload_1 //进行加载x指定了1那么在1这个位置在字节码后面1这个位置就是x他是保存的而不是像数值一样的没有保存 8 iload_2 //进行加载y 9 iadd //操作相加 10 invokevirtual #3 java/io/PrintStream.println : (I)V //运行该方法当然在编译期间自然会检查加载的变量是否符合该方法的参数类型这也是保存的原因上面得到的结果方法会操作获取的相当于istore_1获取iconst_1一样这是因为方法有参数很明显正是因为编译符合所以他方法执行才会合理而不会出现没有参数前面也进行加载或者操作出现值的情况即不会出现iadd或者iconst_1等等但一般编译期间会检查出来的虽然这里是运行期间那么很明显对应的获取out的值他只是获取进行考虑操作方法而执行方法会利用加载的值且是在获取的值上进行的一般class到jvm就是运行期间而java到class就是编译期间了在注解中第17章博客的RetentionPolicy.SOURCE这个有只在java这里即什么期间都没有的说明 13 return //将结果返回//其中0到13我们称为指令地址那么后面的自然就是操作指令了//之前的 0 getstatic #2 java/lang/System.out // getstatic 获取静态字段的值 3 ldc #3 Hello World // ldc 常量池中的常量值入栈 5 invokevirtual #4 java/io/PrintStream.println // invokevirtual 运行时方法绑定调用方法 8 return //void 函数返回//上面为什么没有5或者6呢一般代表是固定的数地址所以不能被占用所以进行跳过这里了解即可PC寄存器的特点 实际上在上面的说明中存放指令地址的就是pc寄存器的作用也就是说他存放指令地址注意对应的pc寄存器只是存放下一个的指令地址如果没有那么自然不会保存而没有的话说明已经执行到最后的字节码了自然是不用保存的一般从0开始当你的解释器识别class的可以认为是jvm虽然他们都是jvm里面的识别后识别后且在执行前虽然可能到c但最后由c执行所以这个过程是可以这样说的通常会让pc寄存器存放下一个指令地址这是可以保证中途的改变而不是固定当然这不是主要的由于即实际上PC寄存器是一块很小的内存空间只存下一条指令的地址几乎可以忽略不计所以主要的是只存放下一个指令也是为了空间节省实际上pc寄存器考虑很多问题如果没有他如果确定你的指令呢如果有他那么他是都存放还是只存放一个呢如果都存放直接的运行的确快但是要考虑空间由于对应的快并没有快多少因为反正你也只是一条一条的操作过去那么后面的完全不需要一直保存并且虽然切换的保存需要切换时间但是对于所占空间来说是微不足道的且由于是利用他执行的时间来保存所以基本是没有切换时间的那么综上所述pc寄存器只保存一个地址是最好的结果如果有其他好的方案自然会使用对应的好的方案具体看以后的jdk版本吧 通过上面的说明这里将前面的class变成c的说明进行修改修改成class通过类似的解释变成二进制就如我们可以将一种爬行动物称为狗狗一样也就是说jvm可以认为是c的另外一个解释器专门解释class的而不是解释c的所以java-class-二进制c-二进制即c相当于我们手写的class只是javajava文件进行了封装容易理解 1区别于计算机硬件的pc寄存器两者不略有不同计算机用pc寄存器来存放伪指令或地址而相对于虚拟 机pc寄存器它表现为一块内存因为他并不是一直存在jvm关闭他一般也会关闭因为当我们使用 Java 命令运行 .class 文件的时候实际上 就相当于启动了一个 JVM 进程当第一次运行那么jvm进程操作所有当都停止那么jvm进程也关闭自然pc寄存器也没有了当然了一般一个Java程序会开启一个JVM进程如果一台机器上运行3个Java程序那么就会有3个运行中的JVM进程一般jvm之间可以通信但是一般不会操作具体的变量通信如果是不同的jvm那么静态是不会共享的只属于自身的jvm中除了操作系统与jvm的共享比如端口只能一人进入但他们都可以操作并判断是否被占用虚拟机的pc寄存器的功能也是存放伪指令更确切的说存放的是将要执行指令的地址 2当虚拟机正在执行的方法是一个本地如native修饰的一般他代表根本方法而像什么java自带的方法则是jdk自带的而不是这个的jre如String就是jdk自带的当然并不是所有不是native都是jdk的有些基础的也是jre的只是native基本都是jre的因为对应的jar包或者zip一般jre是jar且只保存class文件是可以补充使用的而由于可以补充自然在jvm中不会给出对应的什么说明因为只是一个位置而已并不重要具体可以百度这里了解即可方法的时候jvm的pc寄存器存储的值是undefined 3程序计数器是线程私有的它的生命周期与线程相同每个线程都有一个 4此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域只保存一个在运行时保存由于运行基本必然比保存慢所以这里基本不会报错且运行之前就会提醒保存的 一般来说pc寄存器是每个线程都有的因为他是线程私有一般私有的代表线程创建之前都会给他一个pc寄存器或者其他说明私有的东西或者jvm给该线程一个pc寄存器或者其他说明私有的东西 Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的也就是抢占cpu在26章博客有说明在任何一个确定的时刻一个处 理器只会执行一条线程中的指令 因此为了线程切换后能恢复到正确的执行位置每条线程都需要有一个独立的程序计数器各条线程之间的计数 器互不影响独立存储我们称这类内存区域为线程私有的内存从这里可以发现对应获取内容时并没有修改地址所以是一个副本这就是为什么线程之间看起来可以操作同一个数据的原因因为是副本复制总体来说由于pc寄存器是基本操作即基础操作或者说程序的基础操作所以才会使得线程中得到的是副本的当然一般在我们得到指令地址并执行后才会考虑后续是否操作虚拟机中栈或者堆等等存在的操作的即先操作pc寄存器后一般才会考虑操作栈或者堆等等存在的操作的 实际上之所以需要私有是保证他们不会发生冲突或者出现问题所以在一些会发生冲突或者出现问题的情况下一般都会是私有的必然pc寄存器如果不是私有那么可能他操作指令地址时会跳过应该要操作的指令地址即发生冲突或者出现问题 虚拟机栈 什么是虚拟机栈 Java虚拟机栈Java Virtual Machine Stacks也是线程私有的注意是私有的所以每个线程独立拥有即生命周期和线程相同Java虚拟机栈和线程同时创 建用于存储栈帧每个方法在执行时都会创建一个栈帧Stack Frame用于存储局部变量表、操作数栈、动态 链接、方法出口等信息每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过 程 看如下代码 package com.test1;/****/ public class StackDemo {public static void main(String[] args) {StackDemo sd new StackDemo();sd.A();}public void A() {int a 10;System.out.println( method A start);System.out.println(a);B();System.out.println(method A end);}public void B() {int b 20;System.out.println( method B start);C();System.out.println(method B end);}private void C() {int c 30;System.out.println( method C start);System.out.println(method C end);}} 你可以认为一个大栈里面包含栈该栈也包含小栈如变量所在的栈即栈中的栈中的栈虽然小栈没有给出只给出栈栈帧和大栈虚拟机栈当然虚拟机栈是在jvm里面的利用jvm的内存虽然jvm利用物理内存电脑的一般不会利用jvm存在的保存的物理内存即jvm一般是没有上限的当然对应的jvm是一系列的结合他们都有对应物理内存没有的报错处理这里了解即可 好了一个图片你可能并不相信那么现在我给出一个流程看如下 首先在对应的StackDemo sd new StackDemo();进行调试 我们注意左下角首先可以看到红色勾勾打上的是main这里代表是main的线程即主线程即当前线程是主线程main而上面对应的Frames通常代表这里是大栈的地方所以下面的main:8代表已经入栈了现在我们继续执行进入A方法 可以看到A入栈了即的确是放在上面的栈的特性记得是先入后出哦我们执行到最后的C 可以发现都入栈了执行完C可以发现他会出栈直到都执行完毕那么都出栈即程序结束所以一般默认的栈都是mainmain出栈那么说明该程序执行完毕了当然并不是出栈立即关闭自然是优雅的关闭101章博客有过说明但是这只是针对该线程来说的如果有其他线程那么考虑的是他的出栈虽然他的起始线程不是主线程但是对于他来说开启的第一个线程或者说第一个方法就是他的主线程虽然我们可以设置了他的名称至此流程的确正确演示完毕其中A:16B:23中的16和23代表是该16行或者23行开始加入其他的栈的而16行是B();23行是C(); 什么是栈帧 栈帧Stack Frame是用于支持虚拟机进行方法调用和方法执行的数据结构栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息每一个方法从调用至执行完成的过程都对应着一个栈帧在虚拟机栈里从入 栈到出栈的过程 当然了上面的放大结构自然可能有其他的信息但是上面四个是主要的所以主要给出这四个 设置虚拟机栈的大小 -Xss 为jvm启动的每个线程分配的内存虚拟机栈内存大小默认JDK1.4中是256KJDK1.5中是1M当然在不同的操作系统上可能也有不同 Linux/x64 (64-bit): 1024 KB macOS (64-bit): 1024 KB Oracle Solaris/x64 (64-bit): 1024 KB Windows: The default value depends on virtual memory 虽然都可以操作class但是也只是class即jvm可能会与操作系统相匹配 主要是这样的设置 -Xss1m -Xss1024k -Xss1048576 #1024*10241048576 #所以后面表示的是b8个字节8bit我们来看看这个代码 package com.test1;/****/ public class StackTest {static long count 0;public static void main(String[] args) {count;System.out.println(count); //大概在85xxxx代表浮动左右就会溢出但是有波动也就是说他虽然有上限但是每次入栈的栈帧内存可能是忽上忽下的完全平均很难虽然也存在因为需要考虑cpu的变化以及内存的变化一般有些是按照大小有些是按照时间当然这是一个解释另外一种解释就是虽然设置了上限但是有兜底上限即到触发设置的上限时兜底上限就触发了先触发上限这个才会触发使得可以超过一点给错误一些位置而不是等待错误出现那么由于错误出现的快慢程序的任何因素基本都会影响而导致出现的浮动当然错误出现之前需要停止他的执行使得停止打印所以错误后没有对应打印了一般考虑的是兜底上限而不是栈帧不一因为错误最后也有打印那么一般是兜底上限的打印了即他会进行操作一般是设置的某个比例打印完后一般自动结束程序//当然兜底可能有但是上面的解释并不是一定正确只是有一点道理的而已忽略即可main(args); //无限递归虽然入口是这个main方法但并不是不能递归哦} }//注意对应的错误是Exception in thread main java.lang.StackOverflowError一般来说虚拟机栈和本地方法栈有他们因为他是用来考虑栈溢出的所以在前面的图片中他才在异常对应的下面给他们这个为了更加的明白他怎么回事我们点击如下若没有对应的类出现可以选择启动然后一般就有了 在新版idea中一般有些选项是不会显示出来的对于老版本idea来说即需要自己弄出来所以我们点击右边的Modify options英文意思修改选项然后点击如下 那么就出来了他代表虚拟机栈的大小设置若没有设置那么按照默认来操作对应的上限的到这个上限就会出现报错 我们设置成-Xss256k 点击ok退出继续执行来看一看记得在执行class时jvm才会开启进程所以会利用到该配置而不是操作默认的在idea中设置的配置通常会到对应自己给idea设置的jdk里进行设置但是只是副本而已指定文件谁不会呢为了验证可以在其他类里也操作但不进行设置即可即发现的确是副本 可以发现对应的结果到19xx我的是多就停止了所以我大胆猜测默认值是1mJDK1.5中是1M虽然也的确是这样即8个栈帧内存其中一个栈帧中基本没有多余的变量或者其他操作即可以认为是一个空方法进入的大概是1k内存很明显我们可以进行设置来使得提前结束即可以操作该方面的优化使得不会占用使用多数的内存即我们终于手动操作了jvm的第一个优化方案了对于这个博客来说的 局部变量表 上面说明了虚拟机栈现在我们说明他里面的内容栈帧的内部包括栈但是以栈为主所以一般都会将该内部称为栈虽然还有其他内容有四个主要的内容哦看之前的图就知道了 局部变量表Local Variable Table是一组变量值存储空间用于存放方法参数和方法内定义的局部变量包括8种基 本数据类型、对象引用reference类型和returnAddress类型指向一条字节码指令的地址其中64位长度的long和double类型的数据会占用2个局部变量空间Slot具体可以百度空间大小一般可能与类型有关其余的数据类型只占用1个即以32为主 可以看看这个图 其中long类型的数据占用两个空间而int类型的数据和byte类型的数据都只占用一个空间现在我们来进行观察并验证看如下 package com.test1;/****/ public class PC {public static void main(String[] args) {int x 1;int y 2;System.out.println(xy);} } 上面是之前的代码 0 iconst_11 istore_12 iconst_23 istore_24 getstatic #2 java/lang/System.out : Ljava/io/PrintStream;7 iload_18 iload_29 iadd 10 invokevirtual #3 java/io/PrintStream.println : (I)V 13 return上面是对应的字节码的主要内容 现在我们来点击如下 我们看这个名称可以知道是局部变量表我们也点击这个 看他的名称就知道是行号表我们先说明他我们可以看到他有 0 0 8其中起始PC的0代表我们的指令地址也就是pc寄存器的那个0对应iconst_1而后面的行号则对应源码中的那一行也就是int x 1;所以说对应的Code的点击他不只是保存了字节码的主要内容也保存了对应的PC指令与源码行的关系这也是使用对应插件的一个好处但是很明显他只会保存最小的因为其中PC的指令地址中1也算那一行但是我们只会保存一次且从小到大所以这里是0那么同理第二个就是2了主要Nr.只是排列编号并不参与任何说明即行编号但是我们可以进行观察那么行号是11的为什么也存在呢实际上在没有返回值时默认最后一个大括号是返回的行数所以中间有多个回车改变他在源码的行会改变他的行号数的即相当于返回了否则就是return所在的行了至此行号表说明完毕 现在我们继续看对应的局部变量表只是现在给出完整的图 你看到表可能会有一个疑惑其中为什么PC为0时是args实际上这代表上一个变量的存在即记录上一个变量的信息代表到我这里已经使用了他这个空间了其中cp_info #15代表该变量在常量池的位置你点击就知道了 常量池在java用于保存在编译期已确定的已编译的class文件中的一份数据它包括了关于类方法接口等中的常量也包括字符串常量如String s java这种申明方式一般java在常量池中具体可以看20章博客因为String一般是得到常量的值的且他不能修改所以在不是创建对象时String的值通常会认为是常量由于String不能修改所以有时候我们也会将String类型叫做字符串常量类型 很明显他是将所有的信息以字符串来表示那么对应的描述符的l就代表int了引用一般代表类似于全限定名的名称看前面的图就知道了虽然他是数组但是我们只看基础类型数组会让[开头而具体类型不会所以加起来就是int x且是上一个变量的意思所以他对应表示是我使用了x还有该长度 那么对应的长度代表什么实际上长度代表局部变量表空间他是计算出来的当我们分配好后若第一个是14代表我们开辟了4个空间那么如果是编号0那么是14起始默认为10且是代表使用了args空间的且一个空间代表长度2那么该插件会利用计算来记录总空间编号0加上开辟的空间的每有一个对应类型如int那么对应进行减2所以其中一开始是14因为在减去args后还是10且加上了x和y的总体4那么是14到最后回到10为了进行验证可以将y的类型变成long发现从16开始了所以也验证了long是占用两个空间的缘故并且你可以选择不改变为long而是凭空加上一段代码也是一样的赋值代码可以是int yy 1;会发现也从16开始了所以对应的long占用两个的确是正确的总结局部变量表是变量使用空间的地方简单来说就存放变量的地方至此局部变量表说明完毕注意序号直接代表操作上一个变量名称序号那么1就是x所以对应的是istore_1这个1要记得字节码还没有执行他只是保存而已我们可以发现他自身就有很多关系使得执行时的确有对应的来源虽然可能需要jvm的其他识别如istore_1中1代表x外istore_代表存储的意思这就是jvm的作用了并且虽然1代表x但是也需要jvm来识别操作所以class定义初始信息jvm识别信息执行 操作数栈局部变量说明完毕现在来说明这个 操作数栈Operand Stack也称作操作栈是一个后入先出栈LIFO随着方法执行和字节码指令的执行会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈再随着计算的进行将栈中元素出栈到局部变量表或者 返回给方法调用者也就是出栈/入栈操作 通过以下代码演示操作栈的执行 package com.test1;/****/ public class StackDemo2 {public static void main(String[] args) {int i 1;int j 2;int l j;int z i j;} } 对应的主要class文件main里面的 0 iconst_11 istore_12 iconst_23 istore_24 iload_25 istore_3 //对应的值给z了一般3后面就是直接的4了不加_了规定的节省一些数6 iload_1 //很明显这个1也是代表x而不是具体的数字7 iload_28 iadd9 istore 4 11 return //即除了iconst外iload和iadd都可以使得保留数据操作从而相应istore可以进行赋值 现在我说明一下操作数栈的理解你可以认为是存放数据的地方也就是说上面的iconst_1或者iload_2或者iadd的结果都会放在操作数栈里面进行操作如iadd相加或者保存而获取他们操作的变量就放在局部变量表里面每次变量要操作对应数据时就是从操作数栈里面进行拿取从而实现赋值等等至此操作数栈说明完毕这里看明白了那么操作数栈也就明白了 动态链接 Java虚拟机栈中每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用持有这个引用的目的是为了 支持方法调用过程中的动态链接Dynamic Linking 动态链接的作用将符号引用转换成直接引用 案例 package com.test1;/****/ public class DynamicLink {public static void main(String[] args) {Math.random();} }//对应的字节码信息我们主要看对应的main说明的字节码以后也是这样的即以后不提醒了 0 invokestatic #2 java/lang/Math.random : ()D 3 pop2 //大胆猜测是对应的里面的底层特殊变量可能共有得到值若有返回那么就是操作对应的值即他代表方法的默认返回值只是没有人得到而已一般来说之前的istore_1中的is的i代表他是int类型若是其他引用类型一般代表a开头其他基本类型按照首字母小写开头 4 return //我们点击这个#2可以到常量池中如下像下面的类名和描述符就代表他们是一个整体的所以不要认为不是整体的哦前提是没有什么L开头的否则可能说明他的执行一般不会带上下面的操作因为一般只是代表类型而不是真的具体方法即他里面有操作变量调用而不是直接操作方法如之前的java/lang/System.out而他引用对应显示的就是Ljava/io/PrintStream; 从右边可以看到对应的cp info相关的信息没有下滑线了实际上对应的是进入的意思即到#19所以之前的也是这样所以之前的cp_info #15就是代表到#15虽然在局部变量表在有下划线但是他只是去对应位置而已就如我们操作前端的跳转a标签可以设置名称他只是表示不同的名称罢了虽然没有统一 现在我们继续看看这个#2很明显他也保存了对应的类名和对应的描述当然他们总体来说一般指这个方法的说明来源信息最终当没有具体的跳转时说明到头了自己可以点击跳转所以对应的步骤就是找到方法资源执行方法很明显他是通过符号来进行引用的所以我们也认为动态链接的作用是将符号引用转换成直接引用也就是通过符号来引用来得到对应的引用这样是为了保证指向的要不然我怎么知道使用常量池的那个呢或者说如何好的确定使用谁呢因为对应的常量池不是栈帧里面的而是方法区或者说元空间里面的根据版本来决定不同的说明前面有说明比如图片信息 一般来说在不同的地方通常需要引用来进行更好的确定使用谁就如字典你都需要对应的笔画或者什么拼音来确定哪一页而不是都进行找实际上这不是主要的因为对于字节码来说可以直接的显示所以最主要的是若你不操作引用那么你肯定是直接的操作显示出来那好如果有其他人也操作这个那么你又要显示很明显要显示多个而引用是一个总显示只需要少的字节码如#2就可以替换掉java/lang/Math.random : ()D就可以显示了当然一开始是少的最后替换时还是需要替换回来总而言之一开始少10最后多2总体少8这是基本的只是我们这里将该引用称为一个名称而已动态链接可能动态链接是操作如何引用到常量池的说明的所以我们应该要这样的理解这里了解即可 方法返回地址 方法返回地址存放调用该方法的PC寄存器的值 一个方法的结束有两种方式正常地执行完成出现未处理的异 常非正常的退出 无论通过哪种方式退出在方法退出后都返回到该方法被调用的位置方法正常退出时调用者 的PC计数器pc寄存器的值作为返回地址即调用该方法的指令的下一条指令的地址当然方法内容的指向并不会与pc寄存器有关pc寄存器只是给出对应字节码中要执行的地方至于怎么执行他不会过问或者说他对应的执行可能是包含所有联系的也就是说也保存了对应方法的字节码可以自己在对应类里加上方法可以发现会在对应插件的方法哪一类中多出了该方法即除了main外他也存在那么他也会继续工作而当返回时会返回调用者的下一跳指令当然可能pc寄存器或者说给pc寄存器赋值的地方操作会保留对应的调用者的这里了解即可 而通过异常退出的返回地址是要通过 异常表来确定也就是异常信息栈帧中一般不会保存这部分信息无论方法是否正常完成都需要返回到方法被调用的位置程序才能继续进行然后看是否处理异常异常表来决定是否直接返回当然在main中返回的话自然说明程序结束了其中异常表一般是你操作了异常或者抛出什么而出现的你可以试着手动抛出异常来看看是否有对应的异常表信息可能没有具体可以百度这里了解即可 本地方法栈 本地方法栈Native Method Stacks 与虚拟机栈所发挥的作用是非常相似的 其区别只是虚拟机栈为虚拟机执行Java方法也就是字节码 服务 而本地方法栈则是为虚拟机使用到的本地Native 方法服务 特点 1本地方法栈加载native的所有方法native类方法存在的意义当然是填补java代码不方便实现的缺陷而提出的 2虚拟机栈为虚拟机执行Java方法服务而本地方法栈则是为虚拟机使用到的Native方法服务 3是线程私有的它的生命周期与线程相同每个线程都有一个 在Java虚拟机规范中对本地方法栈这块区域与Java虚拟机栈一样规定了两种类型的异常 StackOverFlowError线程请求的栈深度所允许的深度之前的虚拟机栈的溢出就是这个即Exception in thread “main” java.lang.StackOverflowError也就是说虽然之前说明的是虚拟机栈的大小但是实际上只是定义上限因为上限也是可以说成大小的并不是真的拿取的总内存虽然他也能说成大小所以最好建议以测试案例为主看错误即可 OutOfMemoryError本地方法栈虚拟机栈扩展时无法申请到足够的内存我们拿取的内存快没了快用完了不代表物理主机没有上面是超过上限而不是没有内存这里可以认为在栈帧中有上限如果栈帧中的数据超过栈帧本身即栈帧内的内存不够了那么就报这个错误很明显与堆不同的是堆定义的上限中是直接的超过就是超过而这里由于虚拟机栈的原因他还存在栈的深度即栈帧有上司而存在相应错误的堆没有所以这也是为什么只有对应栈即虚拟机栈和本地方法栈有两个报错的主要原因 在后面也会继续补充对OutOfMemoryErrorOOM的说明 这里来说明一下为什么native会影响到java实际上我们都知道他们都要变成二进制执行的那么实际上native也是变成二进制的二进制的操作是可以进行互通的所以会影响到只是你并不知道他干啥了而已或者说不知道他的二进制与你的二进制有什么关系而已 堆 Java 堆概念 简介 对于Java应用程序来说 Java堆Java Heap 是虚拟机所管理的内存中最大的一块Java堆是被所 有线程共享 的一块内存区域引用操作指向我们只保存引用改变指向的内容其他操作引用即操作指向的也会改变即共享一般在虚拟机启动时创建此内存区域的唯一目的就是存放对象实例 Java 世界里几乎所有的对 象实例都在这里分配内存几乎是指从实现角度来看 随着Java语 言的发展 现在已经能看到些许迹象表明日后可能出现值类型的支持 即使只考虑现在 由于即时编译技术的进步 尤其是逃逸分析技术的日渐强大 栈上 分配、 标量替换优化手段已经导致一些微妙的变化悄然发生 所以说Java对象实例都分配在堆上也渐渐变得不是 那么绝对了并不会一成不变哦 堆的特点 1是Java虚拟机所管理的内存中最大的一块 2堆是jvm所有线程共享的堆中也包含私有的线程缓冲区 Thread Local Allocation Buffer TLAB具体作用可以百度 3在虚拟机启动的时候创建 4唯一目的就是存放对象实例几乎所有的对象实例以及数组都要在这里分配内存可能有其他目的如上面说的私有的线程缓冲具体可以百度 5Java堆是垃圾收集器管理的主要区域 6由于Java堆是垃圾收集器管理的主要区域因此很多时候java堆也被称为GC堆Garbage Collected Heap从内存回收的角度来看由于现在收集器 基本都采用分代收集算法所以Java堆还可以细分为新生代和老年代新生代又可以分为Eden 空间、From Survivor空间S0、To Survivor空间S1 给出之前的图片 7java堆是计算机物理存储上不连续的、逻辑上是连续的是堆自己的存放方式不是他里面的内容哦之所以是逻辑上是因为通过某种方式连接起来的即有连续方法连接具体可以百度也是大小可调节的通过-Xms或者-Xmx控制虽然下图没有给出图中可能有错误的所以我们以案例为主 继续给出图片 8方法结束后堆中对象不会马上移出仅仅在垃圾回收的时候时候才移除一般来说垃圾回收机制就是识别到你这个对象没有人使用时就会移除由于是识别到所以并不是在一定时间内移除的即不定期或者间歇的当然对应的回收有很多种这里只是考虑始终操作识别就移除还有其他的GC即其他的如新生代收集在后面会说明 9如果在堆中没有内存完成实例的分配并且或者说堆也无法再扩展时将会抛出OutOfMemoryError异常没有内存了 设置堆空间大小 内存大小-Xmx/-Xms 使用示例-Xmx20m -Xms5m说明 当下Java应用最大可用内存为20M 最小内存为5M 测试 package com.test1;/****/ public class TestVm {public static void main(String[] args) {//补充//byte[] bnew byte[5*1024*1024];//System.out.println(分配了1M空间给数组);System.out.println(Runtime.getRuntime()); //他是私有的构造System.out.print(Xmx);//Runtime.getRuntime()可以取得当前JVM的运行时环境这也是在Java中唯一一个得到运行时环境的方法//Runtime.getRuntime.maxMemory//Java虚拟机能构从操作系统那里挖到的可用的最大内存最大堆的大小以字节为单位如果内存本身没有限制则返回值Long.MAX_VALUE//即Native public static final long MIN_VALUE 0x8000000000000000L;也就是long的最大数正的不是负的操作下面的两个除号结果是8 7960 9302 2207即8万亿mb比内存大多了/*long最大是2^63-1922 3372 0368 5477 5807922 3372 0368 5477 5807除以1024大致得如下取整的因为反过来乘就不对了可以自己测试9007 1992 5474 0991 以这个整数为例除以1024大致得如下取整的因为反过来乘就不对了可以自己测试8 7960 9302 2207*/System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 M);System.out.print(free mem);//Runtime.getRuntime.freeMemory//已经占用但实际并未使用的内存也就是没有使用的内存调用gc可能会增加他的值因为去申请内存了也就是说内存我拿过来了但是你可能没有使用只是这个内存被你分配了即只能给你但不能给其他人可能你并没有使用他//一般情况下内存是挖多少用多少但是也可以是挖多点但我不使用System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 M);System.out.print(total mem);//Runtime.getRuntime.totalMemory//已占用的内存初始堆大小默认的因为有最小的System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 M);/*注意JVM最大分配的堆内存由-Xmx指定默认是物理内存的1/4如当前电脑的总内存-Xmx 也指 maxMemory的值maxMemory()方法但是maxMemory的值一般会小于-Xmx总要留有一点余地除非由最小值来操作了或者快满了比如最大和最小都是一样的那么就是该值了后面设置年轻代和老年代的比例时的配置就是如此注意虽然最大最小也是小于该值快满了也是但大于maxMemory的值虽然冗余少点JVM初始分配的堆内存由-Xms指定默认是物理内存的1/64默认空余堆内存小于40%时JVM就会增大堆继续挖直到-Xmx的最大限制那么不会挖那么这个40%就不会进行拦截了而正是不会直接增大到最大限制所以Runtime.getRuntime().freeMemory()的实际未占用的只是代表当前最大的剩余所以程序执行的结果他们两个相加一般不会是maxMemory()的结果*/}} 执行后看你的结果我的是 Xmx2012.0M free mem122.03204345703125M //这个地方可以自己来操作创建对象来验证是否改变他的值通过验证就算是对应的一个String aa new String(1);都可能使得他减少可能会增加调用gc了具体可以百度测试的体量太小了可能并不能完全测试出减少的情况所以在后面的分配5m体量够大了中会出现减少当然并不决定如果你消耗的内存比申请的少那么他还是会提升的所以该值的提升与否不重要因为他只是记录剩余没有使用的而已而又因为会提高上限所以他的值并不能预测提高还是减少这里了解即可 total mem126.0M //由于他没有具体的小数值可能是忽略的所以看不出来设置成这样记得中间要有空格不管有多少只要有就行即符合格式否则会报错会检验的既然idea可以操作建议那么java自身也可以检验很合理吧 对于的还是之前的操作只是输入不同的所以可以看出对于的VM options是一个在某个位置加上信息的选项你输入的不同那么jvm读取对应的信息虽然是副本所操作的结果自然不同设置好后我们继续执行 Xmx20.0M free mem4.757331848144531M total mem6.0M结果不同了哦 可以发现这里打印出来的Xmx值和设置的值之间是由差异的total Memory和最大的内存之间还是存在一定 差异的就是说JVM一般会尽量保持内存在一个尽可能底的层面而非贪婪做法按照最大的内存来进行分配并且虽然我们最小的是5m但是堆可能需要一些固定操作使得要固定占用一些空间所以是6m 在测试代码中新增如下语句申请内存分配 将上面的注释打开 byte[] bnew byte[5*1024*1024];System.out.println(分配了5M空间给数组);在申请分配了5m内存空间之后total memory上升了同时可用的内存可能也会发生改变可以发现其实JVM在分配内存过 程中是动态的 按需来分配的 我们将5改成20那么会出现如下 Exception in thread main java.lang.OutOfMemoryError: Java heap space之前的说明是OutOfMemoryError本地方法栈虚拟机栈扩展时无法申请到足够的内存 这里就要进行补充回应之前的在后面也会继续补充对OutOfMemoryErrorOOM的说明即堆也加上我们可以发现是扩展时无法申请所以与虚拟机栈一样的是超过上限才会出现报错的自然也是设置的上限虽然他们都也有默认的值这里是对总的拿取的内存不是所有的物理内存设置上限总堆或栈内存而不是操作需要的内存设置上限之前的栈错误即在拿取里面的内存做限制比如我从系统的100中拿取101-10的范围的内存那么你在4-6这个范围地方做限制比如刚好是5这里认为虚拟机栈在该4-6部分进行操作的那么超过5报对应的栈错误有兜底所以会保留虽然有点夸张而类似只是类似的的超过10如果是栈帧报这里的内存错误至于虚拟机栈和本地方法栈的该错误这里并没有演示以及本地方法栈的另外的错误包括堆也是如此具体演示可以看百度 堆的分类 现在垃圾回收器都使用分代理论堆空间也分类如下 在Java7 Hotspot虚拟机中将Java堆内存分为3个部分 青年代Young Generation老年代Old Generation永久代Permanent Generation虽然是说明方法区那里的由于这里与堆有太多联系一般也可以说成是堆的 在Java8以后由于方法区的内存不在分配在Java堆上而是存储于本地内存元空间Metaspace中所以永久代就不 存在了在以前可能是2018年9月25日Java11正式发布以后官网上可以找到了关于Java11中垃圾收集器的官方文档 文档中通常没有提到永久代而只有青年代和老年代了 我们可以通过之前的代码进行试验来看到一些关于他们的信息操作了GC这里了解即可 package com.test1;/****/ //这里重新创建一个 public class test {public static void main(String[] args) {System.out.print(Xmx);System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 M);System.out.print(free mem);System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 M);System.out.print(total mem);System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 M);} } 修改对应配置 PrintGCDetails代表加上GC的打印细节即给出对应的GC打印信息一般还有PrintGC代表基本信息可用认为PrintGCDetails里面包括了PrintGC即我有你的信息但是也有你没有的信息 然后我们执行看看结果 可以发现有很多信息了当然不同jdk版本可能信息不同可以在上图的位置里面找到并自己设置版本不知道可以百度注意了低的版本操作原来的打印可能会报错因为可能原来的版本中没有或者规定了不兼容具体看打印信息有没有报错吧具体问题具体分析一般情况下我们需要将项目版本进行修改前提可以设置通常项目版本和语言版本语言版本不对没有事因为我们通常操作副本即编译版本和运行版本的副本其中语言版本决定是否有idea的检查也就是说idea的功能若没有可能没有启动按钮需要在右上角那里启动了但是项目版本一般需要目标语言版本一致否则报错目标语言版本后面会说明和编译版本和运行版本通常要对应其中高版本可以兼容低版本编译版本比如这个就是副本他都可以修改了不是吗默认没有修改的话一般是项目版本运行版本也是如此 项目版本和语言版本点击这个 这两个就是项目版本和语言版本目标语言版本就是Modules里面的语言版本也就是说该语言版本覆盖了当前的语言版本所以才说他语言版本不对没有事若他不对应项目版本那么运行时会提示报错出现java: 警告: 源发行版 11 需要目标发行版 11目标发行版本就是项目版本源发行版本就是目标语言版本注意目标语言版本可以低于项目版本即高版本兼容低的而运行版本是如下自然也是副本前面也对设置虚拟机栈的大小的对应参数进行加上时也说明了 这个11按照高版本可以兼容低的运行可以大于编译反过来不行因为是运行来识别的class到二进制到执行那么很明显11大于8可以执行不会报错实际上就算报错可能对应的打印信息还是会出现因为并不会是对应的线程的只是对应的不会识别而报错的而已 即不兼容那么会报错的且一般并不决定主要看jdk的开发者但一般都会这样做高版本jdk可以兼容低版本jdk的反过来不行因为高版本是增加一些或者在兼容的情况下修改或者增加而不是单纯的减少一些或者使得不兼容了但是现在这些打印GC信息我们并不需要了解所以这些信息看看就可 而之所以认为对应是副本是因为在不同的类中可以选择不同的选项当然在编译版本那里一般也是副本是因为在不同的项目里也可以进行设置所以idea就是一系列版本的组合使用理解更深了吧这些组合可以认为是通过某种连接来导致使用对方就如引用一样可以通过引用使用其他地方的东西当然这里了解即可 当然还有一点项目版本兼容编译版本高兼容低一般高兼容低的反过来不能兼容这里要注意 总体来说他们基本都需要对应其中项目对应整个项目的操作如依赖版本等等信息虽然运行版本可能覆盖但是只会覆盖一样的所以多余的不会即在某些时候他确可能需要其中一个低版本但是大多数时候可以不对应运行版本编译是编译运行是运行加起来就是整个环境使得操作成功 注意实际上与其说高版本兼容低的还不如是他们自己的判断方式是认为这样的因为项目版本比较重要运行版本比较重要所以项目兼容低高兼容低包括语言编译运行兼容低编译而不是覆盖者兼容低所以这是自己的判断方法一般是重要判断且他们两个却是相反的一个被覆盖项目版本被覆盖一个覆盖运行版本覆盖项目并没有覆盖编译哦但都也是兼容低的所以是自己的判断方式哦一般是重要判断 年轻代我们可以将青年代称为年轻代或者新生代和老年代 JVM中存储java对象可以被分为两类 年轻代Young Gen年轻代主要存放新创建的对象内存大小相对会比较小垃圾回收会比较频繁年轻代分 成1个Eden Space和2个Suvivor Spacefrom 和to 年老代Tenured Gen年老代主要存放JVM认为生命周期比较长的对象经过几次的Young Gen的垃圾回收后仍然存在特别是其中的其他两个区的其中一个一般有阈值比如如果经过几次识别还存在那么放在这里的年老代里面去为什么是其中一个在后面会说明内存大小相对会比较大垃圾回收也相对没有那么频繁 配置新生代和老年代堆结构占比 默认 -XX:NewRatio2注意你复制粘贴记得自己观察一下是否正确因为复制粘贴的可能有问题最好手写标识新生代占1 即默认是设置比例的即1:2即新生代是1且不变因为是比例只需要设置一人即可因为可以将一人除以到1的, 老年代占2新生代占整个堆的1/3 修改占比 -XX:NewRatio4标识新生代占1老年代占4新生代占整个堆的1/5从而导致在占用的过程中也是按照这个比例这里代表是总体的即都占满后是堆的1/5当然过程也是一样的我们也需要堆自己扩大到上限的 在新生代中Eden空间和另外两个Survivor空间一般是占比分别为8:1:1 可以通过操作选项 -XX:SurvivorRatio 调整这个空间比例比如 -XX:SurvivorRatio8就算是上面的比例即其他两个是1且不变因为是比例只需要设置一人即可因为可以将一人除以到1的之所以他们两个是1因为有一个是空闲的在后面会说明所以他们需要一样的 几乎所有的java对象都在Eden区创建但80%的对象生命周期都很短创建出来就会被销毁也有一些幸存者存放在对应的其他两个区中其中一个 从图中可以看出 堆大小 新生代 老年代其中堆的大小可以通过参数 -Xms、-Xmx 来指定 默认的新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 该值可以通过参数 -XX:NewRatio 来指定 即新生 代 ( Young ) 1/3 的堆空间大小老年代 ( Old ) 2/3 的堆空间大小其中新生代 ( Young ) 被细分为 Eden 和 两个Survivor 区域这两个 Survivor 区域分别被命名为 from 和 to以示区分 同样默认的Eden : from : to 8 : 1 : 1 ( 可以 通过参数 -XX:SurvivorRatio 来设定 )即 Eden 8/10 的新生代空间大小from to 1/10 的新生代空间大小 JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务所以无论什么时候总是有一块 Survivor 区域 是空闲着的这里就解释之前的为什么是其中一个在后面会说明这个地方也解释了为什么占比是同样的原因如果不一样那么选择的可能少或者多即对判断不公平发现为什么我重新运行一下程序选择了另外一个有变化了呢因此新生代实际可用的内存空间为 9/10即90%的新生代空间 接下来我们来进行另外一个测试 package com.test2;/*** 这里我们来考虑设置以及年轻代中eden和form和to的比例*/ public class test {public static void main(String[] args) throws InterruptedException {System.out.println(hello);Thread.sleep(30000); //最好再大一点} } 设置如下一般来说加上了-XX通常代表赋值需要号具体还是以我的测试为主 但是虽然设置了那么如何看到效果呢这就需要一个工具了在jdk文件中找到如下主要给出找jdk8版本里面的其他版本中可能没有但可以在网上直接下载该工具但是最好下载jdk8因为他这个工具可能需要某些关联而jdk8是自带的即单独的下载可能运行不了这个工具一般不可以跨版本测试的因为他是有联系的工具且没有互相兼容的哦即jdk8的只能操作jdk8的版本11只能操作11而没有高版本兼容低版本的11操作8 我们双击进入到如下VisualVM英文意思是可视化虚拟机 注意看本地里面的信息现在只有三个好了我们执行上面的程序再过了看看如下 可以发现多出来了一个但是程序结束后他也随着消失自动出现自动消失所以我才会操作睡眠最好再大一点点击他 可以再下面的JVM参数中看到相关的信息我们也可以添加插件点击工具里面的插件当然可能现在对应的服务器不开放了所以下载不了你可以选择使用其他服务器或者说代理具体可以百度这里先给出一个解决方式 到如下 再设置中点击右边的编辑改变URL为https://visualvm.github.io/archive/uc/7u60/updates.xml.gz改变服务器地址了具体可用再这里进行找URKhttp://visualvm.github.io/pluginscenters.html 然后到可用插件可用发现多出后缀了说明有插件了而不是之前的什么都没有里面选择如下进行安装 点击安装后点击下一步即可等等吧如果出现报错说明该URL地址中虽然可用给出可用插件但是他可能并不能去下载那么我们换一个URL再上面的地址中复制这个 继续编辑URL来进行安装吧安装好后可用选择重新打开对应的本地的那个程序点击xx再打开会出现如下 我们可用看到对于的年轻代是901515是120而老年代是480即是14但是对应的年轻代中却是611当然了可能有些是没有记录的他可能会有些冗余不会操作冗余在这里代表保留空间当然了这只是比例我们也知道由于需要按照他们的比例进行分配堆大小所以在达到最大值时中间的数据包括各自上限且是当前的即当前上限是因为堆也会慢慢的增加所以是按照他当前上限来分配的而由于堆有最大那么一般也会使得对应的各自上限也有最大那么当他们继续提升上限时由于冗余的存在那么最后还是看堆总体上限使得出现对应的错误一般来说当操作冗余时不会进行移动了也会按照这个比例虽然这样说但是实际上数据可能并不会只有上限会因为垃圾回收机制的清除所以最好将这个数据看成当前上限值而不是数据来进行添加数据看成当前上限需要堆自己扩大上限的各自上限的总和直到设置的上限其中各自的一个上限提升也会带着其他上限提升这个冗余可以认为是之前说明的但是maxMemory的值一般会小于-Xmx即主要是操作这个地方当然了上面对应的只是代表当前的上限即15和90而并不是真正的数据因为一个hello怎么可能占用15m或者90m呢 扩大的上限一般也是按照比例老年代一般也是即eden导致幸存者他们总体新生代导致老年代 好了我们进行添加设置在对应的设置后面加上-XX:SurvivorRatio8继续执行看这里 可以发现发生了改变至此可以得出结论如果你没有设置按照默认的他会对最大的并且中间操作的进行冗余而设置后最大的冗余而中间上限的不会了虽然后面的最终上限是自动扩大了或者说隐藏起来使得显示都是12了 当然他的显示是固定的也就是说就算你关闭程序他也存在除非你自己退出固定的自然不会改变数值 对象分配过程 JVM设计者不仅需要考虑到内存如何分配在哪里分配等问题并且由于内存分配算法与内存回收算法密切相关 因此还需要考虑GC执行完内存回收后是否存在空间的中间中产生了内存碎片而对于内存来说可以认为是操作数内存的碎片可以认为是占用了他们在前面我们直到二进制和结果之间有中间而中间是有限的对应我的世界来说方块是有限的那么你用一些少一些即中间用来处理操作的方块也就少了那么这个可以操作的我们可以认为是内存 分配过程 1new的对象先放在伊甸园Eden的别称区该区域有大小限制到下面的第二步 2当伊甸园Eden的别称区域填满时程序又需要创建对象那么JVM的垃圾回收器将对伊甸园预期进行垃圾回收Minor GC新生代收集后面会说明的注意是这个GC有些GC一般不需要条件而这个一般需要在Eden填满后操作他在将伊 甸园区域中不再被其他对象引用的对象进行销毁并再加载新的对象放到伊甸园区时剩余的进行移动即他是考虑经历是否垃圾回收来进行移动的当然并不是所有的GC填满才回收有些是始终进行操作的前面说明的GC就是这个但是这里需要进行补充由于GC需要资源所以并不会始终的操作自动所以一般来说GC都需要条件但是为了不进行多出的STW后面会说明所以通常也会存在自动的GC这里与其他博客说明不同的是代表虚拟机关闭的优雅关闭了解即可会直接进行遍历关闭具体可以百度上网查找具体操作一般我们认为自动gc只会在关闭虚拟机触发可以回收所有一般没有限制所以大多数gc我们只能操作手动来进行优化即自动gc我们直接忽略 3然后将伊甸园区中的剩余对象移动到幸存者0区要满足比例注意并非一开始一定是幸存者0区可能是幸存者1区这只是一个随机判断而已随机的判断代码是可以做到的如随机数操作即可这里考虑幸存者0区 4如果再次触发垃圾回收此时上次幸存下来的放在幸存者0区的如果没有回收就会放到幸存者1区 5如果再次经历垃圾回收此时会重新返回幸存者1区改变位置了然后继续会接着再去幸存者0区再次的经历后所以他们只有一人是空闲的这就是主要的原因那么为什么不一直保留呢这样的来回不是很麻烦吗实际上他的空闲只是对他来的数据来说的所以他一般有其他功能由于要使得内存分配合理那么他就应该进行分开来使得他变得流畅而不是都放在一起使得变得卡顿所以需要分开 6如果累计次数到达默认的15次或者放不下了上面的第3步就是第一次即触发垃圾回收一次就算一次根据上面流程即幸存者0区开始那么当经历15次时也就是到幸存者0区了也就是S0时那么当再次经历时就会到养老区了这会进入养老区老年代可以通过设置参数调整阈值 -XX:MaxTenuringThresholdN 7养老区内存不足时会再次触发GC:Major GC 进行养老区的内存清理 8如果养老区执行了Major GC后仍然没有办法进行对象的保存就会报OOM异常java内存溢出错误也就是之前说的OutOfMemoryError错误 可以看到S1中有1和2说明的确是随机的1代表第一次回收到的位置 注意这个图片内容可能并没有满足前面说明的空闲的情况这在标记-复制算法中会提到这里你认为是特殊情况即可 分配对象的流程“方法的下“修改成放的下”因为这个图是复制过了的这里注意即可可能有其他错误如最后的两个否往上指的是是”若还有其他错误自己注意即可 很明显上面的YGC就是新生代收集的作用由于他作用后会考虑对应的S0和S1他们两个在新生代里面的和老年代所以单纯的给出一个细节位置当他操作完后在决定最后的分配内存包括在新生代和老年代的分配因为之前你只是决定是否可以放入而并没有分配内存给他之所以YGC后面也会考虑是老年代也会操作新生代看后面的触发机制就知道了 堆GC Java 中的堆也是 GC 收集垃圾的主要区域GC 分为两种一种是部分收集器Partial GC另一类是整堆收集器 Full GC 部分收集器不是完整收集java堆的的收集器它又分为 新生代收集Minor GC / Young GC只是新生代的垃圾收集 老年代收集 Major GC / Old GC只是老年代的垃圾收集 如CMS GC会单独回收老年代 混合收集Mixed GC收集整个新生代及老年代可能有些gc操作部分老年代的垃圾收集 如G1 GC会混合回收都回收region会区域回收 整堆收集Full GC收集整个java堆和方法区的垃圾收集器自然包括上面的所有位置但可能比较慢 上面我们主要说明Minor GCMajor GCFull GC其他的后面可能会说明 对于后面的触发条件是对应当前上限触发的在前面堆说明的上限中以及讲了要注意哦而报错是针对真正的上限的这也要注意 年轻代GC触发条件 年轻代空间不足就会触发Minor GC 这里年轻代指的是Eden代满则触发而Survivor满不满都不会引发GC操作gc回收了会回收并且移动只是不会触发gc而已那么如果条件没有满足且eden满了或者且并且幸存者也满了且触发gc并没有清除很多或者没有清除即还是认为内存不足的那么通常有另外的规则使得他们会跳过条件直接到老年代首先Survivor到老年然后eden到Survivor或者没有规则的直接到老年代首先Survivor到老年然后eden到Survivor也有可能都放入老年代一般是这个因为这里就相当于幸存的很多导致幸存者不能放入了再后面会说明这样的特殊情况优化那里即很明显他并没有回收因为对应的都是存在的这个地方可以全局搜索先了解一下注意该gc是新生代的gc而新生代包含eden和另外两个幸存者所以他回收这三个且他满的条件自然是数据进入的条件当满后数据进入会卡住等他有空间出现回收这个时候回收的操作会进行考虑是否操作到老年代同理老年代也是如此只是他的考虑是直接报错并且可能当前添加的数据清除即数据慢慢添加的自然记录保存之前的数据如在添加之前判断若添加后是否溢出而进行添加所以可以操作进行保存之前的数据的判断不难防止底层jvm操作是可以实现虽然在jvm中我们不知道他是怎么实现的即识别后是否有其他操作要不然溢出是怎么来的呢而一般这个报错可能会导致程序结束即导致jvm结束关闭那么若真的结束关闭自然都会是释放的当然了一般只是报错那么就按照报错来进行处理并非一定结束当然溢出后若程序没有结束那么一般也会继续触发gc的 Minor GC会引发STWstop the world暂停其他用户的线程基本上所有的线程等垃圾回收接收用户的线程才恢复堆可是线程共享的哦而正是因为这样一般我们尽量不要引发STW或者说该回收来影响线程的操作而自动触发GC基本不会操作这个但有时候我们需要自动触发更快操作完就将变量指向为null等等其他gc会进行监测并移除回收销毁 老年代GC Major GC触发机制 老年代空间不足时也会尝试触发MinorGC然后考虑放入新生代反过来操作了而由于也会触发他所以不只是数据大且也考虑STW了如果空间还是不足则触发Major GC 如果Major GC后 , 内存仍然不足则报错OOMMajor GC的速度比Minor GC慢10倍以上空间一般比较大了在数据大的情况下就算是遍历都需要很久的而我空间不足自然说明快满了 注意触发GC并不代表一定会移除因为垃圾回收只是移除不需要的也就是垃圾所以他也只是尝试而已而前面说过冗余的存在那么最后还是看堆总体上限使得出现对应的错误这就是主要原因尝试的MinorGC因为老年代也会操作新生代所以有冗余也是正常的保留空间来应对特殊情况比例是考虑冗余的冗余在这里代表保留空间 FullGC 触发机制 调用System.gc()手动gc 系统会执行Full GC不是立即执行总要有过渡吧这一般是我们手动GC的一个方法gc都有过渡也就是激活时间就如你在电脑上显示1那么电脑内部自然需要很多流程使得出现1的 老年代空间不足也会触发他的触发可能需要Major GC的某些条件这样要注意哦一般是考虑老年代空间大需要多个GC吧当然gc优先是Major GC然后这个除非你自己手动操作 方法区空间不足也会触发方法区的GC考虑触发 我们一般也不会使用他因为他也会操作STW并不是因为操作了Minor GC引发的哦他自带的且他考虑非常多可能时间还要更长即可能比单纯的老年代还要长即慢 通过Minor GC进入老年代平均大小大于老年代可用内存与上面老年代空间不足的是新生代也是满的虽然老年代也是所以这里可以认为是大数据的老年代空间不足也会触发也就是说从新生代一次性过来的数据在一定时间内移动的数据加起来是一次性的一般包括总数据即在一定时间内突然老年代满了那么触发这个使得将整个堆进行回收因为老年代从新生代来的数据使得老年代慢了说明在那一瞬间一定时间整个堆是满的那么我需要整体回收一般也包括的幸存者而Minor GC好像也会操作三个而新生代包含eden和另外两个幸存者所以他回收这三个 元空间 在JDK1.7之前HotSpot 虚拟机把方法区当成永久代来进行垃圾回收而从 JDK 1.8 开始移除永久代并把方法 区移至元空间它位于本地内存中而不是虚拟机内存中HotSpots取消了永久代那么是不是也就没有方法区了 呢 当然不是方法区是一个规范规范没变它就一直在只不过取代永久代的是元空间Metaspace而已它和永久代有什么不同 1存储位置不同永久代在物理上或者联系上是堆的一部分和新生代、老年代的地址可以认为是连续的而元空间属于本地内存 2存储内容不同在原来的永久代划分中永久代用来存放类的元数据信息、静态变量以及运行时常量池等现在类的元信 息存储在元空间中静态变量和常量池等并入堆中相当于原来的永久代中的数据或者说方法区的数据被元空间和堆内存给瓜分了 为什么要废弃永久代引入元空间 相比于之前的永久代划分Oracle为什么要做这样的改进呢 1在原来的永久代划分中永久代需要存放类的元数据、静态变量和常量等它的大小不容易确定因为这其 中有很多影响因素比如类的总数常量池的大小和方法数量等-XX:MaxPermSize 指定太小很容易造成永久 代内存溢出一般代表永久代的内存上限如堆一样的上限意思 2移除永久代是为融合HotSpot VM与 JRockit VM而做出的努力因为JRockit没有永久代不需要配置永久代 3永久代会为GC带来不必要的复杂度并且回收效率偏低 废除永久代的好处 1由于类的元数据分配在本地内存中元空间的最大可分配空间就是系统可用内存空间不会遇到永久代存在 时的内存溢出错误但是可能会遇到对应自己设置的上限内存给的溢出错误后面会说明即当Metaspace剩余空间不足与堆一样的上限意思只不过他可以默认没有限制而已 2将运行时常量池从PermGen永久代也可以认为是方法区因为是把方法区当成永久代分离出来与类的元数据分开提升类元数据的独立性放入堆中 3将元数据从PermGen剥离出来到Metaspace可以提升对元数据的管理同时提升GC效率 Metaspace元空间相关参数 1-XX:MetaspaceSize初始空间大小可以认为是GC操作的临界点初始上限所以方法区与堆一样的慢慢提高当前上限当实际数据达到该值就会触发垃圾收集进行类型卸载同时GC会对该值进行调整如 果释放了大量的空间就适当降低该值如果释放了很少的空间那么在不超过MaxMetaspaceSize时后面会说明适当提 高该值 2-XX:MaxMetaspaceSize最大空间默认是没有限制的如果没有使用该参数来设置类的元数据的大小其最 大可利用空间是整个系统内存的可用空间在本地内存里面所以他说的就是物理内存如你电脑的所有内存那么只要你有内存他就会进行挖取使用当然一般他不会挖很多而不像堆一样挖了很多而不使用实际上对于虚拟机来说不是java的而是linux的一般也是挖很多的当然一般他也只是设置上限而不是真的挖过来堆特殊一点JVM也可以增加本地内存空间来满足类元数据信息的存储但是如果没有设置最大值则可能存在bug导致Metaspace的空间在不停的扩展会导致机器的内存不足进 而可能出现swap内存被耗尽最终导致进程直接被系统直接kill掉系统自身也会有保护的不会使得出现剧增的情况或者说系统也内置了给你的最大内存的挖取总之电脑很复杂经过这么多年不是我一人可以理解的如果设置了该参数当Metaspace剩余空间不足会抛出java.lang.OutOfMemoryError: Metaspace space因为是内存所有是内存的溢出不是栈哦 3-XX:MinMetaspaceFreeRatio在GC之后最小的Metaspace剩余空间容量的百分比减少为分配空间所导致的 垃圾收集不要回收太多了 4-XX:MaxMetaspaceFreeRatio在GC之后最大的Metaspace剩余空间容量的百分比减少为释放空间所导致的 垃圾收集不要回收太少了除非没有回收的了 方法区 方法区的理解 方法区Method Area 与Java堆一样 是各个线程共享的内存区域 它用于存储已被虚拟机加载 的类型信息、 常量、 静态变量、 即时编译器编译后的代码缓存等数据可能还有其他数据比如方法的存放定义即一份方法多人调用他们这些信息我们统称为类信息因为他们的数据就是从类来的所以可以这样说明 这里说明一下堆和方法区的区别在《Java虚拟机规范》中明确说明“尽管所有的方法区在逻辑上是属于堆的一部分但是简单的实现可能不会 选择去进行垃圾收集或者进行压缩”对HotSpot而言方法区还有一个别名叫做Non-Heap非堆的就是 要和堆分开所以你认为永久代是堆的也是正确的不是也是正确的这里考虑不是逻辑上那么就是具体联系物理上就是存放不一样无论是逻辑上还是物理上我们都可以认为这里考虑物理上 元空间、永久代是方法区具体的落地实现所以这里说明的方法区就看作是一块独立于Java堆的内存空间这里统一说明所以我们先认为常量池也在方法区中尽管他在jdk8及其以后分离了即包括对应的所有相关内容的说明它主要是用来存储所加载 的类信息的 我们来看看创建对象各数据区域的声明 很明显类的信息是在方法区中的 方法区的特点 1方法区与堆一样是各个线程共享的内存区域 2方法区在JVM启动的时候就会被创建并且它实例的物理内存空间和Java堆一样都可以不连续但逻辑可能连续即有连续方式连接 3方法区的大小跟堆空间一样 可以选择固定大小或者动态变化 4方法区的对象决定了系统可以保存多少个类如果系统定义了太多的类导致方法区溢出虚拟机同样会出现 OOM异常Java7之前是 PermGen Space 永久带 Java 8之后 是MetaSpace元空间 5关闭JVM就会释放这个区域的内存当然其他的也是如此一般只要是jvm的一般都会进行释放这也是为什么程序结束GC可以不用回收的原因前提是jvm里面的如果有利用外面的可能需要等会因为通知元空间可能会虽然jvm看起来是跟随class来启动或者关闭的 方法区结构 方法区的内部结构 类加载器将Class文件加载到内存之后将类的信息存储到方法区中然后操作解释执行或者识别执行也就是说方法区就是存放类的信息的包括很多信息前面有说明在虚拟机加载 的类型信息、 常量、 静态变量、 即时编译器编译后的代码缓存等数据这个地方 方法区中存储的内容 类型信息域信息、方法信息运行时常量池 类型信息 对每个加载的类型类Class、接口 interface、枚举enum、注解 annotationJVM必须在方法区中存储以下类型信息 1这个类型的完整有效名称全名 包名.类名 2这个类型直接父类的完整有效名对于 interface或是java.lang. Object都没有父类一般有个没有的标志或者不存放 3这个类型的修饰符 public, abstractfinal等等 4这个类型直接接口的一个有序列表多个实现接口可能按照实现顺序进行排列如implements ab那么就是ab排列 域信息 1域信息即为类的属性成员变量 2JVM必须在方法区中保存类所有的成员变量相关信息及声明顺序 3域的相关信息包括域名称、域类型、域修饰符pυblic、private、protected、static、final、volatile、transient等等 方法信息 JVM必须保存所有方法的以下信息同域信息一样包括声明顺序 1方法名称方法的返回类型或void 2方法参数的数量和类型按顺序 3方法的修饰符public、private、protected、static、final、synchronized、native,、abstract等等 4方法的字节码bytecodes、操作数栈、局部变量表及大小 abstract和native方法除外 5异常表 abstract和 native方法除外每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏 移地址、被捕获的异常类的常量池索引 现在我们看这个代码 package com.test2;import java.io.Serializable;/****/ public class test1 extends Object implements Serializable {private static String name hello java; //静态的变量使用右键来创建相应的构造方法是不会有提示的public test1(String name) {this.name name;}public static void main(String[] args) {int x 100;int y 100;int result testSum(x, y);System.out.println(result);}public static int testSum(int x, int y) {return x y;} } 现在我们执行这个命令javap -v test1 test.txt实际上无论对应的test1是否存在他都会创建test.txt文件改动界面如切屏或者点击目录里面的任何文件稍等就会出现并不需要必须重新操作目录的打开和关闭因为关闭打开也就是改动界面的一种操作只是是局部的而已若存在那么前面得出的结果放入该文件里面注意目录哦我们看看里面的内容即可 注意若test.txt有内容那么清空内容然后放入文字覆盖的原理基本都是这样除非是操作指向的那么只是改变指向而已 对应的信息如下 Classfile xxx/test1.class //xxx目录省略了操作的文件所在地址Last modified 2023年2月13日; size 869 bytes //操作日期和文件大小MD5 checksum 19036838a5b651f22b003899693cf060 //检验的数字这里可能代表该唯一的文件编号可能是区别文件的主要信息因为一般不会根据地址来进行区分因为你可以在其他地方操作该文件class的执行那么地址变了所以需要这个 Compiled from test1.java //编译的文件是来自test1.java public class com.test2.test1 implements java.io.Serializable //类的完整路径com.test2.test1minor version: 0major version: 52flags: (0x0021) ACC_PUBLIC, ACC_SUPER //类修饰符ACC_PUBLICpublic如果么有public即按照默认1那么没有这个ACC_PUBLIC前面的(0x0021)可能改变成0x0020ACC_SUPER是默认有的代表是类this_class: #8 // com/test2/test1super_class: #2 // java/lang/Objectinterfaces: 1, fields: 1, methods: 4, attributes: 1//下面这个Constant pool:代表常量池运行时常量池因为只有运行才会出现所以我们通常也称他为运行时常量池虽然其他基本都是运行出现的但是他在变量上比较特殊所以我们愿意加上运行时 Constant pool:#1 Methodref #2.#3 // java/lang/Object.init:()V#2 Class #4 // java/lang/Object#3 NameAndType #5:#6 // init:()V#4 Utf8 java/lang/Object#5 Utf8 init#6 Utf8 ()V#7 Fieldref #8.#9 // com/test2/test1.name:Ljava/lang/String;#8 Class #10 // com/test2/test1#9 NameAndType #11:#12 // name:Ljava/lang/String;#10 Utf8 com/test2/test1#11 Utf8 name#12 Utf8 Ljava/lang/String;#13 Methodref #8.#14 // com/test2/test1.testSum:(II)I#14 NameAndType #15:#16 // testSum:(II)I#15 Utf8 testSum#16 Utf8 (II)I#17 Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;#18 Class #20 // java/lang/System#19 NameAndType #21:#22 // out:Ljava/io/PrintStream;#20 Utf8 java/lang/System#21 Utf8 out#22 Utf8 Ljava/io/PrintStream;#23 Methodref #24.#25 // java/io/PrintStream.println:(I)V#24 Class #26 // java/io/PrintStream#25 NameAndType #27:#28 // println:(I)V#26 Utf8 java/io/PrintStream#27 Utf8 println#28 Utf8 (I)V#29 String #30 // hello java //看这里#30 Utf8 hello java //到这里连接的链接动态链接#31 Class #32 // java/io/Serializable#32 Utf8 java/io/Serializable#33 Utf8 (Ljava/lang/String;)V#34 Utf8 Code#35 Utf8 LineNumberTable#36 Utf8 LocalVariableTable#37 Utf8 this#38 Utf8 Lcom/test2/test1;#39 Utf8 main#40 Utf8 ([Ljava/lang/String;)V#41 Utf8 args#42 Utf8 [Ljava/lang/String;#43 Utf8 x#44 Utf8 I#45 Utf8 y#46 Utf8 result#47 Utf8 clinit#48 Utf8 SourceFile#49 Utf8 test1.java {//上面很明显说明的是类那么这里就是构造方法了public com.test2.test1(java.lang.String); //构造方法对应的类地址和参数地址descriptor: (Ljava/lang/String;)Vflags: (0x0001) ACC_PUBLIC //构造方法修饰类型Code:stack1, locals2, args_size20: aload_0 //参数变量加载1: invokespecial #1 // Method java/lang/Object.init:()V 这里就代表进入某种方法的意思而不是得到结果4: aload_0 //继续加载上面的变量因为上面变量已经赋值了5: pop //虽然是上面方法的底层返回给他的底层特殊变量可能共有但是什么都没有做即空返回6: aload_1 //代表this7: putstatic #7 // Field name:Ljava/lang/String; 对于静态来说就是初始化name10: return //操作返回LineNumberTable:line 11: 0line 12: 4line 13: 10LocalVariableTable:Start Length Slot Name Signature0 11 0 this Lcom/test2/test1;0 11 1 name Ljava/lang/String;public static void main(java.lang.String[]); //同样的有地址java.lang.Stringdescriptor: ([Ljava/lang/String;)V //代表没有返回类型即Vflags: (0x0009) ACC_PUBLIC, ACC_STATIC //两个类型 Code:stack2, locals4, args_size10: bipush 100 //一些特别的他可能是不同的显示如数值100等等2: istore_1 //一些特别的他可能是不同的显示如第4个变量等等3: bipush 1005: istore_26: iload_17: iload_28: invokestatic #13 // Method testSum:(II)I自带的有那么不需要获取什么11: istore_3 //执行方法是可以操作赋值的那么可以直接这样就如return可以得到一样前提是对应方法会返回虽然这里代表没有返回12: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;15: iload_316: invokevirtual #23 // Method java/io/PrintStream.println:(I)V19: returnLineNumberTable:line 16: 0line 17: 3line 18: 6line 19: 12line 20: 19LocalVariableTable:Start Length Slot Name Signature0 20 0 args [Ljava/lang/String;3 17 1 x I6 14 2 y I12 8 3 result Ipublic static int testSum(int, int);descriptor: (II)I //返回的类型前提是有返回类型他才会有否则一般代表V一般情况下其他类型是按照首字母大写的比如double那么这里是D但是若是Double那么是对应的类的地址那么只要是地址即后面的基本都是areturn不只是仅限于String哦一般来说开头就是a注意一般变量也是这样但凡是操作对应的那么首字母基本都是这里的类似说明你看看后面的字节码信息就知道了就如iload_0必然是加载int类型的首字母是i而返回的类型如这里就是I那么是ireturn而在具体Code里面一般是小写外面descriptor通常是大写flags: (0x0009) ACC_PUBLIC, ACC_STATIC //同样的Code:stack2, locals2, args_size20: iload_01: iload_12: iadd3: ireturn //这里就是返回了返回结果即将加载并操作的进行返回一般来说areturn代表操作String的返回而这里ireturn代表int的返回具体其他类型可以自己看看LineNumberTable:line 23: 0LocalVariableTable: //局部变量表Start Length Slot Name Signature0 4 0 x I0 4 1 y Istatic {}; //静态区域descriptor: ()V //没有返回值flags: (0x0008) ACC_STATIC //自然是静态的Code:stack1, locals0, args_size00: ldc #29 // String hello java 值2: putstatic #7 // Field name:Ljava/lang/String; 变量并不是定义的变量哦通过引用可以找到变量即name5: return //进行赋值很明显他是与方法相同的所以我们一般会说静态在类加载时就加载了LineNumberTable:line 9: 0 } SourceFile: test1.java //源文件名称不是总地址了提取文件名称出来了当然前面可能也指定了编译的该名称但是参数是各有作用的他代表编译者这里代表名称者可能该名称是可以进行改变的具体可以百度找差异//上面不只是针对main方法哦所以你操作插件中会出现多个对应的方法至此该字节码大致说明完毕这就是字节码的所有信息了其中类信息和运行时常量池也基本就是所有的都被加载放入方法区的信息可以发现他的确解释了之前说明的我们可以发现他自身就有很多关系且满足对应的信息在方法区的说明类加载器将Class文件加载到内存之后将类的信息存储到方法区中 方法区设置 方法区的大小不必是固定的JVM可以根据应用的需要动态调整 jdk7及以前 通过-xx:Permsize来设置永久代初始分配空间默认值是20.75MJDK1.8之后-XX:MaxPermSize设置会被忽略一般jdk8以前是没有-XX:MaxMetaspaceSize的具体可以百度 -XX:MaxPermsize来设定永久代最大可分配空间32位机器默认是64M64位机器默认是82M 当JVM加载的类信息容量超过了这个值会报异常OutofMemoryError:PermGen space 查看JDK PermSpace区域默认大小 jps #是java提供的一个显示当前所有java进程pid的命令 jinfo -flag PermSize 进程号 #查看进程的PermSize初始化空间大小 jinfo -flag MaxPermSize 进程号 #查看PermSize最大空间这里可以先不操作我们在jdk8里进行相关测试即可因为我们主要说明jdk8如果可以你自己可以下载jdk7来进行测试其他版本可能报找不到错误 JDK8以后 元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定 默认值依赖于平台windows下-XX:MetaspaceSize是21M-XX:MaxMetaspaceSize的值是-1即没有限制元空间在本地物理内存里面所以没有像jdk7一样有对应默认的上限虽然他的无限制可能也是有限的看后面的值就知道了18446744073709486080 与永久代不同如果不指定大小默认情况下虚拟机会耗尽所有的可用系统内存如果元数据区发生溢出虚拟 机一样会抛出异常OutOfMemoryError:Metaspace -XX:MetaspaceSize设置初始的元空间大小对于一个64位的服务器端JVM来说其默认的-xx:MetaspaceSize值为21MB大约这就是初始的高水位线一旦触及这个水位线FullGC将会被触发方法区的回收卸载操作一般需要他并卸载没用的类即这些类对应的类加载 器不再存活一般来说只是没用的类与自动gc是一样的所以自动gc一般也是全局回收或者也可以说整堆收集全局比较好因为虚拟机都要关闭了即一般他可以认为是全局回收因为大多数的数据都在堆和方法区里面共享的吗即一直保存而其他由于私有那么不会一直保存然后这个高水位线将会重置新的高水位线的值取决于GC后释放了多少元空间如果释放的空间不 足那么在不超过MaxMetaspaceSize时适当提高该值如果释放空间过多则适当降低该值如果初始化的高水位线设置过低上述高水位线调整情况会发生很多次因为相对来说释放的少当然设置的大也是如此只是一般不会设置的占用很多所以以设置的小为主通过垃圾回收器的日志可以观察到FullGC多次调用为了避免频繁地GC他这个会操作STW建议将-XX:MetaspaceSize设置为一个相对较高的值但不会太高设置的大也是如此 jps #查看进程号 jinfo -flag MetaspaceSize 进程号 #查看Metaspace 最大分配内存空间 jinfo -flag MaxMetaspaceSize 进程号 #查看Metaspace最大空间可以进行测试一下首先给出代码 package com.test2;/****/ public class test3 {public static void main(String[] args) throws InterruptedException {System.out.println(Hello World);Thread.sleep(200000);} } 在控制台执行jps随便那个目录就可可以看到带有test3的线程关闭程序他就没有了然后我们执行jinfo -flag MetaspaceSize 进程号进程号写自己的因为每次启动他所占用的进程号并不相同比如我的是jinfo -flag MetaspaceSize 24276对应的端口要存在否则提示拒绝访问 可以看到这个-XX:MetaspaceSize218071041024 * 1024 * 2020971520那么很明显对应的中间有小数20是取整的通过百度的在线计算器得出是20.796875即偏向于21那么默认应该大致是21所以之前说大约即的确是默认 执行jinfo -flag MaxMetaspaceSize 14368得出-XX:MaxMetaspaceSize18446744073709486080这么多字节直接除以比较麻烦我们直接除以1000来估算吧那么他就是18446744073709m很明显是100万亿mb也就是说在当前我们小众的电脑上他的确是无限制的虽然他这是是-1那么这个-1默认应该是这个值了即无限制除非你电脑内存实在够大那么他在无限制的情况下可能也会出现内存溢出的异常 我们进行设置修改注意可以设置加上单位mk没有单位默认是字节单位与之前的主要是这样的设置这个地方的一样的看起来的说明 继续查看会发现改变了-XX:MetaspaceSize104857600-XX:MaxMetaspaceSize524288000 1024 * 1025 * 100104857600那么1024 * 1025 * 500524288000即是正确的同理对应的jdk7的对应设置也是如此前提是jdk7否则一般会提示没有虽然说忽略但是实际可能是没有的如no such flag ‘PermSize’或者no such flag ‘MaxPermSize’no such flag的英文意思没有这样的标志 运行时常量池 常量池vs运行时常量池虽然在前面我们会让常量池称为运行时常量池那么为什么又要这样分了看如下 字节码文件中内部包含了常量池 方法区中内部包含了运行时常量池 很明显实际上他们还是同一个常量池数据只是分开成一个文件中没有加载一个加载没有运行中而已这也是运行时这个说明的由来而不是运行后所以说成是一个都是一个数据来的或者不是一个是否加载都行 所以可以这样说 常量池存放编译期间生成的各种字面量如字符串与符号引用 运行时常量池常量池表在运行时的表现形式 编译后的字节码文件中包含了类型信息、域信息、方法信息等通过ClassLoader将字节码文件的常量池中的信息加 载到内存中存储在了方法区的运行时常量池中 理解为字节码中的常量池 Constant pool 只是文件信息它想要执行就必须加载到内存中而Java程序是靠JVM进行加载更具体的来说是JVM的执行引擎来解释执行的执行引擎在运行时从常量池中取数据而被加载的字节码常量池 中的信息是放到了方法区的运行时常量池中 它们不是一个概念存放的位置是不同的一个在字节码文件中一个在方法区中 对字节码文件反编译也就查看class文件我们的javap就是这样操作的因为原来的是乱码所以这个反编译的意思并不是编译class文件到java的意思而是查看class文件内容的信息的意思即将里面的内容反编译成可以直观的看的信息之后查看常量池相关信息也就是前面Constant pool对应的信息所以这里就不做说明了 要弄清楚方法区的运行时常量池需要理解清楚字节码中的常量池 一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外还包含一项信息那就是常量 池表 Constant pool table包括各种字面量和对类型、域和方法的符号引用 常量池可以看做是一张表虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型 常量池表Constant pool table 以上面的内容为主 Constant pool:#1 Methodref #2.#3 // java/lang/Object.init:()V#2 Class #4 // java/lang/Object#3 NameAndType #5:#6 // init:()V#4 Utf8 java/lang/Object#5 Utf8 init#6 Utf8 ()V#7 Fieldref #8.#9 // com/test2/test1.name:Ljava/lang/String;#8 Class #10 // com/test2/test1#9 NameAndType #11:#12 // name:Ljava/lang/String;#10 Utf8 com/test2/test1#11 Utf8 name#12 Utf8 Ljava/lang/String;#13 Methodref #8.#14 // com/test2/test1.testSum:(II)I#14 NameAndType #15:#16 // testSum:(II)I#15 Utf8 testSum#16 Utf8 (II)I#17 Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;#18 Class #20 // java/lang/System#19 NameAndType #21:#22 // out:Ljava/io/PrintStream;#20 Utf8 java/lang/System#21 Utf8 out#22 Utf8 Ljava/io/PrintStream;#23 Methodref #24.#25 // java/io/PrintStream.println:(I)V#24 Class #26 // java/io/PrintStream#25 NameAndType #27:#28 // println:(I)V#26 Utf8 java/io/PrintStream#27 Utf8 println#28 Utf8 (I)V#29 String #30 // hello java //看这里#30 Utf8 hello java //到这里连接的链接动态链接#31 Class #32 // java/io/Serializable#32 Utf8 java/io/Serializable#33 Utf8 (Ljava/lang/String;)V#34 Utf8 Code#35 Utf8 LineNumberTable#36 Utf8 LocalVariableTable#37 Utf8 this#38 Utf8 Lcom/test2/test1;#39 Utf8 main#40 Utf8 ([Ljava/lang/String;)V#41 Utf8 args#42 Utf8 [Ljava/lang/String;#43 Utf8 x#44 Utf8 I#45 Utf8 y#46 Utf8 result#47 Utf8 clinit#48 Utf8 SourceFile#49 Utf8 test1.java上面常量池就是常量池表也称运行时常量池 在方法中对常量池表的符号引用以main方法为主 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code:stack2, locals4, args_size10: bipush 100 2: istore_1 3: bipush 1005: istore_26: iload_17: iload_2 //这下面的#13就是利用了常量池即对该常量池表的符号引用8: invokestatic #13 // Method testSum:(II)I11: istore_3 12: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;15: iload_316: invokevirtual #23 // Method java/io/PrintStream.println:(I)V19: returnLineNumberTable:line 16: 0line 17: 3line 18: 6line 19: 12line 20: 19LocalVariableTable:Start Length Slot Name Signature0 20 0 args [Ljava/lang/String;3 17 1 x I6 14 2 y I12 8 3 result I 那么他必然是操作引用的吗答是的也就是说常量池也是保存方法的地方只是引用虽然类信息也保存的但是我们在运行时是他来调用的即方法区在获取类信息方法时他识别时虽然是从常量池进行调用的但是他在运行时常量池中虽然也保存在类信息中但是调用确需要运行时常量池来调用所以这里重新定义运行时常量池他就是一个存放引用的地方可能有字面值如字符串字面值就是一个看的到值如String a “1”这个1就是字面值所以前面是这样说的 常量池存放编译期间生成的各种字面量如字符串与符号引用 运行时常量池常量池表在运行时的表现形式 所以说虽然方法在方法区里但是若更加的细分的话可以认为他逻辑上使用引用在运行时常量池中实际上最终还是使用类信息在类信息中而其他被识别的信息类信息自然就是在方法区中的其他地方存放了类信息只是有运行时常量池我们会认为逻辑分开实际上他们都是一起的只是我们假装的分开而已实际上运行时常量池可能也包括了其他的类型域方法的引用的因为只是运行时的表现的 综上所述方法区有实际存放的位置和这些位置信息的引用来结合的也就是class的操作者其中由类加载器加载后才被进行识别执行所以之前的识别都是建立在先加载然后识别的虽然可能并没有说明加载过程因为还没有说明到方法区这里当然了他们加载的信息与class基本是一样的所以你认为是对着class识别也是没有问题 这里给出一个创建对象出现的字节码 0 new #7 com/test1/PC //这个创建对象3 dup //这个代表相应类型的变量得值即创建引用指向该对象//而只有这个得到值后我们才可以进行他的数值提取以及加载如astore_1aload_1当然他们都不是类似于iconst_1的所以是第一个变量astore_1a开头的前面说明过了代表类类型为什么需要常量池 举例来说 public class Solution {public void method() {System.out.println(are you ok);} }这段代码很简单但是里面却使用了 String、 System、 PrintStream及Object等结构如果代码多引用到的结构会更多这里就需要常量池将这些引用转变为符号引用具体用到时采取加载可以认为没有引用来帮你执行你怎么能很好的知道操作那一个呢或者说不会提取这不是主要的因为可以通过直接显示来解决而最主要的是之前的说明的而引用是一个总显示只需要少的字节码如#2就可以替换掉java/lang/Math.random : ()D就可以显示了这个地方 直接内存 直接内存Direct Memory 并不是虚拟机运行时数据区的一部分 在JDK 1.4中新加入了NIONew Input/Output 类 引入了一种基于通道Channel 与缓冲区 Buffer 的I/O方 式 它可以使用Native函数库直接分配堆外内存 然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块 内存的引用进行操作这样能在一些场景中显著提高性能 因为避免了 在Java堆和Native堆中来回复制数据 上面的物理内存映射文件的存储就是直接内存的主要存储作用 NIO的Bu提供一个可以直接访问系统物理内存的类——DirectByteBufferDirectByteBuffer类继承自ByteBuffer但和普通 的ByteBuffer不同普通的ByteBuffer仍在JVM堆上进行分配内存在堆里面其最大内存自己占用堆受到最大堆内存的 限制而DirectByteBuffer直 接分配在物理内存中并不占用堆空间在访问普通的ByteBuffer时系统总是会使用一个内核缓冲区进行操作而DirectByteBuffer所处的位置就相当于这个内核缓冲区因此使用DirectByteBuffer是一种更加接近内存底层的方法 所以它的速度比普通的ByteBuffer可以将ffer进行清除来简写即Bu更快一般这里可以将DirectByteBuffer称为DirectBuffer因为是他DirectByteBuffer的父类接口是一个特殊的类 通过使用堆外内存jdk7也是只是方法区不同而已但他们也是操作直接内存的可以带来以下好处 1改善堆过大时垃圾回收效率减少停顿Full GC时会扫描堆内存回收效率和堆大小成正比Native的内 存由OS操作系统不是jvm中哦因为他底层是c相关的自然是利用操作系统内存也就是物理内存因为一般将操作系统认为就是电脑的负责管理和回收 2减少内存在Native堆和JVM堆拷贝过程避免拷贝损耗降低内存使用 3可突破JVM内存大小限制映射文件在外面即物理内存里面 注意他一边与堆进行操作在后面说明他时实际上他又有堆的上限后面的直接内存溢出会说明的 实战OutOfMemoryError异常 Java堆溢出 堆内存中主要存放对象、数组等只要不断地创建这些对象并且保证 GC Roots 到对象之间有可达路径来避免垃 圾收集回收机制清除这些对象当这些对象所占空间超过最大堆容量时就会产生 OutOfMemoryError 的异常堆 内存异常示例如下主要代码省略了如package或者import的相关代码 /*** 设置最大堆最小堆-Xms20m -Xmx20m先后顺序不重要他只要可以识别的即可就如原来是213而你操作123结果都是一样的只是一个从先看到2大Xmx一个先看到1小Xms到从先看到1小Xms一个先看到2大Xmx了即-Xmx20m -Xms20m到-Xms20m -Xmx20m这样的意思结果反正都是设置的意思只是是否先后设置而已所以先后顺序不重要重要的是结果过程不重要拿到结果才重要*/public class HeapOOM {static class OOMObject {}public static void main(String[] args) {ListOOMObject oomObjectList new ArrayList();while (true) {oomObjectList.add(new OOMObject());}} }运行后会报异常在堆栈信息中可以看到 Exception in thread main java.lang.OutOfMemoryError: Java heap space说明在堆内存空间产生内存溢出的异常虽然前面也测试过 新产生的对象最初分配在新生代新生代满后会进行一次 Minor GC 如果 Minor GC 后空间不足会把该对象和 新生代满足条件的对象如前面的15次他们统称在新生代的放入老年代老年代空间不足时会进行 Full GC他也可以还有Major GC和MinorGC老年代空间不足一般也会触发他们前面说明过了之后如果空间那么一般是老年代了老年代满才会这样新生代是否满并不会因为他们最终是操作到老年代的还不足以存放新对象或者说gc后面在一定时间内又出现总不能又gc所以对于触发某些操作时在操作后若继续一样的情况才会出现兜底操作大多数的类似的都是如此如这里gc的操作则抛 出 OutOfMemoryError 异常前提是真的上限 常见原因 1内存中加载的数据过多如一次从数据库中取出过多数据一般引用包括即对象数据那么自然是堆 2集合对对象引用过多且使用完后没有清空 3代码中存在死循环或循环产生过多重复对象 4堆内存分配不合理 虚拟机栈和本地方法栈溢出 由于HotSpot虚拟机中并不区分虚拟机栈和本地方法栈 因此对于HotSpot来说 -Xoss参数设置本地方法栈大 小或者虚拟机栈大小就是栈大小一般只有远古版本才存在即才会有效果 虽然存在 但实际上是没有任何效果的 栈容量只能由-Xss参数来设定也就是说他们共有一个栈只是根据作用将该栈进行区分逻辑考虑区分物理考虑存放一样这里考虑物理与方法区的那个说明是相反的即逻辑上那么就是具体联系物理上就是存放不一样无论是逻辑上还是物理上我们都可以认为这里考虑物理上关于虚拟机栈和本地方法栈 在 《Java虚拟机规范》 中描述了两种异常 1如果线程请求的栈深度大于虚拟机所允许的最大深度 将抛出StackOverflowError异常 2如果虚拟机的栈内存不允许动态扩展 当扩展栈容量无法申请到足够的内存时申请对栈帧的空间也就是拿内存 将抛出 OutOfMemoryError异常这里决定操作这个由于本地方法栈与虚拟机栈一样的物理保存所以这里也就相当于说明的本地方法栈的错误了 《Java虚拟机规范》 明确允许Java虚拟机实现自行选择是否支持栈的动态扩展 而HotSpot虚拟机的选择是支持 扩展可能默认不支持具体可以百度 所以若在支持的情况下除非在创建线程申请内存时就因无法获得足够内存而出现 OutOfMemoryError异常一开始不让他操作到扩展 否则在线程运行时 是不会因为扩展而导致内存溢出的他自动动态扩展的那么自然没有上限 只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常为了验证 这点 我们可以做两个实验 先将实验范围限制在单线程中操作 尝试下面两种行为是 否能让HotSpot虚拟机产 生OutOfMemoryError异常 1使用-Xss参数减少栈内存容量结果 抛出StackOverflowError异常 2异常出现时输出 的堆栈深度相应缩小其中定义了大量的本地变量 增大此方法帧中本地变量表的长度结果 抛出StackOverflowError异常 且异常出现时输出的堆栈深度相应缩小 首先 对第一种情况进行测试 package com.test3;/****/ public class JavaVMStackSOF {private int stackLength 1;public void stackLeak() {stackLength;stackLeak();}public static void main(String[] args) throws Throwable {JavaVMStackSOF oom new JavaVMStackSOF();try {oom.stackLeak();} catch (Throwable e) {System.out.println(stack length: oom.stackLength);throw e; //在try里面进行抛出并打印错误信息由于是try里面的那么main方法里不用操作抛出因为有最终归属了而不用然jvm来处理即直接的打印错误信息}} }//可以选择设置-Xss128k对应结果 对于不同版本的Java虚拟机和不同的操作系统 栈容量最小值可能会有所限制 这主要取决于操 作系统内存分页 大小假如上述方法中的对应参数设置-Xss128k可以正常用于32位Windows系统下的JDK 6可能64的jdk8也行 但 是如果用于64位Windows系 统下的JDK 11 则会提示栈容量最小不能低于180K 而在一般也是64Linux下这个值则 可能是228K 如果低于这个最小限 制 HotSpot虚拟器启动时会给出如下提示以jdk11为例 The Java thread stack size specified is too small. Specify at least 180k//翻译指定的Java线程堆栈大小太小指定至少180k大于等于180k现在我们继续验证第二种情况 这次代码就显得有些丑陋了但为了多占局部变量表空间 不得不定义一长串变量 具体看如下代码 package com.test3;/****/ public class JavaVMStackSOF1 {private static int stackLength 0;public static void test() {long unused1, unused2, unused3, unused4, unused5,unused6, unused7, unused8, unused9, unused10,unused11, unused12, unused13, unused14, unused15,unused16, unused17, unused18, unused19, unused20,unused21, unused22, unused23, unused24, unused25,unused26, unused27, unused28, unused29, unused30,unused31, unused32, unused33, unused34, unused35,unused36, unused37, unused38, unused39, unused40,unused41, unused42, unused43, unused44, unused45,unused46, unused47, unused48, unused49, unused50,unused51, unused52, unused53, unused54, unused55,unused56, unused57, unused58, unused59, unused60,unused61, unused62, unused63, unused64, unused65,unused66, unused67, unused68, unused69, unused70,unused71, unused72, unused73, unused74, unused75,unused76, unused77, unused78, unused79, unused80,unused81, unused82, unused83, unused84, unused85,unused86, unused87, unused88, unused89, unused90,unused91, unused92, unused93, unused94, unused95,unused96, unused97, unused98, unused99, unused100;stackLength;test();unused1 unused2 unused3 unused4 unused5 unused6 unused7 unused8 unused9 unused10 unused11 unused12 unused13 unused14 unused15 unused16 unused17 unused18 unused19 unused20 unused21 unused22 unused23 unused24 unused25 unused26 unused27 unused28 unused29 unused30 unused31 unused32 unused33 unused34 unused35 unused36 unused37 unused38 unused39 unused40 unused41 unused42 unused43 unused44 unused45 unused46 unused47 unused48 unused49 unused50 unused51 unused52 unused53 unused54 unused55 unused56 unused57 unused58 unused59 unused60 unused61 unused62 unused63 unused64 unused65 unused66 unused67 unused68 unused69 unused70 unused71 unused72 unused73 unused74 unused75 unused76 unused77 unused78 unused79 unused80 unused81 unused82 unused83 unused84 unused85 unused86 unused87 unused88 unused89 unused90 unused91 unused92 unused93 unused94 unused95 unused96 unused97 unused98 unused99 unused100 0;}public static void main(String[] args) {try {test();}catch (Error e){System.out.println(stack length: stackLength);throw e;}}} 会发现异常还是Exception in thread “main” java.lang.StackOverflowError且异常出现时输出的堆栈深度相应缩小那么这个意思是什么呢看上面的System.out.println(“stack length:” stackLength);结果当你删除递归后面的赋值操作会发现该结构变化不大因为他基本没有操作可以忽略当删除定义的变量时结果变得很大也就是类似之前的JavaVMStackSOF类的测试的结果 所以说当定义了大量的变量会使得深度变小即System.out.println(“stack length:” stackLength);结果变小那么为什么会变小呢 看这个解释如果我们将虚拟机栈看成100每个栈帧是10当里面没有任何操作时递归10个方法就会报错那么深度是10而如果有大量的变量那么这个10会自动扩展动态扩展数据少可能不会这也是为什么定义大量的变量来让他进行扩展的原因很明显10如果进行扩展可以变成20或者更大以20为例那么递归只有5次那么深度就是5这也是为什么会变小的原因 实验结果表明 无论是由于栈帧太大还是虚拟机栈容量太小 当新的栈帧内存无法分配的时候 HotSpot虚拟机抛 出的都是StackOverflowError异常注意这是建立在他会扩展的情况下可是如果在不允许动态扩展栈容量大小的虚拟机 上 相同代码则会导致不一样的 情况譬如远古时代的Classic虚拟机 这款虚拟机不支持动态扩展 栈内存的容量可能也支持可能一般默认支持可能并不对可以去百度查看 在Windows上的JDK 1.0.2运 行上面的代码话如果这时候要调整栈容量就应该改 用-oss参数了 得到的结果可能是 stack length:3716 java.lang.OutOfMemoryError可见类似的代码在Classic虚拟机中成功产生了OutOfMemoryError而不是StackOver-flowError异常如果测试时不限 于单线程 通过不断建立线程的方式 在HotSpot上也是可以产生类似的内存溢出异常的 但是这样产生的内存溢出异常和栈空间是否足够并不存在任何直接的关系 主要取决于操作系统本身的内存使用状 态因为线程与虚拟机栈他们是一对一的即线程私有甚至可以说 在这种情况下 给每个线程的栈分配的内存越大 反而越容易产生内存溢出异常原因其实不 难理解 操作系统分配给每个进程的内存是有限制的 譬如32位Windows的单个进程 最大内存限制为2GB如果没有分配自然也会出现该异常也就是物理内存不够了就如之前的虚拟机会耗尽所有的可用系统内存如果元数据区发生溢出一样也会出现这样的错误 HotSpot虚拟机提供了参数可以控制Java堆和方法区这两部分的内存的最大值 那剩余的内存即为2GB操作系统 限制 减去最大堆容量 再减去最大方法区容量 由于程序计数器 消耗内存很小 可以忽略掉 如果把直接内 存和虚拟机进程本身耗费的内存也去掉的话 剩下的内存 就由虚拟机栈和本地方法栈来分配了因此为每个线程 分配到的栈内存越大 可以建立的线程数量自然就越少 建立线程时就越容易把剩下的内存耗尽 代码如下 package com.test3;/****/ public class JavaVMStackOOM {private void dontStop() {while (true) {}}public void stackLeakByThread() {while (true) {Thread thread new Thread(new Runnable() {Overridepublic void run() {dontStop();}});thread.start();}}public static void main(String[] args) throws Throwable {JavaVMStackOOM oom new JavaVMStackOOM();oom.stackLeakByThread();}} 注意 由于他是消耗物理内存的所以我建议你不要执行这个代码卡死了不要怪我如果非要执行记得要先保存当前的工作包括其他工作我测试时都快要关不掉了所以这里很难测试出来 至于为什么容易卡死主要是因为Java的线程是映射到操作系统的内核线程上 无限制地创建线程会对操作系统带来很大压力 上述代码执行 时有很高的风险 可能会由于创建线程数量过多而导致操作系统 假死简单来说也就是内存被一直使用或者说消耗手机没有运行内存都会很卡电脑自然也是一样 //这里给出结果 Exception in thread main java.lang.OutOfMemoryError: unable to create native thread出现StackOverflowError异常时 会有明确错误堆栈可供分析 相对而言比较容易定位到问题所在错误信息一般会给出行数如递归的方法执行不是方法的定义比如前面test方法里面的test();在第几行那么他就是该错误出现的原因 如果使用HotSpot虚拟机默认参数 栈深度在大多数情况下因为每个方法压入栈的帧大小并不是 一样的 所以只能说大多 数情况下这是因为方法并不是相同的就算是相同的可能也会不一样忽上忽下 到达1000~2000是完全没有问题 对于正常的方法调用包括不能 做尾递归优化的递归调用 这个 深度应该完全够用了但是 如果是建立过多线程导致的内存溢 出 在不能减少线程数量或者更换64位虚拟机的 情况下64位一般不会让你出现该错误防止对系统的内存都使用消耗完毕前面说明的系统自身也会有保护的 就只能通过减少最大堆和减少栈容量来换取更多的线程这种通过减少内存的手段来解决内存溢出的 方式 如果没有这方面处理经验 一般比 较难以想到 这一点特别是需要在开发32位系统的多线程应用时注意也 是由于这种问题较为隐蔽 从 JDK 7起 以上提示信息中unable to create native thread英文翻译无法创建本机线程后面 虚拟机可能会特别注明原因在后面加上可能是possibly out of memory or process/resource limits reached英文翻译可能已达到内存不足或进程/资源限制 运行时常量池和方法区溢出 在这之前可能只是大致的说明但好像并没有测试过现在进行测试 由于运行时常量池是方法区的一部分 所以这两个区域的溢出测试可以放到一起进行前面曾经提到HotSpot从JDK 7开始逐步去永久代的计划 并在JDK 8中完全使用元空间来代替永久代的背景故事 在此我们就以测试代码 来观察一下 使用永久代还是元空间来实现方法区 对程序有什么 实际的影响 String的intern()是一个本地方法 它的作用是如果字符串常量池中已经包含一个等于此String对象的 字符串 则返 回代表池中这个字符串的String对象的引用否则 会将此String对象包含的字符串添加到常量池中 并且返回此String对象的引用在JDK 6或更早之前的HotSpot虚拟机中 常量池都是分配在永久代中 我们可以通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小 即可间接限制其中常量池的容量 具体实现看如下代码 package com.test3;import java.util.HashSet; import java.util.Set; import java.util.function.Function;/*** -XX:PermSize6M -XX:MaxPermSize6M*/ public class RuntimeConstantPoolOOM {public static void main(String[] args) {// 使用Set保持着常量池引用 避免Full GC回收对应常量池行为他可是可以操作方法区的SetString set new HashSetString();// 在short范围内足以让6MB的PermSize产生OOM了short i 0;while (true) {set.add(String.valueOf(i).intern());}}} 注意如果你没有jdk6而有jdk8那么操作这个 package com.test3;import java.util.HashSet; import java.util.Set; import java.util.function.Function;/*** -XX:MetaspaceSize9m -XX:MaxMetaspaceSize9m设置一样使得基本不会扩展上限了注意一般有最小最大的说明基本都是上限慢慢加而相同则说明不会加了因为你一开始就到达最顶峰了自然没有扩展的意思了如果最小的大于最大的那么最大的自动设置成最小的即相同其他的也是如此如堆等等前提有最小的设置栈好像没有一般来说MaxMetaspaceSize不能太小就如指定的Java线程堆栈大小太小指定至少180k一样他一般好像是9m这里8m就不行了总要有一个空间的而堆好像是2m1m就不行了当然他们的最小的若小于这些那么默认将他们最小的变成这些即栈180k堆2m可能可以更少比如到k这里先这样的认为方法区9m可能可以更少比如到k这里先这样的认为*/ public class RuntimeConstantPoolOOM {public static void main(String[] args) {// 使用Set保持着常量池引用 避免Full GC回收对应常量池行为他可是可以操作方法区的SetString set new HashSetString();// 若在short范围内不足以让9MB的MetaspaceSize产生OOM注意在这个版本下执行是不对的你先认为是对的后面会给出说明让你重新的更正那么可以扩大范围如变成intshort是2 ^1665536也就是操作添加65536个可能比9m少在前面我们知道8个栈帧一般认为是空方法内存大概是1k内存这个地方但是这里是对应的运行时常量池也就是方法区的保存而如果short没有出现错误可以修改成int因此我并不能确定一个字面值是多少内存通常来说short可以满足6m62914566m6291456/6553696没有余数即一个字面值大概大于96字节数这里你可以自己测试经过我的测试一般9m就不会报错其中9m94371849437184/65536144也就是可以得出结论一个字面值内存空间大约在96到144字节之间注意在程序中只会使用内存空间因为硬盘空间是保存信息的程序的信息可不会在文件里面没有操作文件的话通过增加字符串值会发现同样如此因为一个String他占的空间是一个足够开辟大的就算你自身的数比较小也是这样大就如局部变量表中一个int他占用一个空间虽然可能与类型有关无论你int多大都是这样int i 0;while (true) {set.add(String.valueOf(i).intern());}}} 当然了你可能得不到结果或者需要等待许久一般不会除非超过默认堆大小但是一般很难超过其中long类型一般可以后面会给出说明 运行结果第一个案例错误 Exception in thread main java.lang.OutOfMemoryError: PermGen space //前面的jdk6的一般很快就出现了//space空间从运行结果中可以看到 运行时常量池溢出时 在OutOfMemoryError异常后面跟随的提示信息 是PermGen space 说明运行时常量池的确是属于方法区即JDK 6的HotSpot虚拟机中的永久代 的 一部分 而使用JDK 7或更高版本的JDK来运行这段程序并不会得到相同的结果 无论是在JDK 7中继续使 用-XX:MaxPermSize参数或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize参数把方法区容量同 样限制在6MB 也都不 会重现JDK 6中的溢出异常 循环将一直进行下去 永不停歇出现这种变 化 是因为自JDK 7起 原本存放在 永久代的字符串常量池被移至Java堆之中虽然前面说明是jdk8开始的实际上jdk7开始就这样的所以前面的测试是jdk6那么虽然前面我们以方法区统称而经过这里的说明我们知道实际上运行时常量池是在堆里面的而之前也说明过了永久代中的interned Strings字符串常量池 和 class static variables类静态变量转移到了Java heap堆“ 所以在JDK 7及以上版本 限制方法区的容量对该测试用例来说是毫无 意义的这时候使用-Xmx参数限制最大堆到6MB这里就可以试一下上面说明的9m了然后修改8m看看是否报错经过测试9m不会8m会即前面结论正确即一个字面值内存空间大约在96到144字节之间”记得等待一会哦因为他添加数据也是需要时间的虽然是true只是非常快而已在底层中一个变量的操作都必须等待超级底层相当于我的世界中一个方块在一个时间段只能被操作一次移动四人移动他但是他只能移动一个方向中间必然会使得另外一个进行等待根据理论来说会操作等待我的世界也是可以的虽然你可能认为没有操作虽然他们加起来执行的时间非常短但是也正是如此所以true或者说程序执行没有瞬发的直接得到结果必然需要计算执行时间就能 够看到以下两种运行结果之一 具体取决于哪里的对象分配 时产生了溢出 当你设置堆时你就去运行一下会发现出现异常 Exception in thread main java.lang.OutOfMemoryError: Java heap space这也更加证明了常量池在加载时变成的运行时常量池是在堆里面的虽然我们前面说明时是根据方法区来统一说明的那么有个问题他是在年轻代还是老年代呢实际上就看成类似于对象即可因为都是内存分配的吗 还有一个错误这个错误需要看这个解释根据Oracle官方文档默认情况下如果Java进程花费98%以上的时间执行GC如堆的新生代的eden满但是确一直满回到老年代那么对应的gc可能会一直操作或者会出现总时间的98%以及以上操作了gc而不会报错在上面时间满足的条件下且每次只有不到2%或者更少的总堆大小数据被释放即回收那么满足这两个条件则JVM抛出 此错误 Exception in thread main java.lang.OutOfMemoryError: GC overhead limit exceeded//GC overhead limit exceeded英文意思超过GC开销限制一般情况下我们可能测试不了这个错误可以试着多运行几次所以这个了解即可 最后一个问题前面的Full GC回收对应常量池行为决定会避免吗实际上会的你可以选择继续打开之前的Java VisualVM查看我们按照插件的那个界面可以看到他一直进行动态的操作然后观察老年代那个地方最终可以发现是慢慢的增加的注意了对应的上限是慢慢突破的所以你可以看到新生代是从上到下的看起来是一直上下动的 方法区内存溢出 方法区的其他部分的内容 方法区的主要职责是用于存放类型的相关信息 如类名、 访问修饰符、 常量池、 字段描述、 方法描述等对于这部分区域的测试 基本的思路是运行时产生大量的类去填满方法区 直到溢出为止虽然直接使用Java SE API也可以动态产生类如反射时的 GeneratedConstructorAccessor和动态代理等 但在本 次实验中操作起来比较麻烦看后面代码 这里借助CGLib使得方法区出现内存溢出异常虽然前面有关jdk6的该溢出那jdk8怎么操作呢就是使用这个一般jdk6也行如果没有对应的类自己可以操作添加依赖点击红色地方按照提示即可 package com.test3;import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.Method;/****/ public class JavaMethodAreaOOM {public static void main(String[] args) {while (true) {Enhancer enhancer new Enhancer();enhancer.setSuperclass(OOMObject.class); //类enhancer.setUseCache(false); //缓存enhancer.setCallback(new MethodInterceptor() {Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {return methodProxy.invokeSuper(o, args);}});//主要代码enhancer.create(); //创建代理对象也就是说一直帮你创建对象可以认为他帮你生成class文件执行或者生成java文件再编译成class文件执行}}static class OOMObject {}} 设置方法区大小-XX:MetaspaceSize10m -XX:MaxMetaspaceSize10m执行后会出现 Exception in thread main java.lang.OutOfMemoryError: Metaspace //再某些jdk版本下可能上面的26行即enhancer.create();可能也会报一些他自己独有的错误虽然一般是同时出现可能一般在前面先出现方法区溢出也是一种常见的内存溢出异常 一个类如果要被垃圾收集器回收 要达成的条件是比较苛刻的因为平常情况下一个类必然会被使用而不是无意识的没有使用自然可以被自动gc在经常运行时生成大量动态类的应用场景里 就应该特别关注这些类的回收状况这类场 景除了之前提到的程序使用 了CGLib字节码增强和动态语言外 常见的还有 大量JSP或动态产生JSP 文件的应用JSP第一次运行时需要编译 为Java类所以一个jsp可以看成就是一个类 、 基于OSGi的应用即使是同一个类文件 被不同的加载器加载也会视为不同的类 等在JDK 8以 后 永久代便完全退出了历史舞台 元空间作为其替代者登场在默认设置下 前面列举的那些正常的动态创建 新类型的测试用例已经很难再迫使虚拟机产生方法区的溢出异常了通常等待一会不过 为了让使用者有预防实际应用里出现类 似于上面代码那样的破坏性的操作 HotSpot还是提供了一 些参数作为元空间的防御措施 主要包括 -XX MaxMetaspaceSize 设置元空间最大值 默认是-1 即不限制 或者说只受限于本地内存 大小 -XX MetaspaceSize 指定元空间的初始空间大小 以字节为单位 达到该值就会触发垃圾收集进行类型卸载 同时收集器会对该值进行调整 如果释放了大量的空间 就适当降低该值如果释放 了很少的空间 那么在不超 过-XX MaxMetaspaceSize如果设置了的话 的情况下 适当提高该值 -XX MinMetaspaceFreeRatio 作用是在垃圾收集之后控制最小的元空间剩余容量的百分比 可 减少因为元空间 不足导致的垃圾收集的频率类似的还有-XX Max-MetaspaceFreeRatio 用于控制最 大的元空间剩余容量的百分 比 这些再前面都有说明以对应错误来直接结束上面的无限循环从而进行防御 直接内存溢出 直接内存Direct Memory 的容量大小可通过-XX MaxDirectMemorySize参数来指定这就是为什么之前说的他并不代表全部物理内存的原因即他占用物理内存的一部分 如果不去指定 则默认与Java堆最大值由-Xmx指定 一致 越过了DirectByteBu类直接通 过反射获取Unsafe实例进行内存分配 Unsafe类的getUnsafe()方法指定只有引导类加载器才会返回实例 体现了设计者希望只有虚拟机标准类库里面的 类才能使用Unsafe的功能再101章博客中就给出对应unsafe的一些说明包括有不能使用的说明虽然有具体操作地址给出但是在除了这个地址外基本没有直接的说明原因这里就是主要原因这里提一下具体地址也给出https://jiuaidu.com/jianzhan/1006040/ 在JDK 10时才将Unsafe 的部分功能通过VarHandle开放给外部使用 因为虽然使用DirectByteBu分配内存也会抛出内存溢出异常 但它抛出异常时并没有真正向操作系统申请分配内存 而是通 过计算得知内存无法分配就会 在代码里手动抛出溢出异常 真正申请分配内存的方法是Unsafe的allocateMemory() 由于直接内存是物理内存那么我们需要去操作物理内存的方法具体操作如下注意一般来说jvm存在操作的物理内存就是直接内存而jvm的直接内存可能自身也做限制其他jdk版本基本也是如此使得不会拥有非常多的物理内存当然像什么堆栈等待并不是获取直接内存的他们获取的是其他物理内存所以该直接内存是独立出来的一般直接内存的作用是避免了 在Java堆和Native堆中来回复制数据在前面说明过了这里就提一下 package com.test3;import sun.misc.Unsafe;import java.lang.reflect.Field;/****/ public class DirectMemoryOOM {private static final int _1MB 1024 * 1024;public static void main(String[] args) throws Exception {//Field[] getDeclaredFields()用于获取此Class对象所表示类中所有成员变量信息Field unsafeField Unsafe.class.getDeclaredFields()[0]; //只有这样我们才可以直接的得到并使用Unsafe//void setAccessible(booleanflag)当实参传递true时则反射对象在使用时应该取消 Java 语言访问检查 //设置可以访问private变量的变量值在jdk8之前可能不用设置但是之后包括jdk8不能直接的访问私有属性了因为需要进行设置这个所以不能直接访问私有属性了unsafeField.setAccessible(true);//Object get(Object obj)获取参数对象obj中此Field对象所表示成员变量的数值Unsafe unsafe (Unsafe) unsafeField.get(null);while (true) {//真正申请分配内存的方法是Unsafe的allocateMemory()unsafe.allocateMemory(_1MB); //具体作用是给堆外分配内存也就是认为使用了直接内存//至于作用可以看这个博客https://www.jianshu.com/p/558d323430eb记得自己找到哦建议全局搜索通过allocateMemory方法或者unsafe.allocateMemory(size * INT_SIZE_IN_BYTES);看他解释即可}}} 那么很明显当你运行时直接内存自然会溢出因为一直在使用自然最后就分配不了了从直接内存分配那么他为什么不从其他物理内存分配呢堆和栈也是一样实际上这就是防止java过于去分配内存的原因使得造成操作系统内存使用完所以给出直接内存让你有所限制c/c语言好像没有当然可能也是因为给某些直接分配内存的操作来使用而已或者说他们的限制 运行结果主要错误 Exception in thread main java.lang.OutOfMemoryError由直接内存导致的内存溢出 一个明显的特征是在Heap Dump英文意思堆转储文件中不会看见有什么明显的异常 情况 如果发现 内存溢出之后产生的Dump文件很小不操作 而程序中又直接或间接使用了 DirectMemory典型的间接使用就是NIO 那就可以考虑重点检查一下直接内存方面的原因了这里了解即可 JVM加载机制详解 类装载子系统 类加载子系统介绍 1类加载子系统负责从文件系统或是网络中加载.class文件class文件在文件开头有特定的文件标识 2把加载后的class类信息存放于方法区除了类信息之外方法区统一说明的还会存放运行时常量池信息可能还包括字符串字面量和 数字常量这部分常量信息是class文件中常量池部分的内存映射也就是常量池信息 3ClassLoader只负责class文件的加载至于 它是否可以运行则由Execution Engine执行引擎也就是解释加载的class虽然加载的并没有改变文件总体内容使得解释时操作总体的运行时数据区可以相当于像之前的说明class文件内容一样的说明决定 4如果调用构造器实例化对象则该对象存放在堆区 执行引擎操作方法区内存和堆内容来进行识别因为其他的区域是线程私有的所以一般只有在识别时才会进行操作可能因为主线程也有固定存在 但是总体来说大多数是方法区的操作被识别可能包括堆因为常量池其他的是用来进行辅助识别操作 类加载器ClassLoader角色 1class file 存在于本地硬盘上可以理解为设计师画在纸上的模板而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例 2class file 加载到JVM中被称为DNA元数据模板 3在 .class文件 -- JVM -- 最终成为元数据模板此过程就要一个运输工具类装载器Class Loader扮演一 个快递员的角色 看图可知对象的图我们通过类加载器可以变成Class加载并初始化然后他可以操作多个实例一个类是可以有多个对象的实例化所以前面我才写上n个一模一样的实例而实例可以通过对应的方法得到ClassgetClass()在28章博客中有过操作而Class也可以得到类加载器getClassLoader()在28章博客中也有过操作 类加载的执行过程 我们知道我们写的程序经过编译后成为了.class文件.class文件中描述了类的各种信息最终都需要加载到虚拟机 之后才能运行和使用而虚拟机如何加载这些.class文件.class文件的信息进入到虚拟机后会发生什么变化 类使用的7个阶段所有的阶段也就是最后的执行那么自然包括类加载器运行时数据区执行引擎垃圾回收器等等 类从被加载到虚拟机内存中开始到卸载出内存它的整个生命周期包括加载Loading、验证 Verification、准备Preparation、解析Resolution、初始化Initiallization、使用Using和卸载 Unloading这7个阶段其中验证、准备、解析3个部分统称为连接Linking这七个阶段的发生顺序如下图 图中加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的类的加载过程必须按照这种顺序按部就班地 开始而解析阶段不一定它在某些情况下可以初始化阶段之后在开始这是为了支持Java语言的运行时绑定也称为动态绑定 接下来讲解加载、验证、准备、解析、初始化五个步骤这五个步骤组成了一个完整的类加载过 程看前面的图就知道了类加载子系统使用没什么好说的卸载属于GC的工作 加载 加载是类加载的第一个阶段有两种时机会触发类加载 1预加载 虚拟机启动时加载加载的是JAVA_HOME/lib/下的rt.jar下的.class文件JAVA_HOME一般是我们环境变量对应的值到这里大多数人应该明白是什么这里就不多说了但这里也要注意不同版本的jdk可能对应的.class文件的目录文件不同所以并不是一定是lib/rt.jar一般lib不会变而rt.jar可能不是这个jar包里面的内容是程序运行时通常会用到的像java.lang.*、java.util.、java.io. 等等因此随着虚拟机一起加载且为了知道他加载的文件是那个因为所以并不是一定是rt.jar所以我们可以操作如下 要证明是否一起加载且知道是那个文件这一点很简单写一个空的main函数设置虚拟机参数为-XX:TraceClassLoading来获取类加载信息运行一下 package com.test4;/****/ public class test1 {public static void main(String[] args) {System.out.println(hello);} } 参数设置在前面多次进行了操作就不说明了这里自己复制粘贴即可 通过运行看如下一部分信息 [Opened C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar] [Loaded java.lang.Object from C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar] [Loaded java.io.Serializable from C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar] [Loaded java.lang.Comparable from C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar] [Loaded java.lang.CharSequence from C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar] [Loaded java.lang.String from C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar] [Loaded java.lang.reflect.AnnotatedElement from C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar] [Loaded java.lang.reflect.GenericDeclaration from C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar] [Loaded java.lang.reflect.Type from C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar] [Loaded java.lang.Class from C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar]很明显他并不是在直接的lib里面而是jre里面jdk1.8来说的其他版本不能确定可以自己试一下并且他加载时如java.lang.String就是该文件里面的即的确加载了若要看其他jdk版本的你主要改变运行版本即可因为他是用来加载的 2运行时加载 虚拟机在用到一个其中比如自己写的或者上面自带的.class文件的时候会先去内存中查看一下这个.class文件有没有被加载如果没有就会按照类的 全限定名来加载这个类否则不会加载因为只会加载一次具体一般看class文件里面的检验的数字虽然在同一个目录下也通常不会有两个相同名称文件来进行加载但他可以来区分不同目录的相同名称文件的所以以他为主要区分虽然全限定名也是一个区分方法 那么该加载阶段做了什么其实该加载阶段做了有三件事情 1获取.class文件的二进制流并不是二进制相当于就是得到该文件的流如IO流 2将类信息、静态变量、字节码、常量这些.class文件中的内容放入方法区中 3在内存中生成一个代表这个.class文件的java.lang.Class对象作为方法区这个类的各种数据的访问入口一般 这个Class是在堆里的不过HotSpot虚拟机比较特殊这个Class对象通常是放在方法区中的所以以后这里要记住哦虽然并没有进行语义改变过堆和方法区都是数据的存放形式那么方法区自然可以操作出小的类似的堆空间反正都是程序操作的而已 虚拟机规范对这三点的要求并不具体因此虚拟机实现与具体应用的灵活度都是相当大的例如第一条根本没有 指明二进制字节流要从哪里来、怎么来因此单单就这一条就能变出许多花样来 1比如你可以从zip包中获取这就是以后为什么可以是jar、ear、war格式的基础就如自己写的class也是如此只要可以保存他即可 2从网络中获取典型应用就是Applet 3运行时计算生成典型应用就是动态代理技术程序生成相当于自己写 4由其他文件生成典型应用就是JSP即由JSP生成对应的.class文件 5从数据库中读取这种场景比较少见 总而言之在类加载整个过程中这部分是对于开发者来说可控性最强的一个阶段只要你可以生成class文件就可以进行放入方法区无论你是自己写还是程序生成还是其他生成等等都可以 链接连接 链接包含三个步骤 分别是 验证Verification准备Preparation解析Resolution 三个过程 验证Verification 连接阶段的第一步这一阶段的目的是为了确保.class文件的字节流中包含的信息符合当前虚拟机的要求并且不 会危害虚拟机自身的安全 Java语言本身是相对安全的语言相对C/C来说但是前面说过.class文件未必要从Java源码编译而来可以 使用任何途径产生甚至包括如用十六进制编辑器直接编写来产生.class文件在字节码语言层面上Java代码至少从 语义上是可以表达出来的虚拟机如果不检查输入的字节流对其完全信任的话很可能会因为载入了有害的字节 流而导致系统崩溃所以验证是虚拟机对自身保护的一项重要工作 验证阶段将做一下几个工作具体就不细讲了这是虚拟机实现层面的问题 文件格式验证元数据验证字节码验证符号引用验证 实际上在前面说明class字节码时会有一个存放的地址而他能够很好的进行区分但是他并不是验证的手段主要手段是对应的检验的数字实际上在编译时会生成数字并且保存或者某些解析那么手动的写自然不能进行解决大多数的加密我们自己还并不能有这个能力解密就算可以他里面可能并没有保存对应的加密信息但是他也可能并不会保存也是防止被盗取只是一个解密后的固定所以若你可以解密通常可以进行载入有害字节当然了既然你有这个能力为什么做这个不好的事情呢 准备Preparation 准备阶段是正式为类变量分配内存并设置其初始值的阶段这些变量所使用的内存都将在方法区中分配既然是加载自然是在过程中操作到方法区的所以才说加载到方法区总不能你运行完才进行操作吧那么为什么不出现一个提交的步骤呢关于这 点有两个地方注意一下 1这时候进行内存分配的仅仅是类变量被static修饰的变量而不是实例变量实例变量将会在对象实例化 的时候随着对象一起分配在Java堆中 2这个阶段赋初始值初始值并不是具体值的变量指的是那些不被final修饰的static变量比如public static int value 123value在准 备阶段过后是0初始值而不是123给value赋值为123的动作将在初始化阶段也就是要操作()方法时初始化那里会说明才进行比如public static final int value 123;就不一样了必须赋值可不会操作默认值哦在准备阶段虚拟机就会给value赋值为123不是初始值但初始化不会简单来说常量在准备时进行赋值而静态变量这里不包括常量只有初始值 各个数据类型的零值初始值如下表 其中reference代表所有的引用这里用他来表示而已且也很明显默认是int类型的初始值虽然他是这样认为的也就是分配这个空间int这个过程操作到方法区中前面说过了自然是在过程中操作到方法区的 我们顺便看一道面试题下面两段代码code-snippet 1 将会输出 0而 code-snippet 2 将无法通过编译idea的检查也是一个防线虽然编译的检查是自带的而不是idea的但是他由于会给出具体的错误位置和对应的解决方式所以说成idea的检查也可以 code-snippet 1 public class A {static int a;public static void main(String[] args) {System.out.println(a);}} code-snippet 2 public class B {public static void main(String[] args) {int a;System.out.println(a);}}注意 这是因为局部变量不像类变量那样存在准备阶段类变量有两次赋初始值的过程一次在准备阶段赋予初始值 也可以是指定值另外一次在初始化阶段赋予程序员定义的值 因此即使程序员没有为类变量赋值也没有关系它仍然有一个默认的初始值若有final的必须有初始值即final是只有在准备阶段赋值的属性但局部变量就不一样了如果没有 给它赋初始值是不能使用的而正是因为一开始就赋值那么静态的需要有先后顺序使得可以得到结果在后面初始化中会大致说明而静态构造就是静态操作后的操作算成一个方法在初始化变量之前main方法之前并不是只有main里面才会有打印哦否则对应添加了-XX:TraceClassLoading参数的打印信息是怎么来的呢实际上打印是保留好的在执行后操作打印出现 解析Resolution 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程来了解一下符号引用和直接引用有什么区别 1符号引用 符号引用是一种定义可以是任何字面上的含义而直接引用就是直接指向目标的指针、相对偏移量这个其实是属于编译原理方面的概念符号引用包括了下面三类常量 类和接口的全限定名字段的名称和描述符方法的名称和描述符 这么说可能不太好理解结合实际看一下写一段很简单的代码 package com.test4;/****/ public class TestMain {private static int i;private double d;public static void print() {}private boolean trueOrFalse() {return false;}} 对应的class文件主要看常量池 Constant pool:#1 Methodref #3.#20 // java/lang/Object.init:()V#2 Class #21 // com/test4/TestMain#3 Class #22 // java/lang/Object#4 Utf8 i#5 Utf8 I#6 Utf8 d#7 Utf8 D#8 Utf8 init#9 Utf8 ()V#10 Utf8 Code#11 Utf8 LineNumberTable#12 Utf8 LocalVariableTable#13 Utf8 this#14 Utf8 Lcom/test4/TestMain;#15 Utf8 print#16 Utf8 trueOrFalse#17 Utf8 ()Z#18 Utf8 SourceFile#19 Utf8 TestMain.java#20 NameAndType #8:#9 // init:()V#21 Utf8 com/test4/TestMain#22 Utf8 java/lang/Object看到Constant Pool也就是常量池中有22项内容其中带Utf8的就是符号引用比如#21它的值 是com/test4/TestMain表示的是这个类的全限定名又比如#4为i#5为I它们是一对的表示变量时Integerint类型的名字叫做i#7为D、#6为d也是一样表示一个Doubledouble类型的变量名字为d大写的D表示Double小写的d表示double统称为d上面的int和Integer也是如此统称为i#15、#16表示的都是方法的名字所以这三个类和接口的全限定名字段的名称和描述符方法的名称和描述符都说明完毕 那其实总而言之符号引用和我们上面讲的是一样的是对于类、变量、方法的描述符号引用和虚拟机的内存布 局是没有关系的引用的目标未必已经加载到内存中了 2直接引用 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄直接引用是和虚拟机实现的 内存布局相关的同一个符号引用在不同的虚拟机示例上翻译出来的直接引用一般不会相同如果有了直接引用 那引用的目标必定已经存在在内存中了简单来说原来某些地方是符号引用那么解析就是将该符号变成了该直接的引用比如原来是#21那么你就会变成com/test4/TestMain当然在保存上该文件自然是少的也是使用符号引用的原因前面说明过而引用是一个总显示只需要少的字节码如#2就可以替换掉java/lang/Math.random : ()D就可以显示了这个地方 解析阶段负责把整个类激活串成一个可以找到彼此的网过程不可谓不重要那这个阶段都做了哪些工作呢大 体可以分为 类或接口的解析类方法解析接口方法解析字段解析也就是类方法字段如变量正好是类的所有信息 初始化 类的初始化阶段是类加载过程的最后一个步骤 之前介绍的几个类加载的动作里 除了在加载阶 段用户应用程序 可以通过自定义类加载器的方式局部参与外 其余动作都完全由Java虚拟机来主导控 制直到初始化阶段 Java虚拟机才真正开始执行类中编写的Java程序代码 将主导权移交给应用程序这就说明只有初始化后应用程序才会真正执行 初始化阶段就是执行类构造器()方法的过程类构造器()简称为 cinit ()别称为()可以认为是操作创建Class的并不是程序员在Java代码中直接编写 的方法 它是Javac编译器的 自动生成物()方法是由编译器自动收集类中的所有类变量静态变量类变量就是静态变量前面说明过了类变量被static修饰的变量的赋值动作和静态语句块static{}块 中的 语句合并产 生的实际上你就看成静态语句块执行完包括赋值包括静态方法没有顺序因为赋值是加载后的初始化按照顺序的他就执行或者变量使用他也执行因为你执行或者使用最终都会使得他执行 编译器收集的顺序是由语句在源文件中出现的顺序决定的 静态语句块中只能访问 到定义在静态语句块之前的变量 定义在它之后的变量 在前面的静态语句块可以赋值 但是不能访问看如下代码 public class TestClinit {static {i 0; // 给变量只能是静态变量因为这里是静态代码块当然他会先识别是否有操作到i的情况编译期运行期会保留不会考虑因为是慢慢执行过去的复制可以正常编译通过System.out.print(i); // 这句编译器会提示非法向前正向引用也就是说静态块必须满足先后顺序才可打印对应的变量这是对打印来说的即打印满足先后顺序这是打印的限制}static int i 1; }//实际上只是针对访问来说的访问需要顺序但是修改不会因为访问需要符号顺序从而进行替换而修改时修改虽然可以修改但是上面的结果还是1因为在没有类型static int的情况下保留了赋值的动作编译器自动收集类中的所有类变量的赋值动作当他有类型时先操作之前的赋值然后操作1的赋值即他们总体来说就是按照顺序的只是修改操作时静态是保留动作的使得他是一个特殊的不要类型就可以赋值的情况在方法中必然是不行的所以他是特殊的底层代码基本都可以实现哦不要觉得反常因为他也是人操作出来的自然也可以由人来修改所以按照上面的情况只要你将代码块放在后面就行这样最后由于有类型那么可以直接的赋值//实际上不是静态的操作与静态同理后续了其中也会先操作默认值的最先也包括非法前向引用要注意哦在这个过程中一个类操作完构造方法他才算结束或者到子继续操作这是保证实例的完整 ()方法与类的构造函数即在虚拟机视角中的实例构造器()方法 不同不是构造方法哦它不需要显 式地调用父类构造器在有参数的情况下没有参数一般可以忽略 Java虚拟机会保证在子类的()方法执行前 父类的()方法已经执行 完毕因此在Java虚拟机中第一个被执行的()方法的 类型肯定是java.lang.Object 由于父类的()方法先执行 也就意味着父类中定义的静态语句块必然是要优先于子类的变量赋值 一般是使用就行操作包括他的静态代码块就如下面的代码中 字段B的值将会是2而不是1看后面执行顺序 package com.test4;/****/ public class TestClinit02 {static class Parent {public static int A 1;static {A 2;}}static class Sub extends Parent {public static int B A; public Sub(){System.out.println(1); //不会执行}}public static void main(String[] args) {System.out.println(Sub.B); //使用即前面的或者变量使用他也执行常量可不会哦而直接的使用必然是静态的所以有时候我们也认为是使用静态变量因为如果不是静态的必然需要创建对象那么还不是也操作初始化了//结果上面打印了1和2}} ()方法对于类或接口来说并不是必需的 如果一个类中没有静态语句块 也没有对变量的 赋值一般是使用就行操作看上面的代码就行 那么编译器 可以不为这个类生成()方法接口中不能使用静态语句块规定的因为他不是用来进行操作的而静态语句块可以所以没有静态语句块 但实际上接口仍然有变量初始化的赋值操作 但为了进行兜底因此接口与类一样 都会生成 ()方法 但接口与类不同的是 执行接口的()方法不需要先执行父接口的()方法 因为只有当父接口中定义的变量被使用 时 父接口才会被初始化看成没有使用就存在标记此外 接口的实现类在初始化时也 一样不会执行接口的()方法 Java虚拟机必须保证一个类的()方法在多线程环境中被正确地加锁同步 如果多个线程同时去初始化一个类 那 么只会有其中一个线程去执行这个类的()方法也就是会执行静态代码块 其他线程都需要阻塞等 待只在创建对象那里阻塞 直到活动线程执行完毕()方法那么其他线程之间跳过执行静态代码块不会继续执行哦所以如果在 一个类的()方法中有耗时很长的操作 那就 可能造成多个进程阻塞 在实际应用中这种阻塞往往是很隐蔽的 看如下代码 package com.test4;/****/ public class TestDeadLoop {static class DeadLoopClass {static { // 如果不加上这个if语句 编译器将提示Initializer does not complete normally并拒绝编译该英文意思是初始化器未正常完成//为什么呢实际上编译为class的检查并不是非常严谨的有时候他并不会知道他是一直循环的所以我们才会说实际应用中这种阻塞往往是很隐蔽的if (true) {System.out.println(Thread.currentThread() init DeadLoopClass); //多次执行可以看到不同的结果while (true) {}}}}public static void main(String[] args) {Runnable script new Runnable() {public void run() {System.out.println(Thread.currentThread() start);DeadLoopClass dlc new DeadLoopClass();System.out.println(Thread.currentThread() run over);}};Thread thread1 new Thread(script);Thread thread2 new Thread(script);thread1.start();thread2.start();}} 我们知道只有对应的()加载完类才初始化完毕否则如果类没有初始化完毕那么自然不能创建对象因为对于该类来说没有Class这也是为什么如果没有初始化完会进行阻塞的原因也就是在创建对象操作时若没有初始化那么阻塞而有初始化需要之前的都加载好也就需要静态构造块进行加载 注意只有需要class文件加载才会执行之前上面的所有操作否则像加载连接初始化并不会操作很明显当我们需要new DeadLoopClass()时需要加载Class类那么就会经历这些过程但只会在经历初始化时考虑是否创建对象在前面说明了初始化阶段就是执行类构造器()方法的过程所以当你设置DeadLoopClass dlc null他执行不阻塞因为你只是利用类信息这个在连接中就已经操作了而new DeadLoopClass()就会操作静态代码块因为当你需要提取对象也就是需要Class就必须要完成初始化这个步骤这也是为什么之前说的静态语句块执行完他就执行即静态语句块是构造方法之前执行的构造方法也是激活静态语句块的一个操作静态赋值也算的而常量在准备时就已经赋值了使得常量不可以使用的即不会进行初始化操作因为他不需要而单纯静态需要所以看起来初始化也的确是合并产生的即该对应操作使用就可以认为是初始化而并非先后执行的操作因为看成一体不更好吗反正没有什么拦截虽然实际上是一个意思的一体的但是之前也说明了或者变量使用他也执行即对应的赋值也会执行该初始化注意了其中main方法由于是必然执行的也就是说当前类一定有Class所以若对应的静态代码块在当前类那么他必然执行在main方法之前也符合我们的说明 总而言之初始化的操作需要考虑是否创建对象而进行是否执行静态代码块当然构造块介于静态代码块和构造方法之前优先于构造方法慢于静态代码块而无论有没有初始化都不影响程序的运行因为main必然是操作的就在执行时有操作就进行创建Class否则继续执行即可所以你可以将初始化看成另外一个空间就行比如虚拟机栈帧虽然他又会操作堆里面这也是为什么在前面的图中在其他的是用来进行辅助识别操作下面的图会存在有双向箭头的原因一般是原因之一只是可能没有对应的Class而已也就是不能操作对应的实例但是创建对象会执行静态代码块然后执行初始化从而又会创建ClassClass可以有多个也不是他一个从而创建对象所以正是因为比较麻烦即只要你有第一个Class那么以后创建对象就不会在创建Class了即只有第一次需要 至此我们的加载类加载连接static初始化构造和静态代码块该步骤可以不操作虽然可能有默认的存在说明完毕很明显他们的操作结合在加载后保留到方法区中然后再解释执行时会依次的解释然后执行当然他们必然是有顺序的因为这个步骤就是顺序那么解释起来自然也是按照顺序的所以再说明他们的顺序执行时又何尝不是代码运行的顺序呢即我们的案例实际上就是对的而使用我们就看成就是执行卸载看成就是gc操作即可至此我们的class文件的执行流程说明完毕类加载器就是操作他的 cinit 和 init cinit 方法和 init 方法有什么区别 主要是为了让你弄明白类的初始化和对象的初始化之间的差别 前面我们说明的 cinit 方法大多数只是一种概念通常静态代码块执行后或者静态变量按照main来说因为不是静态的必然是需要对象main可是static哦使用后他才会执行 看如下代码 package com.tets5;/****/ public class ParentA {static {System.out.println(1);}public ParentA() {System.out.println(2);}}class SonB extends ParentA { //没有public当前文件的副本类相当于一个文件操作两个类虽然这个是副本但是对应的public只是一个修饰而已也就是访问权限而已并没有什么作用static {System.out.println(a);}public SonB() {//如果父类只有 有参数的构造那么这里需要手动加上super方法System.out.println(b);}public static void main(String[] args) {ParentA ab new SonB(); //这个是ParentA ab null对应的1a也会出现因为前面说明过了即其中main方法由于是必然执行的也就是说当前类一定有Class所以若对应的静态代码块在当前类那么他必然执行在main方法之前ab new SonB();}} 1 a 2 b 2 b静态只操作一次哦 其中 static 字段和 static 代码块是属于类的在类的加载的初始化阶段就已经被执行类信息会被存放在方法 区在同一个类加载器下这些信息有一份就够了所以上面的 static 代码块只会执行一次它最终对应的是()方法也就是 cinit 方法 所以上面代码的 static 代码块只会执行一次对象的构造方法执行两次再加上继承关系的先后原则不难分析 出正确结果 结论 cinit 方法 的执行时期类初始化阶段该方法只能被jvm调用专门承担类变量的初始化工作只执行一次 init 方法 的执行时期对象的初始化阶段 类加载器 类加载器的作用 类的加载指的是将类的.class文件中的二进制数据读入到内存中将其放在运行时数据区的方法区内然后在创建 一个java.lang.Class对象用来封装类在方法区内的数据结构 注意JVM主要在程序第一次主动使用类的时候才会去加载该类这里对应于Class也就是说JVM并不是在一开始就把一个 程序就所有的类都加载到内存中而是到不得不用的时候才把它加载进来而且只加载一次就如之前的创建对象 类加载器分类 1jvm支持两种类型的加载器分别是引导类加载器和 自定义加载器 2引导类加载器是由c/c实现的自定义加 载器是由java实现的 3jvm规范定义自定义加载器是指派生于继承抽象类ClassLoader的类加载器 4按照这样的加载器的类型划分在程序中我们最常见的类加载器是引导类加载器BootStrapClassLoader、自定 义类加载器Extension Class Loader扩展类加载器、System Class Loader系统类加载器、User-Defined ClassLoader用户自定义类加载器他们只是一个名称并不是类 上图中的加载器划分为包含关系而并非继承关系 下面一般是以jdk8来说明的若有不同可以自行改变版本查找当然也可能已经淘汰了所以也并非一定存在当然也可能是idea查找不到那么可以选择找ClassLoader然后通过他找SecureClassLoader然后找URLClassLoaderExtClassLoader他就是在Launcher里面一般idea找不到sun.misc.Launcher所以这里要注意不同版本对应的目录或者说明可能不同 至于如何找点击如下提示的地方即可 启动类加载器 1这个类加载器使用c/c实现嵌套再jvm内部 2它用来加载Java的核心类库如类似的JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路径下的内容具体路径内容可以百度用于提供JVM自身需要的类 3并不继承自java.lang.ClassLoader没有父加载器 扩展类加载器 1java语言编写由sun.misc.Launcher里面的内部类ExtClassLoader实现具体可以百度查看一般与版本有关 2从java.ext.dirs系统属性所指定的目录中加载类库 或从JDK的安装目录的如类似的jre/lib/ext 子目录扩展目录下加载类库如果用户创建的JAR 放在此目录下也会自动由 扩展类加载器加载派生于派生于一般就是继承 ClassLoader 3父类加载器为启动类加载器并不是继承的意思上图中的加载器划分为包含关系而并非继承关系 系统类加载器 1java语言编写由 sun.misc.Lanucher里面的内部类可以使用$表示AppClassLoader 实现具体可以百度查看一般与版本有关 上面使用$表示的是 sun.misc.Lanucher$AppClassLoader 2该类加载是程序中默认的类加载器一般来说Java应用的类都是由它来完成加载的也就是说之前的类加载操作就是他它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库派生于 ClassLoader最终继承 3父类加载器为扩展类加载器 4通过 ClassLoader.getSystemClassLoader() 方法可以获取 到该类加载器类似的一个图片复制粘贴来的这里一般是jdk8的结果 用户自定义类加载器启动扩展系统自定义是启动类扩展类系统类自定义类的简称 在日常的Java开发中类加载几乎是由三种加载器配合执行的在必要时我们还可以自定义类加载器上面三种又何尝不是自定义的呢也是人写的来定制类的 加载方式通常也是派生于 ClassLoader如果没有自定义默认自定义是系统加载器也就是使用系统加载器那么流程就是jvm通过了启动类加载器扩展类加载器系统类加载器后操作后续自定义加载器由于自定义默认系统加载器那么没有后面的可以认为自定义是一个节点当jvm操作完系统后看看后面是否有节点没有那么不操作很明显由于自定义是系统加载器类那么后面自然没有节点了就类似于这样的说明 当然了上面的图片也只是图片那么现在给出所有获取这里加载器的代码可以自己复制操作这里是jdk11的结果 package com.tets5;/*** 获取不同类加载器* 这里是jdk11在不同版本下可能打印信息不同*/ public class test {public static void main(String[] args) {//获取系统类加载器ClassLoader systemClassLoader ClassLoader.getSystemClassLoader();System.out.println(systemClassLoader); //jdk.internal.loader.ClassLoaders$AppClassLoader2437c6dc//获取扩展加载器即系统加载器的父加载器ClassLoader parent systemClassLoader.getParent();System.out.println(parent); //jdk.internal.loader.ClassLoaders$PlatformClassLoader39ba5a14//通过查看结果发现的确他们都是最终继承ClassLoader也知道他们自己并没有互相联系只是由于方法的联系所以我们才会说加载器划分为包含关系而并非继承关系就是可以通过方法来获得的主要说明//获取启动类加载器ClassLoader parent1 parent.getParent();System.out.println(parent1); //null一般这个加载器通常不能获取或者说这个方式不能获取他是最底层的加载器可不能让你随便的操作哦//获取用户自定义的加载器他的结果一般与系统加载器的结果一样即自定义的通过对自己类的操作加载器可以知道给操作他因为自定义就是操作我们的类的//所以一般这样是得到自定义的方式对于该类来说是自定义加载器因为自定义是最后操作的类那么一个Class可以被加载多次吗答只有1次的虽然前面说明了会判断的每个类都有一个检验数字哦一般生成Class时可能会保留这个检验数字的除非你是不同的类包括不同目录的相同名称的类ClassLoader classLoader test.class.getClassLoader(); //很明显你看到这个就明白在前面操作时的确可以通过Class来反过来获取System.out.println(classLoader); jdk.internal.loader.ClassLoaders$AppClassLoader2437c6dc//核心类库使用的是启动类加载器以String为例他的上一个就是启动类按照惯例也可以说成是他的自定义的加载器ClassLoader classLoader1 String.class.getClassLoader();System.out.println(classLoader1); //null} }//即自定义加载器只是相对于类来说的谁最后操作他那么谁就是他的自定义加载器至于如何看他加载了哪些文件可以百度查找对应的API即可这里就不做说明 双亲委派模型 什么是双亲委派 双亲委派模型工作过程是如果一个类加载器收到类加载的请求它首先不会自己去尝试加载这个类一般是从系统开始往上若有自定义即从自定义开始的虽然需要他操作才行而是把这个 请求委派给父类加载器完成这也是为什么启动类加载器必然先操作的原因每个类加载器都是如此只有当父加载器在自己的搜索范围内找不到指定的类时 一般是ClassNotFoundException子加载器才会尝试自己去加载 注意这里非常重要如果不考虑任何因素实际上他们都没有加载任何一个字节码文件包括String但是我们知道有main方法的操作且是入口自然在这个过程中会加载很多的字节码文件也就会操作初始化这样才会有Class所以这里要注意了 这里给出一个例子 package java.lang;/****/ public class String {static {System.out.println(我们自定义的String加载);} } package test;/****/ public class StringTest {public static void main(String[] args) {String s new String();System.out.println(Stringtest);} } 执行后一般需要jdk8的语言版本否则在编译期会直接报错一般不会打印System.out.println(“我们自定义的String加载”);也就是说他已经加载了而你这个没有加载也就是得不到我们自定义的String所以在编写时他只会给出父加载器的该相同包的该字节码 为什么需要双亲委派模型 为什么需要双亲委派模型呢假设没有双亲委派模型试想一个场景 黑客自定义一个 java.lang.String 类该 String 类具有系统的 String 类一样的功能只是在某个函数稍作修改比如 equals 函数这个函数经常使用如果在这个函数中黑客加入一些病毒代码并且通过自定义类加载器加入到JVM中此时如果没有双亲委派模型那么 JVM 就可能误以为黑客自定义的java.lang.String 类是系统的 String 类既然没有双亲委派模型那么他并不认为String一定是加载的或者说没有加载因为他没有操作父加载器导致病毒代码被执行比如遍历c盘文件然后都删除而有了双亲委派模型黑客自定义的 java.lang.String 类永远都不会被加载进内存因为首先是最顶端的类加 载器加载系统的 java.lang.String 类最终自定义的类加载器无法加载 java.lang.String 类或者只会加载没有父的对应类的只能加载一次哦这也是为什么他加载是加载一次而不是覆盖他的主要原因 这里还是给出一个例子 package java.lang;/****/ public class String {static {System.out.println(我们自定义的String加载);}public static void main(String[] args) {System.out.println(1); //还是上面的但是这里我们操作执行} } 他会报如下错误 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:public static void main(String[] args) 否则 JavaFX 应用程序类必须扩展javafx.application.Application //即他的确没有被加载因为找不到或许你会想我在自定义的类加载器里面强制加载上面的自定义的 java.lang.String 类自定义的而不是他不加载即不去通过调用父加载器不就好了吗?确实这样是可行但是在 JVM 中实际上为了后续也一般不会进行操作因为你这样容易被黑客操作你手写的文件就是一个最好的例子实际上这并不是最重要的最重要的是具体文件的内容你一人也基本只能实现一部分为什么这样说你可以选择看看原来的String文件的内容就知道了你并不能保证都写出来所以在这里的结果下即在上面说明的情况下也会判断一个对象是否是某个完全相同的类型的这个判断手段太多的比如反射就可以如果该对象的实际类型与待比较 的类型的类加载器不同那么会返回false即不同类型这并不是表示加载失败反而是加载成功因为在前面说明过即使是同一个类文件 被不同的加载器加载也会视为不同的类 举个简单例子 ClassLoader1ClassLoader2都加载 java.lang.String类可能是项目说明的包哦或者其他相同的类对应Class1、Class2对象那么Class1对象不属于 ClassLoadr2对象加载的 java.lang.String 类型//一般由于先后顺序所以之前才会说明因为首先是最顶端的类加载器加载系统的 java.lang.String 类最终自定义的类加载器无法加载 java.lang.String 类或者只会加载没有父的对应类的//在后面会说明的那么如何保证他操作的equals是正确的类型的呢实际上虽然看起来是相同的String但是我们知道初始化是最后操作的也就是说中间的如解析操作还是存在即加载的存在一般来说对应的String可能会保留或者在解析时进行操作使得String是对应的String这也就是导致虽然你加载了但是你后加载那么我的为主即由于启动的String先加载那么以启动的为主 当然了他们的流程只是一个明面上的操作在高级黑客中或者顶端黑客中还是防不住的因为我完全可以将原来的String文件改变或者改变底层代码来实现只要你是二进制实现的而互联网就是二进制的操作但硬件层面的必要操作是不可能改变的就如我的世界中将一个方块往前那么他必然是往前的而不会出现错误的可能性除非该硬件改变但是硬件只能由操作他的人改变的而不会因为在他之上的逻辑程序而改变基本都可以黑掉这也是为什么说世界上没有100%安全的防护原因 所以由于双亲是父子子先让父操作先委派你操作所以是双亲委派 如何实现双亲委派模型 双亲委派模型的原理很简单实现也简单每次通过先委托父类加载器加载当父类加载器无法加载时再自己加 载其实 ClassLoader 类默认的 loadClass 方法已经帮我们写好了我们无需去写所以这里说明一下几个重要函数 几个重要函数 loadClass 默认实现如下不同jdk版本可能有些变量不同或者说实现不同但是大体相同这里以jdk11为主 public Class? loadClass(String name) throws ClassNotFoundException {return loadClass(name, false); } 再看看loadClassString nameboolean resolve函数 protected Class? loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) { //加锁保证只有一人进入类加载器可能有很多特别是同级的即主要是防止我们手动的操作// First, check if the class has already been loadedClass? c findLoadedClass(name); //查看是否加载了name名称的类可以认为是全限定名即先从缓存查找该class对象找到就不用重新加载一般代表当前加载器的缓存这就是只会加载一次的原因if (c null) {long t0 System.nanoTime();try {if (parent ! null) { //看看有没有父类加载器//如果找不到则委托给父类加载器去加载c parent.loadClass(name, false); //父类继续操作是否加载还是这个方法哦} else {//如果没有父类则委托给启动加载器去加载没有找到加载的或者说没有加载自然也会返回null虽然看起来他也有判断的成分但是是因为操作的原因导致的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();//之前说过了若有自定义即从自定义开始的那么通过上面的查找说明前面都没有加载自然操作自定义的// 如果都没有找到则通过自定义实现的findClass去查找并加载每个加载器都操作这个一般来说自定义我们自己操作而扩展和应用一般使用他们的父类的通常是一般是URLClassLoader里面的该方法来加载自己 c findClass(name); //使用自己的加载器加载没有加载自然也是null与前面的findBootstrapClassOrNull基本类似反正都是加载只是findBootstrapClassOrNull代表启动而这里代表不是启动的很明显这就是自定义或者不是启动的加载器的操作这就是为什么最后会操作自定义和其他加载器的原因与双亲委派联系起来即该方法及其前面的代码就是双亲委派的逻辑代码/*//这里考虑自定义的protected Class? findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name); //而正是因为他是默认报错的所以我们自定义操作时需要重写该方法来操作加载记住到这里说明是自定义加载器了自然默认的this就是自定义的所以是自定义的该方法这里一般代表加载这里是操作加载的与上面的findBootstrapClassOrNull是类似的作用所以说到这里就可以了后面的就不做说明具体可以百度他与loadClass都是ClassLoader里面的而自定义加载器可是继承他ClassLoader的自然若重写的话会使用自定义加载器的findClass版本}*/// this is the defining class loader; record the statsPerfCounter.getParentDelegationTime().addTime(t1 - t0);PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);PerfCounter.getFindClasses().increment();}}if (resolve) { //是否需要在加载时进行解析一般是false看前面的return loadClass(name, false);resolveClass(c);}return c;}} 从上面代码可以明显看出 loadClass(String, boolean) 函数即实现了双亲委派模型整个大致过程如下 1首先检查一下指定名称的类是否已经加载过如果加载过了就不需要再加载直接返回 2如果此类没有加载过那么再判断一下是否有父加载器如果有父加载器则由父加载器加载即 调用 parent.loadClass(name, false); 或者是调用 findBootstrapClassOrNull来使得加载启动类加载器加载这里简称 bootstrap 类加载器也就是启动加载器的统称 3如果父加载器自然使得 bootstrap 类加载器都没有找到指定的类那么调用当前类加载器自定义的 findClass 方 法来完成类加载启动类加载完返回有值否则没有值具体看c/c的操作这里不做说明 实际上我们看代码就知道自定义类加载器就必须重写 findClass 方法否则他是不可能会加载的默认是抛出异常虽然他并不是抛出的意思直接打印异常但是一般该情况也可以说成是抛出虽然他是直接的打印打印异常就相当于停止后面的操作了一般在try里面他并不会操作catch的printStackTrace的打印直接跳过因为他本身就是一个打印信息而printStackTrace会判断是否操作打印错误的如果你是该直接的打印那么我不打印否则我打印 所以抽象类 ClassLoader 的 findClass 函数默认是抛出异常的而前面我们知道 loadClass 在父加载 器无法加载类的时候就会调用我们自定义的类加载器中的 findeClass 函数因此我们必须要在 loadClass 这 个函数里面实现将一个指定类名称转换为 Class 对象的操作来完成加载这就是自定义加载的核心操作也是我们操作自定义加载器的核心操作 如果是读取一个指定的名称的类为字节数组的话这很好办虽然默认的加载器或者说jdk自带的一般没有这个名称所以通常需要特殊的办法但是二进制是不讲道理的只要可以实现某些能力那么在这个能力上是可以进一步的数组类型你要明确一点但凡是关于数据的那么二进制基本可以操作完毕这也是为什么编程可以实现大多数生活中的某些情况的主要原因无非就是一些数据操作就如我们广泛认为的CRUD增删改查也是如此虽然按照顺序CRUD来说是增查改删但是实际操作顺序是增删改查大多数我们都会说明增删改查所以以增删改查为主换句话说就是如何将字节数组转为 Class 对象呢很简单Java 提供了 defineClass 方法通过这个方法就可以把一个字节数组转为Class对象实际上字节数组的内容是通过class文件得到的而通过这个方式可以直接操作到Class相当于加载了一般自定义加载都会使用他来完成这就是自定义加载是可以自定义操作的主要原因 defineClass 主要的功能是在ClassLoader里面 将一个字节数组转为 Class 对象这个字节数组是 class 文件读取后最终的字节数组如假设 class 文 件是加密过的则需要解密后作为形参传入 defineClass 函数 defineClass 默认实现如下后面会说明的自定义加载器的实现 protected final Class? defineClass(String name, byte[] b, int off, int len)throws ClassFormatError{return defineClass(name, b, off, len, null);}自定义加类加载器 为什么要自定义类加载器 1隔离加载类 模块隔离把类加载到不同的应用程序中比如tomcat这类web应用服务器内部自定义了好几种类加载器用于隔离web应用服务器上的不同应用程序一个应用一个jvm也就是一个main那么多个应用可以是多个main即多个jvm当他们使用时可以在除了jvm自带的加载器外都会有自己的自定义加载器即隔离不同应用操作功能或者隔离代码操作 2修改类加载方式 除了Bootstrap加载器外其他的加载并非一定要引入根据实际情况在某个时间点按需进行动态加载即当我们需要时可以随时加载即并非一定会加载这里就要提一下上面的代码了只有执行到findClass才算加载一般他就代表我们动态加载这也是为什么默认自定义是系统加载器的原因使得必然不会到对应的地方报错虽然他也只是指向而已 3扩展加载源 比如还可以从数据库、网络、或其他终端上加载 4防止源码泄漏 java代码容易被编译和篡改可以进行编译加密类加载需要自定义还原加密字节码就相当于你自己加密文件一样反正是按照你自己的方式来操作的 自定义函数调用过程 自定义类加载器实现 /* 实现方式: 所有用户自定义类加载器都应该继承ClassLoader类 在自定义ClassLoader的子类是我们通常有两种做法: 1重写loadClass方法是实现双亲委派逻辑的地方修改他对应逻辑容易会破坏双亲委派机制不推荐因为你并不能可以好的操作除非你复制粘贴过去但比较麻烦 2重写findClass方法推荐 */首先我们定义一个待加载的普通 Java 类Test.java放在 com.lagou.demo 包下 package com.lagou.dome;/****/ public class Test {public static void main(String[] args) {System.out.println(1);} } 然后执行使得出现class文件看后面的代码来决定放在那里 我们有个问题我们在操作时上面的类可以到其他类里使用实际上是因为在考虑加载时也就是加载Class时里面的内容会导致也会加载对应的类只是加载是从底层开始的即自定义到系统到扩展到启动当自定义是系统时默认直接是系统虽然自定义需要手动但是大致流程是这样但是如果没有手动我们直接的操作他的使用为什么也会出现Class呢这就要注意一个操作因为默认import的原因会导致系统也会加载这就是为什么之前说Java应用的类都是由它来完成加载的的原因所以自定义实际上是手动的哦但也可以实现流程这点要注意 现在我们来编写自定义加载器的实现 package com.lagou.dome;import java.io.*;/****/ public class MyClassLoader extends ClassLoader {//定义字节码文件的路径private String codePath;//定义构造方法public MyClassLoader(String codePath) {this.codePath codePath;}//表示设置父加载器这就是为什么之前说明自定义加载器时没有特别说明他的父加载器是谁因为他可以不找父//所以自定义是特殊的其他的加载器在运行时一般都会默认操作而自定义一般需要在main操作这里补充前面没有说明可以不操作父加载器的说明public MyClassLoader(ClassLoader parent, String codePath) {super(parent);this.codePath codePath;}Overrideprotected Class? findClass(String name) throws ClassNotFoundException {//声明输入流BufferedInputStream bis null;//声明输出流ByteArrayOutputStream baos null;try {//字节码路径String path codePath name .class;//初始化输入流bis new BufferedInputStream(new FileInputStream(path));//初始化输出流baos new ByteArrayOutputStream();//io读写操作int len;byte[] data new byte[1024];while ((len bis.read(data)) ! -1) {baos.write(data, 0, len);}//获取内存中字节数组得到的在这里可没有指定文件哦这是ByteArrayOutputStream的能力byte[] byteCode baos.toByteArray();//调用defineClass 将字节数组转成Class对象//第一个参数一般不设置设置为null即可具体作用可以百度查看//第二个参数存放我们读取class文件的字节数组//第三个参数下标0开始操作//第四个参数数组长度即都操作Class? defineClass defineClass(null, byteCode, 0, byteCode.length);return defineClass;} catch (Exception e) {e.printStackTrace();} finally {try {bis.close();} catch (IOException e) {e.printStackTrace();}try {baos.close();} catch (IOException e) {e.printStackTrace();}}return null; //说明也加载不了那么自然没有操作即出现了某些问题} } 现在我们来使用他 package com.lagou.dome;/****/ public class ClassLoaderTest {//在main里面也验证了一般是从系统开始往上若有自定义即从自定义开始的虽然需要他操作才行而这个操作就是下面我们的手动操作这个开始需要存在父加载器否则这里只会执行一次虽然在启动之前main执行之前已经操作过了//而正是因为已经操作过了所以说一般已经加载的我们是不可能继续加载了只能自己操作自定义加载的操作了因为反正执行父加载器也是没有用的且相同的父加载器优先操作public static void main(String[] args) {//很明显这里是d盘那么我们将之前运行得到的class代码放在d盘MyClassLoader classLoader new MyClassLoader(d:/);try {//System.out.println(classLoader.findClass(Test).getName());//得到ClassClass? clazz classLoader.loadClass(Test); //这里操作了该方法且我们并没有重写所以还是操作了父加载器//这个可以执行但是需要注意的是对应底层的方法defineClass一般只能操作一次如果再次的操作会报错这里要注意 // System.out.println(classLoader.findClass(Test).getName());System.out.println(clazz.getName()); //com.lagou.dome.Test获得Class全限定名System.out.println(clazz.getClassLoader().getClass()); //class com.lagou.dome.MyClassLoader通过加载器来操作的话这里是得到自定义加载器那么是得到他的全限定名要区分自定义哦System.out.println(我是由 clazz.getClassLoader().getClass().getName() 类加载器加载的); //我是由com.lagou.dome.MyClassLoader类加载器加载的System.out.println(clazz); //class com.lagou.dome.TestSystem.out.println(String.class);//class java.lang.StringString s new String(1);System.out.println(s.getClass());//class java.lang.StringSystem.out.println(clazz.getClassLoader().getClass()); //class com.lagou.dome.MyClassLoader//也就是说通过加载器得到的Class结果是加载器的也就是说我们自定义的加载他自身也有Class一般情况下加载器都有自身的Class一般来说自带的启动扩展系统中启动会自动存在c/c操作的既然有存在自然可以操作将我们生成Class所以可以说jvm默认给出Class既然可以操作出来那么手动的存在自然是合理的要不然怎么操作呢自然是他的全限定名这是自然的不要以为前面的加载并初始化是getClass的结果哦那是实例的记得看图哦} catch (ClassNotFoundException e) {e.printStackTrace();}} } 自己进行测试吧这里给出一个测试测试两个不同加载器的结果不同你可以创建任何一个类来继承MyClassLoader并操作出结果然后比较Class的引用是否相同根据测试结果为false即的确不相同也验证了之前的也会判断一个对象是否是某个完全相同的类型的这个判断手段太多的比如反射就可以如果该对象的实际类型与待比较 的类型的类加载器不同那么会返回false即不同类型 ClassLoader源码剖析 类的关系图 关系类图如下 上面图中虚线代表实现否则一般是继承 Launcher核心类的源码剖析可能需要看jdk版本 我们先从启动类说起有一个Launcher类 sun.misc.Launcher这里以jdk8为主可能会有所改变但是大体相同 //部分代码 public class Launcher {private static URLStreamHandlerFactory factory new Launcher.Factory();//静态变量初始化会执行构造方法private static Launcher launcher new Launcher();private static String bootClassPath System.getProperty(sun.boot.class.path);private ClassLoader loader;private static URLStreamHandler fileHandler;public static Launcher getLauncher() {return launcher;}//构造方法执行public Launcher() {Launcher.ExtClassLoader var1;try {//初始化扩展类加载器var1 Launcher.ExtClassLoader.getExtClassLoader();/*static class ExtClassLoader extends URLClassLoader {//该方法里面有个这个存在return new Launcher.ExtClassLoader(var0);即的确会出现了Class当前前提是启动类是有的这也是为什么之前获得启动类加载器为null的原因底层东西不会改变哦public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {final File[] var0 getExtDirs();private static File[] getExtDirs() {String var0 System.getProperty(java.ext.dirs);即扩展类加载器的确是加载这个目录前面也说过了从java.ext.dirs系统属性所指定的目录中加载类库对应的对象 public ExtClassLoader(File[] var1) throws IOException {super(getExtURLs(var1), (ClassLoader)null, Launcher.factory); //传递null当成自己父类也就是不会得到启动类了自己可以点击进去看一般可以找到this.parent var2;ClassLoader类里面的SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);}*/} catch (IOException var10) {throw new InternalError(Could not create extension class loader, var10);}try {//初始化应用类加载器this.loader Launcher.AppClassLoader.getAppClassLoader(var1); //参数代表设置父类/*public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {//操作的是该java.class.path前面也说明了它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库final String var1 System.getProperty(java.class.path); final File[] var2 var1 null ? new File[0] : Launcher.getClassPath(var1);return (ClassLoader)AccessController.doPrivileged(new PrivilegedActionLauncher.AppClassLoader() {public Launcher.AppClassLoader run() {URL[] var1x var1 null ? new URL[0] : Launcher.pathToURLs(var2);return new Launcher.AppClassLoader(var1x, var0); //看这里}});}AppClassLoader(URL[] var1, ClassLoader var2) {super(var1, var2, Launcher.factory); //最终也操作了this.parent var2;的确操作父类加载器this.ucp.initLookupCache(this);}*/} catch (IOException var9) {throw new InternalError(Could not create application class loader, var9);}//设置ContextClassLoader设置为当前加载器根据上面的流程那么就是应用加载器Thread.currentThread().setContextClassLoader(this.loader);String var2 System.getProperty(java.security.manager);if (var2 ! null) {SecurityManager var3 null;if (!.equals(var2) !default.equals(var2)) {try {var3 (SecurityManager)this.loader.loadClass(var2).newInstance();} catch (IllegalAccessException var5) {} catch (InstantiationException var6) {} catch (ClassNotFoundException var7) {} catch (ClassCastException var8) {}} else {var3 new SecurityManager();}if (var3 null) {throw new InternalError(Could not create SecurityManager: var2);}System.setSecurityManager(var3);}} 构造方法 Launcher() 中做了四件事情 其中launcher是staitc的所以初始化的时候就会创建对象也就是触发了构造方法所以初始化的时候就会执行上面四 个步骤 通过观察在应用加载器中存在如下类虽然对应的参数可能还是false /** * var1 类全名 * var2 是否连接该类*/ public Class? loadClass(String var1, boolean var2) throws ClassNotFoundException {int var3 var1.lastIndexOf(46);if (var3 ! -1) {SecurityManager var4 System.getSecurityManager();if (var4 ! null) {var4.checkPackageAccess(var1.substring(0, var3));}}//一般都是false想要返回TRUE可能需要设置启动参数lookupCacheEnabled为true为true时具体的逻辑也是C写的所以做了什么就不进行说明了if (this.ucp.knownToNotExist(var1)) {//如果这个类已经被这个类加载器加载则返回这个类否则返回NullClass var5 this.findLoadedClass(var1);if (var5 ! null) {if (var2) {this.resolveClass(var5); //如果该类没有被link连接则连接否则什么都不做}return var5;} else {throw new ClassNotFoundException(var1);}} else {return super.loadClass(var1, var2); //到这里来所以实际上应用加载器这个方法也执行了只是相当于还没有执行执行了两次loadClass一个子基本没有什么操作一个父ClassLoader即并不是所有加载器都加载父ClassLoader的的}}ClassLoader 源码剖析 ClassLoader类它是一个抽象类其后所有的类加载器都继承自ClassLoader不包括启动类加载器这里我们 主要介绍ClassLoader中几个比较重要的方法 loadClass(String) 注意可以选择设置jdk版本来查看但反正大体相同 该方法加载指定名称包括包名的二进制类型该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法loadClass()方法是ClassLoader类自己实现的该方法中的逻辑就是双亲委派模式的实现其源码如在前面具体说明了这里就不给出了其中loadClass(String name, boolean resolve)是一个重载方法resolve参数代表是否生成class对象的同时进行 解析相关操作 使用指定的二进制名称来加载类这个方法的默认实现按照以下顺序查找类 调用findLoadedClass(String)方法检查这 个类是否被加载过使用父加载器调用loadClass(String)方法如果父加载器为Null类加载器装载虚拟机内置的加载器调 用findBootstrapClassOrNull(String)方法装载类 如果按照以上的步骤成功的找到对应的类并且该方法接收的resolve参数的值为 true虽然一般是falsetrue主要操作自定义的即主要看我们是否操作但是我们一般只会操作加载像双亲委派或者这个操作一般不会进行那么就调用resolveClass(Class)方法来处理类 ClassLoader的子类最好覆盖findClass(String)而不是这个 方法即被重写这个方法默认在整个装载过程中都是同步的线程安全的 findClass(String) 在JDK1.2之前在自定义类加载时总会去继承ClassLoader类并重写loadClass方法从而实现自定义的类加 载类但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法而是建议把自定义的类加载逻辑写在findClass()方法中从前面的分析可知findClass()方法是在loadClass()方法中被调用的当loadClass()方法中 父加载器加载失败后则会调用自己的findClass()方法来完成类加载这样就可以保证自定义的类加载器也符 合双亲委托模式操作了父也就是前面默认的自定义通常虽然也算但是有时候根据定义每次通过先委托父类加载器加载当父类加载器无法加载时再自己加 载来看的话一般只是代表对应的逻辑是双亲委派的也就是该方法和包括该方法调用之前的逻辑代码的逻辑是双亲委派即双亲委派代表的就是这个需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑取而代之的是抛 出ClassNotFoundException异常并且通过前面的自定义加载器的操作我们应该知道的是findClass方法通常是和defineClass方法一起使用的 ClassLoader类中findClass()方法源码在前面也说明了这里就不给出了 defineClass(String name, byte[] b, int off, int len)方法是用来将byte字节流解析成JVM能够识别的Class对象(defineClass中已实现该方法逻辑所以我们只需要使用即可)通过这个方法不仅能够通过class文件创建Class对象从而可以实例化class对象也可以通过其他方 式创建Class对象从而实例化class对象如通过网络接收一个类的字节码然后转换为byte字节流创建对应的Class对象defineClass()方法通常与findClass()方法一起使用一般情况下在自定义类加载器时会直接覆盖ClassLoader的findClass()方法并编写加载规则取得要加载类的字节码后转换成流来操作数据然后调用defineClass()方法生成类的Class对象前面就已经操作过了这里就不演示了 需要注意的是如果直接调用defineClass()方法生成类的Class对象这个类的Class对象可能并没有解析也就验证之前说的它在某些情况下可以初始化阶段之后在开始或者说跳过了也就是说对象有但是可能少些信息也可以理解为链接阶段毕竟解析是链接的最后一步其解析操作需要等待初始化阶段进行可以认为defineClass()有某些标志可以实现只能操作一次这里了解即可 resolveClass() 使用该方法可以使用类的Class对象创建完成也同时被解析所以才会有后面的判断虽然一开始是false前面我们说链接阶段主要是对字节码进行验证 为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用 注意由于解析的存在这里自定义并没有操作具体可以百度通常情况下再次的操作可能会报错这也是对应底层defineClass不能再次执行的原因可以认为为了保留标志因为他操作了使得不能执行具体原因可以百度大概是他的判断的是否加载并不是直接返回而是报异常吧 到这里我们就知道实际上解析也是可以不操作的一般默认不解析所以有时候是使用符号引用的也行因为实际上符合引用更好所以是这样默认的 还有一点你看到前面的代码可以发现自定义的并没有设置父加载器实际上他有默认设置的如果没有设置默认为应用加载器自己打印一下就知道了主要是构造里面的隐藏的父构造造成的 实际上面的流程在对应的引用或者扩展里面可以找到的只要你去看对应的他们的findClass方法就行了仔细一点就能找到 垃圾回收机制及算法 垃圾回收概述 什么是垃圾回收 说起垃圾收集Garbage Collection 简称GC 有不少人把这项技术当作Java语言的伴生产物事实上 垃圾收集的历史远远比Java久远 在1960年诞生于麻省理工学院的Lisp是第一门开始使 用内存动态分配和垃圾收集 技术的语言垃圾收集需要完成的三件事情 哪些内存需要回收 什么时候回收 如何回收 java垃圾回收的优缺点一般来说java有自动的gc和不自动gc的前面也说明过了这里考虑不自动一般不自动看起来需要手动但是里面的操作是自动的所以才会有如下说明gc简称不自动gc所以看到gc就认为不是自动gc的 优点 a不需要考虑内存管理 b可以有效的防止内存泄漏有效的利用可使用的内存 c由于有垃圾回收机制Java中的对象不再有作用域的概念只有对象的引用才有作用域 缺点 java开发人员不了解自动内存管理内存管理就像一个黑匣子过度依赖就会降低我们解决内存溢出/内存泄漏等问题的能力 垃圾回收-对象是否已死 判断对象是否存活 - 引用计数算法 引用计数算法可以这样实现给每个创建的对象添加一个引用计数器每当此对象被某个地方引用时计数值1 引用失效时-1所以当计数值为0时表示对象已经不能被使用引用计数算法大多数情况下是个比较不错的算法 简单直接也有一些著名的应用案例但是对于Java虚拟机来说并不是一个好的选择因为它很难解决对象直接相 互循环引用的问题 优点 实现简单执行效率高很好的和程序交织 缺点 无法检测出循环引用 举个缺点的例子 /* 譬如有A和B两个对象他们都互相引用除此之外都没有任何对外的引用那么理论上A和B都可以被作为垃 圾回收掉但实际如果采用引用计数算法则A、B的引用计数都是1因为A或者B只被对方引用也就是1并不满足被回收的条件如果A和B之间的引用一直存在那么就永远无法被回收了 */案例 package com.lagou.lagou1;/****/ public class App {public static void main(String[] args) {Test object1 new Test();Test object2 new Test();object1.object object2;object2.object object1;object1 null;object2 null;} }class Test {public Test object null; } 这两个对象再无任何引用 实际上这两个对象已 经不可能再被访问 但是它们因为互相引用着对方 导致它们的 引用计数都不为零内部因为你只是将外面的引用设置而已内部的还存在 引用计数算法也 就无法回收它们 但是在java程序中这两个对象仍然会被回收因为java中并没有使用引用计数算法 要解决上面的问题实际上我们除了操作外面的引用外内部的也要进行设置为null一个就行因为单方面的会自然使得回收看后面解释即可这样就行了所以与其说循环引用还不如说内部循环矛盾这是之所以说是内部循环矛盾而不是内部矛盾是因为根据计数他们必须要循环因为只有单方面的话其中一方容易被回收而回收后自然引用也没有那么另外一方也会回收的所以他们需要循环 判断对象是否存活-可达性分析算法 可达性分析算法 在主流的商用程序语言如Java、C#等的主流实现中都是通过可达性分析Reachability Analysis来判断对象是否存活的此算法的基本思路就是通过一系列的GC Roots的对象作为起始点从起始点开始向下搜索到对象的路径搜索所经过的路径称为引用链Reference Chain当一个对象到任何GC Roots下面没有起点为GC Root的都没有引用链时则表明对象不可达即该对象是不可用的除非是循环否则单方面最终是不可用的这里默认单方面所以是这样的说明不可用的当然单方面可能还是存在后面会说明的 你可以说成GC Root是引用而后面的引用链对象是我们的对象只要他们没有人在最终被GC Root指向那么说明就是不可用的当然引用链本身也算GC Root举个例子如果下面的Object_B没有被前面的Object_A那么他会被回收虽然他单方面的指向Object_D 在Java语言中可作为GC Roots的对象包括下面几种 1栈帧中的局部变量表中的reference引用所引用的对象 2方法区总体说明的因为不是元空间中static静态引用的对象 3方法区中final常量引用的对象 4本地方法栈中JNI(Native方法)引用的对象 5Java虚拟机内部的引用 如基本数据类型对应的Class对象 一些常驻的异常对象比如 NullPointExcepitonOutOfMemoryError 等 还有系统类加载器 6所有被同步锁synchronized关键字 持有的对象 7反映Java虚拟机内部情况的JMXBean、 JVMTI中注册的回调、 本地代码缓存等 总之只要是一个对象基本都可以算因为最终对象实例基本都在堆中 JVM之判断对象是否存活 finalize()方法最终判定对象是否存活 即使在可达性分析算法中判定为不可达的对象 也不是非死不可的 这时候它们暂时还处于缓刑阶段 要真正宣告一个对象死亡 至少要经历两次标记过程与引用计数器是不同的一般手动gc有时候也会看我们的策略的也就是说如果手动gc认为你需要回收还要看看是否满足策略的回收也就是只会找没有被引用的对象这就是原因要不然为什么这样找呢才进行考虑所以这里不要认为一定是单方面的会回收哦可能策略还没有满足对于引用来说若是0满足那么回收否则手动gc跳过等待下次找到而下面是第二次标记打上了或者没有必要执行而满足回收但是他的手动gc虽然也会跳过但是必须等你操作完去了才考虑是否跳过看是否复活而引用并没有需要等待的操作自动gc可以没有这样的满足限制或者等待限制但是并非所有普通gc会考虑满足或者该等待限制一般在这之前也包含了STW后面会说明这里注意即可 第一次标记 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链 那它将会被第一次标记 随后进行一次筛选 筛选的条件是此对象是否有必要执行finalize()方法 没有必要 假如对象没有覆盖finalize()方法 或者finalize()方法已经被虚拟机调用过 那么虚拟机将这两种情况都视为没有必要执行满足回收 //Object类里面的jdk8版本 protected void finalize() throws Throwable {}有必要 如果这个对象被判定为确有必要执行finalize()方法 那么该对象将会被放置在一个名为F-Queue的队列之中 并在 稍后由一条由虚拟机自动建立的、 低调度优先级的Finalizer线程去执行它们的finalize() 方法finalize()方法是对象逃脱死亡命运的最后一次机会 稍后收集器将对F-Queue中的对象进行第二次小规模的标记 如果对 象要在finalize()中成功拯救自己只要重新与引用链上的任何一个对象建立关联即可 譬如把自己 this关键字 赋值 给某个类变量或者对象的成员变量 那在第二次标记时它将被移出即将回收的集合方法执行后会操作判断他是否被引用指向这里没有说明具体代码在哪里所以知道即可来考虑是标记还是移除队列这个时候可以看成队列是即将回收的时候对于他来说是的也就是执行该方法时到执行判断时加起来是即将回收的时候如果对象这时候还没有逃 脱判断没有引用那么就会被标记上否则不会标记因为上面是稍后收集器即稍后标记也就是执行对应方法后操作标记 那基本上它就真的要被回收了 当然了他们的方法操作是由于gc来操作的这里与引用计数器也就是引用计数算法不同虽然说需要满足但是通常他会等待他出现满足也就是说如果你执行方法在方法里面加上等待那么我的gc会等待你操作完所以不要以为gc是不会等待的 简单来说没有必要执行第二次的标记都是回收满足的条件 一次对象自我拯救的演示 package com.lagou.lagou1;/*** 此代码演示了两点* 1.对象可以在被GC时自我拯救。* 2.这种自救的机会只有一次 因为一个对象的finalize()方法最多只会被系统自动调用一次*/ public class FinalizeEscapeGC {public static FinalizeEscapeGC SAVE_HOOK null;public void isAlive() {System.out.println(yes, i am still alive :));}Overrideprotected void finalize() throws Throwable {super.finalize();//如果这里操作等待那么第一个gc等待第二个gc不会也就是说gc是不同的或者说会加gc线程或者是多线程可能也会加System.out.println(finalize method executed!);FinalizeEscapeGC.SAVE_HOOK this;}public static void main(String[] args) throws Throwable {SAVE_HOOK new FinalizeEscapeGC();//对象第一次成功拯救自己SAVE_HOOK null;System.gc(); //执行多次第二次以及后面的次数一般不会激活了除非他操作完毕了即基本不做任何操作自动反应gc需要关闭虚拟机所以这里手动来使得触发gc是操作一次的就如前面操作新生代gc可以经历多次一样当前也就是相当于当前代码以及上面代码操作后的结果可能会加上后面代码因为回收需要时间所以这里加上等待// 因为Finalizer方法优先级很低 暂停0.5秒 以等待它一般所有的gc都会去操作是否满足自动gc除外//你可以等待50000而不手动gc会发现没有操作gc因为自动gc需要关闭虚拟机时触发的自动回收的总不能虚拟机关闭里面的内容直接不管了吧需要优雅的关闭也就是自动gc注意手动gc也需要时间STW但是经过测试会发现必须加上下面的等待也就是说STW在Finalizer方法之前就不会等待了那么主线程会继续执行所以为了让他执行完才会操作0.5秒等待Thread.sleep(500);if (SAVE_HOOK ! null) {SAVE_HOOK.isAlive();} else {System.out.println(no, i am dead :();}//下面这段代码与上面的完全相同但是这次自救却失败了SAVE_HOOK null;System.gc();// 因为Finalizer方法优先级很低 暂停0.5秒 以等待它Thread.sleep(500); //对应的方法finalize只会操作一次也就是说会有其他标记的比如地址没有改变即缓存来进行操作该一次恢复总不能给你无限的复活吧因为你执行该方法反正会复活那么自然就不会回收了所以给出限制也就是复活一次//当然如果没有自然不会链接所以根据这里的解释也的确说明了没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过那么满足回收当然就算方法存在如果没有建立连接那么也满足回收第二次进行标记if (SAVE_HOOK ! null) {SAVE_HOOK.isAlive();} else {System.out.println(no, i am dead :();}}}/* 打印结果 finalize method executed! yes, i am still alive :) no, i am dead :(*/ 注意Finalizer线程去执行它们的finalize() 方法这里所说的执行是指虚拟机会触发这个方法开始运行可以认为有多个Finalizer线程来执行或者会创建Finalizer线程来执行多线程一般也会创建实际上是gc的不同导致他们不同 但并不承诺一定会等待它运行结束这样做的原因是 如果某个对象的finalize()方法执行缓慢 或者更极端地发生了死循环 将很可能导 致F-Queue队列中的其他对象永久处于等待 甚至导致整个内存回收子系统的崩溃就如前面我们知道一个gc会等待而另外的gc操作但是并不会一直等待使得他被占用所以在一点时间内如果该gc还在等待默认他没有必要直接回收 再谈引用这里了解即可 在JDK1.2以前Java中引用的定义很传统如果引用类型的数据中存储的数值代表的是另一块内存的起始地址就称这块内存代表着一个引用这种定义有些狭隘一个对象在这种定义下只有被引用或者没有被引用两种状态我们希望能描述这一类对象当内存空间还足够时则能保存在内存中如果内存空间在进行垃圾回收后还是非常紧张则可以考虑抛弃这些对象很多系统中的缓存对象都符合这样的场景在JDK1.2之后Java对引用的概念做了扩充将引用分为 强引用Strong Reference、 软引用Soft Reference、 弱引用Weak Reference和 虚引用Phantom Reference四种这四种引用的强度依次递减 强引用StrongReference 强引用是使用最普遍的引用如果一个对象具有强引用那垃圾回收器绝不会回收它当内存空间不足Java虚拟机宁愿抛出OutOfMemoryError错误使程序异常终止也不会靠随意回收具有强引用的对象来解决内存不足的问 题强引用其实也就是我们平时A a new A()这个意思 软引用SoftReference 如果一个对象只具有软引用则内存空间足够垃圾回收器就不会回收它如果内存空间不足了就会回收这些对 象的内存只要垃圾回收器没有回收它该对象就可以被程序使用软引用可以和一个引用队列ReferenceQueue联合使用如果软引用所引用的对象被垃圾回收器回收Java虚拟机就会把这个软引用加入到 与之关联的引用队列中 弱引用WeakReference 用来描述那些非必须对象 但是它的强度比软引用更弱一些 被弱引用关联的对象只能生存到下一次垃圾收集发 生为止当垃圾收集器开始工作 无论当前内存是否足够 都会回收掉只被弱引用关联的对象在JDK 1.2版之 后提供了WeakReference类来实现弱引用弱引用可以和一个引用队列ReferenceQueue联合使用如果弱引用 所引用的对象被垃圾回收Java虚拟机就会把这个弱引用加入到与之关联的引用队列中 弱引用与软引用的区别在于 更短暂的生命周期因为被弱引用关联的对象只能生存到下一次或者说垃圾收集发 生为止也就是下一次垃圾回收后就不存在了所以一旦发现了只具有弱引用的对象不管当前内存空间足够与否都会回收它的内存 虚引用PhantomReference 虚引用顾名思义它是最弱的一种引用关系如果一个对象仅持有虚引用在任何时候都可能被垃圾回收器回收虚引用主要用来跟踪对象被垃圾回收器回收的活动 虚引用与软引用和弱引用的一个区别在于 1虚引用必须和引用队列 ReferenceQueue联合使用 2当垃圾回收器准备回收一个对象时如果发现它还有虚引用就会在回收对象的内存之前把这个虚引用加入到 与之 关联的引用队列中 垃圾收集算法也就是回收中的操作在满足前面的判断是否存活而实现的回收操作 分代收集理论 思想也很简单就是根据对象的生命周期将内存划分然后进行分区管理当前商业虚拟机的垃圾收集器 大多 数都遵循了分代收集Generational Collection的理论进 行设计 分代收集名为理论 实质是一套符合大多数 程序运行实际情况的经验法则 它建立在两个分代假说之上 1弱分代假说Weak Generational Hypothesis 绝大多数对象都是朝生夕灭的 2强分代假说Strong Generational Hypothesis 熬过越多次垃圾收集过程的对象就越难以消亡 这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则 收集器应该将Java堆划分 出不同的区域 然后将回收对象依据其年龄年龄即对象熬过垃圾收集过程的次数 分配到不同的区 域之中存储显而易见 如 果一个区域中大多数对象都是朝生夕灭 难以熬过垃圾收集过程的话 那 么把它们集中放在一起 每次回收时只 关注如何保留少量存活而不是去标记那些大量将要被回收的对 象 就能以较低代价回收到大量的空间如果剩下 的都是难以消亡的对象 那把它们集中放在一块 虚拟机便可以使用较低的频率来回收这个区域 这就同时兼顾 了垃圾收集的时间开销和内存的空间有 效利用 在Java堆划分出不同的区域之后 垃圾收集器才可以每次只回收其中某一个或者某些部分的区域因而才有 了Minor GC“Major GC”“Full GC这样的回收类型的划分也才能够针对不同的区域安 排与里面存储对象存亡特征相匹配的垃圾收集算法因而发展出了标记-复制算法”“标记-清除算法”标记-整理算法等针对性的垃圾收集算 法注意之前的判断是否存活是考虑回收的而这些算法是考虑回收后的操作也就是说在是否存活考虑回收时虽然他们可能有标记记录那么这个标记可用认为是这里的标记而复杂清除整理就是操作这些已经标记的前面只是说明回收并没有说明回收的如何回收即这里的那个对应 他针对不同分代的类似名词 为避免产生混淆 在这里统一定义 : 部分收集Partial GC 指目标不是完整收集整个Java堆的垃圾收集 其中又分为 1新生代收集Minor GC/Young GC 指目标只是新生代的垃圾收集 2老年代收集Major GC/Old GC 指目标只是老年代的垃圾收集其中一般CMS收集器会有单 独收集老年代的行为 3混合收集Mixed GC 指目标是收集整个新生代以及可能是部分老年代的垃圾收集其中一般G1收集器会有这种行为 整堆收集Full GC 收集整个Java堆和方法区的垃圾收集 标记-清除算法 什么是标记-清除算法? 最早出现也是最基础的垃圾收集算法是标记-清除Mark-Sweep 算法 在1960年由Lisp之父 John McCarthy所 提出如它的名字一样 算法分为标记和清除两个阶段 首先标记出所有需要回 收的对象 在标记完成后 统一回收清除掉所有被标记的对象 也可以反过来 标记存活的对象 统一回 收所有未被标记的对象 标记过程就是对象是否属于垃圾的判定过程 这在前面讲述垃圾对象标记 判定算法判断对象是否存活时其实已经介绍过了之所 以说它是最基础的收集算法 是因为后续的收集算法大多都是以标记-清除算法为基础 对其 缺点进行改进而得到 的 标记-清除算法有两个不足之处 第一个是执行效率不稳定 如果Java堆中包含大量对象 而且其中大部分是需要被回收的 这时必须进行大量标 记和清除的动作 导致标记和清除两个过 程的执行效率都随对象数量增长而降低但这个并不是主要的因为谁都要标记所以主要的不足是如下第二个不足 第二个是内存空间的碎片化问题 标记、 清除之后会产生大 量不连续的内存碎片 空间碎片太多可能会导致当以 后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作 标记-复制算法 什么是标记-复制算法也有标记哦只是在清除操作里进一步升级 标记-复制算法常被简称为复制算法 为了解决标记-清除算法面对大量可回收对象时执行效率低的问题 1969年Fenichel提出了一种称为半区复制Semispace Copying的垃圾收集算法 它将可用 内存按容量划分为大小相等的两块 每次只使用其中的一 块当这一块的内存用完了 就将还存活着 的对象复制到另外一块上面 然后再把已使用过的内存空间一次清理 掉如果内存中多数对象都是存活的 这种算法将会产生大量的内存间复制的开销 但对于多数对象都是可回收 的情况 算法需要复制的就是占少数的存活对象 而且每次都是针对整个半区进行内存回收 分配内存时也就不 用考虑有空间碎片的复杂情况 只要移动堆顶指针原来的进行初始化比如设置为null不指向 按顺序分配即可 上图中有点问题原来的是8个变成5个了这里注意即可后面的回收后状态那边应该是8个的图是复制粘贴来的 但是这种算法也有缺点标记也算缺点虽然因为可用比较少而减少了该缺点 1需要提前预留一半的内存区域用来存放存活的对象经过垃圾收集后还存活的对象这样导致可用的对 象区域减小一半总体的GC更加频繁了 2如果出现存活对象数量比较多的时候需要复制较多的对象成本上升效率降低 3如果99%的对象都是存活的对于老年代来说那么老年代是无法使用这种算法的因为成本太高 注意事项 现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代 IBM公司曾有一项专门研 究对新生代朝生 夕灭的特点做了更量化的诠释新生代中的对象总体来说有98%熬不过第一轮收集大多数对象都是使用了然后清除我们程序员比较规范的情况下因此 并不需要按照1∶1的比例来划分 新生代的内存空间Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的 Survivor空间 每 次分配内存只使用Eden和其中一块Survivor发生垃圾搜集时 将Eden和Survivor中仍 然存活的对象一次性复制到 另外一块Survivor空间上则就是为什么有一个是空闲的底层原因 然后直接清理掉Eden和已用过的那块Survivor空 间HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1 1给不规范一点余地所以不是根据98%为主的比例哦 也即每次新生代中可用内存空间为整个新 生代容量的90%Eden的80%加上一个Survivor的10% 只有一个Survivor空间 即10%的新生代是会 被浪费的 但是要注意对应的复制过去后并不是都初始化而是移动的进行初始化这就是为什么前面说明这里时两个都利用到了但总体来说并不是一个幸存者空间空间初始化即也违背了空闲的说明实际上这种是特殊情况一般来说可能受某些决策影响他看起来可以保留更多的数据但是复制的也更加多看起来提高了上限这里解释之前的这在标记-复制算法中会提到这里你认为是特殊情况即可 那么这里就要说明一下正常情况实际上我们之前认为在移动后起始放入幸存者也会改变成移动的这就是为什么之前说明的此时会重新返回幸存者1区改变位置了 标记-整理算法 标记-复制算法在对象存活率较高时就要进行较多的复制操作 效率将会降低更关键的是 如果不想浪费50%的 空间 就需要有额外的空间进行分配担保 以应对被使用的内存中所有对象都100%存活的极端情况 所以在老年 代一般不能直接选用这种算法因为老年代空间太多以及存活可能太多总不能少50%或者大幅度的降低效率吧 针对老年代对象的存亡特征 1974年Edward Lueders提出了另外一种有针对性的标记-整 理Mark-Compact 算 法 其中的标记过程仍然与标记-清除算法一样 但后续步骤不是直接对可回收对象进行清理 而是让所有存活 的对象都向内存空间一端移动 然后直接清理掉边界以外的内存 标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法 而后者是移动式的是否移动 回收后的存活对象是一项优缺点并存的风险决策 是否移动对象都存在弊端 移动则内存回收时会更复杂 不移动则内存分配时会更复杂从垃圾收集的停顿时间 来看 不移动对象停顿时间会更短 甚至可以不需要停顿 但是从整个程序的吞吐量来看 移动对象会更划算所以各有利弊如果需要加入更多空间那么一般整理比较好少gc总体来说好点否则清除比较好 垃圾收集器 垃圾收集器概述 1垃圾回收器与垃圾回收算法 垃圾回收算法分类两类 第一类算法判断对象生死的算法如引用计数法、可达性分析算法 第二类收集死亡对象方法也就是垃圾收集算法如标记-清除算法、标记-复制算法、标记-整理算法 一般的实现采用分代回收算法第二类总称根据不同代的特点应用不同的算法第二类垃圾回收算法是内存回收的方法论垃圾收集器是算法的落地实现因为我们总要有操作不总不能说说而已和回收算法一样目前还没有出现完美的收集器需要因为各自区域操作不同要不然怎么会出现第二类的各种呢所以没有完美的就算你在这个地方是好的那么在其他地方可能是坏的所以要根据具体的应用场景选择最合适的收集器进行分代收集 2垃圾收集器分类 串行垃圾回收Serial 串行垃圾回收是为单线程环境设计且只使用一个线程进行垃圾回收会暂停所有的用户线程不适合交互性强的服务器环境 并行垃圾回收Parallel 多个垃圾收集器线程并行工作同样会暂停用户线程适用于科学计算、大数据后台处理等多交互场景 注意先考虑他们可以都有和改变后面会进行补充这些回收收集器只是实现方式包括前面说明的和后面说明的具体回收可能受区域影响来改变收集死亡对象方法也就是无论新生代还是老年代也好或者是方法区也好都会操作他们但是我们设置时是各自的分开的只是可能会在对应的区域分开执行gc组合或者根据区域改变对应gc的收集死亡对象方法一般我们是操作并行的前面操作的说明的gc不同就是这样即前面说明了也就是说gc是不同的或者说会加gc线程或者是多线程可能也会加 并发垃圾回收CMS一般会优先考虑用于老年代收集虽然都可以操作但是我们一般用于老年代收集 用户线程和垃圾回收线程同时执行不一定是并行的可能是交替执行可能一边垃圾回收一边运行应用线程 不需要停顿用户线程专门操作老年代的虽然触发他可能会操作STW但是对于CMS来说他并不是操作STW只是老年代会触发而已在前面说明了老年代空间不足时也会尝试触发MinorGC然后考虑放入新生代反过来操作了而由于也会触发他所以不只是数据大且也考虑STW了如果空间还是不足则触发Major GC这里就代表Major GC所以对于Major GC来说是不需要停顿的而对于老年代需要互联网应用程序中经常使用适用对响应时间有要求的场景 G1垃圾回收 G1垃圾回收器将堆内存分割成不同的区域然后并发地对其进行垃圾回收混合收集 3七种垃圾收集器及其组合关系 根据分代思想我们有7种主流的垃圾回收器 新生代垃圾收集器Serial 、 ParNew 、Parallel Scavenge 老年代垃圾收集器Serial Old 、 Parallel Old 、CMS 整理收集器G1 很明显先考虑他们可以都有和改变后面会进行补充虽然前面说虽然都可以操作但是可能会在对应的区域分开执行gc来满足他们回收且具体性能可能不同所以我们默认的组合也是不同的在前面也具体说明了对应gc那就是相对好的性能的gc且一般也需要根据具体情况即老年代空间多并发都并行的意思不要以通常名称来定义新生代空间少同步或者并行这里表示gc的并行当然也因为都可以操作所以一般在满足情况下设置时分开使得在新老存在对应gc使用可能也因为这样使得对应gc虽然都是回收但是回收算法使得必然按照区域了但自然也会使得可能在新中会区分是否是eden来进一步改变算法也就在设置的情况会认为不会使得都可以操作了可能有如下多种组合有多个实现对应操作的gc 垃圾收集器的组合关系关系可能根据jdk版本而发生改变 上面的组合中若没有那么会报错且不能设置多个新生代否则也报错一般只设置老年代那么只会操作老年但多个老年也是不行的否则也报错这些都只是对参数来说的所以默认的不算 最后注意若你只设置老年代那么会覆盖默认的新生代但是不会覆盖参数新生代只覆盖他的老其中先后顺序决定谁先替换默认的以及在谁前面替换默认的自然在前面参数是参数只要写上自然我们要显示出来 上面你可以先大致了解需要结合后面的学习以及后面说明的常用垃圾收集器参数来进行了解 JDK8中默认使用组合是Parallel Scavenge GC 、ParallelOld GC JDK9默认是用G1为垃圾收集器 JDK14 弃用了Parallel Scavenge GC 、Parallel OldGC JDK14 移除了 CMS GC 虚线代表弃用但是可以使用只是不建议上面的图中可能是按照jdk8来说明的 4GC性能指标 吞吐量即CPU用于运行用户代码的时间与CPU总消耗时间的比值吞吐量 运行用户代码时间 / ( 运行用户代码时 间 垃圾收集时间 )例如虚拟机共运行100分钟垃圾收集器花掉1分钟那么吞吐量就是99%一般来说吞吐量越大越好所以一般我们需要保证用户代码时间多或者垃圾回收时间少反正总值是100分钟所以需要看gc算法的好坏了快点回收完所以一般并行的收集比较好所以前面才会说一般我们是操作并行的 暂停时间执行垃圾回收时程序的工作线程被暂停的时间 内存占用java堆所占内存的大小 收集频率垃圾收集的频次 Serial收集器 单线程收集器单线程的意义不仅仅说明它只会使用一个CPU或一个收集线程去完成垃圾收集工作更重要的是它在垃圾收集的时候必须暂停其他工作线程直到垃圾收集完毕 Stop The World这个词语也 许听起来很酷 但这项工作是由虚拟机在后台自动发起和自动完成的 在用户 不可知、 不可控的情况 下把用户的正常工作的线程全部停掉 这对很多应用来说都是不能接受的 示意了Serial/Serial Old收 集器的运行过程考虑 Serial收集器也并不是只有缺点Serial收集器由于简单并且高效那么对于单CPU环境来说多核由于Serial收集器没有线程间的交互专心做垃圾收集自然可以做获得最高的垃圾收集效率总体来说该一个gc线程比多线程中的某个线程要快点因为他不用抢夺cpu而节省中间的时间了相当于其他没有快速抢夺的来说但是我的cpu充沛那么算力会大点因为cpu内部也是分开操作算力的这就是为什么cpu越好好和多都算可能多有某个上限那么计算越好的原因 使用方式-XX:UseSerialGC 案例 package heihei;import java.util.ArrayList;/*** -XX:PrintCommandLineFlags 查看程序使用的默认JVM参数* -XX:UseSerialGC 设置垃圾回收器为年轻代垃圾回收器使用serial且设置了老年代使用serial Old设置了两个哦*/ public class test {public static void main(String[] args) {ArrayListbyte[] list new ArrayListbyte[]();while (true){byte[] b new byte[1024];list.add(b);try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}} }/* -XX:PrintCommandLineFlags设置好后仍然是之前的VM options的操作可以看到对应的参数的比如我的结果就是 -XX:InitialHeapSize131782592 -XX:MaxHeapSize2108521472 -XX:PrintCommandLineFlags -XX:UseCompressedClassPointers -XX:UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:UseParallelGC -XX:MaxHeapSize相当于-Xmx的他们可以同时设置并且谁在后面那么谁进行覆盖比如-Xmx5m -Xmx200m不会报错且结果是200m而-Xmx5m -XX:MaxHeapSize200m结果也是200m他们单位默认字节可以是km但是其他乱写的单位不行的比如jjj那肯定不行一般是规定有限的他一般是mk和不写的字节没有规定他的单位那么只能不写了可以看到-XX:UseParallelGC就是使用的gc看看项目版本我这里是jdk8由于JDK8中默认使用组合是Parallel Scavenge GCParallelOld GC那么他可能代表就是这两个一般是的其中老年代那里会说明一般是默认的虽然是UseParallelGC不同jdk版本可能默认的是不同的可以自己进行测试我们继续设置参数-XX:UseSerialGC补充即可结果如下 -XX:InitialHeapSize131782592 -XX:MaxHeapSize2108521472 -XX:PrintCommandLineFlags -XX:UseCompressedClassPointers -XX:UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:UseSerialGC 变成-XX:UseSerialGC了可能代表就是前面说明的置垃圾回收器为年轻代垃圾回收器使用serial且老年代使用serial Old一次设置两个 */在说明后面之前首先建议你全局搜索常用垃圾收集器参数来看看对应的功能 ParNew 收集器 ParNew收集器实质上是Serial收集器的多线程并行版本 除了同时使用多条线程进行垃圾收集之外其余的行为包括Serial收集器可用的所有控制参数 、 收集算法、 Stop The World、 对象分配规则、 回收策略等都与Serial收集器完全一致 在实现上这两种收集器也共用了相当多的代码 ParNew收集器的工作过程 ParNew收集器在单CPU服务器上的垃圾收集效率绝对不会比Serial收集器高 但是在多CPU服务器上效果会明显比Serial好我可以能操作多个gc操作的虽然我一人不行但是2个打1个还是打的过的 使用方式-XX:UseParNewGC 设置线程数: XX:ParllGCThreads package heihei;import java.util.ArrayList;/*** -XX:UseParNewGC 并行垃圾回收器收集新生代这里包括老年代即Serial Old也可以进行改变老年代即使用下面一种* -XX:UseConcMarkSweepGC CMS的并行垃圾回收器用来改变老年代的使得对应Serial Old加上CMS但要注意他们一般并不是互相覆盖的也就是说只要你写了我就会操作覆盖否则不会*/ public class test1 {public static void main(String[] args) {ArrayListbyte[] list new ArrayListbyte[]();while (true){byte[] b new byte[1024];list.add(b);try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}}/*使用前面的设置操作-XX:UseParNewGC将之前的-XX:UseSerialGC进行替换结果如下-XX:InitialHeapSize131782592 -XX:MaxHeapSize2108521472 -XX:PrintCommandLineFlags -XX:UseCompressedClassPointers -XX:UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:UseParNewGC 变成了-XX:UseParNewGC注意记得是替换不要补充否则会报错的只能识别一个设置包括老年多余的设置会报错认为有标记表示已经设置了可以补充这个-XX:UseConcMarkSweepGC不属于上面的标记因为是改变老年代的结果如下-XX:InitialHeapSize131782592 -XX:MaxHeapSize2108521472 -XX:MaxNewSize697933824 -XX:MaxTenuringThreshold6 -XX:OldPLABSize16 -XX:PrintCommandLineFlags -XX:UseCompressedClassPointers -XX:UseCompressedOops -XX:UseConcMarkSweepGC -XX:-UseLargePagesIndividualAllocation -XX:UseParNewGC 多出了其他信息和-XX:UseConcMarkSweepGC其他信息了解即可*/ } Parallel Scavenge收集器 1什么是Parallel Scanvenge 又称为吞吐量优先收集器和ParNew收集器类似是一个新生代收集器使用复制算法的并行多线程收集器 Parallel Scavenge是Java1.8默认的收集器特点是并行的多线程回收以吞吐量优先 2特点 Parallel Scavenge收集器的目标是达到一个可控制的吞吐量Throughput吞吐量运行用户代码时间/(运行用户代码时间垃圾收集时间) (虚拟机总共运行100分钟垃圾收集时间为1分钟那么吞吐量就是99%) 自适应调节策略自动指定年轻代、Eden、Suvisor区的比例虽然默认是811一般也会自动的设置这个在后面会说明 3适用场景 适合后台运算交互不多的任务如批量处理订单处理科学计算等因为太多不好控制自适应调节更加的麻烦需要时间 3参数 1使用方式-XX:UseParallelGC对应老年代的那个后面会说明后面的参数一般只能是这个gc才会生效否则相当于没有加上加上不会报错只是不会生效而已一般都代表设置什么值使用时判断是否是这个gc使得不是这个gc的话就算设置也不使用可能受jdk版本影响一般在说明gc时后面也给出参数那么他们就是这样的关系就如CMS的CMSInitiatingOccupancyFraction但并非一定哦所以这里建议主要参考后面说明的常用垃圾收集器参数 2最大垃圾收集停顿时间-XX:MaxGCPauseMillis -XXMaxGCPauseMillis参数允许的值是一个大于0的毫秒数默认参数是毫秒比如-XX:MaxGCPauseMillis100就是100毫秒不要加上单位哦否则可能报错 收集器将尽力保证内存回收花费的时间不超过用户设定值不过 不要异想天开地认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收集速度变得更快 垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的 系统把新生代调得小一些 收集300MB新生代肯定比收集500MB快 但这也直接导致垃圾收集发生得更频繁 就如原来10秒收集一次、 每次停顿100毫秒 现在变成5秒收集一次、 每次停顿70毫秒停顿时间 的确在下降 但吞吐量也降下来了运行时间变成5秒了而30毫秒对于他5秒是可以忽略的即吞吐量的确下降了 3吞吐量大小-XX:GCTimeRatio案例-XX:GCTimeRatio20不要不加参数哦否则可能会报错且要是一个整数否则也报错也不能超过范围注意这里给出了很多的限制实际上很多参数都有限制我们只需要按照大众需求即可这里特别的提一下 -XX:GCTimeRatio参数的值则应当是一个大于0小于100的整数 也就是垃圾收集时间占总时间的比率具体如何进行比较看后面就行假设GCTimeRatio的值为n那么系统将花费不超过1/(1n)的时间用于垃圾收集譬如把此参数设置为19 那允许的最大垃圾收集时间就占总时间的5%即1/(119) 默认值为99 即允许最大1%即1/(199) 的垃圾收集时间当然这个最大表示垃圾收集时间的最大但是对于设置来说是最小的最大 4设置年轻代线程数XX:ParllGCThreads 当cpu核心数小于等于8默认与cpu核数相同当cpu核数超过8ParllGCThreads设置为 3(5*CPU_COUNT)/8 绝对大于8因为将极端的8放入结果是8但是大于8的所以大于8如果是9可能因为取整还是等于8的CPU_COUNT是cpu核心数 5与Parallel Scavenge收集器有关的还有一个参数-XX:UseAdaptiveSizePolicy有了这个参数之后就不要手工 指定年轻代、Eden、Suvisor区的比例晋升老年代的对象年龄等因为虚拟机会根据系统运行情况进行自适应调节 package heihei;import java.util.ArrayList;/** 是设置格式可能是固定的注意即可通过我的观察基本上操作值的不会有否则会有而实际上代表添加设置那么你在设置时变成-可能就是删除改设置了通过测试我们知道有个默认的-XX:UseParallelGC当我们设置减时他还存在说明默认的是最后操作的覆盖他所以下面的-XX:UseParallelGC是否设置反正被覆盖而正是因为一开始没有那么操作减没有什么作用因为反正是没有的虽然这里有覆盖其他反正也是没有只是没有这个默认覆盖而已所以我们只能使用jinfo来减了* -XX:UseParallelGC 使用parallel scavenge 垃圾回收器也包括Serial Old * -XX:MaxGCPauseMillis 设置暂停时间不推荐* -XX:UseAdaptiveSizePolicy 自适应配置策略EdenSuvisor区的比例晋升老年代的对象年龄等* -XX:ParllGCThreads 设置年轻代线程数*/ public class test2 {public static void main(String[] args) {ArrayListbyte[] list new ArrayListbyte[]();while (true) {byte[] b new byte[1024];list.add(b);try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}} }/* 使用-XX:PrintCommandLineFlags -XX:UseParallelGC结果如下 -XX:InitialHeapSize131782592 -XX:MaxHeapSize2108521472 -XX:PrintCommandLineFlags -XX:UseCompressedClassPointers -XX:UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:UseParallelGC -XX:UseParallelGC 没有改变默认的也是这个也就是说无论你是否加上结果都一样标记操作了并使得覆盖了 我们继续补充-XX:MaxGCPauseMillis100结果 -XX:InitialHeapSize131782592 -XX:MaxGCPauseMillis100 -XX:MaxHeapSize2108521472 -XX:PrintCommandLineFlags -XX:UseCompressedClassPointers -XX:UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:UseParallelGC 多出了-XX:MaxGCPauseMillis100 继续补充-XX:UseAdaptiveSizePolicy结果 -XX:InitialHeapSize131782592 -XX:MaxGCPauseMillis100 -XX:MaxHeapSize2108521472 -XX:PrintCommandLineFlags -XX:UseAdaptiveSizePolicy -XX:UseCompressedClassPointers -XX:UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:UseParallelGC 多出了-XX:UseAdaptiveSizePolicy 我们也可以在控制台打印jps根据当前程序运行的进程号操作如下 jinfo -flag UseAdaptiveSizePolicy 进程号如果打印出来-XX:UseAdaptiveSizePolicy虽然也有时认为是值的意思比如将UseAdaptiveSizePolicy替换成MetaspaceSize就是对应的值在前面测试过了 了说明使用了自适应配置策略如果是-XX:-UseAdaptiveSizePolicy说明没有使用注意在某些jdk版本下可能默认是使用的虽然在运行时的打印不是控制台没有出现 继续补充XX:ParllGCThreads在不同jdk版本中可能-会去除但一般不会可能会报错所以结果就不给出了 */前面说明的3个基本都是新生代的现在我们来说明老年代的虽然他们前面和后面在设置上是这样也根据区域但是实际上他们都可以操作新老或者方法区所以我们说明的gc是设置的这样的因为他们是根据性能来决定设置的操作的在前面也说明了且一般也需要根据具体情况而区域自然操作对应算法而他们都有 实际上在前面我虽然说他们都有那是因为在没有固定的情况下由于我们已经选择好了java自然会让他们固定也就是说使得不会有其他的操作那么按照这样的逻辑Serial Old收集器只能操作老年代且采用标记-整理算法其他的都是如此这里就进行补充即补充前面说明的先考虑他们可以都有和改变后面会进行补充实际上是改变说明 Serial Old收集器 Serial Old是Serial收集器的老年代版本 它同样是一个单线程收集器 使用标记-整理算法这个收集器的主要意 义也是供客户端模式下的HotSpot虚拟机使用 特点 1针对老年代 2采用标记-整理算法 3单线程收集 执行流程 应用场景主要用于Client模式 1在JDK1.5及之前与Parallel Scavenge收集器搭配使用JDK1.6有Parallel Old收集器可搭配 2作为 CMS收集器的后备预案 在并发收集发生Concurrent Mode Failure时使用 这里再次给出之前的图 上面的CMS的备用方案是Serial Old所以他们之间有连接就是防止CMS不能使用的情况一般CMS可能会突然不能使用一般是他自身的原因吧 参数设置 使用方式-XX:UseSerialGC设置两个的前面操作过了所以具体测试就不给出了 注意事项 需要说明一下 Parallel Scavenge收集器架构中本身可能有PS MarkSweep收集器来进行老年代收集 并非直接调用Serial Old收集器 但是这个PS MarkSweep收集器与Serial Old的实现几乎是一样的 所以在官方的许多资料中都是 直接以Serial Old代替PS MarkSweep进行讲解 Parallel Old收集器 来满足之前说明的对应老年代的那个后面会说明 Parallel Old是Parallel Scavenge收集器的老年代版本虽然在jdk8中是默认的 支持多线程并发收集 基于标记-整理算法实现这个收集器 是直到JDK 6时才开始提供的 在此之前 新生代的Parallel Scavenge收集器一直处于相 当尴尬的状态 原因是如 果新生代选择了Parallel Scavenge收集器 老年代除了Serial OldPS MarkSweep 收集器以外别无选择 其他表 现良好的老年代收集器 如CMS无法与它配合工作 Parallel Old收集器的工作过程前面对应的年轻代没有给出 应用场景 JDK1.6及之后用来代替老年代的Serial Old收集器特别是在Server模式对于Client模式来说是多数的多CPU的情况下这样在注重吞吐量以及CPU资源敏感的场景就有了Parallel Scavenge加Parallel Old收集器的给力应用组合 设置参数 -XX:UseParallelOldGC指定使用Parallel Old收集器 package heihei;import java.util.ArrayList;/*** -XX:UseParallelOldGC指定使用Parallel Old收集器即改变对应的老年代*/ public class test3 {public static void main(String[] args) {ArrayListbyte[] list new ArrayListbyte[]();while (true) {byte[] b new byte[1024];list.add(b);try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}}/*设置加上-XX:PrintCommandLineFlags -XX:UseParallelOldGC结果如下-XX:InitialHeapSize131782592 -XX:MaxHeapSize2108521472 -XX:PrintCommandLineFlags -XX:UseCompressedClassPointers -XX:UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:UseParallelOldGC 变成了-XX:UseParallelOldGC */ } CMS 收集器 CMS垃圾回收器 CMSconcurrent mark sweep是以获取最短垃圾收集停顿时间为目标的收集器CMS收集器的关注点尽可能缩短 垃圾收集时用户线程的停顿时间停顿时间越短就越适合与用户交互的程序目前很大一部分的java应用几种在互联网 的B/S系统服务器上这类应用尤其注重服务器的响应速度系统停顿时间最短给用户带来良好的体验CMS收 集器使用的算法是标记-清除算法实现的并非一定移动因为是否移动对象都存在弊端这里就进行考虑那么他的固定就是不移动 CMS垃圾收集过程 整个过程分4个步骤 1初始标记 2并发标记 3重新标记 4并发清除 其中 初始标记 和 重新标记 都需要stopTheWorldSTW CMS整个过程比之前的收集器要复杂整个过程分为4个阶段即初始标记并发标记 、重新标记、并发清除 1初始标记Initial-Mark阶段这个阶段程序所有的工作线程都将会因为Stop-the-Wold机制而出现短暂的的暂停这个阶段的主要任务是标记GC Roots 能够关联到的对象直接关联的关联的关联不标记一旦标记完成后就恢复之前被暂停的的所有应用由于直接关联对象比较小所以这里的操作速度非常快 2并发标记Concurrent-Mark阶段从GC Roots的直接关联对象开始遍历整个对象图的过程这个过程耗时较 长但是不需要暂停用户线程用户线程可以与垃圾回收器一起运行 3重新标记Remark阶段由于并发标记阶段程序的工作线程会和垃圾收集线程同时运行或者交叉运行 因此为了修正并发标记期间因为用户继续运行而导致标记产生变动的那一部分对象的标记记录这个阶段 的停顿时间通常比初始标记阶段长一些但也远比并发标记阶段时间短 4清除并发Concurrent-Sweep阶段此阶段清理删除掉标记判断已经死亡的对象也就是重新标记标记的可能清除不会考虑可达性分析算法的判断即不考虑复活虽然我说gc基本都考虑但是并非一定的并释放内存空间由于不需 要移动存活对象所以这个阶段可以与用户线程同时并发运行但是也需要考虑空间不连续不连续的空间碎片的问题了各有好处但是考虑到可以并行那么总体还是不移动比较好 由于最消耗事件的并发标记与并发清除阶段都不需要暂停工作因为整个回收阶段是低停顿低延迟的总得来说就是将延迟长的进行并行操作短的进行处理或者修正操作这就是CMS的好处 并发可达性分析 当前主流编程语言的垃圾收集器基本上都是依靠可达性分析算法来判定对象 是否存活的 可达性分析算法理论上 要求全过程都基于一个能保障一致性的快照中才能够进行分析 垃圾回收器的工作流程大体如下 1标记出哪些对象是存活的哪些是垃圾可回收 2进行回收清除/复制/整理如果有移动过对象复制/整理还需要更新引用 三色标记 三色标记Tri-color Marking作为工具来辅助推导 把遍历对象图过程中遇到的对象 按照是否访问过这个条 件标记成以下三种颜色 要找出存活对象根据可达性分析从GC Roots开始进行遍历访问可达的则为存活对象 我们把遍历对象图过程中遇到的对象按是否访问过这个条件标记成以下三种颜色 白色尚未访问过 黑色本对象已访问过而且本对象引用到 的其他对象 也全部访问过了 灰色本对象已访问过但是本对象 引用到 的其他对象尚未全部访问完全部访问后他会转换为黑色不包括其他对象只是他本对象 假设现在有白、灰、黑三个集合表示当前对象的颜色其遍历访问过程为 1初始时所有对象都在 【白色集合】中 2将GC Roots 直接引用到的对象 挪到 【灰色集合】中 3从灰色集合中获取对象 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中不存在其他对象也没有关系照样本身变黑并将本对象 挪到 【黑色集合】里面 4重复步骤3直至【灰色集合】为空时结束 5结束后仍在【白色集合】的对象即为GC Roots 不可达可以进行回收上图就是最终结果 注如果标记结束后对象仍为白色意味着已经找不到该对象在哪了不可能会再被重新引用 当Stop The World 简称 STW时对象间的引用 是不会发生变化的可以轻松完成标记而当需要支持并 发标记时即标记期间应用线程还在继续跑对象间的引用可能发生变化多标和漏标的情况就有可能发生 多标-浮动垃圾 假设已经遍历到E变为灰色了此时应用执行了 objD.fieldE null 此刻之后对象E/F/G是应该被回收的然而因为E已经变为灰色了其仍会被当作存活对象继续遍历下去 最终的结果是这部分对象仍会被标记为存活即本轮GC不会回收这部分内存 这部分本应该回收 但是 没有回收到的内存被称之为浮动垃圾浮动垃圾并不会影响应用程序的正确性只是 需要等到下一轮垃圾回收中才被清除因为当我们清除白色下次会继续放入白色 漏标 假设GC线程已经遍历到E变为灰色了此时应用线程先执行了 var G objE.fieldG; objE.fieldG null; // 灰色E 断开引用 白色G objD.fieldG G; // 黑色D 引用 白色G此时切回GC线程继续跑因为E已经没有对G的引用了所以不会将G放到灰色集合尽管因为D重新引用了G但 因为D已经是黑色了不会再重新做遍历处理一路过去自然黑色不处理了最终导致的结果是G会一直停留在白色集合中最后被当作垃圾 进行清除这直接影响到了应用程序的正确性是不可接受的因为g应该是不能被回收的而他回收了我们操作的引用指向了 不难分析漏标只有同时满足以下两个条件时才会发生 条件一灰色对象 断开了 白色对象的引用即灰色对象 原来成员变量的引用 发生了变化 条件二黑色对象 重新引用了 该白色对象即黑色对象 成员变量增加了 新的 引用对象 从代码的角度看 var G objE.fieldG; // 1.读 objE.fieldG null; // 2.写 objD.fieldG G; // 3.写1读取 对象E的成员变量fieldG的引用值即对象G 2对象E 往其成员变量fieldG写入 null值 3对象D 往其成员变量fieldG写入 对象G 我们只要在上面这三步中的任意一步中做一些手脚将对象G记录起来然后作为灰色对象再进行遍历即可比 如放到一个特定的集合等初始的GC Roots遍历完并发标记该集合的对象 遍历即可对他考虑重新标记使得移除白色也就是从G开始并发标记但他是灰色的开始而并发标记就是给出初始标记对应对象后面的流程并顺便判断三色反正是一路的所以我们才说重写标记是修正最后操作白色清除是最后哦 重新标记是需要STW的因为应用程序一直在跑的话该集合可能会一直增加新的对象而初始标记也是防止增加起始指向所以也需要STW导致永远都跑不完当然并发标记期间也可以将该集合中的大部分先跑了从而缩短重新标记STW的时间这个是优化问 题了 CMS收集器三个缺点 1CMS收集器对CPU资源非常敏感 其实面向并发设计的程序都对CPU资源比较敏感在并发阶段它虽然不会导致用户线程停顿但是会因为占用 了一部分线程而导致应用程序变慢总吞吐量会降低CMS默认启动的回收线程数是处理器核心数量 3 /4 也就是说 如果处理器核心数在四个或以上 并发回收时垃圾收集线程一般越大只占用不超过25%的 处理器运算资源 并 且会随着处理器核心数量的增加而下降但是当处理器核心数量不足四个时 CMS对用户程序的影响就可能变得 很大如果应用本来的处理器负载就很高 还要分出一半的运算能 力去执行收集器线程 就可能导致用户程序的 执行速度忽然大幅降低 2CMS收集器无法处理浮动垃圾可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生前面也说明了他的触发可能需要Major GC的某些条件这里就是即整理后可能会再次的操作这个Full GC因为老年代满也是会操作触发的前面说明的这样要注意哦一般是考虑老年代空间大需要多个GC吧当然gc优先是Major GC然后这个除非你自己手动操作 由于CMS并发清理阶段用户线程还在运行着伴随程序运行自然就还会有新的垃圾不断产生这一部分垃圾出现在 标记过程之后CMS无法在当次收集中处理掉它们只好留待下一次GC时再清理掉这一部分垃圾就称为浮动垃圾同样也是由于在垃圾收集阶段用户线程还需要持续运 行 那就还需要预留足够内存空间提供给用户线程使 用 因此CMS收集器不能像其他收集器那样等待 到老年代几乎完全被填满了再进行收集 必须预留一部分空间供 并发收集时的程序运作使用否则可能会影响用户线程的操作使得内存溢出 在JDK 5的默认设置下 CMS收集器当老年代使用了68%的空间后就会被激活 这是一个偏保守的设置如果在实际应用中老年代增长并不是太快可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高CMS的触发 百分比设置案例-XX:CMSInitiatingOccupancyFraction80表示80%不要加其他参数哦否则会报错的 降低内存回收频率 获取更好的性能 到了JDK 6时 CMS收集器的启动 阈值就已经默认提升至92%但这又会更容易面临另一种风险 要是CMS运行期间预留的内存无法满 足程序分配新对象的需要运行期间因为是有并行的那么可能他使得报错并结束但不会操作如类似的关闭虚拟机的exit的操作虽然内存溢出可能是这样而不会是等gc后出现内存溢出而是gc中出现错误 就会出现一次并发失败Concurrent Mode Failure在内存溢出之前操作了自然是这个错误 这时候虚拟机将不 得不启动后备预案 冻结用户线程的执行 临时 启用Serial Old收集器来重新进行老年代的垃圾收集 但这样停顿时间就很长了但可以整理这就是之前说的防止CMS不能使用的情况保存不使用了这个时候我总不能继续操作CMS使得重新开始吧这样又会容易出现并发失败所以这个时候直接考虑清除 3空间碎片CMS是一款基于标记-清除算法实现的收集器所有会有空间碎片的现象 当空间碎片过多时将会给大对象分配带来很大麻烦往往会出现老年代还有很大空间剩余但是无法找到足够大 的连续空间来分配当前对象不得不触发一次Full GC也就是老年代满了触发 为了解决这个问题 CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数默认是开启的此参数从 JDK 9开始废弃一般没有给出案例说明直接就这样写 用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程 由于这个 内存 整理必须移动存活对象 是无法并发的这样空间碎片问题是解 决了 但停顿时间又会变长 因此虚拟机设计者 们还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction此参数从JDK 9开始废弃案例-XX:CMSFullGCsBeforeCompaction2 这个参数的作用是要 求CMS收集器在执行过并执行过程中若干次数量 由参数值决定 不整理空间的Full GC之后当然没有操作整理的 下一次进入Full GC前会先进行碎 片整理默认值为0 表 示每次进入Full GC时前都进行碎片整理自然费时间因为需要等待该gc也包括STW哦一般我们说明的STW是gc导致的其他只是改gc线程需要等待的时间而已如果也再STW里面那么自然等待时间会使得STW总体变长的比如这里当然有些停顿是STW导致有些不是再对应的解释中已经可以进行联系起来具体就不多说了这里只是提一下所以这里是一个优化的地方我们最好设置几次而不是默认这也是为什么在jdk9中废弃的原因上面再jdk9开始都没有这些参数所以若你的jdk是9以及以上那么设置时运行会报错 G1收集器 G1垃圾收集器简介 Garbage First是一款面向服务端应用的垃圾收集器主要针对配备多核CPU及大容量内存的机器以极高概率满足GC停顿时间的同时还兼具高吞吐量的性能特征 G1收集器特点 1G1把内存划分为多个独立的区域Region 2G1仍然保留分代思想保留了新生代和老年代但他们不再是物理方面的隔离说明而是一部分Region的集合比如他们各自内存物理上没有分开但是逻辑上认为是一部分的Region的集合但还是逻辑上分开的 3G1能够充分利用多CPU、多核环境硬件优势尽量缩短STW 4G1整体整体采用标记整理算法局部是采用复制算法不会产生内存碎片 5G1的停顿可预测能够明确指定在一个时间段内消耗在垃圾收集上的时间不超过设置时间 6G1跟踪各个Region里面垃圾的价值大小会维护一个优先列表每次根据允许的时间来回收价值最大的区域从而保证在有限事件内高效的收集垃圾 Region区域 G1不再坚持固定大小以及固定数量的 分代区域划分 而是把连续的Java堆划分为多个独立区域Region 每一 个Region都可以 根据需要 扮演新生代的Eden空间、 Survivor空间 或者老年代空间 将整个堆空间细分为若干个小的区域但要注意也是逻辑上的划分的只是G1可以通过对应的引用来选择间接的操作gc但是可能也会由于设置的原因jvm会改变堆结构但我感觉不会这里可以选择百度查看 使用G1收集器时它将整个Java堆划分成约2048个大小相同的独立Region块每个Region块大小根据堆空间的实 际大小而定比如Region块大小是为2的N次幂的堆大小即1MB, 2MB, 4MB, 8MB, 16MB,32MB一般是1mb最小32mb最大可能最大可以更大具体可以百度我们这些乘以2048可能就是堆大小了一般堆空间可能会与他对应而调整向下调整 虽然还保留有新生代和老年代的概念但新生代和老年代不再是物理隔离的了它们都是一部分Region不需要连续的集合通过Region的动态分配方式实现逻辑上的连续 G1垃圾收集器还增加了一种新的内存区域叫做Humongous内存区域如图中的H块主要用于存储大对象如果超过1.5个regionsRegion就放到H一般被视为老年代而中间的1.1等等多余1的可能会放在S里面 G1 GC过程 G1提供了两种GC模式Young GC和Mixed GC两种均是完全Stop The World的 Young GC选定所有年轻代里的Region通过控制年轻代的regionRegion个数即年轻代内存大小来控制young GC的时间开销 Mixed GC选定所有年轻代里的Region外加根据global concurrent marking统计得出收集收益高的若干老年代Region这就是为什么之前说明了可能有些gc操作部分老年代的原因在用户指定的开销目标范围内尽可能选择收益高的老年代Region 在G1 GC垃圾回收的过程一个有四个阶段 初始标记和CMS一样只标记GC Roots直接关联的对象 并发标记进行GC Roots Traceing过程 最终标记修正并发标记期间,因程序运行导致发生变化的那一部分对象 筛选回收根据时间来进行价值最大化收集 下面是G1收集的示意图 Safwpoint英文意思安全点 G1 YoungGC YoungGC执行前 堆分为大约2000个区域最小大小为1Mb最大大小为32Mb蓝色区域保存老年代对象绿色区域保存年轻对象 执行YoungGC 将存活的对象即复制或移动到一个或多个幸存者区域如果满足老化阈值则某些对象将被提升到老年代区域 G1的年轻GC结束 最近升级的对象以深蓝色显示幸存者区域为绿色 总而言之关于G1的年轻一代可以说以下几点 1堆是单个内存空间分为多个区域 2年轻代内存由一组非连续区域组成 3年轻一代的垃圾收集器或年轻的GC出现STW将停止所有应用程序线程以进行操作 4年轻的GC使用多个线程并行完成 5将活动对象复制到新的幸存者或老年代的地区 G1 Mix GC 初始标记阶段initial markSTW 存活的对象的初始标记背负在年轻的垃圾收集器上在日志中此标记为 GC pause (young)(inital-mark) 并发标记阶段Concurrent Marking 如果找到空白区域如X所示则在Remark阶段将其立即删除另外计算确定活跃度的信息 最终标记阶段RemarkSTW 空区域将被删除并回收现在可以计算所有区域的区域活跃度 筛选回收阶段/复制清理阶段CleanupSTW G1选择活度最低的区域这些区域可以被最快地收集然后与年轻的GC同时收集这些区域这在日志中表示为[GC pause (mixed)] 因此年轻代和老年代都是同时收集的前面的只是操作年轻代 筛选回收阶段-(复制/清理)阶段之后 选定的区域已被收集并压缩为图中所示的深蓝色区域和深绿色区域 总结 并发标记阶段 1活动信息是在应用程序运行时同时计算的上面一块是可以包含很多的Region所以操作后才会是少点的包含且Region可以代表扮演新生代的Eden空间、 Survivor空间 或者老年代空间等等虽然上面有颜色区分 2该活动信息标识在疏散暂停期间最适合回收的区域 3像CMS中没有清扫阶段 最终标记阶段 1使用开始快照SATB算法该算法比CMS使用的算法快得多 2完全回收空区域 筛选回收阶段 1同时回收年轻一代和老一代 2老年代地区是根据其活跃度来选择的 上面只是大致说明了解即可 G1会使用到的常用参数并不一定代表是属于操作G1的因为他自己也是被设置的-XX:UseG1GC 参数/默认值含义-XX:UseG1GC使用 G1 垃圾收集器他直接操作就他一人没有什么新老-XX:MaxGCPauseMillis200设置期望达到的最大GC停顿时间指标JVM会尽力实现但不保 证达到-XX:InitiatingHeapOccupancyPercent45代表45%mixed gc中也有一个阈值参数 当老年代大小占整个堆大小百分比达到该阈值时会触发一次mixed gc默认值为 45-XX:NewRatio2新生代与老生代(new/old generation)的大小比例(Ratio)默认值为 2-XX:SurvivorRatio8eden/survivor 空间大小的比例(Ratio)默认值为 8-XX:MaxTenuringThreshold15提升年老代的最大临界值(tenuring threshold)默认值为 15-XX:ParallelGCThreadsnn是未知数不能加否则报错可以设置为4设置垃圾收集器在并行阶段使用的线程数默认值随JVM运行的平台不同而不同-XX:ConcGCThreadsnn是未知数不能加否则报错可以设置为4并发垃圾收集器使用的线程数量默认值随JVM运行的平台不同而 不同-XX:G1ReservePercent10设置堆内存保留为假天花板的总量以降低提升失败的可能性默认值是 10-XX:G1HeapRegionSize1m默认字节单位可以加单位来改变如k与前面的单位一样的可以加哦也就是有单位使用单位的否则默认字节单位虽然有些地方说明不能加单位使用G1时Java堆会被分为大小统一的的区(region)此参数可以指定每个heap堆区的大小默认值将根据 heap size 算出最优解最小值为1Mb, 最大值为 32Mb 上面的了解即可 常用指令与可视化调优工具 常用指令了解即可 jps jps 是java process Status Tool, Java版的ps命令查看java进程及其相关的信息如果你想找到一个java进程的pid那可以用jps命令替代linux中的ps命令了简单而方便 命令格式jps [options] [hostid]带有[]的可以不加后面其他命令也基本是这样的认为 options参数解释显示的进程id一般是对应的所有相关java进程id所以可以看到显示多个 -l显示进程id后面接着显示主类全名或jar路径 -q显示进程id这个与单纯的jps不同的是单纯的jps会出现类名而他只出现进程id -m显示进程id后面接着显示JVM启动时传递给main()的参数 -v显示进程id后面接着显示jvm启动时显示指定的JVM参数 hostid主机或其他服务器ip具体作用可以百度通常是jvm的外部吧 最常用示例 jps -l //输出jar包路径存在的话或者说是直接操作jar里面的class而不是直接存在的class类全名 jps -m //输出main参数 jps -v //输出JVM参数参考代码 package coo;/****/ public class Demo1_Jps {public static void main(String[] args) throws InterruptedException {Thread.sleep(1000000);} } 我们启动上面的代码时可能jps -m在对应的进程后面的类里面没有什么值也就是说没有main对应参数但是args是存在的啊这里我们打印一下 package coo;/****/ public class Demo1_Jps {public static void main(String[] args) throws InterruptedException {System.out.println(args.length); //为0for (int i 0; i args.length; i) {System.out.println(args[i] 第 i 个);}Thread.sleep(1000000);} } 也就是说对应的显示是o我们需要在设置里操作如下也就是main方法的参数设置值 继续运行执行jps -m可以看到hhhh了通过测试对应的长度是1当你设置hhhh hhhh中间有空格那么长度是2在命令行cmd中我们手动操作运行时我们需要在如java xx.java k k或者java xx k k这两个k都是即长度为2 而jps -v后面就是我们之前的VM options的设置也就是jvm参数虽然有隐藏的信息你可以加上-XX:UseG1GC来看看结果就知道了当然他有很多值可以显示的包括-XX:PrintCommandLineFlags但确不会给出隐藏的对应程序运行-XX:PrintCommandLineFlags设置的得到的显示的其他信息而是某些路径信息或者其他信息具体可以自己操作看看当然jps对应操作的至少要成功运行而不是没有运行所以jps -v出现的必然是正确的配置哦 jinfo jinfo是用来查看JVM参数和动态修改部分JVM参数的命令 命令格式 jinfo [option] pid代表必须要加 options参数解释 no options没有该操作也就是jinfo pid如jinfo 10000 输出所有的系统属性和参数 -flag name打印指定名称的参数 -flag [|-] name打开或关闭参数 -flag name name设置参数 -flags打印所有参数自然包括jvm的参数包括隐藏的信息 -sysprops打印系统配置 代码还是之前的代码这里你可以自己测试记得加上进程号id哦否则虽然不会报错但是会有提示虽然报错也是提示或者说异常也是提示一般是提示格式问题 案例 假设你的运行的代码的进程id是10000注意我们将他也认为是进程id因为是一个程序的 jinfo -flag PrintGCDetails 10000 //查看是否生效虽然有些是认为值的意思比如MetaspaceSize jinfo -flag PrintGCDetails 10000 //使得他生效一般代表前面出现就是生效如-XX:PrintGCDetails而-XX:PrintGCDetails就是没有生效 //我们继续查看是否生效若报错那么一般是jdk版本的原因可能他不准改变了你可以修改成jdk8来进行测试那么一般不会报错然后查看的结果如果变成了说明操作成功 //当然并不是所有的设置都可以改变无论jdk版本比如UseParallelGC就是这样也就是gc操作一般不能被修改因为已经确定gc了可能有操作处初始化了那么修改有点不好可能以后会允许 //其他的自己测试这里是因为有版本问题所以提一下了 //并且我们可以发现无论是-的还是设置值的都可以进行操作即PrintGCDetails替换前面说明的任何设置除了本质设置如Xmx等等由于一些基本的使用我们不能直接操作所以下面的命令主要查看那些参数可以使用jinfo命令来操作对应的其他管理 Xms初始堆大小默认为物理内存的1/64(1GB)默认MinHeapFreeRatio参数可以调整该值具体显示可能是0空余堆内存小于40%时JVM就会增大堆直到-Xmx的最大限制 -Xmx最大堆大小默认MaxHeapFreeRatio参数可以调整空余堆内存大于70%时JVM会减少堆直到 -Xms的最小限制 -Xmn新生代的内存空间大小前面没有说明这里注意一下注意此处的大小是eden 2 survivor space)与jmap -heap中显示的New gen是不同的整个堆大小新生代大小 老生代大小 永久代大小 在保证堆大小不变的情况下增大新生代后将会减小老生代大小对应的新老比值一般也会改变此值对系统性能影响较大Sun官方推荐配置为整个堆的3/8 -XX:SurvivorRatio新生代中Eden区域与Survivor区域的容量比值默认值为8两个Survivor区与一个Eden区的比值为2:8一个Survivor区占整个年轻代的1/10 -Xss每个线程的堆栈大小虚拟机栈JDK5.0以后每个线程堆栈大小为1M以前每个线程堆栈大小为256K应根据应用的线程所需内存大小进行适当调整在相同物理内存下减小这个值能生成更多的线程但是操作系统对一个进程内的线程数还是有限制的不能无限生成根据经验来说值在3000~5000左右一般小的应用 如果栈不是很深 应该是128k够用的大的应用建议使用256k这个选项对性能影响比较大需要严格的测试和threadstacksize选项解释很类似但是官方文档似乎没有解释 在论坛中有这样一句话:“-Xss is translated in a VM flag named ThreadStackSize”一般设置上面说明的值就可以了 -XX:PermSize设置永久代(perm gen)初始值默认值看前面的说明 -XX:MaxPermSize设置持久代最大值默认值看前面的说明 上面只是大致的给出而已也并非都可以操作可能也看jdk版本 jstat jstat命令是使用频率比较高的命令主要用来查看JVM运行时的状态信息包括内存状态、垃圾回收等 命令格式 jstat [option] VMID [interval] [count]注意一般没有jstat pid的操作 其中VMID是进程idinterval是打印间隔时间毫秒count是打印次数不加的话那么默认一直打印注意这个操作需要interval来触发如果interval不加那么自然就只有如类似的jstat -class 139368那么就打印一次且没有间隔而加了不设置打印次数那么打印无限次若设置了打印次数那么就是打印设置的次数还要注意的是间隔不会影响一开始的第一个打印也就是说如果是5000那么打印后才会进行等待5秒你仍然可以操作jstat -class 139368 5000139368是pid即进程id来进行测试 option参数解释通常情况下对于大小的记录都是字节保存的这里注意即可下面操作-gccapacity所出现的结果可能就是这样但我们可是通常情况下哦在这篇博客中都可以认为是这样的 -classclass loader的行为统计 -compilerHotSpt JIT编译器行为统计 -gc垃圾回收堆的行为统计 -gccapacity各个垃圾回收代容量(youngoldperm)和他们相应的空间统计 -gcutil垃圾回收统计概述 -gccause垃圾收集统计概述同-gcutil附加最近两次垃圾回收事件的原因 -gcnew新生代行为统计 -gcnewcapacity新生代与其相应的内存空间的统计 -gcold年老代和永生代行为统计 -gcoldcapacity年老代行为统计 -printcompilationHotSpot编译方法统计 常用示例及打印字段的解释 jstat -gcutil 11666 1000 3 11666假设为pid每隔1000毫秒打印一次打印3次 可能的结果如下有两个不显示的不给说明了 字段解释 S0survivor0使用百分比 S1survivor1使用百分比 EEden区使用百分比 O老年代使用百分比 M元数据区使用百分比 CCS压缩使用百分比 YGC年轻代垃圾回收次数 YGCT年轻代垃圾回收消耗时间 FGCFull GC垃圾回收次数 FGCTFull GC垃圾回收消耗时间 GCT垃圾回收消耗总时间 jstat -gc 11666 1000 3 -gc和-gcutil参数类似只不过输出字段不是百分比而是实际的值有两个不显示的不给说明了可能的结果如下 字段解释 S0Csurvivor0大小 S1Csurvivor1大小 S0Usurvivor0已使用大小 S1Usurvivor1已使用大小 ECEden区大小 EUEden区已使用大小 OC老年代大小 OU老年代已使用大小 MC方法区大小 MU方法区已使用大小 CCSC压缩类空间大小 CCSU压缩类空间已使用大小 YGC年轻代垃圾回收次数 YGCT年轻代垃圾回收消耗时间 FGCFull GC垃圾回收次数 FGCTFull GC垃圾回收消耗时间 GCT垃圾回收消耗总时间 当然只要知道对应的意思即可并不需要背下来因为这并无意义除非你有强迫症手动滑稽在上面给出的案例示例是大多数会用到的包括前面的jps和jinfo 现在我们根据代码来进行操作首先我们需要学习一下System.in.read()这个代码 package coo;import java.io.IOException;/****/ public class tets2 {public static void main(String[] args) throws IOException {//system.in.read()方法的作用是从键盘读出一个字符然后返回它的Unicode码他本身需要等待你输入当你回车时结束等待按下Enter结束输入int read System.in.read();System.out.println(read);while (true) {int read1 System.in.read();System.out.println(read1);break;}//当你输入1时按下回车注意重中之重按下回车这个也算也就是说上面会打印两个值结果就是49110回车的//但是还要注意只要你再次的执行System.in.read();他又会需要你阻塞那么总值可能是这样的//首先我们将回车看成p输入保留的地方看成[]一开始是空的当我们输入1时回车结束那么保留的地方是[1p]这里认为赋值时给Unicode码值//那么他首先会按照顺序来给那么上面的read就是1的Unicode码值那么保留的就是[p]后续又会进行输入1那么保留的值是[p1p]根据顺序将p给read1//所以上面打印p的Unicode码值那么结果也就是49和10了} }//大多数情况下我们可以只用System.in.read()来进行阻塞而回车就代表解除阻塞即大多数情况下我们是这样使用了而不操作返回值我们了解后看这个代码 package coo;import java.io.IOException;/*** -verbose:gc一般是在控制台输出GC情况然而-XX:PrintGCDetails包括了-verbose:gc所以-verbose:gc可以不写* 记得设置如下参数* jvm参数-Xms20m -Xmx20m -Xmn10m -XX:UseSerialGC -XX:PrintGCDetails -verbose:gc*/ public class test {public static void main(String[] args) throws IOException {final int _1m 1024*1024;byte[] b1 new byte[2 * _1m];System.out.println(创建b1...);System.in.read();byte[] b2 new byte[2 * _1m];System.out.println(创建b2...);System.in.read();byte[] b3 new byte[2 * _1m];System.out.println(创建b3...);System.in.read();} } 现在我们运行操作jstat -gc pid来看看结果可能代表假设的或者自己的的结果如下新生代一般有值的因为你jvm启动自然会利用到一般来说System.in.read();比普通的打印会多点可能jvm在识别执行他时会初始化比较多的对象 回车继续打印 可以看到刚好是20485049.2到7097.2也就是2 * 1024 * 1024分开来分析就是1024字节 1k*1024 1024k1m * 22m正好是1024 * 1024 *2 2m所以他们的单位是keden默认8m因为前面设置了新生代为10m-Xmn10m而对比那么eden自然是8m即都对应起来了 再次的回车可以看程序的打印日志 操作了gc因为eden满了当然他的操作可能并不是最终结果因为是中间出现的所以可能与下面的最终结果不符合 然后继续打印控制台 很明显他并没有回收因为对应的都是存在的那么在这个时候由于年轻代是不能放入幸存者这里是一个特殊情况因为都存在幸存即都没有回收幸存者放不下的比例在此自然绝对放不下前面我们也说过条件没满eden和幸存者都满是可以有特殊的规则到老年代的而这里也是一个特殊情况我可能会将当前的进行划分来分到幸存者和老年代里面或者都放入老年代之所以是或者是因为幸存者没有时一般会保留一点到幸存者而若幸存者有那么都放入老年代即eden和幸存者都放入虽然他们也操作了方法区解释如下981.02192.74096.0-162999.5138342.910734.1新生代老年代和方法区变化的总空间可能规则导致其他信息出现981.02192.740967269.7幸存者eden老年代操作的空间10734.1-7269.73 464.4方法区变化大小若不考虑原来的增加的2048那么中间多了1416.4即规则导致创建的方法区空间因为7097.220489145.27269.7所以规则导致增加了方法区空间甚至也影响了大小可能的操作设置且中间操作使得放入方法区了可能以常量池来进行保存了堆数据了那么他们的数据变化的可能是符号引用导致的所以是7269.7而不是9145.2而符号引用需要真的保存才可既然是引用必然需要保存信息吧即常量池是保存引用的信息的哦虽然前面并没有具体说明即真的在常量池中使得方法区变多 上面只是我的猜测具体你可以到百度查看 最后注意java中的不是端口而是进程ip一般来说Linux 内核的进程 PID 最大值并非 131070而是 32768 32 位系统和 2 的 22 次方也就是419430464 位系统所以一般我们java通常的进程id上限也是4194304可能windows也是这样一般PID从0开始包括0即是大于等于0的整数上面说明的就是上限 一个进程里面可以有子进程线程实际上一个进程只是一个线程组所以进程id也是线程组id即该组的位置而线程id是该线程的标识虽然他们都分配PID即这就是有些PID的关闭可能会导致多个PID关闭的原因当然主线程可不是进程虽然他java对他可能操作某些绑定即操作守护线程等等具体可以到101章博客查看只是进程需要开辟线程组的内存使用所以进程只是一个逻辑说明而已就如端口一样 注意对于线程来说可能PID的显示是TID所以都分配PID所以看到TID要明白是线程哦 jstack jstack是用来查看JVM线程快照的命令线程快照是当前JVM线程正在执行的方法堆栈集合使用jstack命令可以定位线程出现长时间卡顿的原因例如死锁死循环等jstack还可以查看程序崩溃时生成的core文件中的stack英文意思堆栈信息该信息通常表示代码的第几行的信息因为堆栈虚拟机栈而该栈就表示存在栈帧而栈帧就表示方法那么自然可以表示方法的位置而由于栈帧里面又有其他信息那么总体来说堆栈就能够表示栈里面的任何信息了因为就算是main也是虚拟机栈里面的 命令格式 jstack [options] pid option参数解释 -F当使用jstack pid存在该操作无响应时强制输出线程堆栈 -m同时输出java堆栈和c/c堆栈信息混合模式 上面两个可能操作不了实际上是执行了因为并不是格式错误但是确不操作或者忽略了导致出现需要进行解决一般都是提示Use jhsdb jstack instead除了上面说明的存在该操作这个具体使用方式可以百度反正我们主要使用下面的 -l 来进行操作所以这里跳过即可 -l除了输出堆栈信息外还显示关于锁的附加信息 上面的操作我们在后面死锁中会测试一下你实际上也可以通过测试System.in.read();使用-l时可以看到一些信息即调用信息自己可以看看这里了解即可 cpu占用过高问题 1使用Process Explorer工具找到cpu占用率较高的线程 2在thread卡中找到cpu占用高的线程id 3线程id转换成16进制 4使用jstack -l pid查看进程的线程快照一般我们也能手动找到但是若在代码非常多的情况下我们可以通过全局来进行查找 5线程快照中找到指定线程并分析代码 为了测试给出代码 package coo;/****/ public class test3 {public static void main(String[] args) {System.out.println(1);while (true);//相当于无限循环即while(true){}实际上我们也可以将他称为阻塞} } 启动上面的代码现在我们给出Process Explorer工具工具下载地址如下 链接https://pan.baidu.com/s/1FvrkcQKf-LegiP3KVctc9Q 提取码alsk 点击即可先同意一般他会自动退出窗口然后再次的点击就行然后找到如下 上代码启动的是30040这里正好也找到了实际上在任务管理器的详细信息里也可以找到只是没有这么方便的找到需要你慢慢的翻找如 点击上面选项中的详细信息 点击上面选项中的进程任务管理器进入默认是这个的点击 可以看到虽然有这样的从属关系但是对应的显示并没有PID而这个工具可以直接的操作从属并给出PID的值这就是有时候工具的好处虽然其他信息没有直接显示了一般需要双击他可能有些少些如电源使用情况但是对于PID的查找来说他是有优势的 当然工具之所以是工具除了可以把表示的显示外最主要的是可以有特殊的操作更快的找到或者进行操作这是原来的任务管理器所没有的操作比如选项中有查找功能可以选择输入java.exe或者进程id来进行查找然后点击查找的会自动帮你定位的这也是一个好处且并非要java.exe直接输入java相关的基本都会帮你找出然后自己可以选择定位即可虽然一般帮你定位到最上层 当然这并非是主要的最主要的操作是如下 现在我们双击他也就是查看该应用程序运行的情况或者说该main方法运行的情况一般来说一个main方法代表一个应用程序点击Threads即可 TID代表线程id我们可以看到最大CPU占用是151796实际上在对应关系中线程一般喜欢与16进制的id进行关联用NID表示看下面的图就知道了虽然下面的TID可能是操作的线程的另外一种表示而15179610进制的16进制是250f4现在我们回到idea的控制台中输入jstack -l 30040在对应的打印结果中可以看到如下 可以看到250f4而我们的第9行就是阻塞的地方while (true);一直运行会一直拿取CPU操作因为线程需要使用CPU操作任何操作都会需要因为判断和往后面执行都算内存也是虽然他并不会一直操作内存即内存基本不变但是CPU会一直使用在回收和使用中达到CPU平衡也就是CPU占用过高的地方 通过上面我们可以明白他这个工具可以知道CPU占用过高的进程id以及对应的线程id以及其他信息要注意只有与网络有关的进程才需要占用端口号因为端口只是操作网络的而已对于内部的其他操作进程并非一定操作端口哦反正是线程组所以进程id可以比端口多的 而我们可以通过他然后再控制台中输出命令来精确的找到对应第几行代码虽然命令本身不需要通过它但是若在代码非常多的情况下我们可以通过全局来进行查找那么就非常重要了那么这个工具的主要作用就是找到对应进程id或者线程id的相关信息了以及在代码多的情况下来精确对应的信息这个更主要 全局查找操作 通过上面分析这就是最主要的操作所以工具的确是非常好的当一个应用程序发现占用的cpu资源比较高时可以通过这个工具找到线程id从而全局找信息然后进行分析这是该工具的主要作用并不是主要看PID的信息哦 jstack检查死锁问题 先给出案例 package coo;/****/ public class DeadLock {private static Object obj1 new Object();private static Object obj2 new Object();public static void main(String[] args) {new Thread(new Thread1()).start();new Thread(new Thread2()).start();}private static class Thread1 implements Runnable {public void run() {synchronized (obj1) {System.out.println(Thread1 拿到了 obj1 的锁);try {// 停顿2秒的意义在于让Thread2线程拿到obj2的锁Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (obj2) {System.out.println(Thread1 拿到了 obj2 的锁);}}}}private static class Thread2 implements Runnable {public void run() {synchronized (obj2) {System.out.println(Thread2 拿到了 obj2 的锁);try {// 停顿2秒的意义在于让Thread1线程拿到obj1的锁Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (obj1) {System.out.println(Thread2 拿到了 obj1 的锁);}}}} } 然后执行指令 jstack -l 11666 一般情况下可以在打印结果后面看到Found 1 deadlock的信息也就是找到1个死锁的意思阻塞可没有操作很多CPU哦因为并没有往后面持续执行虽然可能有少部分的自旋具体信息如下我这里是这样当然由于进程id或者线程id的分配基本随机基本是这样的即不同那么你的执行可能与下面有部分差别 可以看到信息基本可以确定是死锁了在后面就是Found 1 deadlock的信息且到头了 jmap jmap可以生成 java 程序的 dump 文件 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及finalizer 队列 命令格式 jmap [option] pid option参数解释 如果使用不带option选项参数的jmap打印共享对象映射将会打印目标虚拟机中加载的每个共享对象的起始 地址、映射大小以及共享对象文件的路径全称 -heap打印java heap摘要 -histo[:live]打印堆中的java对象统计信息 上面三个包括不带的可能执行不了也是提示Use jhsdb jstack instead不带的除外一般背抛弃了因为他提示了格式错误具体可以百度 -clstats打印类加载器统计信息 -finalizerinfo打印在f-queue中等待执行finalizer方法不要陌生了给你一段在前面的话Finalizer线程去执行它们的finalize() 方法的对象这个执行了也就是No instances waiting for finalization found找不到等待完成的实例只不过没有而已 -dump: dump-options生成java堆的dump文件 dump-options //注意像命令什么的为了保证博客的显示一般需要dump-options变成 dump-options才可自己注意一下就行dump-options的替换 live只转储存活的对象如果没有指定则转储所有对象 formatb二进制格式 file file转储文件到 file这个必须有上面两个可以不加 常用示例 jmap -dump:live,formatb,filedump.bin 11666也可以是d:/dump.bin这样写就是当前目录下正反斜杠都行的主要是文件系统可以这样一般来说斜杠的操作是对方是否可以识别的问题 11666是进程若在控制台的一个目录下进行操作那么dump.bin就创建在那个目录下面其中上面的,代表分开的意思可以补充但是file file必须存在否则格式不对 若出现Heap dump file created代表操作成功若出现File exists代表存在并没有覆盖哦其他的基本是格式问题的提示了 上面这个命令是要把java堆中的存活对象信息转储到dump.bin文件 当然了上面的你可以自己测试但要注意的是如果你不知道打印的信息是什么意思你可以选择百度因为信息这么多虽然我们可以根据英文来大致的了解但是并不是一定正确的 jhat jhat是用来分析jmap生成dump文件的命令jhat内置了应用服务器可以通过网页查看dump文件分析结果jhat一般是用在离线分析上 命令格式 jhat [option] [dumpfile] option参数解释 -stack false关闭对象分配调用堆栈的跟踪 -refs false关闭对象引用的跟踪 -port portHTTP服务器端口默认是7000 -debug intdebug级别 -version分析报告版本 常用实例 jhat dump.bin可以这样的直接分析但是jhat可能操作不了具体可以百度如果可以操作我们通过网页输入localhost:7000即可观察了这里了解即可 最后像有些操作不了的并不需要死磕因为并无意义就如在linux中你会将所有的命令及其作用都背下来吗很显然不会就算你会那么如果以后也添加一些命令你岂不是又要背这种是没有意义的 JVM常用工具 Jconsole 监控管理工具 JconsoleJava Monitoring and Management Console是从java5开始在JDK中自带的java监控和管理控制台用于对JVM中内存线程和类等的监控是一个基于JMXjava management extensions的GUI性能监测工具jconsole使用jvm的扩展机制获取并展示虚拟机中运行的应用程序的性能和资源消耗等信息 直接在jdk版本里面的bin目录下点击jconsole.exe即可启动这里以jdk8为主你也可以在idea的控制台输入jconsole也可以启动 内存监控 先给出测试代码 package test;import java.util.ArrayList; import java.util.List;/****/ public class JConsoleDemo {static class OOMObject {public byte[] placeholder new byte[8 * 1024];}public static void fillHeap(int num) throws InterruptedException {ListOOMObject list new ArrayListOOMObject();for (int i 0; i num; i) {Thread.sleep(200);list.add(new OOMObject());}System.gc();}public static void main(String[] args) throws Exception {fillHeap(1000000);System.gc(); //注意方法结束后我们知道他里面的内容会清除也就是说上面的list相当于自动的设置为null那么这个gc会导致都进行清理所以要考虑方法执行完毕哦这是重中之重在前面可能并没有具体说明过所以我们有时候在前面也说过虚拟机结束操作自动的gc与这里实际上是类似的} }//设置-Xms100m -Xmx100m -XX:UseSerialGC运行控制台输入jconsole到如下 点击上面可以看到他是我们启动的进程也就是我们启动的程序然后点击界面的连接 上面的图每次到都会刷新 然后点击不安全连接开始连接注意这个时候若程序结束了那么会连接失败的 然后你可以看到如下 他们的变化就是程序运行使得的变化这也就是监控当然其他的选项可以自己观察这里只需要知道什么东西就行了 线程监控 查看CPU使用率及活锁阻塞线程 代码准备 package test;import java.io.BufferedReader; import java.io.InputStreamReader;/****/ public class test4 {/*** 线程死循环演示*/public static void createBusyThread() {Thread thread new Thread(new Runnable() {public void run() {while (true) ;}}, testBusyThread);System.out.println(启动testBusyThread 线程完毕..);thread.start();}/*** 线程锁等待演示*/public static void createLockThread(final Object lock) { //局部变量是可以设置为final的Thread thread new Thread(new Runnable() {public void run() {synchronized (lock) {try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}, testLockThread);thread.start();System.out.println(启动testLockThread 线程完毕..);}public static void main(String[] args) throws Exception {System.out.println(main 线程..);BufferedReader br new BufferedReader(new InputStreamReader(System.in));System.out.println(redLine阻塞);br.readLine();createBusyThread();System.out.println(redLine阻塞);br.readLine();Object obj new Object();createLockThread(obj);System.out.println(main 线程结束..);} } 自己看选项中的线程并自己测试吧 查看死锁线程 代码 package test;/****/ public class test5 {/*** 线程死锁等待演示*/static class SynAddRunalbe implements Runnable {int a, b;public SynAddRunalbe(int a, int b) {this.a a;this.b b;}public void run() {synchronized (Integer.valueOf(a)) {synchronized (Integer.valueOf(b)) {System.out.println(a b);}}}}public static void main(String[] args) {for (int i 0; i 100; i) {new Thread(new SynAddRunalbe(1, 2)).start();new Thread(new SynAddRunalbe(2, 1)).start();}} } 编译运行, 在线程页签可查看死锁描述点击检测死锁即可一般只会监测一次反正存在就行但通常是最开始和最终的靠拢包括jstack的查看死锁的那个地方个数也是主要显示一般是这个Found one Java-level deadlock英文意思发现一个Java级死锁地方但要注意外面的等待不是死锁的说明所以一般显示不是很多的不要以为他们都属于死锁所以Found one Java-level deadlock后面一般并不多这是因为1、2两个数值在Integer类的缓存常量池[-128, 127]范围内这样当多次调用Integer.valueOf()方法时不会再每次都创建对象而是直接返回缓存常量池中的对象所以上面两个线程的同步代码块中实际上只创建了两个锁对象且在某一时刻会出现互相持有对方的锁即死锁现象 VisualVM 可视化优化工具 简介 VisualVM 是一个工具它提供了一个可视界面用于查看 Java 虚拟机 (Java Virtual Machine, JVM) 上运行的基于Java 技术的应用程序Java 应用程序的详细信息VisualVM 对 Java Development Kit (JDK) 工具所检索的 JVM 软件相关数据进行组织并通过一种使您可以快速查看有关多个 Java 应用程序的数据的方式提供该信息您可以查看本地应用程序以及远程主机上运行的应用程序的相关数据。此外还可以捕获有关 JVM 软件实例的数据并将该数据保存到本地系统以供后期查看或与其他用户共享 概述与插件安装 VisualVM基于NetBeans平台开发因此它一开始就具备了插件扩展的特性通过插件支持VisualVM可以做许多事情, 例如 1显示虚拟机进程和进程的配置、环境信息(jps、jinfo) 2监视应用程序的CPU、GC、堆、方法区及线程的信息(jstat、jstack) 3dump及分析堆转储快照(jmap、jhat) 4方法级的程序运行性能分析, 找出被调用最多、运行时间最长的方法 5离线程序快照收集程序的运行时配置、线程dump、内存dump等信息建立一个快照, 可以将快照发送开发者处进行bug反馈等等 当然这个在前面我们已经操作过了即jvisualvm.exe所以这里只是进行补充说明这里也提供另外一种打开方式在控制台输入jvisualVM一般也能打开当然如果不行你点击jvisualvm.exe也可以因为并非都可以被任何操作启动的就如jdk可能设置了他只能点击执行二进制是万能的给出限制不过分吧 堆转储快照 两种方式生成堆dump文件前面有操作命令的这里利用工具来进行 可以先执行如下代码 package test;import java.io.IOException; import java.util.ArrayList;/****/ public class test6 {public static void main(String[] args) throws IOException, InterruptedException {System.in.read();fun();System.in.read();}private static void fun() throws InterruptedException {ArrayListCapacity list new ArrayList();for (int i 0; i 10000; i) {Thread.sleep(400);list.add(new Capacity());}}}class Capacity {private byte[] big new byte[8 * 1024 * 1024]; //8m } 执行后操作如下 第一种在应用程序窗口中右键单击应用程序节点选择堆 Dump即可创建记得点击带有pid显示的否则可能不能选择堆 Dump 第二种在监视页签中选择堆 Dump即可创建成功 他们中间会有对应的地址存在的显示可以自己看 你可以选择执行前面的死锁代码然后点击线程查看一般会提示检测到了死锁选择线程Dump就相当于观看jstack -l 177568或者jstack 177568的执行结果他们基本是相同的意思可以认为默认加上-l那么Found one Java-level deadlock地方的显示也是同样的 分析程序性能了解即可 在Profiler页签中可以对程序运行期间方法级的CPU和内存进行分析这个操作会对程序运行性能有很大影响所以一般不再生产环境使用CPU分析将会统计每个方法的执行次数、执行耗时内存分析则会统计每个方法关联的对象数及对象所占空间 GC日志分析 GC日志是一个很重要的工具它准确记录了每一次的GC的执行时间和执行结果通过分析GC日志可以优化堆设置和GC设置或者改进应用程序的对象分配模式 GC日志参数了解即可 不同的垃圾收集器输出的日志格式各不相同但也有一些相同的特征熟悉各个常用垃圾收集器的GC日志是进行JVM调优的必备一步解析GC日志首先需要收集日志常用的有以下JVM参数用来打印输出日志信息 GC日志参数如下 参数说明-XX:PrintGC打印简单GC日志类似-verbose:gc-XX:PrintGCDetails打印GC详细信息-XX:PrintGCTimeStamps输出GC的时间戳以基准时间的形式-XX:PrintGCDateStamps输出GC的时间戳以日期的形式-XX:PrintHeapAtGC在进行GC的前后打印出堆的信息-Xloggc:…/logs/gc.log指定输出路径收集日志到日志文件 -XX:PrintGCDateStamps可以结合-XX:PrintGCTimeStamps一起显示其中有些设置如-XX:PrintHeapAtGC和-XX:PrintGCDateStamps和-XX:PrintGCTimeStamps可能受版本影响而不能设置一般来说jdk8可以jdk11可能设置时运行代码会报错 例如使用如下参数启动 -Xms28m -Xmx28m //开启记录GC日志详细信息包括GC类型、各个操作使用的时间并且在程序运行结束打印出JVM的内存占用情况 -XX:PrintGCDetails -XX:PrintGCDateStamps //这个还可以开启滚动的生成日志即滚动的方式来显示信息 -XX:UseGCLogFileRotation -Xloggc:E:/logs/gc.log //指定输出路径收集日志到日志文件包括很多日志一般比打印的控制台不是命令的要多因为有隐藏不显示的日志信息要不然为什么有时候需要log4j.properties日志的配置呢常用垃圾收集器参数及其利用方式 参数描述UseSerialGC虚拟机在运行在 Client 模式下的默认值打开此开关后使用 SerialSerial Old 收集器组合进行内存回收UseParNewGC使用 ParNew Serial Old 收集器组合进行内存回收UseConcMarkSweepGC使用 ParNew CMS Serial Old 的收集器组合尽心内存回收当 CMS 出现 Concurrent Mode Failure 失败后会使用 Serial Old 作为备用收集器UseParallelOldGC使用 Parallel Scavenge Parallel Old 的收集器组合UseParallelGC使用 Parallel Scavenge Serial Old PS MarkSweep的收集器组合SurvivorRatio新生代中 Eden 和任何一个 Survivor 区域的容量比值默认为 8PretenureSizeThreshold直接晋升到老年代对象的大小单位是ByteUseAdaptiveSizePolicy动态调整 Java 堆中各区域的大小以及进入老年代的年龄ParallelGCThreads设置并行 GC 时进行内存回收的线程数GCTimeRatioGC 时间占总时间的比率默认值为99只在 Parallel Scavenge 收集器的时候生效MaxGCPauseMillis设置 GC 最大的停顿时间只在 Parallel Scavenge 收集器的时候生效CMSInitiatingOccupancyFraction设置 CMS 收集器在老年代空间被使用多少后触发垃圾收集默认是 68%仅在 CMS 收集器上生效CMSFullGCsBeforeCompaction设置 CMS 收集器在进行多少次垃圾回收之后启动一次内存碎片整理UseG1GC使用 G1 (Garbage First) 垃圾收集器MaxGCPauseMillis设置最大GC停顿时间GC pause time指标target这是一个软性指标(so会尽量去达成这个目标G1HeapRegionSize使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小默认值将根据 heap size 算出最优解最小值为 1Mb最大值为 32Mb 注意背下来并无意义在实际操作中可能都不怎么使用到所以只需要了解即可 GC日志分析 日志的含义 GC 日志理解起来十分简单因为日志本来就是要给开发人员看的所以设计的很直观 举个例子我们来分别说明各个部分所代表的含义类似于前面的再次的回车可以看程序的打印日志下面的图片内容 GC (Allocation Failure) [PSYoungGen: 6146K-904K(9216K)] 6146K-5008K(19456K), 0.0038730secs] [Times: user0.08 sys0.00, real0.00 secs]之前的图片 将上面 GC 日志抽象为各个部分然后我们再分别说明各个部分的含义了解即可 [a(b)[c:d-e(f), g secs] h-i(j), k secs] [Times: user:l sysm, realn secs] //g secs好像没有因为是PSYoungGen他一边好像不显示并非是完美的哦任何操作都会有疏漏要不然为什么说安全是99%而不会是100%但图片有 /* a: GC 或者是 Full GC b: 用来说明发生这次 GC 的原因 c: 表示发生GC的区域这里表示是新生代发生了GC上面那个例子是因为在新生代中内存不够给新对象分配了然后触发了 GC前面的PS代表Parallel Scavenge的gc默认的通常不同版本是不同的默认jdk8默认的是这个在前面的垃圾收集器的组合关系后面就有说明 d: GC 之前该区域已使用的容量 e: GC 之后该区域已使用的容量 f: 该内存区域的总容量 g: 表示该区域这次 GC 使用的时间 h: 表示 GC 前整个堆的已使用容量 i: 表示 GC 后整个堆的已使用容量 j: 表示 Java 堆的总容量 k: 表示 Java堆 这次 GC 使用的时间 l: 代表用户态消耗的 CPU 时间 m: 代表内核态消耗的 CPU 时间 n: 整个 GC 事件从开始到结束的墙钟时间Wall Clock Time */使用ParNewSerial Old的组合进行内存回收 设置JVM参数 -Xms20M -Xmx20M -Xmn10M -XX:UseParNewGC -XX:PrintGCDetails -XX:SurvivorRatio8 -XX:PrintCommandLineFlags//对应老年代使用默认的所以这里不是使用ParNewSerial Old测试代码 package test;/****/ public class test11 {private static final int _1MB 1024 * 1024;public static void testAllocation() {byte[] allocation1, allocation2, allocation3, allocation4;allocation1 new byte[2 * _1MB];allocation2 new byte[2 * _1MB];allocation3 new byte[2 * _1MB];allocation4 new byte[4 * _1MB]; //出现一次 Minor GC}public static void main(String[] args) {testAllocation();} } 一般来说虽然完美设置的年轻代是10但是在分配时可能少点即9216/10249而堆大小是20而19456/102419即的确少点也解释了之前说明的虽然最大最小也是小于该值快满了也是但大于maxMemory的值虽然冗余少点这个地方的注释实际上是认为少了一个幸存者而已所以对于堆来说显示的就是这样的说明 结果分析 通过上面的GC日志我们可以看出一开始出现了 MinorGC引起GC的原因是 内存分配失败因为分配allocation的时候Eden区已经没有足够的区域来分配了所以发生了本次 MinorGC 经过 MinorGC 之后新生代的已使用容量从8145K-7692K然而整个堆的内存总量却几乎没有减少原因就是由于发现新生代没有可以回收的对象所以不得不使用内存担保将allocation13 三个对象提前转移到老年代也就是之前说明的特殊情况幸存者可存放不了这么多此时再在 Eden 区域为 allocation 分配 4MB 的空间因此最后我们发现新生代占用了 4MB4871k取整老年代占用了 6MB6144k取整加起来比我们的总10m多这是因为jvm初始化自然会使得新生代有值可以操作System.in.read();来看看是否多了若多了说明前面说明的一般来说System.in.read();比普通的打印会多点可能jvm在识别执行他时会初始化比较多的对象没有问题当然并不是他一人还有其他初始化是底层操作的所以有初始数据占用新生代是很正常的只是你加上了他会变更多而已 使用Parallel ScavengeParallel Old的组合进行内存回收 设置参数jdk8为主有默认那么只需要改变老年代即可虽然他操作了覆盖没有设置新生代操作覆盖自然会替换替替换是看谁先谁后的但是作用只是操作覆盖而已老覆盖新但是与默认一样的 -Xms20M -Xmx20M -Xmn10M -XX:UseParallelGC -XX:PrintGCDetails -XX:SurvivorRatio8测试代码 package test;/****/ public class test22 {private static final int _1MB 1024 * 1024;public static void testAllocation() {byte[] allocation1, allocation2, allocation3, allocation4;allocation1 new byte[2 * _1MB];allocation2 new byte[2 * _1MB];allocation3 new byte[2 * _1MB];allocation4 new byte[4 * _1MB]; //出现一次 Minor GC}public static void main(String[] args) {testAllocation();} } 注意在某些情况下gc的打印信息可能不会打印但确操作了这可能是该gc对于某种数量的数据是选择不打印的策略的这里了解即可 大对象回收分析 大对象直接进入老年代虚拟机提供一个参数 -XX:PretenureSizeThreshold 用来设置直接在老年代分配的对象的大小如果对象大于这个值就会直接在老年代分配这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制 参数 -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:UseParNewGC -XX:PrintGCDetails -XX:PretenureSizeThreshold3145728 //1024*1024*33145728 他单位是字节默认的可以是km测试代码 package test;/****/ public class test33 {private static final int _1MB 1024 * 1024;public static void testPreteureSizeThreshold() {byte[] allocation;allocation new byte[4 * _1MB];}public static void main(String[] args) {testPreteureSizeThreshold();}} 查看结果时若新生代有且老年代也有说明是对的jvm初始化的原因修改更大的值若老年代没有那么更加对 日志分析工具 日志工具简介 GC日志可视化分析工具GCeasy和GCviewer通过GC日志可视化分析工具我们可以很方便的看到JVM各个分代的内存使用情况、垃圾回收次数、垃圾回收的原因、垃圾回收占用的时间、吞吐量等这些指标在我们进行JVM调优的时候是很有用的 GCeasy是一款在线的GC日志分析器可以通过GC日志分析进行内存泄露检测、GC暂停原因分析、JVM配置建议优化等功能而且是可以免费使用他的在线分析工具https://gceasy.io/index.jsp或者https://gceasy.io/他们两个基本是一样的一般选择文件并点击分析Analyze即可 GCViewer是一款实用的GC日志分析软件免费开源使用你需要安装jdk或者java环境才可以使用软件为GC日志分析人员提供了强大的功能支持有利于大大提高分析效率 测试准备 编写代码生成gc.log日志准备分析 package test;import java.util.ArrayList;/** 注意记得存在logs这个目录否则会提示你的* -Xms100M -Xmx100M -XX:SurvivorRatio8 -XX:PrintGCDetails -Xloggc:D://logs/gc.log*/ public class test44 {private static final int _1MB 1024 * 1024;public static void main(String[] args) {ArrayListbyte[] list new ArrayListbyte[]();for (int i 0; i 500; i) {byte[] arr new byte[1024 * 1024];list.add(arr);try {Thread.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}}} } 现在我们进行执行若存在对应文件则清空写入这是IO流中的写操作的作用然后到上面的https://gceasy.io/进入然后选择文件进行分析自己看看就行了注意只需要文件的内容是正确的即可至于如何正确可以百度与后缀无关所以就算你修改成txt也行 简单来说之前的监控是操作指定为主可视化是动态的而这里是可以只对文件操作如GCeasyGCViewer至此所有分析操作都给出完毕 GCViewer GCViewer是一个小工具可以可视化展示 生成的详细GC输出支持Sun / OracleIBMHP和BEA的Java虚拟机。它是GNU LGPL下发布的免费软件 下载地址https://sourceforge.net/projects/gcviewer/ 使用简介 java -jar gcviewer-1.37-SNAPSHOT.jar当然到这里你肯定是配置好java环境变量全局使用的 出现如下记住一般使用命令行执行的会与命令行一同存活也就是说命令行窗口关闭他也会关闭相当于该命令行是main 打开之后点击File-Open File打开我们的GC日志具体描述自己看看吧记得放大否则可能看不到 JVM调优实战 前面说明的基本都是使用及其观察现在我们来进行实战 tomcat与JVM调优 tomcat服务器在JavaEE项目中使用率非常高所以在生产环境对tomcat的优化也变得非常重要了对于tomcat的优化主要是从2个方面入手一是tomcat自身的配置另一个是 tomcat所运行的jvm虚拟机的调优 安装tomcat部署项目 下载并安装tomcat可能你是操作过的但是复习一下并不是不可以 下载地址https://tomcat.apache.org 点击tar.gz这个版本记得点击左边下载Download的Tomcat 8而不是图片的这是因为移动的原因所以没有显示出来当然就算你点击其他地方一般也不会到上面的右边显示或者整体显示所以并不用担心下载后自己准备一个虚拟机具体操作看54章博客或者自己百度找教程 然后放入虚拟机中54章博客也有具体操作当然方式有很多具体百度即可 然后操作如下命令是相对的记得观察就行 tar -xvf apache-tomcat-8.5.85.tar.gz #当然随着时间的推移该名称可能有所改变所以这里记得写上你自己的文件 cd apache-tomcat-8.5.85/conf/ #修改配置文件配置tomcat管理用户 vim tomcat-users.xml #如果没有vim那么vi一般都会有这是基本的否则怎么操作文件呢除非人为干预 #写入如下消息 role rolenamemanager/ role rolenamemanager-gui/ role rolenameadmin/ role rolenameadmin-gui/ user usernametomcat passwordtomcat rolesadmin-gui,admin,manager-gui,manager/:wq退出配置可以访问Server Status #注意在Linux中windows一般不会即无论是7还是8都不用下面的配置操作照样可以登录这可能是文件系统的问题因为我在windows中确不用设置所以我才说应该是文件系统的原因如果是tomcat7配置了tomcat用户就可以登录了但是tomcat8不行需要修改一个配置文件否则访问会报403 #回到cd apache-tomcat-8.5.85相对的 vim webapps/manager/META-INF/context.xmlContext antiResourceLockingfalse privilegedtrue CookieProcessor classNameorg.apache.tomcat.util.http.Rfc6265CookieProcessorsameSiteCookiesstrict / !-- Valve classNameorg.apache.catalina.valves.RemoteAddrValveallow127\.\d\.\d\.\d|::1|0:0:0:0:0:0:0:1 / --Manager sessionAttributeValueClassNameFilterjava\.lang\.(?:Boolean|Integer|Long|Number|String)|org\.apache\.catalina\.filters\.CsrfPreventionFilter\$LruCache(?:\$1)?|java\.util\.(?:Linked)?HashMap/ /Context#我们注释掉上面的移动配置就行然后回到bin目录下执行./startup.sh我的虚拟机ip是192.168.164.128所以在浏览器上输入192.168.164.128:8080就可以看到如下了 点击Server Status输入用户密码如果没有之前的注释配置他直接报错403可不会让你输入用户名和密码哦然后到如下 上面就可以看到一些信息因为Server Status的英文意思是服务器状态 部署web项目 为了有个项目可以操作现在到如下地址拿取项目 链接https://pan.baidu.com/s/12sqXaJ3c7WHO7OvLG99X1A 提取码alsk 拿取后将该项目放在webapps文件下就行然后启动去访问192.168.164.128:8080/test/quick若出现了嘿嘿嘿说明操作成功即没有问题 最后要注意的是在启动时会查找war文件首先看看当前项目是否有该文件的前缀名称比如a.war即看看是否有a文件若有那么直接不操作否则解压他操作变成a可能在其他博客也说明过但是也只是可能我们以这里为主 使用Apache Jmeter进行测试 将web应用部署到服务器之后使用jmeter对其进行压力测试 下载安装jmeter在80章博客也操作过这复习一下 下载地址http://jmeter.apache.org/download_jmeter.cgi 选择zip即可当然如果下载比较慢可以换一个渠道虽然可能更加的慢 链接https://pan.baidu.com/s/19IalQnsnDcR6OXjdgQ4Kiw 提取码alsk 使用步骤 解压找到并双击jmeter.bat一般是bin目录里面的出现如下记得等一下 设置中文 设置外观主题 添加线程组 在Test Plan英文意思测试计划虽然可以修改名称然后ctrlc保存即可弹出的框框可以选择取消右键就行右键-添加-线程用户-线程组 使用线程模拟用户的并发 设置1000个线程每个线程循环10次也就是tomcat会接收到10000个请求 添加Http请求 右键线程组-添加-取样器-HTTP请求 添加监控 在HTTP请求右键-添加-监听器加上这三个他们都是看结果的并不影响请求 现在我们来操作如下 上面的/test/quick可以写成test/quick他默认会帮你加上/了可能有不加上但是就算加上也没有问题因为浏览器认为是一个最终 然后点击这里察看一般是查看没有什么都是完美的 运行后提示信息选择no没看聚合报告他代表很多信息其他的你可以选择查看注意这个运行是线程组下面的所有操作比如你有两个HTTP请求那么一起操作可能平分或者多一个请求一般我们考虑平分的说明 可以保存然后打开文件选项中打开或者就是Test Plan中打开 也可以加大线程数使得可能出现异常Read timed out那么聚合报告中的异常可能就不是0.00%了始终没有抢到服务器接收的线程数量一般是设置有限的而不是你的没有设置可以认为服务器是线程池而你不是一般来说tomcat是没有启用线程池的所以你增加线程数时可能会明显的比较卡顿因为占用内存可能也分版本限制但是一般只是针对接收来说的在进行访问方法可能是默认由线程池来进行处理他们的线程操作从而使得方法操作有限这也是为什么有时候多人访问同一个方法时可能有相同的线程 调整tomcat参数进行优化 优化吞吐量 禁用AJP服务 什么是AJP呢 AJPApache JServer Protocol是定向包协议WEB服务器和Servlet容器一般是业务服务器即WEB服务器只是得到请求但是他是将请求进行提交了类似于Nginx通过TCP连接来交互为了节省SOCKET创建的昂贵代价WEB服务器会尝试维护一个永久的TCP来连接到servlet容器并且在多个请求和响应周期过程会重用连接 Tomcat在 server.xml中配置了两种连接器 1第一个连接器监听8080端口负责建立HTTP连接在通过浏览器访问Tomcat服务器的Web应用时使用的就是这个连接器 2第二个连接器监听8009端口负责和其他的HTTP服务器其他也是服务器建立连接在把Tomcat与其他HTTP服务器集成时就需要用到这个连接器AJP连接器可以通过AJP协议和一个web容器类似于Tomcat或者Nginx进行交互 其中Nginxtomcat的架构一般用不着AJP协议可能是其他协议反正只是包交换而已所以把AJP连接器禁用若存在那么修改conf下的server.xml文件将AJP服务禁用掉即可 Connector port8009 protocolAJP/1.3 redirectPort8443 / #一般来说基本都是注释掉的即禁用的重启tomcat查看效果可以看到AJP服务以及不存在了 实际上在linux中进入底行模式进行搜索/即/AJP回车后按n即可进行一步一步的找第一个开始最后一个使用到第一个而N则是往上找第一个开始使用到最后一个 如果注释去掉那么我这里是这样 而没有去掉就是 当然我们一般并不会需要他所以防止他进行操作就算他没有对方但是也会进行使用些内存所以禁用掉也是一个小的提升的 设置执行器线程池 频繁地创建线程会造成性能浪费所以使用线程池来优化 在tomcat中每一个用户请求都是一个线程所以可以使用线程池提高性能修改server.xml文件设置接收线程操作线程池 !--将注释打开这个Executor nametomcatThreadPool是主要的打开但确需要使用后面基本有默认的-- Executor nametomcatThreadPool namePrefixcatalina‐exec‐ maxThreads500 minSpareThreads50 prestartminSpareThreadstrue maxQueueSize100/ !-- 参数说明 maxThreads最大并发数默认设置200一般建议在500 ~ 1000根据硬件设施和业务来判断 minSpareThreadsTomcat初始化时创建的线程数默认设置25 prestartminSpareThreads在Tomcat初始化的时候就初始化minSpareThreads 的参数值如果不等于 trueminSpareThreads 的值可能就没啥效果了而是操作默认当然可能其他版本的tomcat不需要但最好写上 maxQueueSize最大的等待队列数超过则拒绝请求 --!-- A Connector represents an endpoint by which requests are receivedand responses are returned. Documentation at :Java HTTP Connector: /docs/config/http.htmlJava AJP Connector: /docs/config/ajp.htmlAPR (HTTP/AJP) Connector: /docs/apr.htmlDefine a non-SSL/TLS HTTP/1.1 Connector on port 8080--Connector port8080 executortomcatThreadPool protocolHTTP/1.1connectionTimeout20000redirectPort8443 /!-- A Connector using the shared thread pool-- 若不加上executor“tomcatThreadPool”出现 实际上并没有使用可能是存在但不使用造成上面的某些出现R 加上executor“tomcatThreadPool” 他的显示-1并不对其实是500因为他可能无法正确的展示 当然对应的图片的显示可能是错误的你可以自己测试但是并不需要理会因为并没有什么意义 其中对于属性设置来说并非是有固定位置的就如executortomcatThreadPool可以在port前面也行 然后你自己对比一下设置之前的执行结果聚合报告和设置之后的结果的平均值通常来说设置之后的吞吐量会比较高记得多执行几次注意在操作之前记得删除或者清除对应的监听器或者监听器信息然后创建一个因为可能之前存在的数据是会进一步影响结果的具体原因一般是软件的问题所以设置之后性能的确提升了 当然了如果你设置最大并发数是1可以同时操作的线程并不与初始化线程数冲突哦且初始化线程数是1自然性能是很低的其中设置很多线程数也没有什么用比如你设置50000你应该很明显的知道有些肯定没有操作所以并不是设置越大越好即要考虑线程占用的资源而考虑占用的资源可能会影响tomcat内部的使用情况那么可能还会导致吞吐量变低但一般只是对设置的少来说的除非超级大那么可能比没有设置的还要低但一般可能超过某些值会变成默认所以一般不会考虑超级大的即介于设置少不是非常少如是1到没有设置中间 实际上有很多的细节可以说明比如设置小数点或者负数怎么样当然如果你硬要考虑这些那么你会浪费超多的时间因为并没有意义配置何其之多你只需要会使用即可因为该知识只是给你使用的若别人稍微修改一下那么你的知识还是对的吗所以并不需要死磕强迫症除外一般来说小数取整负数默认但是大多数都是直接不然呢启动即启动保存对于启动实际上tomcat可以重复启动不覆盖的有些框架软件可能不是所以通常需要先关闭再启动哦可能也与版本有关而大多数基本都会像tomcat这样的在之前的博客中可能并没有操作过但是与上面的设置小数点或者负数怎么样一样的无意义所以就没有说明了具体自己操作时可以顺便测试比如后面的nio2虽然无意义因为启动的操作也是有很多框架也是这样的 设置最大等待队列 默认情况下请求发送到tomcat如果tomcat正忙那么该请求会一直等待这样虽然 可以保证每个请求都能请求 到但是请求时间就会变长 有些时候我们也不一定要求请求一定等待可以设置最大等待队列大小原本没有即都等待虽然等待也会出现异常等待久了如果超过就不等待了这样虽然有些请 求是直接失败的但是请求时间会虽短即更快的执行完当然等待队列的大小不要太大否则还不如等待时间但也不要太小否则很多失败的容易出现操作问题比如数据库的添加最主要的是用户操作这方面的操作用户为主嘛 !-- 最大等待数为100maxQueueSize100-- Executor nametomcatThreadPool namePrefixcatalina‐exec‐ maxThreads500 minSpareThreads100 prestartminSpareThreadstrue maxQueueSize100/然后自己再次的测试吧这里就不测试了一般异常变多但是吞吐量也变多 设置nio2的运行模式 tomcat的运行模式有3种 1bio 默认的模式性能非常低下没有经过任何优化处理和支持 2nio nio(new I/O)是Java SE 1.4及后续版本提供的一种新的I/O操作方式即java.nio包及其子包Java nio是一个基于缓冲区、并能提供非阻塞I/O操作的Java API因此nio 也被看成是non-blocking I/O的缩写它拥有比传统I/O操作(bio)更好的并发运行性能 3apr 安装起来最困难具体使用方式可以百度但是从操作系统级别来解决异步的IO问题大幅度的提高性能推荐使用nio不过在tomcat8中有最新的nio2速度更快建议使用nio2 设置nio2 Connector executortomcatThreadPool port8080 protocolorg.apache.coyote.http11.Http11Nio2Protocol connectionTimeout20000 redirectPort8443 /关闭启动会发现 变成nio2了 tomcat8之前的版本用的是BIO推荐使用NIO默认的之前的一般不是但是tomcat8中有最新的NIO2速度更快建议设置使用NIO2 理论上nio2的效果会优惠nio可以进行测试但一般会发现设定为nio2对于提升吞吐量效果不是很明显所以可以根据自己的测试 情况选择合适的io模型一般情况下可以不用修改反正波动很大基本没有什么明显的提升 调整JVM参数进行优化 测试通过jvm参数进行优化为了测试一致性依然将最大线程数设置为500初始化线程数50 启用nio2运行模式 设置并行垃圾回收器 在bin目录下面找到catalina.sh编辑他vim catalina.sh #首先通过底行模式输入/JAVA_OPTS查找一般是JAVA_OPTS$JAVA_OPTS $JSSE_OPTS地方然后在JAVA_OPTS$JAVA_OPTS -Djava.protocol.handler.pkgsorg.apache.catalina.webresources后面补充如下 JAVA_OPTS-XX:UseParallelGC -XX:UseParallelOldGC -Xms64m -Xmx512m -XX:PrintGCDetails -XX:PrintGCTimeStamps -XX:PrintGCDateStamps -XX:PrintHeapAtGC -Xloggc:../logs/gc.log#然后注释掉JAVA_OPTS$JAVA_OPTS -Djava.protocol.handler.pkgsorg.apache.catalina.webresources#你要知道这不是配置而是命令的执行sh后缀所以先后顺序是重要的#注意上面的一些配置可能需要删除如-XX:PrintGCTimeStamps -XX:PrintGCDateStamps -XX:PrintHeapAtGC否则可能启动不了jdk版本问题前面说明过了直接启动可能看不到日志可以这样的启动./startup.sh tail -f ../logs/catalina.out就能看到类似的在idea中执行的对应可以会出现jdk11的错误显示了查看GC日志文件 我们使用线程组执行然后将对应的文件内容放在gceasy.io查看gc中是否存在问题 你可以自己进行分析然后改变对应的参数值来进行优化比如提升一下-Xms512m -Xmx512m然后继续查看结果一般会发现吞吐量上升了 理论上而言设置为G1垃圾收集器性能是会提升的但是会受制于多方面的影响也不一定绝对有提升 这里就需要你对于设置及其观察的分析了所以需要自己考虑因为我的测试必然与你的有所差别所以就不给出图片了 至此JVM相关的优化说明完毕实际上优化只是针对空间不变来说的如果你的电脑内存超大或者计算能力超强或者硬盘超大那么对应的优不优化又有什么关系呢直接设置顶配即可就如你对mysql分库分表但只要资金足够直接买一个或者多个服务器就行了手动滑稽
http://www.w-s-a.com/news/358839/

相关文章:

  • 咸阳网站设计建设公司小程序打包成app
  • 做视频网站视频文件都存放在哪做旅游宣传图的网站有哪些
  • 地方门户类网站产品推广惠州市中国建设银行网站
  • 网站建设公司推荐5788移动版wordpress
  • 产品类型 速成网站淘宝怎么建立自己的网站
  • 南京优化网站建设公司的网站怎么建设
  • 做网站开发能挣钱月嫂云商城网站建设
  • 包装网站模板新手入门网站建设
  • 做网站的天津哪个公司做网站
  • 网站建设摊销时间是多久微信官网免费下载安装
  • 网站解析是做a记录吗群晖 wordpress 阿里云
  • 涉县移动网站建设公司常州做网站的公司有哪些
  • 网站批量创建程序中国十大人力资源公司
  • 菏泽网站建设 梧桐树二次开发创造作用
  • 维护网站费用长沙广告设计公司排名
  • 模仿别人网站侵权wordpress 修改链接失效
  • wordpress文章设置受密码保护南宁网站优化公司哪家好
  • 网站开发工程师介绍设计类的网站
  • 嘉兴seo网站推广中山建网站多少钱
  • 高端汽车网站建设帮别人做网站自己为什么会被抓
  • 网站开发实验室建设方案wordpress 主题丢失
  • 珠宝网站建设平台分析报告郑州最新发布
  • 世界杯最新排名泉州seo网站关键词优
  • 广州公司网站提供如何推广新品
  • 网站建设如何描述沈阳网站建设推广平台
  • 用dw制作个介绍家乡网站学生个人简历
  • 建设银行企业网站访问不了wordpress搬到谷歌服务器
  • 网站建设与网站优化销售别墅庭院园林景观设计公司
  • 沈阳红方城网站建设专业的微网站哪家好
  • 医院网站asp东营信息发布平台