jvm简介
一,JVM运行机制
1,JVM启动流程
执行java xxx.class命令>>装载配置文件(根据当前路径和系统版本寻找jvm.cfg文件)>>根据配置寻找JVM.dll(JVM.dll是JVM的主要实现)>>初始化JVM获得JNIEnv接口(findClass等操作通过这个接口实现)>>找到main方法并运行
2,JVM基本结构
Class文件通过类加载器子系统加载到内存。
JVM的内存空间分为:方法区,Java堆,Java栈,本地方法栈。JVM提供GC(垃圾回收器)管理内存回收。
PC寄存器(ProgramCounter),程序计数器寄存器,每个线程都有一个PC寄存器,每个线程启动的时候,都会创建一个PC寄存器。PC寄存器保存有当前正在执行的JVM指令的地址。PC寄存器的内容总是指向下一条将被执行指令的地址,这个地址可以是一个本地指针,也可以是在方法区中相应于给方法起始指令的偏移量。执行本地(native)方式时,PC的值为undefined。
方法区,保存装载类的类信息,如,类型的常量池,字段,方法信息,方法字节码。JDK6,String等常量信息置于方法区,JDK7,已经移动到了堆。方法区通常和永久区(Perm)关联在一起。
Java堆,应用系统对象都保存在Java堆中。所有线程共享Java堆,对分代GC来说,堆也是分代的,是GC的主要工作区。
Java栈,为线程私有,栈由一些列帧组成,因此Java栈也叫做帧栈。帧保存一个方法的局部变量、操作数栈、常量池指针。每一次方法调用创建一个帧,并压栈。
Java栈中的局部变量表,包含参数和局部变量。
Java栈的操作数栈,Java没有寄存器,所有参数传递使用操作数栈。
使用Java栈上分配,有助于防止内存泄露。小对象,在没有逃逸的情况下,可以直接分配在栈上。直接分配在栈上,可以自动回收,减轻GC压力。大对象或者逃逸对象无法栈上分配。
JVM的内存模型,每一个线程有一个工作内存和主存独立。工作内存中存放主存中变量的值和拷贝。当数据从主内存复制到工作存储时,必须出现两个动作,1,由主内存执行的读操作,2,由工作内存执行的相应的load操作。当数据从工作内存拷贝到主内存时,也出现两个操作,1,由工作内存执行的存储store操作,2,由内存执行的相应的写操作。每一个操作都是原子的,即执行期间不会被中断。对于普通变量,一个线程中更新的值,不能马上反应在其他变量中。如果需要其他线程中立即可见,需要使用volatile关键字。
可见性,是指一个线程修改了变量,其他线程可以立即知道。保证可见性的方法,volatie,synchronized,final。
指令重拍的基本原则,1,程序顺序原则,一个线程内保证语义的串行性;2,volatile规则,volatile变量的写,先发生于读;3,锁规则,解锁unlock必然发生在随后的加锁lock前;4,传递性,a先于b,b先于c,那么a必然先于c;5,线程的start方法先于它的每一个动作;6,线程的所有操作先于线程的终结(Thread.join());7,线程的中断(interrupt())先于被中断线程的代码;8,对象的构造函数执行结束先于finalize()方法。
二,常用JVM参数配置
1,Trace跟踪参数
-verbose:gc
-XX:+printGC 打印GC简要信息。
-XX:+PrintGCDetails 打印GC详细信息
-XX:+PrintGCTimeStamps 打印GC发生的时间戳。
-Xloggc:f/gc.log,指定GC log的位置,输出到文件。
-XX:+TraceClassLoading 监控类的加载。
-XX:+PrintClassHistogram,按下Ctrl+Break后,打印类的信息。
2,堆的分配参数
-Xmx 指定最大堆
-Xms 指定最小堆
比如,-Xmx20m -Xms5m
-Xmn 设置新生代大小。
-XX:NewRatio 新生代(eden+2*s)和老年代(不包含永久去)的比值,比如4表示,新生代:老年代=1:4,年轻代占堆的1/5。
-XX:SurvivorRatio 设置两个Survivor区和eden的比,比如,8表示两个Survivor:eden=2:8,一个Survivor占年轻代的1/10。
例子:-Xmx20m –Xms20m –Xmn1m –XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError OOM导出文件到堆。
-XX:+HeapDumpPath OOM导出的路径。
比如,-Xmx20m –Xms5m –XX:+HeapDumpOnOutOfMemoryError –XX:HeapDumpPath=f:/b.dump
-XX:OnOutOfMemoryError 在OOM时,执行一个脚本,比如-XX:OnOutOfMemoryError=f:/f/xxx.bat %p;当程序OOM时,在f:/b.txt中将会生成线程的dump;可以在OOM时,发送邮件,或者其他操作。
-XX:PermSize -XX:MaxPermSize 设置永久区的初始空间和最大空间;表示,一个系统可以容纳多少个类型。
3,栈的分配参数
栈,通常只有几百K,决定了函数调用的深度,每个线程都有独立的栈空间,局部变量、参数分配在栈上。
三,GC
1,概念
Garbage Collection垃圾收集。1960年,lisp使用了GC。Java中,GC的操作对象是堆空间和永久区。
2,GC算法
1)引用计数法
老牌垃圾回收算法,通过引用计算来回收垃圾。
引用计数器的实现相对比较简单,对于一个对象a,只要任何一个对象引用了a,则a的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象a的引用计数器的值为0,则对象a就不可能再被使用。
引用计数法的缺陷,引用和去引用伴随加法和减法,影响性能;很难处理循环引用,比如垃圾对象的循环引用。
Java中没有使用引用计数法。
2)标记清除
标记清除是现代垃圾回收算法的思想基础,标记清除算法将垃圾回收分为两个阶段,标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
3)标记压缩
标记压缩算法适合用于存活对象较多的场合,如老年代。它在标记清除算法的基础上做了一些优化。和标记清除算法一样,标记压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。
3)复制算法
与标记清除算法相比,复制算法是一种相对高效的回收方法。不适用存活对象较多的场合,如老年代。将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
复制算法的缺陷是空间浪费,整合标记清理思想。
3,分代
依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代。根据不同代的特点,选取合适的收集算法,少量对象存活,适合复制算法;大量对象存活,适合标记清理或者标记压缩。
4,对象的可触及性
从根节点可以触及到的对象,是可触及的。一旦所有引用被释放,就是可复活状态,在finalize()中可能复活该对象。在finalize()后,可能会进入不可触及状态,不可触及的对象不可复活,可以回收。应该避免使用finalize(),操作不慎可能导致错误。因为finalize()的优先级低,不能确定其调用时机,一般是在GC时调用,GC的发生时机是不确定的。可以使用try-catch-finally来代替它。
根包括的种类,栈中引用的对象;方法区中静态成员或者常量引用的对象(全局对象);JNI方法栈中引用的对象。
5,Stop-The-World
这种现象,是Java中一种全局暂停的现象。全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互。多半由于GC引起,Dump线程,死锁检查,堆Dump。
全局停顿产生的原因是垃圾源源不断,GC永远不能清理出空间来用,只好全局停顿,以便能够清理出空间。
全局停顿的危害是,长时间服务停止,没有响应;遇到HA系统,可能引起主备切换,严重危害生产环境。
6,GC参数
1)串行收集器
古老,稳定,效率高。可能产生较长的停顿。
-XX:+UserSerialGC 对新生代、老年代可以使用串行回收;新生代复制算法;老年代标记压缩。
2)并行收集器
ParNew收集器
-XX:+UserParNewGC Serial收集器新生代的并行版本;支持复制算法;多线程实现,需要多核支持。
-XX:ParallelGCThreads 可以限制线程数量。
Paralle收集器
类似ParNew,新生代采用复制算法,老年代采用标记压缩,更加关注吞吐量。
-XX:+UserParallelGC 使用Parallel收集器+老年代串行。
-XX:+UserParallelOldGC 使用Parallel收集器+并行老年代。
-XX:MaxGCPauseMills 表示足底啊停顿时间,单位毫秒;GC尽力保证回收时间不超过设定值。
-XX:GCTimeRatio 取值范围是0-100;垃圾收集时间占总时间的比;默认99,即最大允许1%时间做GC。
这两个参数的矛盾的,停顿时间和吞吐量不可能同时调优。
3)CMS收集器
Concurrent Mark Sweep 并发标记清除;采用标记清除算法;并发阶段会降低吞吐量;是一个老年代收集器,新生代使用ParNew。
-XX:+UseConcMarkSweepGC。
CMS运行过程比较复杂,主要是实现了标记的过程,可以分为4个部分,初始标记,根可以直接关联到对象,速度快;并发标记,和用户线程一起执行,是主要的标记过程,标记所有对象;重新标记,由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正;并发清除,和用户线程一起执行,基于标记结果,直接清理对象。
特点:1,尽可能降低停顿;2,会影响系统整体吞吐量的性能,比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半;清理不彻底,因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理;因为和用户线程一起运行,不能在空间快满时再清理。
-XX:CMSInitiatingOCCupancyFraction设置触发GC的阀值。如果不幸内存预留空间不够,就会引起concurrent code failure。当发生concurrent code failure时,使用串行收集器作为后备。
-XX:+UseCMSCompactAtFullCellection Full GC后,进行一次碎片的整理,整理过程是独占的,会引起停顿时间变长。
-XX:+CMSFullGCsBeforeCompaction,设置进行几次Full GC后,进行一次碎片整理。
-XX:ParallelCMSThreads,设定CMS的线程数量。
四,类加载器
1,Java虚拟机与程序的生命周期
Java虚拟机结束生命周期的情况:执行了System.exit()方法;程序正常执行结束;程序在执行中遇到了异常或错误而异常终止;由于操作系统出现错误而导致而导致Java虚拟机进程终止。
2,类的加载、连接和初始化
加载,查找查找并加载类的二进制数据,转为方法区数据结构,在Java堆中生成对应的java.lang.Class对象。
连接,验证,确保被加载的类的正确性,文件格式的验证,元数据验证,字节码验证,符号引用验证;准备,为类的静态变量分配内存,并将其初始化为默认值;解析,把类中的符号引用转换为直接引用。
初始化,为类的静态变量赋予正确的初始值。
3,对类的使用
Java程序对类的使用方式可以分为两种,主动使用和被动使用。
主动使用的情况:
创建类的实例;
访问某个类或接口的静态变量,或者对该静态变量赋值;
调用类的静态方法;
反射,如Class.forName(“com.ten.test.User”);
初始化一个类的子类;
Java虚拟机启动时被标明为启动类的类,比如JavaTest。
以上情况外的都是类的被动使用。
所有的Java虚拟机实现必须在每个类或接口被Java程序首次主动使用时才初始化他们。
4,类的加载
指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区的数据结构。
加载.class文件的方式:
从本地系统中直接加载;通过网络下载.class文件;从zpi、jar等归档文件中加载.class文件;从专有数据库中提取.class文件;将Java源文件动态编译为.class文件。
类的加载的最终产品是位于堆区中的Class对象。Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
5,两种类加载器
Java虚拟机自带的加载器,根类加载器(Bootstrap),使用c++编写,无法在Java代码中获得该类;扩展类加载器(Extension),使用Java代码实现;系统类加载器(System),使用Java代码实现;
用户自定义的类加载器,java.lang.ClassLoader的子类,用户可以定制类的加载方式。
类加载器并不需要等到某个类被首次主动使用时再加载它。
JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)。
如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
6,类的验证
类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。类的验证的内容,类文件的结构检查,语义检查,字节码验证,二进制兼容性的验证。
7,类的准备
在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值。
8,类的解析
在解析阶段,Java虚拟机会把类的二进制数据中的符号引用替换为直接引用。
9,类的初始化
在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化有两种途径:1,在静态变量的声明处进行初始化;2,而静态变量C没有被显式初始化,它将保持默认值0。
静态变量的声明语句,以及静态代码块都被看做类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序来依次执行它们。
类的初始化步骤,1,假如这个类还没有被加载和连接,那就先进行加载和连接;2,假如类存在直接的父类,并且这个父类还没有被初始化,那就先初始化直接的父类;3,假如类中存在初始化语句,那就执行这些初始化语句。
当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。在初始化一个类时,并不会先初始化它所实现的接口。在初始化一个接口时,并不会先初始化它的父接口。
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
程序中对子类的主动使用会导致父类被初始化,但对父类的主动使用并不会导致子类初始化。
只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以是对类或接口的主动使用。
10,类的初始化时机
调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
11,类加载机制
类加载器用来把类加载到Java虚拟机中。从JDK1.2版开始,类的加载过程采用父亲委托机制,这种机制能更好地保证Java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当Java程序请求加载器loader1加载Sample类时,loader1首先委托自己的父加载器去加载Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载Sample类。
在父亲委托机制中,各个加载器按照父子关系关系形成了树形结构,除了根类加载器以外,其余的类加载器都有且只有一个父加载器。
若有一个类加载器能成功加载某个类,那么这个类加载器被称为定义类加载器,所有能成功返回Class对象的引用的类加载器(包括定义类加载器)都被称为初始类加载器。
加载器之间的父子关系实际上指的是加载器对象之间的包装关系,而不是类之间的继承关系。一对父子加载器可能是同一个加载器类的两个实例,也可能不是。在子加载器对象中包装了一个父加载器对象。
当生成一个自定义的类加载器时,如果没有指定它的父加载器,那么系统类加载器就将成为该类加载器的父加载器。
父委托机制的优点是能够提高软件系统的安全性。因为在此机制下,用户自定义的类加载器不可能加载应该由父加载器加载的可靠类,从而防止不可靠甚至恶意的代码代替由父类加载器加载的可靠代码。例如,java.lang.Object类总是由根类加载器加载,其他任何用户自定义的类加载器都不可能加载含有恶意代码的java.lang.Object类。
12,Java虚拟机自带的3种加载器
根(Bootstrap)加载器,该加载器没有父类加载器。它负责加载虚拟机的核心类库,如java.lang.*等。根加载器从系统属性sun.boot.class.path所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承java.lang.ClassLoader类。
扩展(Extension)类加载器,它的父加载器为根加载器。它从java.ext.dirs系统属性所指定的目录目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库,如果把用户创建的JAR文件放在这个目录下,也许会有扩展类加载器加载。扩展类加载器是纯Java类,是java.lang.ClassLoader类的子类。
系统(System)类加载器,也称为应用加载器,它的父加载器为扩展类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。系统类加载器是纯Java类,是Java.lang.ClassLoad类的子类。
13,命名空间
每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类:在不同的命名空间中,有可能会出现类的完整名字(包括类的名字)相同的两个类。
14,运行时包
由同一个类加载器加载的属于相同包的类组成了运行时包。决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看定义类加载器是否相同。只有属于同一运行时包的类才能互相访问包课件(即默认访问级别)的类和类成员。这样的限制能避免用户自定义的类冒充核心类库的类,去访问核心类库的包可见成员。假设用户自己定义了一个类java.lang.Hpp,并由用户自定义的类加载器加载,由于java.lang.Hpp和核心类java.lang.*由不同的类加载器加载,它们属于不同的运行时包,所以java.lang.Hpp不能访问核心类库Java.lang包中的可见成员。
15,创建自定义的类加载器
创建用户用户自己的类加载器,只需要扩展java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,该方法根据参数指定的类的名字,返回对应的Class对象的引用。
创建一个MyClassLoad类
public class MyClassLoader extends ClassLoader{
private String name;
private String path = "f:\\test";
private String fileType = ".class";
public MyClassLoader(String name){
super();
this.name = name;
}
public MyClassLoader(ClassLoader parent, String name){
super(parent);
this.name = name;
}
@Override
public String toString() {
return this.name;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = this.loadClassDate(name);
return this.defineClass(name, data, 0, data.length);
}
private byte[] loadClassDate(String name) {
InputStream inputStream = null;
byte[] data = null;
ByteArrayOutputStream byteArrayOutputStream = null;
try{
this.name = this.name.replace(".", "\\");
inputStream = new FileInputStream(new File(this.path + name + this.fileType));
byteArrayOutputStream = new ByteArrayOutputStream();
int ch = 0;
while(-1 != (ch = inputStream.read())){
byteArrayOutputStream.write(ch);
}
data = byteArrayOutputStream.toByteArray();
}catch(Exception e){
}finally{
try{
if(null != inputStream){
inputStream.close();
}
if(null != byteArrayOutputStream){
byteArrayOutputStream.close();
}
}catch (Exception e){
}
}
return data;
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader1 = new MyClassLoader("loader01");
myClassLoader1.setPath("f:\\test\\t1\\");
MyClassLoader myClassLoader2 = new MyClassLoader(myClassLoader1, "laoder02");
myClassLoader2.setPath("f:\\test\\t2\\");
MyClassLoader myClassLoader3 = new MyClassLoader(null, "loader3");
myClassLoader3.setPath("f:\\test\\t3\\");
test(myClassLoader2);
test(myClassLoader3);
}
public static void test(ClassLoader classLoader) throws Exception{
Class clazz = classLoader.loadClass("Person");
Object o = clazz.newInstance();
}
}
创建两个自定义的类Person,Man略。
16,不同类加载器的命名空间的关系
用一个命名空间内的类是相互可见的。
子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。
由父加载器加载的类不能看见子加载器加载的类。
如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。
17,类的卸载
当类的对象不再被引用,即不可触及时,Class对象就会结束生命周期,类在方法区内的数据也会被卸载。从而结束一个类的生命周期。一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
由用户自定义的类加载器所加载的类是可以被卸载的。
一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。
五,性能监控工具
1,系统性能监控
确定系统的整体状态,基本定位问题所在。
Linux系统的工具
uptime
19:40:31 up 35 days, 5:15, 1 user, load average: 0.00, 0.00, 0.00
top
vmstat 统计系统的CPU,内存,swap,io等情况。如果CPU占用率高,上下文切换频繁,说明系统有线程正在频繁切换。
pidstat 不是系统自带的,需要安装,可以细致观察进程,监控CPU,IO,内存。
Windows下
使用任务管理器查看。
命令行perfmon打开性能监视器。
Windows命令行工具
pslist,需要安装,可用于自动化数据收集;显示Java程序的运行情况。
2,Java自带的工具
作用是查看Java程序运行细节,进一步定位问题。
1)jps
列出Java进程,类似ps命令;参数-q可以指定jps只输出进程ID,不输出类的短名称;参数-m可以用于输出传递给Java进程(主函数)的参数;参数-l可以用于输出主函数的完整路径;参数-v可以显示传递给JVM的参数。
2)jinfo
可以用来查看正在运行的Java应用程序的扩展参数,支持在运行时,修改部分参数;-flag<name>,打印指定JVM的参数值;-flag[+|-]<name>,设置指定JVM参数的布尔值;-flag<name>=<value>,设置指定JVM参数的值。
3)jmap
生成Java应用程序的堆快照和对象的统计信息。
jmap –histo 2999 > d:\histo.txt
4)jstack
打印线程dump
-l,打印锁数量;-m,打印Java和native的帧信息;-F,强制dump,当jstack没有响应时使用。
5)JConsole
图形化监控工具,可以查看Java应用程序的运行概况,监控堆信息、永久区使用情况、类加载情况等。
6)Visual VM
功能强大的多合一故障诊断和性能监控的可视化工具。
六,堆分析
1,内存溢出
内存溢出的原因:堆溢出,永久区溢出,栈溢出,直接内存溢出。
2,MAT
Memory Analyzer
基于Eclipse的软件。分析dump的堆的数据。
浅堆,一个对象结构所占用的内存大小,对象大小按照8字节对齐,浅堆大小和对象的内容无关,只和对象的结构有关。
深堆,一个对象被GC回收后,可以真实释放的内存大小。只能通过对象访问到的所有对象的浅堆之和。
七,锁相关
1,对象头Mark
Mark Word,对象头的标记,32位。描述对象的hash、锁信息,垃圾回收标记,年龄。指向锁记录的指针,指向monitor的指针,GC标记,偏向锁线程ID。
2,偏向锁
大部分情况是没有竞争的,所以可以通过偏向来提高性能。锁会偏向于当前已经占有锁的线程。将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark。只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步。当其他线程请求相同的锁时,偏向模式结束。
-XX:+UserBiasedLocking,默认启用。在竞争激烈的场合,偏向锁会增加系统负担。
3,轻量级锁
BasicObjectLock,嵌入在线程栈中的对象。
由于普通的锁处理性能不够好,轻量级锁是一种快速的锁定方法。如果对象没有锁定,将对象头的Mark指针保存到锁对象中。将对象头设置为指向锁的指针。
如果轻量级锁失败,表示存在竞争,升级为重量级锁,即常规锁。在没有竞争的前提下,减少传统锁使用OS互斥量产生的性能损耗。在竞争激烈时,轻量级锁会做很多额外操作,导致性能下降。
4,自旋锁
当竞争存在时,如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作。
JDK1.6中,使用-XX:+UserSpinning开启。JDK1.7中,改为内置实现。
如果同步快很长,自旋失败,会降低系统性能。如果同步块很短,自旋成功,节省线程挂起切换时间,提供系统性能。
内置于JVM中的获取锁的优化方法和获取锁的步骤:1,偏向锁可用优先尝试偏向锁;2,轻量锁可用会先尝试轻量级锁;3,以上都失败,尝试自旋锁;4,再失败,尝试普通锁,使用OS互斥量在操作系统层挂起。
5,应用程序中锁的优化
减少锁的持有时间;
减小锁粒度,将大对象拆成小对象,增加并行度,降低锁竞争,偏向锁、轻量级锁的成功率提高,比如,ConcurrentHashMap的实现;
6,锁分离
根据功能进行锁分离;比如,ReadWriteLock,读多写少的情况,可以提高性能。
只要操作互不影响,锁就可以分离,比如LinkedBlockingQueue,采用队列和链表。
7,锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。但是,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统资源,不利于系统的优化。
8,锁消除
在即时编辑器,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。
9,无锁
锁是悲观的操作。无锁是乐观的操作。无锁的一种实现方式,CAS,Compare And Swap,非阻塞的同步。在应用层面判断多线程的干扰,如果有干扰,则通知线程重试。
八,Class文件结构
JVM平台不只能编译Java语言,.rb,.groovy也可以编译为.class。Class文件具有语言无关性。
文件结构包括魔数,用来表示是一个Class文件;版本,常量池,访问符,类、超类、接口,字段,方法,属性。