JVM虚拟机
JAVA的组成
1:JDK 开发工具包
2:JRE 运行环境
3:JVM 虚拟机
java跨平台运行(在 不同的操作系统上运行)的关键在于JVN虚拟机。
汇编 | 编译器 | 应用平台 |
---|---|---|
Windows | VC | Windows |
AT&T | GCC | UNIX;MAC |
ARM | Android |
配置系统环境变量:
方式一:JDK8及之前
JAVA_HOME jdk安装目录
CLASSPATH .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;
Path %JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;
方式二:JDK8之后
因为JDK8之后的版本下面是没有JRE的,合并到一块儿了。
Path jdk安装目录\bin;
JVM的组成
JVM由三部分组成:类加载器,执行引擎,运行时数据区。
类加载器
类加载器通过一个类的全限定名来转换为描述这个类的二进制字节流。
对于任意一个类,被同一个类加载器加载后都是唯一的,但如果被不同加载器加载后,就不是唯一的了。即使是源于同一个Class文件、被同一个JVM加载,只要加载类的加载器不同,那么类就不同。
如何判断类是否相同,可以使用Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果进行判断,也可以使用instanceof关键字进行对象所属关系的判断。
下面我们写一个不同类加载器加载后的类,看一下对instanceof关键字运算有什么影响:
public class OneMoreStudy {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream inputStream = getClass().getResourceAsStream(fileName);
if (inputStream == null) {
return super.loadClass(name);
}
byte[] array = new byte[inputStream.available()];
inputStream.read(array);
return defineClass(name, array, 0, array.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object object = myLoader.loadClass("OneMoreStudy").newInstance();
System.out.println("class name: " + object.getClass().getName());
System.out.println("instanceof: " + (object instanceof OneMoreStudy));
}
}
运行结果:
class name: OneMoreStudy
instanceof: false
在运行结果中,第一行可以看出这个对象确实是OneMoreStudy
类实例化出来的,但在第二行中instanceof运算结果是false,说明在JVM中存在两个OneMoreStudy
类,一个是由系统应用程序类加载器加载的,另一个是由我们自定义的类加载器加载的。虽然都是来自同一个Class文件,在同一个JVM里,但是被不同的类加载器加载后,仍然是两个独立的类。
通过不同的类加载器,可以从不同的源加载类的二进制数据文件:
1)从本地文件系统加载class文件。
2)从JAR包加载class文件。JDBC编程时用到的数据库驱动类(com.mysql.jdbc.Driver)就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
3)通过网络加载class文件。
4)把一个Java源文件动态编译,并执行加载。
启动类加载器Bootstrap ClassLoader
它负责将存放在%JAVA_HOME%\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是JVM识别的类库加载到JVM内存中。它仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载。它是由C++语言实现的,无法被Java程序直接引用。
扩展类加载器Extension ClassLoader
它负责加载%JAVA_HOME%\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。它由sun.misc.Launcher.ExtClassLoader实现,开发者可以直接使用扩展类加载器。
应用程序类加载器Application ClassLoader
它负责加载用户类路径(ClassPath)上所指定的类库。由于它是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它由sun.misc.Launcher.AppClassLoader来实现,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
自定义类加载器User ClassLoader
自定义类加载器继承自java.lang.ClassLoader,并覆写findClass方法,它的作用是将特殊用途的类加载到内存中。
覆写loadClass方法可以违背双亲委派原则。
双亲委派模型
英文全称:Parents Delegation Model
问题:为什么需要双亲委派模型?
①唯一性:双亲委派机制使得类加载出现层级,父类加载器加载过的类,子类加载器不会重复加载,可以防止类重复加载,保持唯一性;
②安全性:使得类的加载出现优先级,防止了核心API被篡改,提升了安全性,所以越基础的类就会越上层进行加载,反而一般自己的写的类,就会在应用程序加载器(Application)直接加载。
总而言之,双亲委托模型是为了防止内存中出现多份同样的字节码,保证一个类在JVM中的唯一性。
某个类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;如果父类加载器无法完成加载任务,将抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载,依次类推。
类加载器之间的层次关系
除了顶层的启动类加载器外,其余的类加载器都必须有自己的父类加载器。类加载器之间的父子关系,一般不会以继承的关系来实现,而是都使用组合关系来复用父类加载器。
-
类加载器收到类加载的请求。java.lang.ClassLoader的loadClass()方法
-
将这个请求委派给父类加载器去尝试加载,一层层的委托传送到顶层的启动类加载器中。
-
启动类加载器检查是否能够加载当前的这个类,能加载就结束,使用当前的加载器。
如果当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,抛出异常通知子加载器进行加载。
-
重复步骤 3。
实现双亲委派模型的代码
实现双亲委派模型的代码都集中在java.lang.ClassLoader的loadClass()方法之中,如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先,检查该类是否已经被加载过了
Class<?> c = findLoadedClass(name);
//如果没有加载过,就调用父类加载器的loadClass()方法
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类加载器为空,就使用启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//如果在父类加载器中找不到该类,就会抛出ClassNotFoundException
}
if (c == null) {
//如果父类找不到,就调用findClass()来找到该类。
long t1 = System.nanoTime();
c = findClass(name);
//记录统计数据
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
破坏双亲委派模型
自定义类加载器,重写ClassLoader的loadClass()方法
在上面例子代码中,就是重写了ClassLoader的loadClass()方法,破坏了双亲委派模型,产生了不唯一的类。
所以不提倡开发人员覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型。
SPI(服务提供者接口)
Java提供了很多SPI(Service Provider Interface,服务提供者接口),允许第三方为这些接口提供实现,常见的SPI有JDBC、JNDI、JCE、JAXB和JBI等。
SPI的接口由Java核心库来提供,而这些SPI的实现代码则是作为Java应用所依赖的jar包被包含进类路径(ClassPath)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器来加载的;SPI的实现类是由系统类加载器来加载的。引导类加载器是无法找到SPI的实现类的,因为依照双亲委派模型,启动类加载器无法委派系统类加载器来加载类。
这时候就会使用线程上下文类加载器(Thread Context ClassLoader),在JVM中会把当前线程的类加载器加载不到的类交给线程上下文类加载器来加载,直接使用Thread.currentThread().getContextClassLoader()来获得,默认返回的就是应用程序类加载器,也可以通过java.lang.Thread类的setContextClassLoader()方法进行设置。
而线程上下文类加载器破坏了双亲委派模型,也就是父类加载器请求子类加载器去完成类加载的动作,但为了实现功能,这也是一种巧妙的实现方式。
OSGi(开放服务网关协议)
OSGi(Open Service Gateway Initiative,开放服务网关协议)技术是面向Java动态化模块化系统模型,程序模块(称为Bundle)无需重新引导可以被远程安装、启动、升级和卸载。实现程序模块热部署的关键则是它自定义的类加载器机制的实现。
在OSGi中,类加载器不再是双亲委派模型中的树状结构,而是一个较为复杂的网状结构,类加载的规则简要介绍如下:
- 若类属于java.*包,则将加载请求委托给父加载器
- 若类定义在启动委托列表(org.osgi.framework.bootdelegation)中,则将加载请求委托给父加载器
- 若类属于在Import-Package中定义的包,则框架通过ClassLoader依赖关系图找到导出此包的Bundle的ClassLoader,并将加载请求委托给此ClassLoader
- 若类资源属于在Require-Bundle中定义的Bundle,则框架通过ClassLoader依赖关系图找到此Bundle的ClassLoader,将加载请求委托给此ClassLoader
- Bundle搜索自己的类资源( 包括Bundle-Classpath里面定义的类路径和属于Bundle的Fragment的类资源)
- 若类在DynamicImport-Package中定义,则开始尝试在运行环境中寻找符合条件的Bundle
如果在经过上面一系列步骤后,仍然没有正确地加载到类资源,则会向外抛出类未发现异常。
类加载流程
加载(将class文件读到内存中,有且只为其创建一个java.lang.Class对象,一旦一个类被加载就不会被再次载入。
.java源文件–javac.exe编译执行器–>.class字节码文件–java.exe jvm–>机器码)
验证(验证Class文件的文件格式,元数据,字节码,符号引用)
准备(为一些类变量分配内存,并将其初始化为默认值)
解析(将符号引用替换为直接引用。类和接口、类方法、接口方法、字段等解析)
初始化
加载Loading
将类的class文件(二进制字节流)读到内存中,有且只为其创建一个java.lang.Class对象,一旦一个类被加载就不会被再次载入。
注:
1)类加载何时会出现
1)访问类的实例(new对象的时候);
2)访问某个类或者接口的静态变量,或者对该静态变量赋值;
3)调用类的静态方法;
4)反射
5)初始化一个类的子类(会先初始化父类)父类static——>子类static——>父类构造——>子类构造
6)JVM启动时标明的启动类,即文件名和类名相同的类;
2)一个类用其全限定类名和其类加载器作为唯一的标识
3)类加载由JVM类加载器完成,且其加载一般符合双亲委派模型。
连接
验证Verification
用于检测被加载的类是否具有正确的数据结构,并和其他类协调一致。
验证的目的在于保证Class文件的字节流包含的信息符合当前虚拟机的要求,不会危害虚拟机的安全。
其可以分为以下几种验证方式:
文件格式验证
主要验证字节流是否符合文件规范,并且可以被当前的虚拟机处理;
元数据验证
对字节码表述的信息进行验证,判断是否符合Java规范;
字节码验证
分析数据流和控制,确定语义合法,保证类方法在运行时不会有危害;
符号引用验证
主要是针对符号引用转换为直接引用的时候,去确定访问类型涉及到的引用情况,保证引用一定可以被访问的到(符号引用的格式被明确规定在Class文件中)。
准备Preparation
为类变量分配内存并设置默认值
解析Resolution
把间接引用转换为直接引用
初始化Initialization
执行()赋初始值,初始化类变量、静态代码块
static a=3;
在准备阶段a只是准备好但是没有赋值,所以a=0;
在初始化阶段会为a赋初始值,所以a=3;
使用Using
卸载Unloading
运行时数据区-JVM内存结构
JVM内存结构 | 作用 | 特点 |
---|---|---|
方法区 | class文件;运行时常量池(jdk1.7之前包括String) | 垃圾回收 用于存放被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等 JVM模型规范。具体实现是元空间(直接内存)和永久代(jdk1.7后永久代并入元空间) |
堆 | new创建出来的对象分配内存的地方;(jdk1.7之后包括String) | 垃圾回收的重点 可分为新生代,老生代。 通过new创建出来的对象都放在堆空间。 堆空间中的对象都有独立的内存空间地址。 对象都有默认的初始化值。 当对象不再使用的时候,没有变量指向该堆中的内存空间,会在不确定的时间点被回收。 |
虚拟机栈 | JVM执行Java方法(例如main) | java方法执行的内存模型,每个方法执行都会创建一个栈帧(局部变量表、操作数栈、动态链接、方法出口),每个方法从开始调用到结束都对应一个栈帧在虚拟机栈中入栈到出栈的过程。 |
本地方法栈 | JVM执行本地方法(调用操作系统的指令) | HotSpot把本地方法栈和虚拟机栈合二为一 |
程序计数器 | 当前线程执行字节码的行号指示器 |
OOP-Klass Java对象模型
Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的。而这个关于Java对象自身的存储模型称之为Java对象模型。
HotSpot虚拟机中(Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机),设计了一个OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。
每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个 instanceKlass 对象,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc 对象,这个对象中包含了对象头以及实例数据。对象的引用在方法栈中。
JMM Java内存模型
JMM定义了JVM在计算机内存(RAM)中的工作方式。
// 示例
private volatile boolean initFlag = false;
JMM原子操作 | 用途 | 特点 |
---|---|---|
read | 读取 | 从主内存读取数据 |
load | 加载 | 将主内存的数据写入工作内存 |
use | 使用 | 从工作内存读取数据计算 |
assign | 赋值 | 将计算好的值重新赋值到工作内存中 |
store | 存储 | 将工作内存的值传递到主内存 |
write | 写入 | 将store传递的变量值写入主内存中的变量 |
lock | 加锁 | 将主内存变量加锁,标记为线程独占状态 |
unlock | 解锁 | 将主内存变量解锁,解锁后其他线程可以锁定该变量 |
JMM面临的问题
可见性:如何保证共享变量的副本变化后其他线程可见?
有序性:如何保证第一步到第六步按顺序执行?
原子性:如何保证第一步到第六步完整执行?
垃圾回收机制(GC)
GC:garbage collec
新老持三代
新生代Young Generation
新生代 Young Generation = Eden+Survivor From+Survivor To = 8:1:1
新生成的对象。
复制算法
老年代Old Generation
老年代:1)新生代经历了一次垃圾回收,对象存活一次年龄就+1,当age>15(4字节)时,放到老年代。2)大对象
标记-清除或者标记-整理
持久代Permanent Generation
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
垃圾回收流程
垃圾回收主要是对堆空间进行处理。将堆划分为老年代和新生代。
新生代 = Eden:Survivor1:Survivor2 = 8:1:1
1)标记Eden空间存活的对象。
2)SurvivorFrom空间存放上次GC存活的对象。
仍存活的对象会根据年龄值来决定去向:如果age>15则放到老年代,如果age<15则标记。
3)Eden标记存活对象+SurvivorFrom标记存活对象 复制到 SurvivorTo空间
4)Eden与SurvivorFrom全部清除掉
5)本次垃圾回收结束后交换SurvivorFrom与SurvivorTo。
注:当Eden没有足够空间分配给对象时,将发起一次Minor GC,当执行Minor GC后还不足以为对象分配空间,大对象直接进入老年代(可以用参数设置大对象直接进入老年代避免频繁Minor GC)。
老年代:1)新生代对象存活一次年龄就+1,当age>15时,放到老年代。2)大对象
注:发生Minor GC之前会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果大于说明MinorGC安全;否则会判断是否允许担保失败,如果允许担保失败,判断老年代最大连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试MinorGC,否则执行FullGC。
判断对象是否已死
引用计数算法
给对象添加一个引用计数器,每当一个地方引用它时计数器+1,引用失效计数器-1,计数器=0说明不再引用。
优点:实现简单,判定效率高
缺点:无法解决对象相互循环引用的问题。假设A引用B,B引用A,那么这两个对象将不会被回收,造成内存泄漏
可达性分析算法
当一个对象到GC Roots没有引用链相连(GC Roots到这个对象不可达)时,证明此对象不可用。
可作为GC Roots根节点的对象:
1)虚拟机栈(栈帧中的本地变量表)中引用的对象
2)本地方法栈中JNI(Native方法)引用的对象
3)方法区中静态变量引用的对象
4)方法区中常量引用的对象
引用
引用划分种类的意义:我们希望描述这样一类对象,当内存空间还足够时,则能保留在内存中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。
强引用
只要强引用存在,垃圾收集器永远不会收集被引用的对象
Object object=new Object();
弱引用
被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
WeakReference<Object> object=new WeakReference<Object>(new Object());
软引用
在发生内存溢出之前,垃圾收集器会把这类对象进行第二次回收,如果这次回收还没有足够内存才会抛出内存溢出异常。
SoftReference<Object> object=new SoftReference<Object>(new Object());
虚引用
一个对象是否有虚引用不会对其生存时间构成影响,也无法通过虚引用获取对象实例,唯一目的就是能在对象被回收时收到一个系统通知
ReferenceQueue<String> referenceQueue = new ReferenceQueue<String>();
PhantomReference<String> object=new PhantomReference<String>(new String(),referenceQueue);
finalize()
即使在可达性分析算法中不可达的对象,也并非是非死不可的,要真正被回收至少经历两次标记过程,如果没有与GCRoots的引用链,对象将会被第一次标记和筛选(执行finalize()方法)
注意:并不建议使用该方法。
对象是否覆盖finalize方法
是:JVM执行finalize()方法
否:JVM回收对象
JVM是否执行过该对象的finalize方法
是:JVM回收对象(每个对象finalize方法只执行一次)
否:JVM执行finalize()方法
垃圾回收算法
标记清除(碎片化)
复制算法(浪费空间)
标记整理算法(效率比前两者差)
分代收集算法(老年代一般使用“标记-清除”、“标记-整理”算法,年轻代一般用复制算法)
标记-清除 老年代
两个阶段:标记存活对象,清除没有标记的对象。
根据GC Roots可达性分析,标记所有GC Roots直接或间接引用的对象,未被标记的对象就是未被引用的垃圾对象。
清除所有未被标记的对象。
缺点:1、产生内存碎片;2、扫描了整个空间两次
概念 | 适用场合 | 缺点 |
---|---|---|
通过GC Roots,标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后在清除阶段,清除所有未被标记的对象。 | 1)存活对象较多的情况下比较高效 2)老年代 | 1)容易产生内存碎片,再来一个比较大的对象时(典型情况:该对象的大小大于空闲表中的每一块儿大小但是小于其中两块儿的和),会提前触发垃圾回收 2)扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象) |
标记-整理 老年代
先执行标记-清除,让所有的存活对象都向一端移动,最后清理掉边界以外的内存
从根节点开始对所有可达对象做一次标记,将所有的存活对象压缩到内存的一端。然后清理边界外所有的空间。
这种方法既避免了标记-清除碎片的产生,又不像复制需要两块相同的内存空间。
概念 | 适用场合 | 缺点 |
---|---|---|
从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。 | 老年代 | 需要一块儿空的内存空间 需要复制移动对象 |
复制 新生代
根据GC Roots可达性分析,标记所有GC Roots直接或间接引用的对象,并将这些存活的对象复制到一块儿新的内存,之后将原来的那一块儿内存全部回收掉。
将堆划分为老年代和新生代。
新生代 = Eden:Survivor1:Survivor2=8:1:1
1)标记Eden空间存活的对象。
2)SurvivorFrom空间存放上次GC存活的对象。
仍存活的对象会根据年龄值来决定去向:如果age>15则放到老年代,如果age<15则标记。
3)Eden标记存活对象+SurvivorFrom标记存活对象 复制到 SurvivorTo空间
4)Eden与SurvivorFrom全部清除掉
5)本次垃圾回收结束后交换SurvivorFrom与SurvivorTo。
注:当Eden没有足够空间分配给对象时,将发起一次Minor GC,当执行Minor GC后还不足以为对象分配空间,大对象直接进入老年代(可以用参数设置大对象直接进入老年代避免频繁Minor GC)。
老年代:1)新生代对象存活一次年龄就+1,当age>15时,放到老年代。2)大对象
注:发生Minor GC之前会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果大于说明MinorGC安全;否则会判断是否允许担保失败,如果允许担保失败,判断老年代最大连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试MinorGC,否则执行FullGC。
概念 | 适用场合 | 缺点 |
---|---|---|
从GC Roots进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉 | 存活对象较少,垃圾对象多的情况下比较高效 扫描了整个空间一次(标记存活对象并复制移动) 新生代:基本上98%的对象是"朝生夕死"的,存活下来的会很少 | 需要一块儿空的内存空间 需要复制移动对象 对象存活率高时,复制效率较低 |
分代
将内存分为各个年代。将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),堆之外就是持久代(Permanet Generation)。
在不同年代使用不同的算法,而使用不同的垃圾会回收算法
①新生代:对象存活率低,使用复制算法
②老年代:对象存活率高,只有少量需要回收的对象,且无担保空间,使用标记-整理或者是标记-清除算法
垃圾收集器
垃圾收集器 | 特点 | 垃圾回收算法 | 分代 | 线程 |
---|---|---|---|---|
Serial | 串行。客户端模式 | 复制 | 新生代 | 单线程 |
Serial Old | 串行。客户端模式 | 标记-整理 | 老年代 | 单线程 |
Parallel Scavenge | 吞吐率。后台运算而不需要太多交互的分析任务。 | 复制 | 新生代 | 多线程 |
Parallel Old | 吞吐率。后台运算而不需要太多交互的分析任务。 | 标记-整理 | 老年代 | 多线程 |
ParNew | Serial的多线程版本。服务端模式 | 复制 | 新生代 | 多线程 |
CMS | 最短GC停顿时间。增量更新。服务端模式 | 标记-清除 | 老年代 | 多线程 |
G1 | CMS的升级。原始快照 | 标记-清除 | ||
ZGC |
垃圾收集时为什么需要停顿所有线程?
如果不停顿所有线程,对象引用关系可能会不断变化,对对象的可达性分析结果准确性就无法保证。
CMS
全称:Concurrent Mark Sweep并发标记扫描
收集过程:
1)初始标记:标记GC Roots能直接关联的对象,单线程速度很快,用户进程要停止(第一次stop the world)。
2)并发标记:所有old对象,与用户进程并发工作
3)重新标记:使用原始快照的方式重新标记对象,修正并发标记期间用户进程继续运行而产生变化的标记,防止错标对象(具体原因见三色标记)。耗时比初始标记长,但远小于并发标记。第二次stop the world
4)并发清除:清除标记的 GC Roots 不可达对象
缺点:
- 使用标记-清除算法,会产生内存碎片
- 无法回收浮动垃圾
- 对CPU资源敏感
三色标记Tri-Color Marking
由于此过程是在和用户线程并发运行的情况下,对象的引用处于随时可变的情况下,那么就会造成多标和漏标的问题。
- 白色:对象没有被垃圾回收器访问过。在初始阶段,所有的对象都是白色的,在可达性分析后如果对象还是白色,则说明这个对象是不可达的,需要被回收。
- 黑色:对象已经被垃圾回收器访问过,且这个对象所有的引用都已经被扫描过。黑色代表这个对象被扫描过,且是存活的,如果有其他对象引用指向黑色对象,则无需重新扫描。黑色对象不可直接指向某个白色对象。
- 灰色:对象已经被垃圾收集器扫描过,但这个对象上还存在至少一个引用没有被扫描过。
写屏障 + 增量更新
G1
之前将堆划分为老年代和新生代,大小不等的区域。而G1将堆划分为多个大小相等的独立区域Region。
跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
Region = Eden + Survivor + Old
Humnongous属于Old的一种。
收集过程:
1)初始标记:只标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
2)并发标记:从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
3)最终标记:最终标记阶段是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
4)筛选回收:筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
ZGC
是一个Scalable、Low Latency的垃圾回收器。所以它的目的是**「降低停顿时间」**,由此会导致吞吐量会有所降低。吞吐量降低问题不大,横向扩展几台服务器就能解决问题了啦。
- 支持TB量级的堆。这你受得了吗?我们生产环境的硬盘还没有上TB呢,这应该可以满足未来十年内,所有JAVA应用的需求了吧。
- 最大GC停顿时间不超10ms。这你受得了吗?目前一般线上环境运行良好的JAVA应用Minor GC停顿时间在10ms左右,Major GC一般都需要100ms以上(G1可以调节停顿时间,但是如果调的过低的话,反而会适得其反),之所以能做到这一点是因为它的停顿时间主要跟Root扫描有关,而Root数量和堆大小是没有任何关系的。
- 奠定未来GC特性的基础。牛逼,牛逼!
- 最糟糕的情况下吞吐量会降低15%。这都不是事,停顿时间足够优秀。至于吞吐量,通过扩容分分钟解决。
- 它的停顿时间不会随着堆的增大而增长!也就是说,几十G堆的停顿时间是10ms以下,几百G甚至上T堆的停顿时间也是10ms以下。
染色指针
1)染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。使得理论上只要还有一个空闲Region,ZGC就能完成收集。
2)染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作。实际上,到目前为止ZGC都并未使用任何写屏障,只使用了读屏障(一部分是染色
指针的功劳,一部分是ZGC现在还不支持分代收集,天然就没有跨代引用的问题)。能够省去一部分的内存屏障,显然对程序运行效率是大有裨益的,所以ZGC对吞吐量的影响也相对较低。
内存屏障(Memory Barrier)的目的是为了指令不因编译优化、CPU执行优化等原因而导致乱序执行,它也是可以细分为仅确保读操作顺序正确性和仅确保写操作顺序正确性的内存屏障的。
3)染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。现在Linux下的64位指针还有前18位并未使用,它们虽然不能用来寻址,却可以通过其他手段用于信息记录。如果开发了这18位,既可以腾出已用的4个标志位,将ZGC可支持的最大堆内存从4TB拓展到64TB,也可以利用其余位置再存储更多的标志,譬如存储一些追踪信息来让垃圾收集器在移动对象时能将低频次使用的对象移动到不常访问的内存区域。
JVM排查
jdk命令行调优工具 | 可视化调优工具 |
---|---|
jps查看系统内所有HotSpot进程 | jconsole和jvisualvm查看内存回收情况 |
jinfo显示虚拟机配置信息 | BTrace跟踪调试方法 |
jstat收集虚拟机运行数据 | jprofiler监控每个类的内存占用 |
jmap生成指定进程堆转储快照(heapdump文件) | MAT工具分析内存占用 |
jhat分析heapdump文件 | |
jstack显示虚拟机线程快照 |
#查看系统中所有线程
top
#查看内存使用情况
free -m
#观察这个java线程占用CPU及内存的使用率
top -H -p pid
#CPU使用率过高。得到该Java进程的线程快照jstack.log
jstack pid > jstack.log
#内存使用率过高。查看造成内存泄漏的类。jmap命令导出head dump文件
jmap -dump:live,format=b,file=文件名 pid
#jvm配置
jmap -heap pid
#堆的统计
jmap -histo
CPU Load过高
频繁Young GC
产生原因:频繁的创建对象,马上又被回收了。
可能出现的原因:
1)业务代码。在循环体里创建对象,新生代的Survivor区太小导致对象频繁的被gc
2)新生代大小太小
频繁Full GC
产生原因:老年代
可能出现的原因:
1)新生代的Survivor区太小,导致对象频繁的被gc,然后超过年龄进入老年代
2)一次性加载过多的数据进入内存,搞出很多大对象,直接进入老年代
FullGC时间长
频繁PermSpace GC
内存泄漏
内存溢出
JVM调优
主要是防止Error 错误,例如oom,而产生OOM的原因有两点:
1)分配的少了:JVM配置层面
2)占用的太多:Java层面
JVM调优可参考的数据 |
---|
系统运行日志 |
异常堆栈 |
GC日志 |
线程快照(threaddump/javacore文件) |
堆转储快照(heapdump/hprof文件) |
JVM配置层面
堆的初始值与最大值设置为相等,-Xms和-Xmx的值设置成相等
新生代尽量设置大一些
年轻代和老年代1:3
年轻代8:1:1
打开内存快照
禁止系统GC。-XX:+DisableExplicitGC
https://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html
JVM参数 | 含义 | 默认值 | |
---|---|---|---|
-Xms | 初始堆大小 | 物理内存的1/64(<1GB) | 默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制. |
-Xmx | 最大堆大小 | 物理内存的1/4(<1GB) | 默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制 |
-Xmn | 年轻代大小(1.4or lator) | 注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。 整个堆大小=年轻代大小 + 年老代大小 + 持久代大小. 增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8 | |
-XX:NewSize | 设置年轻代大小(for 1.3/1.4) | ||
-XX:MaxNewSize | 年轻代最大值(for 1.3/1.4) | ||
-XX:PermSize | 设置持久代(perm gen)初始值 | 物理内存的1/64 | |
-XX:MaxPermSize | 设置持久代最大值 | 物理内存的1/4 | |
-Xss | 每个线程的堆栈大小 | JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右 一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长) 和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:"” -Xss is translated in a VM flag named ThreadStackSize” 一般设置这个值就可以了。 | |
-XX:ThreadStackSize | Thread Stack Size | (0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.] | |
-XX:NewRatio | 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) | -XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5 Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。 | |
-XX:SurvivorRatio | Eden区与Survivor区的大小比值 | 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10 | |
-XX:LargePageSizeInBytes | 内存页的大小不可设置过大, 会影响Perm的大小 | =128m | |
-XX:+UseFastAccessorMethods | 原始类型的快速优化 | ||
-XX:+DisableExplicitGC | 关闭System.gc() | 这个参数需要严格的测试 | |
-XX:MaxTenuringThreshold | 垃圾最大年龄 | 如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率 该参数只有在串行GC时才有效. | |
-XX:+AggressiveOpts | 加快编译 | ||
-XX:+UseBiasedLocking | 锁机制的性能改善 | ||
-Xnoclassgc | 禁用垃圾回收 | ||
-XX:SoftRefLRUPolicyMSPerMB | 每兆堆空闲空间中SoftReference的存活时间 | 1s | softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap |
-XX:PretenureSizeThreshold | 对象超过多大是直接在旧生代分配 | 0 | 单位字节 新生代采用Parallel Scavenge GC时无效 另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象. |
-XX:TLABWasteTargetPercent | TLAB占eden区的百分比 | 1% | |
-XX:+CollectGen0First | FullGC时是否先YGC | false | |
并行收集器相关参数 | |||
-XX:+UseParallelGC | Full GC采用parallel MSC (此项待验证) | 选择垃圾收集器为并行收集器.此配置仅对年轻代有效.即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集.(此项待验证) | |
-XX:+UseParNewGC | 设置年轻代为并行收集 | 可与CMS收集同时使用 JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值 | |
-XX:ParallelGCThreads | 并行收集器的线程数 | 此值最好配置与处理器数目相等 同样适用于CMS | |
-XX:+UseParallelOldGC | 年老代垃圾收集方式为并行收集(Parallel Compacting) | 这个是JAVA 6出现的参数选项 | |
-XX:MaxGCPauseMillis | 每次年轻代垃圾回收的最长时间(最大暂停时间) | 如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值. | |
-XX:+UseAdaptiveSizePolicy | 自动选择年轻代区大小和相应的Survivor区比例 | 设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开. | |
-XX:GCTimeRatio | 设置垃圾回收时间占程序运行时间的百分比 | 公式为1/(1+n) | |
-XX:+ScavengeBeforeFullGC | Full GC前调用YGC | true | Do young generation GC prior to a full GC. (Introduced in 1.4.1.) |
CMS相关参数 | |||
-XX:+UseConcMarkSweepGC | 使用CMS内存收集 | 测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明.所以,此时年轻代大小最好用-Xmn设置.??? | |
-XX:+AggressiveHeap | 试图是使用大量的物理内存 长时间大内存使用的优化,能检查计算资源(内存, 处理器数量) 至少需要256MB内存 大量的CPU/内存, (在1.4.1在4CPU的机器上已经显示有提升) | ||
-XX:CMSFullGCsBeforeCompaction | 多少次后进行内存压缩 | 由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生"碎片",使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理. | |
-XX:+CMSParallelRemarkEnabled | 降低标记停顿 | ||
-XX+UseCMSCompactAtFullCollection | 在FULL GC的时候, 对年老代的压缩 | CMS是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。 可能会影响性能,但是可以消除碎片 | |
-XX:+UseCMSInitiatingOccupancyOnly | 使用手动定义初始化定义开始CMS收集 | 禁止hostspot自行触发CMS GC | |
-XX:CMSInitiatingOccupancyFraction=70 | 使用cms作为垃圾回收 使用70%后开始CMS收集 | 92 | 为了保证不出现promotion failed(见下面介绍)错误,该值的设置需要满足以下公式**CMSInitiatingOccupancyFraction计算公式** |
-XX:CMSInitiatingPermOccupancyFraction | 设置Perm Gen使用到达多少比率时触发 | 92 | |
-XX:+CMSIncrementalMode | 设置为增量模式 | 用于单CPU情况 | |
-XX:+CMSClassUnloadingEnabled | |||
辅助信息 | |||
-XX:+PrintGC | 输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs] | ||
-XX:+PrintGCDetails | 输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs] | ||
-XX:+PrintGCTimeStamps | |||
-XX:+PrintGC:PrintGCTimeStamps | 可与-XX:+PrintGC -XX:+PrintGCDetails混合使用 输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs] | ||
-XX:+PrintGCApplicationStoppedTime | 打印垃圾回收期间程序暂停的时间.可与上面混合使用 | 输出形式:Total time for which application threads were stopped: 0.0468229 seconds | |
-XX:+PrintGCApplicationConcurrentTime | 打印每次垃圾回收前,程序未中断的执行时间.可与上面混合使用 | 输出形式:Application time: 0.5291524 seconds | |
-XX:+PrintHeapAtGC | 打印GC前后的详细堆栈信息 | ||
-Xloggc:filename | 把相关日志信息记录到文件以便分析. 与上面几个配合使用 | ||
-XX:+PrintClassHistogram | garbage collects before printing the histogram. | ||
-XX:+PrintTLAB | 查看TLAB空间的使用情况 | ||
XX:+PrintTenuringDistribution | 查看每次minor GC后新的存活周期的阈值 | Desired survivor size 1048576 bytes, new threshold 7 (max 15) new threshold 7即标识新的存活周期的阈值为7。 | |
GC性能方面的考虑
对于GC的性能主要有2个方面的指标:吞吐量throughput(工作时间不算gc的时间占总的时间比)和暂停pause(gc发生时app对外显示的无法响应)。
- Total Heap
默认情况下,vm会增加/减少heap大小以维持free space在整个vm中占的比例,这个比例由MinHeapFreeRatio和MaxHeapFreeRatio指定。
一般而言,server端的app会有以下规则:
- 对vm分配尽可能多的memory;
- 将Xms和Xmx设为一样的值。如果虚拟机启动时设置使用的内存比较小,这个时候又需要初始化很多对象,虚拟机就必须重复地增加内存。
- 处理器核数增加,内存也跟着增大。
- The Young Generation
另外一个对于app流畅性运行影响的因素是young generation的大小。young generation越大,minor collection越少;但是在固定heap size情况下,更大的young generation就意味着小的tenured generation,就意味着更多的major collection(major collection会引发minor collection)。
NewRatio反映的是young和tenured generation的大小比例。NewSize和MaxNewSize反映的是young generation大小的下限和上限,将这两个值设为一样就固定了young generation的大小(同Xms和Xmx设为一样)。
如果希望,SurvivorRatio也可以优化survivor的大小,不过这对于性能的影响不是很大。SurvivorRatio是eden和survior大小比例。
一般而言,server端的app会有以下规则:
- 首先决定能分配给vm的最大的heap size,然后设定最佳的young generation的大小;
- 如果heap size固定后,增加young generation的大小意味着减小tenured generation大小。让tenured generation在任何时候够大,能够容纳所有live的data(留10%-20%的空余)。
经验&&规则
-
年轻代大小选择
- 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择).在此种情况下,年轻代收集发生的频率也是最小的.同时,减少到达年老代的对象.
- 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度.因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用.
- 避免设置过小.当新生代设置过小时会导致:1.YGC次数更加频繁 2.可能导致YGC对象直接进入旧生代,如果此时旧生代满了,会触发FGC.
-
年老代大小选择
- 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数.如果堆设置小了,可以会造成内存碎 片,高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间.最优化的方案,一般需要参考以下数据获得:
并发垃圾收集信息、持久代并发收集次数、传统GC信息、花在年轻代和年老代回收上的时间比例。 - 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象.
- 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数.如果堆设置小了,可以会造成内存碎 片,高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间.最优化的方案,一般需要参考以下数据获得:
-
较小堆引起的碎片问题
因为年老代的并发收集器使用标记,清除算法,所以不会对堆进行压缩.当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象.但是,当堆空间较小时,运行一段时间以后,就会出现"碎片",如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记,清除方式进行回收.如果出现"碎片",可能需要进行如下配置:
-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩.
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩 -
用64位操作系统,Linux下64位的jdk比32位jdk要慢一些,但是吃得内存更多,吞吐量更大
-
XMX和XMS设置一样大,MaxPermSize和MinPermSize设置一样大,这样可以减轻伸缩堆大小带来的压力
-
使用CMS的好处是用尽量少的新生代,经验值是128M-256M, 然后老生代利用CMS并行收集, 这样能保证系统低延迟的吞吐效率。 实际上cms的收集停顿时间非常的短,2G的内存, 大约20-80ms的应用程序停顿时间
-
系统停顿的时候可能是GC的问题也可能是程序的问题,多用jmap和jstack查看,或者killall -3 java,然后查看java控制台日志,能看出很多问题。(相关工具的使用方法将在后面的blog中介绍)
-
仔细了解自己的应用,如果用了缓存,那么年老代应该大一些,缓存的HashMap不应该无限制长,建议采用LRU算法的Map做缓存,LRUMap的最大长度也要根据实际情况设定。
-
采用并发回收时,年轻代小一点,年老代要大,因为年老大用的是并发回收,即使时间长点也不会影响其他程序继续运行,网站不会停顿
-
JVM参数的设置(特别是 –Xmx –Xms –Xmn -XX:SurvivorRatio -XX:MaxTenuringThreshold等参数的设置没有一个固定的公式,需要根据PV old区实际数据 YGC次数等多方面来衡量。为了避免promotion faild可能会导致xmn设置偏小,也意味着YGC的次数会增多,处理并发访问的能力下降等问题。每个参数的调整都需要经过详细的性能测试,才能找到特定应用的最佳配置。
promotion failed:
垃圾回收时promotion failed是个很头痛的问题,一般可能是两种原因产生,第一个原因是救助空间不够,救助空间里的对象还不应该被移动到年老代,但年轻代又有很多对象需要放入救助空间;第二个原因是年老代没有足够的空间接纳来自年轻代的对象;这两种情况都会转向Full GC,网站停顿时间较长。
解决方方案一:
第一个原因我的最终解决办法是去掉救助空间,设置-XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0即可,第二个原因我的解决办法是设置CMSInitiatingOccupancyFraction为某个值(假设70),这样年老代空间到70%时就开始执行CMS,年老代有足够的空间接纳来自年轻代的对象。
解决方案一的改进方案:
又有改进了,上面方法不太好,因为没有用到救助空间,所以年老代容易满,CMS执行会比较频繁。我改善了一下,还是用救助空间,但是把救助空间加大,这样也不会有promotion failed。具体操作上,32位Linux和64位Linux好像不一样,64位系统似乎只要配置MaxTenuringThreshold参数,CMS还是有暂停。为了解决暂停问题和promotion failed问题,最后我设置-XX:SurvivorRatio=1 ,并把MaxTenuringThreshold去掉,这样即没有暂停又不会有promotoin failed,而且更重要的是,年老代和永久代上升非常慢(因为好多对象到不了年老代就被回收了),所以CMS执行频率非常低,好几个小时才执行一次,这样,服务器都不用重启了。
-Xmx4000M -Xms4000M -Xmn600M -XX:PermSize=500M -XX:MaxPermSize=500M -Xss256K -XX:+DisableExplicitGC -XX:SurvivorRatio=1 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled -XX:LargePageSizeInBytes=128M -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:log/gc.log
CMSInitiatingOccupancyFraction值与Xmn的关系公式
上面介绍了promontion faild产生的原因是EDEN空间不足的情况下将EDEN与From survivor中的存活对象存入To survivor区时,To survivor区的空间不足,再次晋升到old gen区,而old gen区内存也不够的情况下产生了promontion faild从而导致full gc.那可以推断出:eden+from survivor < old gen区剩余内存时,不会出现promontion faild的情况,即:
(Xmx-Xmn)*(1-CMSInitiatingOccupancyFraction/100)>=(Xmn-Xmn/(SurvivorRatior+2)) 进而推断出:
CMSInitiatingOccupancyFraction <=((Xmx-Xmn)-(Xmn-Xmn/(SurvivorRatior+2)))/(Xmx-Xmn)*100
例如:
当xmx=128 xmn=36 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((128.0-36)-(36-36/(1+2)))/(128-36)*100 =73.913
当xmx=128 xmn=24 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((128.0-24)-(24-24/(1+2)))/(128-24)*100=84.615…
当xmx=3000 xmn=600 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((3000.0-600)-(600-600/(1+2)))/(3000-600)*100=83.33
CMSInitiatingOccupancyFraction低于70% 需要调整xmn或SurvivorRatior值。
Java层面
避免创建过大的对象及数组
避免同时加载大量数据
集合中的对象用完后及时清空
在合适场景使用软引用、弱引用
尽量避免长时间等待外部资源(数据库、网络、设备资源等)
设置合理的线程数
尽可能使用局部变量
调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。
尽量减少对变量的重复计算
明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的。所以例如下面的操作:
for (int i = 0; i < list.size(); i++) {…}
应替换为
int length = list.size();
for (int i = 0, i < length; i++) {…}
这样,在list.size()很大的时候,就减少了很多的消耗。
尽量采用懒加载的策略,即在需要的时候才创建
String str = "aaa";
if (i == 1){
list.add(str);
}
//建议替换成
if (i == 1){
String str = "aaa";
list.add(str);
}
异常不应该用来控制程序流程
异常对性能不利。抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用名为fillInStackTrace()的本地同步方 法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建 了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。
不要将数组声明为public static final
因为这毫无意义,这样只是定义了引用为static final,数组的内容还是可以随意改变的,将数组声明为public更是一个安全漏洞,这意味着这个数组可以被外部类所改变。
不要创建一些不使用的对象,不要导入一些不使用的类
这毫无意义,如果代码中出现"The value of the local variable i is not used"、“Theimport java.util is never used”,那么请删除这些无用的内容
程序运行过程中避免使用反射
反射是Java提供给用户一个很强大的功能,功能强大往往意味着效率不高。不建议在程序运行过程中使用尤其是频繁使用反射机制,特别是 Method的invoke方法。如果确实有必要,一种建议性的做法是将那些需要通过反射加载的类在项目启动的时候通过反射实例化出一个对象并放入内存。
使用数据库连接池和线程池
这两个池都是用于重用对象的,前者可以避免频繁地打开和关闭连接,后者可以避免频繁地创建和销毁线程。
容器初始化时尽可能指定长度
容器初始化时尽可能指定长度,如:new ArrayList<>(10); new HashMap<>(32); 避免容器长度不足时,扩容带来的性能损耗。
ArrayList随机遍历快,LinkedList添加删除快