****目标:****调整Java虚拟机的参数使得性能达到最优。
****原则:****无监控不调优。
程序计数器:它是一块较小的线程私有的内存空间。它可以看做是当前线程所执行字节码的行号显示器。通过改变这个计数器的值可以选择执行的字节码。借助这个性质我们可以使用计数器来恢复被打断的线程。每一个线程都有自己的一个程序计数器互不影响。
****虚拟机栈:****是一块线程私有的内存空间。每起一个线程就会起一个线程栈。而在一个线程中可以调用多个方法,每起一个方法就会起一个栈帧所以一个线程栈中就会有多个栈帧。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。而每一个栈帧中存放着局部变量表,操作函数等信息。我们一般粗糙的把Java中的内存分为堆和栈,里面所说的堆就是指这个堆或者是仅仅指局部变量表部分。当线程请求的栈的深度大于当前虚拟机的栈的深度的时候就会触发StackOverFlowError,当遇到能够动态扩展的虚拟机栈在扩展之后仍然没有满足当前栈的请求的话那么就会抛出OutOfMemoryError。
本地方法栈:Java访问C语言等其它语言所用到的栈,我们访问不了。调优不了。但是它从原理上与虚拟机栈相似。
****堆:****是最大JVM的内存同时它也是线程共享的。它里面只能存对象,但是反过来对象不一定都存在堆里后面我们会提到栈上分配。从内存分配的角度来看线程共享的Java堆可能划分出多个线程私有的分配缓冲区(也就是我们常常提到的ThreadLocal变量)。
方法区:与堆一样是一个线程共享的内存区****。****它用于存储已经被加载的类信息、常量、静态变量等。他还有一个名字就是永久区,但是只有HotSpot虚拟机上开发才有这样的说法而其它的机是没有永久区这个概念的。
运行时常量池:他是方法区的一部分,类文件中有一项信息是常量池,它用来存放在编译期生成的字面量和符号引用。这部分内容将在类加载后在运行时常量池中存放。不仅仅可以将常量池的内容在类加载完装到运行时常量池,而且就算在编译期不在常量池的内容也能够在程序运行时装载到运行时常量池(比如说String的intren()方法)。需要注意的是:运行时常量池相对于常量池来说是动态的。
直接内存:它不是Java虚拟机中定义的内存区域。它直接通过Native函数库分配到堆外内存。
我们能够优化的地方只有堆,堆也是JVM内存最大的存储区域。
堆内存和方法区都是线程所共享的。
栈内存和本地方法栈,PC计数器,每个线程所独有的。
堆内存
Eden伊甸园:
Survivor:幸存者
新生代比例经验值:8 :1 :1
Tenured:老年代
总体比例:1 : 2 或者是 3 :7
流程:当我们第一次new出一个对象来的时候特别大的对象会放在老年代,其它的普通对象直接放在新生代。每次的survivor之间进行copy的时候另一个survivor会被回收,也就是时时刻刻都有一个survivor为空。由于这个算法是基于内存的复制所以效率很高。
如果经历了很多次的GC都没有回收的话就会被放入老年代。
引用计数算法
没有引用指向的对象就是垃圾?不完全是,比如说环形垃圾互相引用的对象。
所以使用循环引用的方法去判断垃圾是不行的。
正向可达算法
首先要得到在堆内存中一定不是垃圾的根对象,我们称之为GCRoots。顺着GCRoots的引用往下找顺藤摸瓜摸到的就是好瓜,摸不到的就是烂瓜(垃圾)。在Java中可作为GCRoots的对象有:
①虚拟机栈栈帧中的局部变量列表中的引用对象。
②方法区中静态属性引用的对象。
③方法区中常见的引用对象。
④本地方法栈中的引用对象。
新建对象优先在Eden区分配,如果分配不下就会发生MinorGC。MinorGC主要是回收Eden区与Survivor中的垃圾。如果垃圾回收之后对象还是放不下的话那么就会放在老年代。
FullGC是用来清理整个堆空间的,包括新生代老年代。所以FullGC会造成很大的开销所以要避免FullGC。
造成FullGC的常见原因以及解决方法:
①System.gc()会触发FullGC所以要避免FullGC。
②老年代的内存不足,主要是有大的对象又放了进来。这时我们可以适当地增大Survivor区、老年代的大小。
③永久代满也会触发FullGC。可以适当地增大永久带的大小或者是开启CMS回收永久代选项。
Copy会把内存区域分为两个部分A、B而且肯定有一个区域为空(假设B区域为空)。在垃圾回收的时候首先会用正向可达算法将所有的存活对象找到。****然后会把A中的所有存活对象拷贝到B区域并且压缩。****最后回收A区中的垃圾。在洗一次GC之前,产生的新对象会被放到B区域来如此往复。它的效率非常之高。这个算法的缺点就是浪费内存,永远会浪费掉一半的内存。
为什么eden的区域比survivor大。就是因为在eden中的对象大多数会被回收所以存活下来的对象会比较少这时就比较适合使用拷贝算法(拷贝的量比较少)。
首先将幸存的对象压缩到一端然后再进行GC这样的话也会得到连续的可用的空间。效率比copy略低。这个算法常常用于老年代,在新生代用的是copy的算法。
JVM使用分代算法
New
存活对象少,使用copying占用的内存空间也不大,效率也高。
Old
垃圾少,一般使用mark-compact标记压缩。
除此之外还有Mark-cleaning(标记清理)。
****当堆内存的使用率超过70%的时候,****GC才会启动回收。
发生在新生代的回收 — minor gc 初代回收
发生在老生代的回收 — full gc 完全回收
当new出来的对象比较小的时候回方到eden区域,如果new出来的对象比较大的时候那么就会放到tenured区去。
- 标准参数所有的JVM都应该支持。
-X非标准参数,每个JVM都应该实现。
-XX不稳定参数(扩展参数),下一个版本可能会取消。
Serial Collector
XX + UseSerialGC 序列化垃圾收集器,一个单线程的收集器,实际中使用的并不多。
Parallel Collector
并发量大,但是在每次垃圾收集的时候回导致JVM停顿。
CMS
并发收集,分区处理。停顿时间短,在垃圾收集的时候,JVM还可以运行。
G1
不仅停顿时间短(这是一个平衡点)而且并发量大。
当new出一个对象来JVM会经历这样的几个分配过程。
标量替换,将一成员变量拿出来当做普通的数据类型往栈上存。
无需调整
当栈上分配不了也就是栈空间满了会来到线程本地分配。每一个线程在执行的时候会给自己分配一块自己专用的内存,叫做线程本地内存。(一个?)线程本地内存默认占用eden内存的1%。如果每一个线程都要放入eden的同一块区域那么这个区域就要进行加锁,但是每个线程的数据都有自己的一块独立的区域那么就不需要加锁了,不加锁就提高了访问效率。
无需调整
当上述两种都分配不了那么就先看看自己是否是一个大对象,如果是就分配到老年代。
如果自己是一个不太大的对象就分配到eden区来。
使用TLAB会提升一截,使用逃逸分析和标量替换性能又能够提升一截。
在eclipse中的run configuration中配置:
-XX:DoEscapeAnalysis //不做逃逸分析 与栈上分配有关
-XX:-EliminateAllocations //不做标量提换 与栈上分配有关
-XX:UseTLAB// 不使用本地缓存
-XX:+PrintGC // 打印GC过程
在实际的环境中我们要权衡并发的数量和并发的深度的关系。
在实务上我们需要通过一些工具来判断在程序中造成内存溢出的原因,这里就介绍一个实例
代码如下:
package test;
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List
参数如下(在Run Configuration的时候来在VMarguments中进行设置):
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=C: mpjvm.dump (生成堆内存相关的文件)
-XX:+PrintGCDetails (打印GC的详细过程)
-Xms10M (初始堆大小)
-Xmx10M (最大堆大小)
最后会在c盘的tmp目录下面生成一个jvm.dump的文件。将jvm.dump导入到jdk文件夹下的bin目录中的jvisualvm.exe中。然后我们观测到了造成内存溢出的是由byte[]造成的,如下图所示。
改动tomcat–>bin–>contalina.bat下面的的set JAVA_OPTS参数
使用Jmeter工具启动多个线程来对tomcat进行性能测试观察配置参数之前(也就是将set JAVA_OPTS中的配置注释掉)与解开注释前后每一秒并发数量的多少来判断性能的提升。
最后,在实务上我们不推荐手动使用gc()来垃圾回收,这样会破坏我们的设定回收策略。
最后,特地感谢马老师一个真正做教育的老师!