宜昌网站seo公司,做那种事免费网站,邀请注册推广赚钱,网站如果实现微信支付吗前言#xff1a;关于JVM#xff0c;其实有很多大厂开发了不同版本的JVM#xff0c;比较知名的有#xff1a;Sun HotSpot VM、BEA JRockit VM、IBM J9 VM、 Azul VM、 Apache Harmony、 Google Dalvik VM、 Microsoft JVM等等。现在使用的比较多的JDK8版本就是Sun HotSpot V…前言关于JVM其实有很多大厂开发了不同版本的JVM比较知名的有Sun HotSpot VM、BEA JRockit VM、IBM J9 VM、 Azul VM、 Apache Harmony、 Google Dalvik VM、 Microsoft JVM等等。现在使用的比较多的JDK8版本就是Sun HotSpot VM与BEA JRockit VM合并之后开发出的JDK版本。 一、JVM的构成
JVM主要由三个子系统构成分别为类加载器子系统、运行时数据区内存结构和字节码执行引擎。 为了更好的理解JVM我们来看一下JVM的全貌图。 当我们开发Java程序时首先会编写.java文件然后编译器会将.java文件编译成.class文件。
JVM中会通过类装载子系统将.class文件的内容装载到JVM的运行时数据区而JVM的运行时数据区又会分为方法区、堆、栈、本地方法栈和程序计数器 几个部分。
在装载class文件的内容时会将class文件的内容拆分为几个部分分别装载到JVM运行时数据区的几个部分。其中程序计数器的作用是记录程序执行的下一条指令的地址。
方法区也叫作元空间主要包含了运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应的Class实例的引用等信息。
在JVM中程序的执行是通过执行引擎进行的执行引擎会调用本地方法的接口来执行本地方法库进而完成整个程序逻辑的执行。
我们常说的垃圾收集器是包含在执行引擎中的在程序的运行过程中执行引擎会开启垃圾收集器并在后台运行垃圾收集器会不断监控程序运行过程中产生的内存垃圾信息并根据相应的策略对垃圾信息进行清理。
大家需要注意的是栈、本地方法栈和程序计数器是每个线程运行时独占的而方法区和堆是所有线程共享的。所以栈、本地方法栈和程序计数器不会涉及线程安全问题而方法区和堆会涉及线程安全问题。 二、Java运行时数据区
Java虚拟机在执行Java程序的过程中会将其管理的内存划分为若干个不同的数据区域
这些区域有各自的用途、创建和销毁的时间有些区域随虚拟机进程的启动而存在有
些区域则是依赖用户线程的启动和结束来建立和销毁。Java虚拟机所管理的内存包括
以下几个运行时数据区域如图 2.1、程序计数器
PC(Program Counter)用来存放当前线程欲执行下一条指令的地址线程私有的。
2.2、虚拟机栈
虚拟机栈是Java执行方法的内存模型。每个方法被执行的时候都会创建一个栈帧把栈帧压入栈当方法正常返回或者抛出未捕获的异常时栈帧就会出栈。
1栈帧栈帧存储方法的相关信息包含局部变量数表、返回值、操作数栈、动态链接
a、局部变量表包含了方法执行过程中的所有变量。局部变量数组所需要的空间在编译期间完成分配在方法运行期间不会改变局部变量数组的大小。
b、返回值如果有返回值的话压入调用者栈帧中的操作数栈中并且把PC的值指向 方法调用指令 后面的一条指令地址。
c、操作数栈操作变量的内存模型。操作数栈的最大深度在编译的时候已经确定写入方法区code属性的max_stacks项中。操作数栈的的元素可以是任意Java类型包括long和double32位数据占用栈空间为164位数据占用2。方法刚开始执行的时候栈是空的当方法执行过程中各种字节码指令往栈中存取数据。
d、动态链接每个栈帧都持有在运行时常量池中该栈帧所属方法的引用持有这个引用是为了支持方法调用过程中的动态链接。
2线程私有
2.3、本地方法栈
1调用本地native的内存模型。
2线程独享。
2.4、方法区
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据
1线程共享的
2运行时常量池
A、是方法区的一部分
B、存放编译期生成的各种字面量和符号引用
C、Class文件中除了存有类的版本、字段、方法、接口等描述信息还有一项是常量池存有这个类的 编译期生成的各种字面量和符号引用这部分内容将在类加载后存放到方法区的运行时常量池中。
2.5、堆Heap
Java对象存储的地方
1Java堆是虚拟机管理的内存中最大的一块
2Java堆是所有线程共享的区域
3在虚拟机启动时创建
4此内存区域的唯一目的就是存放对象实例几乎所有对象实例都在这里分配内存。存放new生成的对象和数组
5Java堆是垃圾收集器管理的内存区域因此很多时候称为“GC堆” 三、Java内存模型
Java 内存模型(简称 JMM)和内存区域是不一样的东西。内存区域是指 JVM 运行时将数据分区域存储强调对内存空间的划分即运行时数据区(Runtime Data Area)。 JMM 是共享内存的并发模型线程之间主要通过读-写共享变量(堆内存中的实例域静态域和数组元素)来完成隐式通信。JMM 控制 Java 线程之间的通信决定一个线程对共享变量的写入何时对另一个线程可见。 Java内存模型规定所有变量都存储在主内存中每个线程还有自己的工作内存。
1 线程的工作内存中保存了被该线程使用到的变量的拷贝从主内存中拷贝过来线程对变量的所有操作都必须在工作内存中执行而不能直接访问主内存中的变量。
2 不同线程之间无法直接访问对方工作内存的变量线程间变量值的传递都要通过主内存来完成。
3 主内存主要对应Java堆中实例数据部分。工作内存对应于虚拟机栈中部分区域。 3.1、Java线程之间的通信由内存模型JMMJava Memory Model控制
1JMM决定一个线程对变量的写入何时对另一个线程可见。
2线程之间共享变量存储在主内存中
3每个线程有一个私有的本地内存里面存储了读/写共享变量的副本。
4JMM通过控制每个线程的本地内存之间的交互来为程序员提供内存可见性保证。
3.2、可见性、有序性
1当一个共享变量在多个本地内存中有副本时如果一个本地内存修改了该变量的副本其他变量应该能够看到修改后的值此为可见性。
2保证线程的有序执行这个为有序性。保证线程安全
3.3、JVM 主内存与工作内存
Java 内存模型的主要目标是定义程序中各个变量的访问规则即在虚拟机中将变量(线程共享的变量)存储到内存和从内存中取出变量这样底层细节。
Java 内存模型中规定了所有的变量都存储在主内存中每条线程还有自己的工作内存线程对变量的所有操作都必须在工作内存中进行而不能直接读写主内存中的变量。
这里的工作内存是 JMM 的一个抽象概念也叫本地内存其存储了该线程以读/写共享变量的副本。就像每个处理器内核拥有私有的高速缓存JMM 中每个线程拥有私有的本地内存。
不同线程之间无法直接访问对方工作内存中的变量线程间的通信一般有两种方式进行一是通过消息传递二是共享内存。Java 线程间的通信采用的是共享内存方式线程、主内存和工作内存的交互关系如图 这里所讲的主内存、工作内存与 Java 内存区域中的 Java 堆、栈、方法区等并不是同一个层次的内存划分这两者基本上是没有关系的如果两者一定要勉强对应起来那从变量、主内存、工作内存的定义来看主内存主要对应于 Java 堆中的对象实例数据部分而工作内存则对应于虚拟机栈中的部分区域。 四、堆的内存划分 Java堆的内存划分如图所示分别为年轻代、Old Memory老年代、Perm永久代。其中在Jdk1.8中永久代被移除使用MetaSpace代替。
1、新生代
1使用复制清除算法Copinng算法原因是年轻代每次GC都要回收大部分对象。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间然后垃圾回收的时候把存活对象放到未使用的Survivor划分出from、to空间中清空Eden和刚才使用过的Survivor空间。
2分为Eden、Survivor From、Survivor To比例默认为811
3内存不足时发生Minor GC2
2、老年代
1采用标记-整理算法mark-compact原因是老年代每次GC只会回收少部分对象。
3、Perm用来存储类的元数据也就是方法区。
1Perm的废除在jdk1.8中Perm被替换成MetaSpaceMetaSpace存放在本地内存中。原因是永久代进场内存不够用或者发生内存泄漏。
2MetaSpace元空间元空间的本质和永久代类似都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于元空间并不在虚拟机中而是使用本地内存。
4、堆内存的划分在JVM里面的示意图 五、 判断对象是否要回收的方法可达性分析法
1、 可达性分析法通过一系列“GC Roots”对象作为起点进行搜索如果在“GC Roots”和一个对象之间没有可达路径则称该对象是不可达的。不可达对象不一定会成为可回收对象。进入DEAD状态的线程还可以恢复GC不会回收它的内存。把一些对象当做root对象JVM认为root对象是不可回收的并且root对象引用的对象也是不可回收的
2、 以下对象会被认为是root对象1 虚拟机栈栈帧中本地变量表中引用的对象2 方法区中静态属性引用的对象3 方法区中常量引用的对象4 本地方法栈中Native方法引用的对象
3、 对象被判定可被回收需要经历两个阶段1 第一个阶段是可达性分析分析该对象是否可达2 第二个阶段是当对象没有重写finalize()方法或者finalize()方法已经被调用过虚拟机认为该对象不可以被救活因此回收该对象。finalize()方法在垃圾回收中的作用是给该对象一次救活的机会
4、 方法区中的垃圾回收1 常量池中一些常量、符号引用没有被引用则会被清理出常量池2 无用的类被判定为无用的类会被清理出方法区。判定方法如下A、 该类的所有实例被回收B、 加载该类的ClassLoader被回收C、 该类的Class对象没有被引用
5、 finalize():1 GC垃圾回收要回收一个对象的时候调用该对象的finalize()方法。然后在下一次垃圾回收的时候才去回收这个对象的内存。2 可以在该方法里面指定一些对象在释放前必须执行的操作。 六、 发现虚拟机频繁full GC时应该怎么办full GC指的是清理整个堆空间包括年轻代和永久代
1 首先用命令查看触发GC的原因是什么 jstat –gccause 进程id
2 如果是System.gc()则看下代码哪里调用了这个方法
3 如果是heap inspection(内存检查)可能是哪里执行jmap –histo[:live]命令
4 如果是GC locker可能是程序依赖的JNI库的原因 七、常见的垃圾回收算法
1、Mark-Sweep标记-清除算法
1思想标记清除算法分为两个阶段标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象清除阶段就是清除被标记对象的空间。
2优缺点实现简单容易产生内存碎片
2、Copying复制清除算法
1思想将可用内存划分为大小相等的两块每次只使用其中的一块。当进行垃圾回收的时候了把其中存活对象全部复制到另外一块中然后把已使用的内存空间一次清空掉。
2优缺点不容易产生内存碎片可用内存空间少存活对象多的话效率低下。
3、Mark-Compact标记-整理算法
1思想先标记存活对象然后把存活对象向一边移动然后清理掉端边界以外的内存。
2优缺点不容易产生内存碎片内存利用率高存活对象多并且分散的时候移动次数多效率低下
4、分代收集算法目前大部分JVM的垃圾收集器所采用的算法
思想把堆分成新生代和老年代。永久代指的是方法区
1 因为新生代每次垃圾回收都要回收大部分对象所以新生代采用Copying算法。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间然后垃圾回收的时候把存活对象放到未使用的Survivor划分出from、to空间中清空Eden和刚才使用过的Survivor空间。
2 由于老年代每次只回收少量的对象因此采用mark-compact算法。
3 在堆区外有一个永久代。对永久代的回收主要是无效的类和常量 八、GC使用时对程序的影响
垃圾回收会影响程序的性能Java虚拟机必须要追踪运行程序中的有用对象然后释放没用对象这个过程消耗处理器时间 GC时为什么要停顿所有Java线程
因为GC先进行可达性分析。可达性分析是判断GC Root对象到其他对象是否可达假如分析过程中对象的引用关系在不断变化分析结果的准确性就无法得到保证。 九、几种不同的垃圾回收类型
1Minor GC从年轻代包括Eden、Survivor区回收内存。
A、当JVM无法为一个新的对象分配内存的时候越容易触发Minor GC。所以分配率越高内存越来越少越频繁执行Minor GC B、执行Minor GC操作的时候不会影响到永久代Tenured。从永久代到年轻代的引用被当成GC Roots从年轻代到老年代的引用在标记阶段直接被忽略掉。
2Major GC清理整个老年代当eden区内存不足时触发。
3Full GC清理整个堆空间包括年轻代和老年代。当老年代内存不足时触发
分代回收
HotSpot JVM把年轻代分为了三部分1个Eden区和2个Survivor区分别叫from和to。一般情况下新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后如果仍然存活将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC年龄就会增加1岁当它的年龄增加到一定程度时就会被移动到年老代中。
因为年轻代中的对象基本都是朝生夕死的所以在年轻代的垃圾回收算法使用的是复制算法复制算法的基本思想就是将内存分为两块每次只用其中一块当这一块内存用完就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
在GC开始的时候对象只会存在于Eden区和名为“From”的Survivor区Survivor区“To”是空的。紧接着进行GCEden区中所有存活的对象都会被复制到“To”而在“From”区中仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中没有达到阈值的对象会被复制到“To”区域。经过这次GC后Eden区和From区已经被清空。这个时候“From”和“To”会交换他们的角色也就是新的“To”就是上次GC前的“From”新的“From”就是上次GC前的“To”。不管怎样都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程直到“To”区被填满“To”区被填满之后会将所有对象移动到年老代中。 十、类加载机制概念 类加载器把class文件中的二进制数据读入到内存中存放在方法区然后在堆区创建一个java.lang.Class对象用来封装类在方法区内的数据结构。 类加载的步骤如下
1、加载查找并加载类的二进制数据把class文件里面的信息加载到内存里面
2、连接把内存中类的二进制数据合并到虚拟机的运行时环境中
1验证确保被加载的类的正确性。包括
A、类文件的结构检查检查是否满足Java类文件的固定格式 B、语义检查确保类本身符合Java的语法规范 C、字节码验证确保字节码流可以被Java虚拟机安全的执行。字节码流是操作码组成的序列。每一个操作码后面都会跟着一个或者多个操作数。字节码检查这个步骤会检查每一个操作码是否合法。 D、二进制兼容性验证确保相互引用的类之间是协调一致的。
2准备为类的静态变量分配内存并将其初始化为默认值
3解析把类中的符号引用转化为直接引用比如说方法的符号引用是有方法名和相关描述符组成在解析阶段JVM把符号引用替换成一个指针这个指针就是直接引用它指向该类的该方法在方法区中的内存位置
3、初始化为类的静态变量赋予正确的初始值。当静态变量的等号右边的值是一个常量表达式时不会调用static代码块进行初始化。只有等号右边的值是一个运行时运算出来的值才会调用static初始化。 十一、双亲委派模型
1、当一个类加载器收到类加载请求的时候它首先不会自己去加载这个类的信息而是把该请求转发给父类加载器依次向上。所以所有的类加载请求都会被传递到父类加载器中只有当父类加载器中无法加载到所需的类子类加载器才会自己尝试去加载该类。当当前类加载器和所有父类加载器都无法加载该类时抛出ClassNotFindException异常。
2、意义
提高系统的安全性。用户自定义的类加载器不可能加载应该由父加载器加载的可靠类。比如用户定义了一个恶意代码自定义的类加载器首先让系统加载器去加载系统加载器检查该代码不符合规范于是就不继续加载了
3、定义类加载器如果某个类加载器能够加载一个类那么这个类加载器就叫做定义类加载器
4、初始类加载器定义类加载器及其所有子加载器都称作初始类加载器。
5、运行时包1由同一个类加载器加载并且拥有相同包名的类组成运行时包2只有属于同一个运行时包的类才能访问包可见default的类和类成员。作用是 限制用户自定义的类冒充核心类库的类去访问核心类库的包可见成员。
6、加载两份相同的class对象的情况A和B不属于父子类加载器关系并且各自都加载了同一个类。
双亲委派模型的破坏
一个典型的例子便是JNDI服务JNDI现在已经是Java的标准服务它的代码由启动类加载器去加载(在JDK 1.3时放进去的rt.jar)但JNDI的目的就是对资源进行集中管理和查找它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码但启动类加载器不可能“认识”这些代码那该怎么办?
为了解决这个问题Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的 setContextClassLoaser()方法进行设置如果创建线程时还未设置它将会从父线程中继承 一个如果在应用程序的全局范围内都没有设置过的话那这个类加载器默认就是应用程序类加载器。
有了线程上下文类加载器就可以做一些“舞弊”的事情了JNDI服务使用这个线程上下 文类加载器去加载所需要的SPI代码也就是父类加载器请求子类加载器去完成类加载的动 作这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器实际上已经 违背了双亲委派模型的一般性原则但这也是无可奈何的事情。Java中所有涉及SPI的加载动 作基本上都采用这种方式例如JNDI、JDBC、JCE、JAXB和JBI等。 十二、JVM调优参数
在JVM中主要是对堆新生代、方法区和栈进行性能调优。各个区域的调优参数如下所示。 堆-Xms、-Xmx 新生代-Xmn 方法区元空间-XX:MetaspaceSize、-XX:MaxMetaspaceSize 栈线程-Xss
为了更加直观的表述我们可以将JVM的内存区域和对应的调优参数总结成下图所示。 在设置JVM启动参数时需要特别注意方法区元空间的参数设置。
1、一般来说当survivor区不够大或者占用量达到50%就会把一些对象放到老年区。通过设置合理的eden区survivor区及使用率可以将年轻对象保存在年轻代从而避免full GC使用-Xmn设置年轻代的大小
2、对于占用内存比较多的大对象一般会选择在老年代分配内存。如果在年轻代给大对象分配内存年轻代内存不够了就要在eden区移动大量对象到老年代然后这些移动的对象可能很快消亡因此导致full GC。通过设置参数-XX:PetenureSizeThreshold1000000单位为B标明对象大小超过1M时在老年代(tenured)分配内存空间。
3、一般情况下年轻对象放在eden区当第一次GC后如果对象还存活放到survivor区此后每GC一次年龄增加1当对象的年龄达到阈值就被放到tenured老年区。这个阈值可以同构-XX:MaxTenuringThreshold设置。如果想让对象留在年轻代可以设置比较大的阈值。
4、设置最小堆和最大堆-Xmx和-Xms稳定的堆大小堆垃圾回收是有利的获得一个稳定的堆大小的方法是设置-Xms和-Xmx的值一样即最大堆和最小堆一样如果这样子设置系统在运行时堆大小理论上是恒定的稳定的堆空间可以减少GC次数因此很多服务端都会将这两个参数设置为一样的数值。稳定的堆大小虽然减少GC次数但是增加每次GC的时间因为每次GC要把堆的大小维持在一个区间内。
5、一个不稳定的堆并非毫无用处。在系统不需要使用大内存的时候压缩堆空间使得GC每次应对一个较小的堆空间加快单次GC次数。基于这种考虑JVM提供两个参数用于压缩和扩展堆空间。
1-XX:MinHeapFreeRatio 参数用于设置堆空间的最小空闲比率。默认值是40当堆空间的空闲内存比率小于40JVM便会扩展堆空间
2-XX:MaxHeapFreeRatio 参数用于设置堆空间的最大空闲比率。默认值是70 当堆空间的空闲内存比率大于70JVM便会压缩堆空间。
3当-Xmx和-Xmx相等时上面两个参数无效
6、通过增大吞吐量提高系统性能可以通过设置并行垃圾回收收集器。
1-XX:UseParallelGC:年轻代使用并行垃圾回收收集器。这是一个关注吞吐量的收集器可以尽可能的减少垃圾回收时间。
2-XX:UseParallelOldGC:设置老年代使用并行垃圾回收收集器。
7、尝试使用大的内存分页使用大的内存分页增加CPU的内存寻址能力从而系统的
性能。-XX:LargePageSizeInBytes 设置内存页的大小
8、使用非占用的垃圾收集器。-XX:UseConcMarkSweepGC老年代使用CMS收集器
降低停顿。
9、-XXSurvivorRatio3表示年轻代中的分配比率survivor:eden 2:3 十三、JVM性能调优的工具
1jpsJava Process Status输出JVM中运行的进程状态信息(现在一般使用jconsole)
2jstack查看java进程内线程的堆栈信息。
3jmap用于生成堆转存快照
4jhat用于分析jmap生成的堆转存快照一般不推荐使用而是使用Ecplise Memory Analyzer
5jstat是JVM统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等。
6VisualVM故障处理工具 十四、JVM调优实战
14.1、Major GC和Minor GC频繁
首先优化Minor GC频繁问题。通常情况下由于新生代空间较小Eden区很快被填满就会导致频繁Minor GC因此可以通过增大新生代空间来降低Minor GC的频率。例如在相同的内存分配率的前提下新生代中的Eden区增加一倍Minor GC的次数就会减少一半。
扩容Eden区虽然可以减少Minor GC的次数但会增加单次Minor GC时间么扩容后Minor GC时增加了T1扫描时间但省去T2复制对象的时间更重要的是对于虚拟机来说复制对象的成本要远高于扫描成本所以单次Minor GC时间更多取决于GC后存活对象的数量而非Eden区的大小。因此如果堆中短期对象很多那么扩容新生代单次Minor GC时间不会显著增加。
14.2、请求高峰期发生GC导致服务可用性下降
由于跨代引用的存在CMS在Remark阶段必须扫描整个堆同时为了避免扫描时新生代有很多对象增加了可中断的预清理阶段用来等待Minor GC的发生。只是该阶段有时间限制如果超时等不到Minor GCRemark时新生代仍然有很多对象我们的调优策略是通过参数强制Remark前进行一次Minor GC从而降低Remark阶段的时间。 另外类似的JVM是如何避免Minor GC时扫描全堆的 经过统计信息显示老年代持有新生代对象引用的情况不足1%根据这一特性JVM引入了卡表card table来实现这一目的。卡表的具体策略是将老年代的空间分成大小为512B的若干张卡card。卡表本身是单字节数组数组中的每个元素对应着一张卡当发生老年代引用新生代时虚拟机将该卡对应的卡表元素设置为适当的值。如上图所示卡表3被标记为脏卡表还有另外的作用标识并发标记阶段哪些块被修改过之后Minor GC时通过扫描卡表就可以很快的识别哪些卡中存在老年代指向新生代的引用。这样虚拟机通过空间换时间的方式避免了全堆扫描。
14.3、STW过长的GC
对于性能要求很高的服务建议将MaxPermSize和MinPermSize设置成一致JDK8开始Perm区完全消失转而使用元空间。而元空间是直接存在内存中不在JVM中Xms和Xmx也设置为相同这样可以减少内存自动扩容和收缩带来的性能损失。虚拟机启动的时候就会把参数中所设定的内存全部化为私有即使扩容前有一部分内存不会被用户代码用到这部分内存在虚拟机中被标识为虚拟内存也不会交给其他进程使用。
14.4、外部命令导致系统缓慢
一个数字校园应用系统发现请求响应时间比较慢通过操作系统的mpstat工具发现CPU使用率很高并且系统占用绝大多数的CPU资 源的程序并不是应用系统本身。每个用户请求的处理都需要执行一个外部shell脚本来获得系统的一些信息执行这个shell脚本是通过Java的 Runtime.getRuntime().exec()方法来调用的。这种调用方式可以达到目的但是它在Java 虚拟机中是非常消耗资源的操作即使外部命令本身能很快执行完毕频繁调用时创建进程 的开销也非常可观。Java虚拟机执行这个命令的过程是:首先克隆一个和当前虚拟机拥有一样环境变量的进程再用这个新的进程去执行外部命令最后再退出这个进程。如果频繁执行这个操作系统的消耗会很大不仅是CPU内存负担也很重。用户根据建议去掉这个Shell脚本执行的语句改为使用Java的API去获取这些信息后系统很快恢复了正常。
14.5、由Windows虚拟内存导致的长时间停顿
一个带心跳检测功能的GUI桌面程序每15秒会发送一次心跳检测信号如果对方30秒以内都没有信号返回那就认为和对方程序的连接已经断开。程序上线后发现心跳 检测有误报的概率查询日志发现误报的原因是程序会偶尔出现间隔约一分钟左右的时间完 全无日志输出处于停顿状态。
因为是桌面程序所需的内存并不大(-Xmx256m)所以开始并没有想到是GC导致的 程序停顿但是加入参数-XX:PrintGCApplicationStoppedTime-XX:PrintGCDateStamps- Xloggc:gclog.log后从GC日志文件中确认了停顿确实是由GC导致的大部分GC时间都控 制在100毫秒以内但偶尔就会出现一次接近1分钟的GC。
从GC日志中找到长时间停顿的具体日志信息(添加了-XX:PrintReferenceGC参数) 找到的日志片段如下所示。从日志中可以看出真正执行GC动作的时间不是很长但从准 备开始GC到真正开始GC之间所消耗的时间却占了绝大部分。
除GC日志之外还观察到这个GUI程序内存变化的一个特点当它最小化的时候资源 管理中显示的占用内存大幅度减小但是虚拟内存则没有变化因此怀疑程序在最小化时它的工作内存被自动交换到磁盘的页面文件之中了这样发生GC时就有可能因为恢复页面文件的操作而导致不正常的GC停顿。在Java的GUI程序中要避免这种现象可以 加入参数“-Dsun.awt.keepWorkingSetOnMinimizetrue”来解决。 十五、Minor GC和Full GC触发条件
Minor GC触发条件当Eden区满时触发Minor GC。Full GC触发条件 调用System.gc时系统建议执行Full GC但是不必然执行老年代空间不足方法区空间不足通过Minor GC后进入老年代的平均大小大于老年代的可用内存由Eden区、From Space区向To Space区复制时对象大小大于To Space可用内存则把该对象转存到老年代且老年代的可用内存小于该对象大小 十六、G1和CMS的比较 CMS收集器是获取最短回收停顿时间为目标的收集器因为CMS工作时GC工作线程与用户线程可以并发执行以此来达到降低停顿时间的目的只有初始标记和重新标记会STW。但是CMS收集器对CPU资源非常敏感。在并发阶段虽然不会导致用户线程停顿但是会占用CPU资源而导致引用程序变慢总吞吐量下降。 CMS仅作用于老年代是基于标记清除算法所以清理的过程中会有大量的空间碎片。 CMS收集器无法处理浮动垃圾由于CMS并发清理阶段用户线程还在运行伴随程序的运行自热会有新的垃圾不断产生这一部分垃圾出现在标记过程之后CMS无法在本次收集中处理它们只好留待下一次GC时将其清理掉。 G1是一款面向服务端应用的垃圾收集器适用于多核处理器、大内存容量的服务端系统。G1能充分利用CPU、多核环境下的硬件优势使用多个CPUCPU或者CPU核心来缩短STW的停顿时间它满足短时间停顿的同时达到一个高的吞吐量。 从JDK 9开始G1成为默认的垃圾回收器。当应用有以下任何一种特性时非常适合用G1Full GC持续时间太长或者太频繁对象的创建速率和存活率变动很大应用不希望停顿时间长(长于0.5s甚至1s)。 G1将空间划分成很多块Region然后他们各自进行回收。堆比较大的时候可以采用采用复制算法碎片化问题不严重。整体上看属于标记整理算法,局部(region之间)属于复制算法。 G1 需要记忆集来记录新生代和老年代之间的引用关系这种数据结构在 G1 中需要占用大量的内存可能达到整个堆内存容量的 20% 甚至更多。而且 G1 中维护记忆集的成本较高带来了更高的执行负载影响效率。所以 CMS 在小内存应用上的表现要优于 G1而大内存应用上 G1 更有优势大小内存的界限是6GB到8GB。Card TableCMS中的结构是一个连续的byte[]数组扫描Card Table的时间比扫描整个老年代的代价要小很多G1也参照了这个思路不过采用了一种新的数据结构 Remembered Set 简称Rset。RSet记录了其他Region中的对象引用本Region中对象的关系属于points-into结构谁引用了我的对象。而Card Table则是一种points-out我引用了谁的对象的结构每个Card 覆盖一定范围的Heap一般为512Bytes。G1的RSet是在Card Table的基础上实现的每个Region会记录下别的Region有指向自己的指针并标记这些指针分别在哪些Card的范围内。 这个RSet其实是一个Hash TableKey是别的Region的起始地址Value是一个集合里面的元素是Card Table的Index。每个Region都有一个对应的Rset。 参考链接
《架构师进阶系列》第1章JVM整体架构与调优参数说明 JVM 史上最最最完整知识总结