JVM内存

jvm虚拟机

jvm虚拟机类似vmware等,只不过虚拟的机器在实际中不存在,只是软件环境。

目前jvm使用的是hotspot虚拟机(hotspot最初是一家小公司开发,后被sun收购)

jvm执行class文件,除了java可以生产class外,jruby、groovy等语言也可以生成class让jvm执行。

内部以补码表示数据,因为这样做加法可以直接用补码相加,非常方便.

正数的补码是本身,负数的补码是按位取反加1.

jvm启动流程

 

其中windows的jvm.cfg在

jvm.dll在

 

linux的jvm.cfg在

 

 

jvm结构

分为方法区、堆、栈等,此外在jvm外还可以直接在操作系统上分配内存,称为直接内存。jmm是java内存模型(java memory model)

PC计数器(程序计数器、PC寄存器)

当前线程的执行的行号指示器。

  • 每个线程拥有一个PC寄存器
  • 在线程创建时创建
  • 指向下一条指令的地址
  • 执行本地方法(Native)时,它的值为undefined
  • jvm中唯一一个不会产生OutOfMemoryError的区域。

 

方法区

保存装载的类信息,通常也叫永久区(Perm)

方法区是jvm的一个规范,是一个逻辑区,不同的虚拟机的实现不一样,hotspot是把方法区放到了堆的永久代中(jdk8以前),但在jdk8以后,永久代被移除,方法区放到了本地内存中(元空间),因此jdk8中的-XX:MaxPermSize已经失效了,取而代之的是-XX:MaxMetaspaceSize参数

1、存放类、方法、接口等描述信息

2、常量池:数据在编译期被确定,编译到了class文件,分为:

字面量:文本字符串、声明为final的常量值等;

符号引用:类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符

3、运行时常量池:

方法区的一部分,所有线程共享。虚拟机加载Class后把常量池中的数据放入到运行时常量池。运行时常量池可以在程序运行的时候动态增加,比如String.intern()方法,会将程序中的字符串放入运行时常量池。

JDK6时,String等常量信息置于方法区

JDK7时,已经移动到了堆

例如:

1、

/**

* @Described:常量池内存溢出探究

*

* 运行参数: -XX:PermSize=6M -XX:MaxPermSize=6M

*/

 

public class ConstantOutOfMemory {

    public static void main(String[] args) throws Exception {

        try {

            List<String> strings = new ArrayList<String>();

            int i = 0;

            while (true) {

                strings.add(String.valueOf(i++).intern());

            }

        } catch (Exception e) {

            e.printStackTrace();

            throw e;

        }

    }

}

其中String.valueOf(i++).intern()不断向常量池写数据,导致PermGen space异常

 

例2:

import java.lang.reflect.Method;

 

import junit.framework.TestCase;

import net.sf.cglib.proxy.Enhancer;

import net.sf.cglib.proxy.MethodInterceptor;

import net.sf.cglib.proxy.MethodProxy;

 

/**

* @Described:方法区溢出测试

* 使用技术 CBlib

* @VM args : -XX:PermSize=10M -XX:MaxPermSize=10M

*/

public class MethodAreaOutOfMemory {

 

    public static void main(String[] args) {

        while (true) {

            Enhancer enhancer = new Enhancer();

            enhancer.setSuperclass(TestCase.class);

            enhancer.setUseCache(false);

            enhancer.setCallback(new MethodInterceptor() {

                @Override

                public Object intercept(Object arg0, Method arg1, Object[] arg2,

                MethodProxy arg3) throws Throwable {

                    return arg3.invokeSuper(arg0, arg2);

                }

            });

            enhancer.create();

        }

    }

}

通过不断加载类,导致方法区溢出。

jdk1.6会报错:

 

jdk1.7报错:

 

加参数打印gc日志,-XX:+PrintGCDetails

可以看到有多次full gc。新生代和老年代使用都只有0%,永久代用到了99%

存放对象,所有线程共享。堆是分代的,分为年轻代、老年代等

堆分为:年轻代、老年代、永久代(也叫方法区,存放加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,jdk1.8以前方法在堆里面,1.8以后是单独的,在元空间)。

 

年轻代也叫新生代,分为1个eden区和2个servivor区(s0和s1),新生代存放新创建的对象,满了以后,会触发Scavenge GC,回收非存活对象,同时将存活对象放入servivor区,servivor区的from和to会在gc时从from移动到to,当to满的时候,就把对象移动到老年代,同时from和to互换。因此只有多次Scavenge GC都存活的对象,才会放入老年代。Scavenge GC不会触发老年代和永久代的垃圾回收。老年代满了以后,会触发full gc,会清除老年代和永久代的非存活对象。

servivor区的两个总有一个是空的,from和to的大小是相等的,因此统计大小的时候只算其中一个的。

新生代GC也叫Young GC或者Minor GC,老年代GC叫Full GC

引用:

当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制"年老区(Tenured)"。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。

 

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden区能尽快空闲出来。

 

full gc对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。

有如下原因可能导致Full GC:

· 年老代(Tenured)被写满

· 持久代(Perm)被写满

· System.gc()被显示调用

·上一次GC之后Heap的各域分配策略动态变化

 

垃圾回收优先回收年轻代对象,年轻代多次未被回收的对象进入老年代。永久代很少回收.

 

堆溢出测试:

public class Test {

    public static void main(String[] args) {

        List<String> list = new ArrayList<String>();

        while (true) {

            list.add(new String());

        }

    }

}

 

 

 

每个线程创建的时候创建一个栈,线程每一次调用方法时创建一个帧,然后压入线程栈中。

帧由局部变量表、操作数栈、帧数据区(如常量池指针等)组成。

 

java没有数据寄存器,所有参数传递都靠"操作数栈",有返回值的方法返回时,会把返回值放到调用者方法的操作数栈中。

 

线程的栈空间通常很小,只有几百k,因为每个线程单独占用一个栈空间,几百k乘以线程数,就是总的占用空间,因此,栈空间越小,服务器所能运行的线程数越多,而栈空间决定了函数调用的深度,在有递归调用等情况下,栈空间不能太小。

局部变量表:包含方法参数和局部变量

如:

这里右边是局部变量表的槽位,1个槽位32位,long占64位,所以占用了2个槽位(1和2),其他类型如引用类型都占32位

 

第二个方法runInstance是非静态的,所以第一个槽位存放了this的引用

 

一个线程的栈如果满了会溢出,导致java.lang.StackOverflowError一异常。

增大调用深度,可以通过配置-Xss的栈空间参数或者调用减少方法的参数和局部变量(减少方法消耗的栈空间)等方式。

栈空间和调用深度

如设置栈空间大小为1m时:

-Xss1m

 

设置为5m时:

-Xss5m

深度增大为163907了。

 

 

栈的操作示例

先将a、b入栈,然后出栈计算,将结果入栈,最后返回时将结果出栈。

 

 

 

栈上分配(逃逸分析)

1.7以上的jvm内部会自动分析方法内创建的对象会不会被方法外引用(如返回对象引用、将对象引用赋值给全局变量等),如果不会,则会尝试将对象创建到栈上,而不是堆上,提高访问效率。分析的过程叫做"逃逸分析"

如:

public class OnStackTest {

public static void alloc(){

byte[] b=new byte[2];

b[0]=1;

}

public static void main(String[] args) {

long b=System.currentTimeMillis();

for(int i=0;i<100000000;i++){

alloc();

}

long e=System.currentTimeMillis();

System.out.println(e-b);

}

}

如果运行时加参数

-XX:+DoEscapeAnalysis -XX:+PrintGC

其中+DoEscapeAnalysis表示需要进行逃逸分析,+PrintGC表示打印gc日志,此时会在栈上分配new byte[2],只执行了4毫秒,没有gc操作。

 

如果用参数-XX:-DoEscapeAnalysis -XX:+PrintGC表示不进行逃逸分析,new byte[2]将在堆上分配,打印如下:

进行了多次gc,且执行时间用了502毫秒。

 

逃逸分析优势:

消除同步,线程同步的代价是相当高的,同步的后果是降低并发性和性能。逃逸分析可以判断出某个对象是否始终只被一个线程访问,如果只被一个线程访问,那么对该对象的同步操作就可以转化成没有同步保护的操作,这样就能大大提高并发程度和性能。也叫"锁省略"

栈上分配:避免堆上分配的开销,性能得到很大提升。

劣势:

栈上分配受限于栈的空间大小,一般自我迭代类的需求以及大的对象空间需求操作,将导致栈的内存溢出;故只适用于一定范围之内的内存范围请求,大对象或者逃逸对象无法在栈上分配。

 

对象创建时在栈上保存引用,堆上创建对象。

 

线程工作内存和主内存(volatile)

每一个线程有一个独立的工作内存,所有线程有共享的主内存,对象的成员变量等就放在主内存中,线程操作主内存时,先从主内存把变量复制一份到工作内存,线程执行完以后再把更新后的变量写回主内存。

读取时:线程load,主内存read

回写时:线程store,主内存write

 

因为线程执行过程中使用的都是从主内存拷贝的变量,因此线程执行过程中,如果其他变量改变了主内存变量值,当前线程是不知道的,如果变量加了volatile关键字修饰,则每次操作都会立即同步到主内存。

 

如:

public class VolatileStopThread extends Thread {

    private volatile boolean stop = false;

 

    public void stopMe() {

        stop = true;

    }

 

    public void run() {

        int i = 0;

        while (!stop) {

            i++;

        }

        System.out.println("Stop thread");

    }

 

    public static void main(String args[]) throws InterruptedException {

        VolatileStopThread t = new VolatileStopThread();

        t.start();

        Thread.sleep(1000);

        t.stopMe();

        Thread.sleep(1000);

    }

}

 

一个线程不断i++,另一个线程通过stop标记控制它停止,如果没有volatile,则无法控制其停止,因为它只会读取工作内存里的stop标记,不知道另一个线程已经改变了主内存的stop值。

 

也可以用synchronize,synchronize方法执行完时,会回写主内存。不过同步开销较大。

 

volatile不能代替synchronize,因为它只能保证每次操作从主内存读写,而不能保证线程安全性。,如:

 

count++本身线程不安全,有可能两个线程并发同时读取到同样的count值,然后+1,回写两次同样的值。

结论:

  1. volatile解决了线程间共享变量的可见性问题
  2. 使用volatile会增加性能开销
  3. volatile并不能解决线程同步问题
  4. 解决i++或者++i这样的线程同步问题需要使用synchronized或者AtomicXX系列的包装类,同时也会增加性能开销

 

 

指令重排

一个线程内运行的语句,在不破坏逻辑的情况下,可能会被编译重排以提高运行效率,如:

a=1;

b=2;

这种语句,可能b=2先执行,a=1后执行。线程内观察语句是有序的,线程外观察则是无序的,如:

 

 

可以用synchronize做方法同步,保证writer执行完了再执行reader 。(synchronize会对本对象加锁,该对象内的所有synchronize方法都用同一个锁,如果是static的synchronize方法,会在类上加锁,该类创建的所有对象的static synchronize方法用同一个锁)

如:

 

使用volatile修饰变量参与的方法,可以防止发生指令重排。

 

直接内存

直接内存不属于jvm内部使用的内存,而是直接分配在操作系统上。通常是NIO相关类使用,如果操作系统内存不足,它也会抛出OutOfMemory异常,如果dump文件很小,则可能是直接内存溢出。

常用jvm配置参数

jvm配置参数在Run Configurations里面配置,如:

可以在window->preferences->Java->Installed JRE中设置每个jdk的默认启动参数,如:

 

trace跟踪参数

-XX:+PrintGC或-verbose:gc:打印gc日志简要信息

如:

其中第一行:64512K:回收前使用的堆大小,584K:回收后使用的堆大小,246784K:堆的总大小,0.0025309 secs:回收所用时间(秒)

 

-XX:+PrintGCDetails:打印gc详细信息

在程序运行完成后打印。

如:

 

-Xloggc:log/gc.log指定gc日志的保存文件路径

 

-XX:+PrintHeapAtGC: 在每一次gc前后打印堆的信息

与PrintGCDetails不同的是,PrintGCDetails是在程序结束之后打印堆的信息,而PrintHeapAtGC是在每一次gc前后打印堆信息。

PrintHeapAtGC通常和打印gc日志一起用

如:-XX:+PrintHeapAtGC -XX:+PrintGCDetails

 

可以看出gc前eden占用100%,gc后eden占用0%,完全被回收了。from区使用从0%变到了2%

 

-XX:+TraceClassLoading: 打印类加载过程

如:

 

-XX:+PrintClassHistogram打印类的实例数、总大小等信息

程序运行中按下ctrl+break键(待测试),控制台打印类的实例数、总大小等信息

如:

 

堆内存设置:

-Xmx –Xms:设置堆的初始值,最大值

-Xmx20m -Xms5m

指定最大堆20M,最小5M

可以用Runtime.getRuntime()的方法取得当前最大堆、空闲堆、当前堆大小(会自动扩展,扩展最大到最大堆大小)

可以看出分配1M的byte数组以后空闲堆减少了1M

 

 

不设置的话默认是根据操作系统来的,如16G内存下默认打印如下

 

注意,这里跟eclipse的运行内存是没有关系的,eclipse运行内存在eclipse.ini中配置,如:

-Xmn -XX:NewRatio-XX:SurvivorRatio设置新生代、幸存代大小比例

  • -Xmn
    • 设置新生代大小
  • -XX:NewRatio
    • 设置老年代比例,新生代为1
    • 4 表示老年代:新生代=4:1,即年轻代占堆的1/5
  • -XX:SurvivorRatio
    • 设置eden比例,from为1
    • 8表示eden :from =8:1,因此eden(from+to)=8:2,即一个from占年轻代的1/10
    •  

    官方推荐新生代占堆的3/8

    幸存代的from和to区分别占新生代的1/10,也就是eden:from:to=8:1:1

测试:

    代码:

public class Test {

    public static void main(String[] args) {

        byte[] a = null;

        for (int i = 0; i < 10; i++) {

            // 分配1Mbyte数组

            a = new byte[1*1024*1024];

        }

    }

}

 

 

1、参数:

-Xmx20m -Xms20m -Xmn1m

-XX:+PrintGCDetails

输出

此时年轻代分1m内存,老年代分19m(=19456K)。

from和to分别512K,eden为0k,total是eden+from=512k

总内存20m=老年代的19m+年轻代1m

 

由于eden、from都不到1m,因此对象直接放到了老年代,最后老年代占用10890k,相当于10m多点。eden占用-2147483648%表示0,-2147483648%是最小的int,分母为0,所以是它。
没有触发gc

 

2、参数:

-Xmx20m -Xms20m -Xmn10m

-XX:+PrintGCDetails

输出:

此时年轻代分10m,eden区分8m,from区1m,to区1m,给to区1m, total=eden+to=9m(=9216K)

有10个1m的byte数组进入后,由于eden放不下,所以发生了gc,gc回收内存为7841k-1560k=6281k约6m,数组在内存中未释放的剩余约4m,

此时内存占用年轻代是3860k,老年代1024k,加起来大于4m,符合逻辑。

 

 

3、参数:

-Xmx20m -Xms20m -Xmn15m

-XX:+PrintGCDetails

输出:

年轻代15m,老年代5m,eden区12m,from和to各1.5m,所以total=eden的12m +from的1.5m=13.5m(=13824k)

此时10m的byte数组都存到了eden区,没有发生gc。老年代使用率为0.

 

4、参数:

-Xmx20m -Xms20m -Xmn6m

-XX:+PrintGCDetails

年轻代6m,老年代14m

垃圾回收了2次,第一次4829k-1552k=3227k,第二次5809k-2568k=3241k,一共6518k约为6.365m,byte数组剩余约3.635m,而内存中还占用年轻代2677k+老年代2080k=4757k大于3.635,符合逻辑。

 

 

5、参数:

-Xmx20m -Xms20m -Xmn6m -XX:SurvivorRatio=2

-XX:+PrintGCDetails

年轻代6m

SurvivorRatio是eden区和from区的比例2:1,因此eden和(from+to)的比例为1:1,eden分3m,from区1.5m,to区1.5m,发生了4次垃圾回收。

这里每个区的空间大小都是以512K为单位,不能在细分,因为总空间过小,而比例过细会有一些四舍五入的情况,如:

-Xmx20m -Xms20m -Xmn6m -XX:SurvivorRatio=3

此时

根据计算eden应该是6*1024*3/(3+1+1)=3686.4k

而实际分了4096k,因为必须是512k的整数倍。

 

 

6、参数:

-Xmx20m -Xms20m -XX:NewRatio=3 -XX:SurvivorRatio=2

-XX:+PrintGCDetails

 

NewRatio表示老年代:新生代=3:1,因此20m内存分了老年代15m,新生代5m(eden区3m、from区1m、to区1m)

 

-XX:+HeapDumpOnOutOfMemoryError -XX:+HeapDumpPath:OOM(Out Of Memory)时导出堆到文件

如:

-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump

dump出的文件跟堆最大大小差不多

 

-XX:OnOutOfMemoryError:OOM时执行脚本

如:

-Xmx20m -Xms20m

"-XX:OnOutOfMemoryError=D:/printstack.bat %p"

注意双引号不能去掉,否则会执行不了,如脚本内容为:

D:/work/j2ee/jdk/jdk1.7/bin/jstack -F %1 > D:/a.txt

打印线程信息,存到a.txt中。

 

内存溢出时,会自动创建D:/a.txt,如内容为:

控制台打印

-XX:PermSize -XX:MaxPermSize -XX:MaxMetaspaceSize

设置永久代大小,PermSize:初始大小,MaxPermSize:最大值,如:

-XX:PermSize=10M -XX:MaxPermSize=10M

 

Jdk8去掉了永久代,因此-XX:PermSize -XX:MaxPermSize都无效了,方法区放到了本地内存的元空间中,需要用-XX:MaxMetaspaceSize参数进行设置。

-Xss设置栈空间大小

如-Xss1m

OutOfMemory的几种情况

各部分内存加起来不能大于操作系统可分配内存,否则会OutOfMemory。

1、堆溢出

1.1、heap space,应用程序需要分配的堆内存大于-xmx设定值时。解决方案是增加堆空间,及时释放内存。

1.2、heap space,应用程序需要分配的堆内存没有大于-xmx设定值,但操作系统没有多余空间分配给jvm时,所以一般将-xms与-xm设定成相同,一开始就把空间分配给jvm,避免扩展的时候操作系统内存不够。

2、栈溢出

2.1、unable to create new native thread,操作系统用户数限制(如linux默认的普通用户只能创建1024个线程)

2.2、unable to create new native thread,操作系统没有更多内存创建线程。解决方案是减小栈空间或者减小堆空间,使其能创建更多线程。

2.3、java.lang.StackOverflowError,栈空间单个线程内存溢出,解决方案是增加栈空间,或者减少方法参数、减少调用深度。

3、方法区溢出

3.1、PermGen space,加载的类过多等情况。

4、直接内存溢出

OutOfMemory可能没有特殊打印:

1、测试程序:

    public static void main(String[] args) throws Throwable {

        Field field = Unsafe.class.getDeclaredField("theUnsafe");

        field.setAccessible(true);

        Unsafe unsafe = (Unsafe) field.get(null);

        while (true) {

            unsafe.allocateMemory(1024 * 1024);

        }

    }

 

打印:

2、测试程序:

    public static void main(String[] args) throws Throwable {

List<ByteBuffer> bufs = new ArrayList<ByteBuffer>();

        for(int i=0;i<102400;i++){

            bufs.add(ByteBuffer.allocateDirect(1024*1024));

        }

    }

打印:

 

通过排查gc日志可以发现堆空间各个区都有剩余,从而断定是直接内存溢出。

对象内存分配

new创建对象时,首先到常量池找到该类的符号引用,检查该类是否已被加载,如果没有,先加载类,再在堆区分配内存,分配内存时根据垃圾回收策略采用不同的方式,用Serial、ParNew等带压缩过程的收集器时,采用"指针碰撞"算法分配,用CMS等基于标记清除算法的收集器时采用"空闲列表"方式分配。

 

内存分配完成后,写对象头(类的引用、对象的哈希码、对象的GC分代年龄等),设置所有字段都为0。然后执行<init>,设置对象字段的值为程序制定值,然后执行构造函数。

GC

概念:垃圾收集,java中GC的对象是堆和永久区。

引用计数法

通过对象的引用数量来管理,为0的就回收,COM、ActionScript、Python等语言在使用,java没有使用。

引用计数法的问题

1、引用和去引用伴随加法和减法,影响性能

2、很难处理循环引用

这种情况虽然三个对象不能被访问到,但引用数量都为1,不会被回收。

标记清除法

分标记、清除两个阶段,标记阶段,将从根节点可达对象进行标记,清除阶段,把没有标记的对象都清除。

标记压缩法

跟标记清除类似,只是在清除阶段,把所有标记的对象压缩到内存的一端,把边界外的都清除。

复制算法

将空间分为2块,只使用其中1块,垃圾清理时,将正在使用的内存中的存活对象复制到未被使用的那空间,然后清空正在使用的内存,交换两块空间的角色。因此不适用于存活对象较多的场合,如老年代。

 

jvm采用的gc算法(整合标记清理和复制算法)

 

jvm的老年代作为复制算法的担保空间。

from和to区作为复制空间,只是用其中一块,垃圾回收时,将eden区的大对象复制到老年代,将from区存活对象中年龄足够大的对象复制到老年代,之后将eden区和from区的其他存活对象都复制到to区,清空eden区和from区,然后交换from区和to区的角色。

分代思想

根据不同代的特点,选取合适的收集算法

1、少量对象存活,适合复制算法

2、大量对象存活,适合标记清理或者标记压缩

可触及性

对象的可触及状态分为:可触及、不可触及、可复活

可触及:从根节点可以触及到该对象

不可触及:从根节点不可能再触及到该对象

可复活:当前从根节点不可触及到该对象,但以后有可能再次变成可触及。

 

垃圾回收只回收不可触及状态的对象。

 

finalize()方法在gc之前调用,方法里的代码可能把对象又变为可触及的,此时gc就不会回收它,不过finalize()方法每个对象只能调用一次,第二次再gc时就不会调用了,如:

第一次gc时,finalize()方法让可复活对象复活了(obj = this),因此第一次gc时输出obj可用,而由于finalize()只能执行一次,所以第二次gc时没有再复活,输出了obj是null。

 

由于finalize()调用时间不确定(gc优先级低,执行时间不确定),操作不慎容易导致错误,因此要尽量少使用它,可以使用try-catch-finally来替代它。

根的种类

标记清理时从根节点开始查找,根节点的种类

1、栈中引用的对象

2、方法区中静态成员或者常量引用(全局对象)

3、JNI方法栈中引用对象。

Stop-The-World

这是jvm中所有的java代码都暂停现象,gc的时候会引起,此外dump线程、堆dump、死锁检查时也会引起。此时只有native代码可以执行,但不能和jvm进行交互。

危害

1、长时间服务停止,没有响应

2、遇到HA系统,可能引起主备切换,严重危害生产环境

 

GC收集器

默认GC收集器根据不同操作系统情况而不同,客户端模式下默认采用串行回收器,server模式下默认采用-XX:+UseParallelGC并行回收期器。

如:

打印日志如下(默认并行回收器):

-Xmx10M -Xms10M -XX:+PrintGCDetails

串行收集器

使用单个线程做GC操作。

优点:比较稳定,收集效率高

缺点:可能引起较长时间停顿

 

参数:

-XX:+UseSerialGC

启用以后,新生代和老年代都会使用串行回收,新生代使用复制算法,老年代使用标记压缩算法,

日志如:

-Xmx10M -Xms10M -XX:+PrintGCDetails -XX:+UseSerialGC

 

并行收集器

ParNew收集器

多线程做GC操作,只能作用于新生代

参数:

-XX:+UseParNewGC

启用以后,新生代并行,老年代还是串行,新生代使用复制算法,老年代标记压缩算法。

 

-XX:ParallelGCThreads

限制线程数量,如-XX:ParallelGCThreads=20

 

日志如:

-Xmx10M -Xms10M -XX:+PrintGCDetails -XX:+UseParNewGC

 

Parallel收集器

 

类似ParNew,更关注吞吐量

 

引用:

Parallel Scavenge收集器的关注点与其他收集器不同, ParallelScavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称为"吞吐量优先"收集器。

该垃圾收集器,是JAVA虚拟机在Server模式下的默认值,使用Server模式后,java虚拟机使用Parallel Scavenge收集器(新生代)+ Serial Old收集器(老年代)的收集器组合进行内存回收。

 

 

 

-XX:+UseParallelGC

使用Parallel收集器+ 老年代串行,新生代使用复制算法,老年代标记压缩算法。

如:

-Xmx10M -Xms10M -XX:+PrintGCDetails -XX:+UseParallelGC

 

 

-XX:+UseParallelOldGC

老年代并行使用Parallel收集器,新生代采用系统默认收集器(系统一般默认就是UseParallelGC收集器),老年代使用标记清除算法。

如:

-Xmx10M -Xms10M -XX:+PrintGCDetails -XX:+UseParallelOldGC

 

-XX:MaxGCPauseMills

设定一次gc的最大停顿时间,jvm会尽力保证每次gc在该时间范围内(会自动调整堆大小等参数),如果设置过小,会导致gc次数增加,谨慎使用。GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。

 

-XX:GCTimeRatio

设定应用程序线程占用的cpu时间比例(会自动调整各以达到该设定参数),gc占用cpu默认为1,如果设置为9,表示90%时间用于应用程序线程,10%时间用于gc线程。默认99,即默认99%的时间用于应用程序线程,1%时间用于gc线程。

应用程序线程的占用cpu时间比例决定了系统的吞吐量,因此值越大,吞吐量越高。

 

-XX:MaxGCPauseMills与-XX:GCTimeRatio是互相矛盾的,最大停顿时间越小,就要求gc占用cpu时间高才行,停顿时间和吞吐量不可能同时调优,实际中前者设定的优先级高于后者,先满足-XX:MaxGCPauseMills的设定,简单说就是要么把吞吐量少分给gc线程,要么把停顿时间少分给gc线程,不能都少分,因为工作量(需回收垃圾数量)一定。

 

并发收集器

Concurrent Mark Sweep简称CMS收集器

可以同应用程序线程并发执行,大大降低系统停顿时间,采用并发的标记清除算法(不能用标记压缩算法,因为并行执行,被移动的对象可能正被使用),只作用于老年代。

缺点是:并发阶段由于占用cpu资源,会导致系统吞吐量降低,由于在清理阶段,应用程序还在运行,因此清理不彻底,没有一个时间点是垃圾完全清理完的状态。

因为和用户线程一起运行,不能在空间快满时再清理

-XX:CMSInitiatingOccupancyFraction设置触发GC的阈值

如果不幸内存预留空间不够,就会引起concurrent mode failure,不过此时CMS会自动改用串行回收器回收。

 

-XX:+UseConcMarkSweepGC

开启以后新生代使用ParNew收集器,老年代使用CMS收集器

 

运行过程:

 

分几个阶段:

初始标记:标记根可以直接关联的对象(全局停顿)

并发标记:标记全部存活对象(并发执行)

重新标记:由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正(全局停顿)

并发清除:清理不存活对象(并发执行)

并发重置:为下一次标记做准备

 

由于主要过程是并发标记,且可以并发执行,所以全局停顿的时间会很短。

日志如:

-Xmx10M -Xms10M -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC

 

各阶段的日志:

CMS-initial-mark:初始标记

CMS-concurrent-mark:并发标记

CMS-mark:重新标记

CMS-concurrent-sweep:并发清除

CMS-concurrent-reset:并发重置

 

 

错误日志,如:

 

-XX:+ UseCMSCompactAtFullCollection
设置在gc之后进行碎片整理(标记清除算法会产生大量碎片),不过会引起全局停顿(因为要移动对象,此时不能让应用程序使用对象)

 

-XX:+CMSFullGCsBeforeCompaction

设定几次full gc之后进行碎片整理

 

XX:ParallelCMSThreads

设定CMS线程数量

类装载验证

类装载验证流程

 

分为加载、链接、初始化

 

加载:取得类的二进制流,转为方法区数据结构,在Java堆中生成对应的java.lang.Class对象,加载的类来源可以是class文件、jar包里的类、网络上的类等。

 

链接:分验证、准备、解析3个阶段

a:验证:验证class文件格式、元数据验证(是否有父类、是否继承了final类、非抽象类实现了所有抽象方法等)、字节码验证、符号引用验证(访问的方法或字段是否存在且有足够的权限

)。

b:准备:分配内存,为方法区中的类成员变量设置初始值,int、long等初始化为0,boolean为false、对象为null。对于static final修饰的基本类型、String类型,在准备阶段就会被赋上设定的值如:public static final int v=1

c:解析:符号引用(方法名等)替换为直接引用(偏移地址)。

 

初始化:

执行类构造器<clinit>(class init的缩写)

a:为static成员变量赋设置的值

b:执行static{}块。

(对象的初始化类似,先为父类成员赋设置的值,执行父类构造函数,然后是子类的,即子类的<clinit>调用前保证父类的<clinit>被调用)

 

ClassLoader

ClassLoader概念和分类

ClassLoader是一个抽象类,它的实例负责类装载过程中的加载阶段,将java字节码读取到JVM中,它可以定制,满足不同的字节码流获取方式。

 

ClassLoader分类:

Bootrap ClassLoader(启动ClassLoader),对应rt.jar 或者用启动参数-Xbootclasspath设定

Extension ClassLoader(扩展ClassLoader),对应%JAVA_HOME%/lib/ext/*.jar

App ClassLoader(应用ClassLoader),对应Classpath下

Custom ClassLoader(自定义ClassLoader),对应自定义路径

 

Bootrap ClassLoader没有父亲,其他都有父亲,也就是上一级,如App ClassLoader的Parent是Extension ClassLoader,而Extension ClassLoader的父亲是Bootrap ClassLoader。

Bootrap ClassLoader不是由java编写的,而是jvm实现的一部分,jvm启动时首先加载Bootrap ClassLoader,然后Bootrap ClassLoader再去加载Extension ClassLoader、App ClassLoader等。

双亲委托模式

原理

找已经加载的类从下往上

加载类从上往下,先到rt.jar中找类加载。

加载类时,会先让父亲ClassLoader去加载,如果没有加载到,自己再加载,最顶层的parent就是Bootrap ClassLoader。

自定义类加载器时,只需要实现findClass方法即可。

使用双亲委托模式原因:

1、避免类重复加载

2、出于安全性考虑,不能随意用自定义的String等类。

 

实验:

在default package创建一个HelloLoader类,然后把HelloLoader.class放到D:/,

然后把I am in BootLoader改为I am in AppLoader创建FindLoader

不加参数,执行后打印I am in AppLoader

加参数-Xbootclasspath/a:D:/,执行后打印I am in BootLoader

说明加载类时先用Bootstrap ClassLoader加载。

 

public class HelloLoader {

    public static void hello() {

        System.out.println("I am in BootLoader");

    }

}

public class HelloLoader {

    public static void hello() {

        System.out.println("I am in AppLoader");

    }

}

 

 

public class FindLoader {

 

    public static void main(String[] args) {

        HelloLoader helloLoader = new HelloLoader();

        helloLoader.hello();

    }

}

 

也可以自己指定Class Loader加载类,而不用系统默认的ClassLoader顺序,需要在java代码中手动调用指定ClassLoader的加载类方法。

 

破坏

很多情况下需要在顶层ClassLoader中加载下层ClassLoader的类,比如顶层的一些类方法工厂需要创建下层ClassLoader的类对象等,而根据双亲模式这是不允许加载的。

 

双亲模式是默认的模式,但不是必须这么做

 

采用上下文加载器可以实现parent加载下级的类,原理是在parent加载器中传入下级ClassLoader的实现。

 

Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent

OSGi的ClassLoader形成网状结构,根据需要自由加载Class

Class.forName初始化

Class.forName与ClassLoader的loadClass类似,都是加载类,不过loadClass只做加载,不过链接(link)和初始化(initialize),而Class.forName默认是3步操作都做,也可以设定只做第一步(forName第二个参数设置为false,ClassLoader.loadClass(className,false);)。

JDBC Driver中只能使用Class.forName,因为他的static块中执行了一些操作,而这是在第3步。

static {

try {

java.sql.DriverManager.registerDriver(new Driver());

} catch (SQLException E) {

throw new RuntimeException("Can't register driver!");

}

}

 

使用Class.forName,然后执行newInstance()可以创建对象,如:

Class c = Class.forName("A");

factory = (AInterface)c.newInstance();

 

这同new创建的效果一样,不过将forName与newInstance可以实现解耦,类名可以作为参数传入,而AInterface可以是接口,这样可以实现工厂方法模式。

 

newInstance只能调用无参构造函数。如果没有默认构造函数则无法创建对象实例。

 

性能监控工具

uptime

系统当前时间,开机到现在经过的时长,连接数(每个终端算一个连接),平均负载(过去1, 5, 15 分钟,系统平均要负责运行几个进程)

 

单核cpu的满负载是load average为1

双核cpu的满负载是load average为2

4核cpu的满负载是load average为4

以此类推。

 

查看cpu核数命令:

grep 'model name' /proc/cpuinfo | wc -l

 

top

 

vmstat

统计系统的CPU,内存,swap,io等情况

CPU占用率很高,上下文切换频繁,说明系统有线程正在频繁切换

 

pidstat

细致观察进程工具,监控CPU、IO、内存,它可以查看某个进程下每个线程的情况。

安装:

1、下载

wget http://pagesperso-orange.fr/sebastien.godard/sysstat-11.5.4.tar.gz

 

2、解压安装

tar -zxf sysstat-11.5.4.tar.gz

cd sysstat-11.5.4

./configure

make

make install

 

使用:

如:

pidstat -p 4165

pidstat -p 4165 -u

其中4165是进程号,默认是查看cpu情况(相当于加了-u,监控cpu)

指定刷新间隔时间与次数

如:

pidstat -p 4165 1 3

-t查看线程

加-t可以查看线程情况,如:

-d查看磁盘io情况

如:

pidstat -p 4165 -d –t

查看4165这个进程下所有线程的磁盘io情况

windows性能监控工具

任务管理器

 

perfmon性能监视器(查看每个线程)

开始-运行,perfmon命令

 

Process Explorer

需要安装,可以查看每个线程情况。

pslist

需要安装,命令行显示,可用于自动化测试等。

Java自带工具

当排查出系统性能是由于java进程影响所致,进一步可以使用java自带工具进行排查。

自带工具都在jdk安装目录下,如:

具体实现在lib\tools.jar\sun\tools里

 

jps 查看进程

查看java进程

如windows上执行此程序后:

 

jps查看

 

-q不显示类名

 

-m显示传递给主函数的参数

Test主函数没有传递参数,所以没有,Tomcat服务器上的可能有参数,如:

 

-l显示主函数完整路径

Tomcat服务器上输出,是tomcat的类。

 

-v显示jvm运行参数

 

Tomcat服务器上输出:

都是自己配置的参数

jinfo 查看或修改jvm参数

  • -flag <name>:打印指定JVM的参数值
  • -flag [+|-]<name>:设置指定JVM参数的布尔值
  • -flag <name>=<value>:设置指定JVM参数的值

 

查看,如:

jinfo -flag PrintGCDetails 8704

其中8704是进程id,这里查看是否开启了GC日志详情打印

 

修改,如:

jinfo -flag +PrintGCDetails 8704

 

 

jmap 打印堆快照,dump堆

打印堆快照,如:

jmap -histo 8324

打印java进程8324的堆快照

 

由于太长,一般会重定向存到文件里

如windows上:

jmap -histo 8324 > d:\a.txt

 

linux上:

jmap -histo 4165 > /home/kkk/a.txt

 

 

 

dump堆,如:

windows:

jmap -dump:format=b,file=d:\heap.hprof 8324

 

linux:

 

jstack打印进程内的线程信息

jstack 进程号

如:

jstack 8324

会打印出该java进程内所有的线程信息

由于内容太多,一般会重定向存到文件

jstack 8324 > d:\jstack.txt

 

 

选项:

-l 打印锁信息

-m 打印java和native的帧信息

-F 强制dump,当jstack没有响应时使用

 

JConsole图形化查看

命令行输入jconsole即可打开

 

进去后可进行各种查看

右上角的"执行GC",可以强制触发gc

线程页可以查看每个线程的状态。

Visual VM图形化查看

比jconsole功能更强大

运行jdk安装目录的bin下的jvisualvm.exe即可打开

开jmx监控

远程tomcat开jmx监控:

 

一. 修改远程机器JDK配置文件

1.进入JAVA_HOME\jre\lib\management\目录

如:

cd /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/lib/management

2.

cp jmxremote.password.template jmxremote.password

c.打开jmxremote.password文件,去掉 # monitorRole QED 和 # controlRole R&D 这两行前面的注释符号

sudo vim jmxremote.password

 

二. 修改远程机器上需要被监控的程序的配置文件

1.进入TOMCAT_HOME\bin目录

2.打开catalina.sh文件,加入如下信息:

JAVA_OPTS="$JAVA_OPTS -Djava.rmi.server.hostname=192.168.128.129

-Dcom.sun.management.jmxremote=true

-Dcom.sun.management.jmxremote.port=18999

-Dcom.sun.management.jmxremote.ssl=false

-Dcom.sun.management.jmxremote.authenticate=false"

3.重启Tomcat服务.

 

三. 客户端VisualVM配置 (我客户端用的是WinXP).

a.直接右键点击Remote,选择Add Remote Host...

b.在弹出的界面中输入远程机器的IP地址(192.168.128.129),这个IP地址会加入到Remote节点下.

c.右键点击这个IP地址,选择Add JMX Connection, 在弹出的界面中输入刚配置的端口号(18999), 这个连接会加入到该IP节点下.

d.双击这个连接,选择Open.

 

 

问题排查步骤

先找出问题进程是否是java(top命令),如果是,查看java中哪个线程的问题(pidstat),然后通过jstack命令找出该线程id(10进制转16进制,如:printf %x 3467)当前执行的代码,检查该代码。

 

以上也可以不使用命令,而用jconsole、visual VM等工具查询更方便

 

参考"服务器排查.rtf"

 

 

 

死锁

线程间执行同步方法等情况是,互相等待对方释放锁资源而导致都无法执行(通常是多个线程形成环状)的情况。

 

对于简单的死锁,jstack最后会输出出来,对于复杂的死锁,则需要手工排查。

 

使用MAT工具分析堆

MAT是分析堆的工具Memory Analyzer (MAT),官网:

http://www.eclipse.org/mat/

 

下载解压,打开

 

可以打开正在运行的程序堆,或者打开一个堆文件进行分析。

 

打开正在运行的java堆。

 

 

几个常用视图:

1、对象内存占用情况:

 

2、对象支配树

 

3、线程信息

 

4、图形化查看对象占用内存情况

 

5、查看大对象outgoing(出引用,它引用的其他对象)和incoming(入引用,引用本对象的对象)对象,一般用出引用排查

 

一般通过支配树视图即可查看哪些对象的深堆较大,然后层层找到占用内存高的对象

 

支配树

如果访问对象2必须经过对象1,则1就支配2,垃圾回收1时,2也会一起回收。

浅堆和深堆

浅堆表示对象结构所占内存大小。

深堆表示该对象内存及仅能被该对象所能触及到的(即它支配的)其他对象的所有浅堆的大小的总和,也就是垃圾回收时该对象能够释放的内存大小。

这里对象A的浅堆为A的大小

A深堆大小为A+D

A的实际大小为A+C+D

由于C同时还被B所触及,所以不能算入A的深堆。

 

如String类型的对象浅堆都是24字节,跟字符串长度无关,其中8个字节是一个char[]引用,而深堆大小则是根据字符串长度来决定。

jdk6的String结构:

 

jdk7的String结构

 

示例:

Point.java

 

Line.java

 

定义两个类Point.java和Line.java

首先创建abcdefg7个Point对象,然后设置到aLine、bLine、cLine、dLine4条线上。

再把7个Point引用设置为null,观察浅堆和深堆情况。

关系图:

堆内存图:

dLine对象,浅堆为24字节,深堆也是24字节,这是因为dLine对象内的两个点f和g没有被设置为null,因此,即使dLine被回收,f和g也不会被释放。

 

对象cLine内的引用对象d和e由于仅在cLine内还存在引用,因此只要cLine被释放,d和e必然也作为垃圾被回收,即d和e在cLine的保留集内,因此cLine的深堆为24*2+24=72字节。

 

对于aLine和bLine对象,由于两者均持有对方的一个点,因此,当aLine被回收时,公共点a在bLine中依然有引用存在,故不会被回收,点a不在aLine对象的保留集中,因此aLine的深堆大小为24+24=48字节。对象bLine与aLine完全一致

使用MAT分析tomcat内存溢出实例

1、servlet代码:

public class T extends HttpServlet {

    private static final long serialVersionUID = 1L;

 

    public static Map<String, Object> sessions = new ConcurrentHashMap<String, Object>();

    

    @Override

    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 每个请求都放一个1M数组到全局sessions

        sessions.put(UUID.randomUUID().toString(), new byte[1024]);

    }

 

public T() {

}

}

运行参数:

-Xmx32M -Xms32M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump

制定32M内存,溢出时自动dumpa.dump文件

 

2、用jmeter压测该servlet直到内存溢出

 

3、用MAT打开a.dump文件

发现总共25M,有一个类对象占用了19M,查看outgoing

发现sessions(ConcurrentMap组成的list)的深堆大小为19M

继续进入ConcurrentMap的每个segments下的table发现都是被1M的数组占用。

使用visual VM分析堆

导入堆dump文件后,可以查看类信息,在下面手动输入类名进行过滤。

 

双击类名,可以查看实例信息,左侧点某个实例,右侧显示实例属性信息,及被引用信息

 

使用OQL查询语言进行查询,如:

1:

select {a:p.x,b:p.y} from testJava3.Point p

 

 

其中p.x和p.y是Point的属性名

 

2:

select referrers(p) from testJava3.Point p

where p.x==0 && p.y==0

 

线程安全和锁

线程安全

多线程操作共同的对象,会引起线程安全问题如:

ArrayList的线程不安全示例(ArrayList底层使用数组实现,不是线程安全的)

 

public class AddToList implements Runnable {

    public static List<Integer> numberList = new ArrayList<Integer>();

 

    @Override

    public void run() {

        for (int i = 0; i < 1000000; i++) {

            // 多线程同时add,会造成ArrayIndexOutOfBoundsException异常,因为ArrayList底层使用数组实现,线程不安全。

            numberList.add(0);

        }

    }

 

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(new AddToList());

        Thread t2 = new Thread(new AddToList());

        t1.start();

        t2.start();

        

        // 确保异步线程都结束后,主线程再继续往下执行,打印numberListsize,否则打印的size0.

        while (t1.isAlive() || t2.isAlive()) {

            Thread.sleep(1);

        }

        System.out.println(numberList.size());

    }

}

2个线程同时调用同一个ArrayListadd方法,造成数组越界。

打印:

 

线程安全的解决办法

1、互斥同步(也叫阻塞同步)

同一段代码同一时间只能让一个线程执行。

利用操作系统级别的互斥锁(Mutex Lock),多个线程抢占互斥量,同一时间只能有一个线程可以抢占。其他线程试图抢占,会被挂起,等待互斥量解锁。

互斥同步需要操作系统由用户态切换到内核态,非常消耗性能。

2、非阻塞同步

采用类似乐观锁的机制,操作系统提供了CAS(Compare-and-Swap)等函数实现。

cas有3个参数cas(v,a,b),当v==a时,让v=b,否则v不变,函数返回v的旧值,通过比较v的值和函数返回值判断v是否更新成功。

3、无同步

将线程不安全的代码改成安全的,也叫可重入代码。如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等

JVM的锁优化

为了减少互斥同步的消耗,jvm做了一些优化,如在线程挂起之前做一些空循环等待,用乐观锁的方式做一些优化等,这些优化跟对象头Mark紧密相关。

对象头Mark

对象头分为2到3部分。(每一部分长度占一个字,具体根据操作系统而定,32位操作系统则为32位)

第1部分:叫做Mark Word。存储内容根据状态不同而不同:

其中,最后2位是状态位,根据最后2位状态的不同,前面30位存的内容也不同。

如:状态为01 (没有锁定时) 时:25bit用于存储对象哈希码(HashCode),4bit用于存储对象分代年龄,1bit存是否偏向锁标志,2bit用于存储锁标志位。

 

状态为00时:前面30位存指向锁记录的指针,后面2bit用于存储锁标志位。

 

第2部分:存储方法区的类的引用地址,通过它才能找到它是哪个类的对象。

第3部分:只有数组对象才有,存储数组的长度。

 

轻量级锁

线程会在栈中开辟一块空间,存放锁对象的信息,类似一张表,也叫做Lock Record,包含"对象的Mark Word"、"owner"等字段,每一个同步对象会保存一条记录。

执行步骤:

1、执行同步方法之前先执行monitorenter,获取对象锁时,先把对象的Mark Word前30bit数据保存到Lock Record,同时将对象Mark Word的地址保存在Lock Record的owner字段,以便知道锁定的是哪个对象。

 

2、然后通过cas方法将对象的Mark Word更新为指向Lock Record中Mark Word的指针,如果成功,则表示获取锁成功,更新Mark Word锁状态为00,执行同步代码。这样对象的Mark Word就相当于迁移到栈中来了。

 

3、如果不成功,检查对象Mark Word是否已经执行本栈,如果是,直接执行同步代码。如果不是,表示有其他线程获取了锁,将对象Mark Word设置为互斥锁(重量级锁)的地址,更新对象Mark Word锁状态为10(膨胀为重量级锁),同时进入阻塞状态,后面等待锁的线程也会被挂起

 

4、同步代码执行完成,执行monitorexit,释放锁时,通过cas方法还原对象的Mark Word内容,如果成功,表示锁释放成功。如果失败,表示Mark Word被其他线程改过,且改为了互斥锁的地址。此时会唤醒阻塞的线程。

 

流程图:

 

获取锁成功cas的图示

CAS前:

 

CAS后:

栈中保存了对象的mark word,对象的mark word内容换成了栈的地址(stack pointer)

 

轻量级锁在没有多线程竞争的情况下,不需要使用互斥锁,只需要做cas操作,提供了效率。

偏向锁

1、偏向锁获取过程:

  (1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。

  (2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。

  (3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。

  (4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。

  (5)执行同步代码。

 

2、偏向锁的释放:

  偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为"01")或轻量级锁(标志位为"00")的状态。

 

偏向锁可以减少cas操作

偏向锁在同一个线程多次使用同一个对象的锁时可以减少同步时间消耗,但对于频繁切换线程使用该锁时,会增大开销。

 

设置:jvm参数:

  • -XX:+UseBiasedLocking

默认是开启的

-XX:BiasedLockingStartupDelay=0

表示jvm已启动就开启偏向模式,默认是启动一段时间以后再开启。

 

自旋锁

线程在没有获取到锁时,做一些空操作等待,

JDK1.6中-XX:+UseSpinning开启,JDK1.7中,去掉此参数,改为内置实现

如果同步块很长,自旋失败,会降低系统性能;如果同步块很短,自旋成功,节省线程挂起切换时间

锁总结

轻量级锁、偏向锁、自旋锁都不是Java语言层面的锁优化方法,是内置于JVM中的获取锁的优化方法和获取锁的步骤。

使用顺序为

偏向锁>轻量级锁>自旋锁>重量级锁

减小锁粒度

将大对象拆成小对象可以增加并行度,降低锁竞争。如ConcurrentHashMap。但代价是增加同步开销。

 

锁分离(读写分离)

读锁允许多个线程同时读

锁粗化

与减小锁粒度相反,将多个同步代码块合并,较小同步开销,代价是增大了锁竞争。

锁消除

逃逸分析等。

无锁

也就是乐观锁,如cas操作,java.util.concurrent.atomic包使用无锁实现

Class文件结构

jvm执行文件都是class文件,出java外,jRuby、groovy语言都可以生成class文件供jvm执行。

以下以此java文件编译的class文件为例:

 

package com.test;

 

public class TestClass {

    private int m;

    public int inc() {

        return m + 1;

    }

}

二进制文件和文本文件

文件都是由二进制数1、0组成,8位组成一个数字,也叫一个字节,字节是计算机的最小单位,通常说的偏移位置这些,都是以字节为单位,不是二进制数的位置。一个字节数字所能表示的范围是0到255(8位全1是255)根据字节内容不同,及字节间组合内容不同,表示不同含义。

 

如果直接编辑1、0会很难理解,因此通常将1个字节分成2个4位,每个4位用1个16进制数表示(也就是0到F,F就是十进制的15,二进制的1111),所以编辑器看起来都这样:

左边00000000h等表示该行第一个字节的起始位置。h表示16进制,10h就是十进制的16。

 

如果编辑文本文件,这样编辑显然不方便,因为还需要记住哪些字节的组合对应了哪些字符,不同编码方式的对应也不一样(如utf-8编码中61对应字母a,62对应字母b)

如:

 

编辑器可以选择用二进制或者是文本方式编辑,文本方式就不用去记住。

 

而一般的非文本文件如果用文本方式查看,则会出现乱码,因为很多字节在文本中找不到对应。如:

 

 

文件的类型通常在文件的头部声明(扩展名识别只是给用户看的),方便应用程序识别,文本文件的头部不需要声明,因为它本身就不需要识别,只需要存取内容。

全限定名、简单名称、描述符

全限定名:类的全限定名就是将类全名中的"."替换为"/",如com/test/TestClass。

简单名称:就是没有类型和参数修饰的方法或者字段名称,如inc()方法和m字段的简单名称就是inc和m。

描述符:字段或方法的描述符用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示

对于数组类型,每一维度将使用一个前置的"["字符来描述,如一个定义 为"java.lang.String[][]"类型的二维数组,将被记录为:"[[Ljava/lang/String;",一个整型数 组"int[]"将被记录为"[I"。

 

用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的 严格顺序放在一组小括号"()"之内。如方法void inc()的描述符为"()V",方法 java.lang.String toString()的描述符为"()Ljava/lang/String;",方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)的描述符为"([CII[CIII)I"。

 

class文件结构

class文件可以用jclasslib工具查看字节码信息。

class文件内容没有任何分隔符,全部紧密排列,先约定好各位置的含义。对于个数不定的情况(如常量个数),都先定义好个数,对于变长的内容(如常量字符串)都先定义好长度,然后按定义的长度去划分。

 

 

其中u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数

 

class概览信息

用jclasslib打开class,在General Information可以看到(除了magic)

magic(魔数)

也就是class的文件头声明,表示它是一个class文件,固定是CAFEBABE(由于16进制字母只有A-F,所以不能是COFEBABY)

class版本号(minor_version、major_version)

 

版本号对应表:

 

访问标志(access_flags)

访问标志描述了类的public、private、protect等属性,如下表:

 

 

 

这里这个类只有ACC_PUBLIC、ACC_SUPER为真,其他ACC_FINAL、 ACC_INTERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、 ACC_ENUM等6个标志都为假,所以access_flags=0x0001|0x0020=0x0021

类索引、父类索引与接口索引(this_class、super_class、interfaces_count、interfaces)

类索引、父类索引都是一个引用,指向常量池的类和父类的全限定名。如果没有实现任何接口,接口索引集就是00。父类索引不可能为0,因为至少都是Object的子类。

 

常量池(constant_pool_count、contant_pool)

常量池存放字面量和符号引用:

字面量:字符串、数字等,通常是CONSTANT_Utf8_info、CONSTANT_Integer_info、CONSTANT_Float_info等类型。

 

符号引用:它是一个引用。可以指向类、方法等。(Class文件中不会保存各个方法、字段的最终内存布局信息,当虚拟机运行时,需要从常量池获得对应的 符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。)

 

常量的个数是常量constant_pool_count-1,预留了一个。(设计者将第0项常量空出来是有特殊考虑的,这样做的目的在 于满足后面某些指向常量池的索引值的数据在特定情况下需要表达"不引用任何一个常量池 项目"的含义

 

由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大值,既u2类型能表达的最大值65535(2个字节)。所以Java程 序中如果定义了超过64KB英文字符的变量或方法名,将会无法编译

常量池类型表

 

格式,如:

其中,tag是标志位,用于区分常量类型。

 

如:

 

使用jclasslib查看如下:

 

 

也可以用javap命令查看常量池:

javap -verbose TestClass.class

 

这里第一个常量类型是07,对应CONSTANT_Class_info,是一个指向类名的引用,值是02,含义是指向第二个常量的引用.

第二个常量类型是01,对应CONSTANT_Utf8_info,是一个字符串,内容是com/test/TestClass。上图中#2表示指向第二个常量位置的引用。

 

 

字段表集合(fields_count、fields)

描述类中字段的个数,每个字段的访问权限、名称等信息

 

这里只有一个字段,它的access_flags是2,对应了ACC_PRIVATE,可以推断出代码是:

"private int m;"

 

带属性的字段,如:

 

如:n这个字段

方法表集合(methods_count、methods)

与字段表很类似,它是描述类的方法,访问权限,参数等

 

这里有2个方法,一个是系统自动添加的构造方法,另一个是用户自定义的inc()方法。

有一个属性,值是常量Code,说明此属性是方法的描述。

 

java中不能仅依靠返回值不同定义重载,因为方法签名不包含返回值,这样会报错。而class文件中是允许仅依靠返回值不同定义重载的,因为jvm的方法签名包含了返回值。

 

属性表集合(attributes_count、attributes)

属性表集合除了在class类后面,在字段、方法后面也可以有,说明它们的一些特性。

如:

这个就是在构造方法init后面的属性。

属性表的结构:

attribute_name_index是属性的名字,指向常量区的一个引用,有很多种类型,如:Code、ConstantValue、Deprecated等,不同属性根据attribute_name_index指向的字符串内容不同来区分。

attribute_length:属性info的长度,编译器通过它才知道属性定义在哪里结束。

info:属性的内容,不同属性内容结构不同,格式也不同

如上面的max_stack、max_locals、code_length、code合起来就是Code这种属性的info。

 

属性表的类型:

Code属性

当属性表的attribute_name_index指向常量池的字符串为Code时,它就是个Code属性,通常跟在方法后面,方法的代码都在code属性里。

它的结构如下(从max_stack到attributes就是上面的info)。

 

 

可以用jclasslib查看代码对应的指令,如:

max_stack

操作数栈的最大深度

max_locals

局部变量表的空间大小(也就是槽位个数,槽位就是slot),对于byte、char、float、int、short、boolean 和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这 两种64位的数据类型则需要两个Slot来存放。

 

并不是 在方法中用到了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals的值,当pc计数器超出一个局部变量的作用域时,这个局部变量 所占的Slot可以被其他局部变量所使用。

如:

此时槽位数是3,分别代表this引用、变量a、变量b

 

此时槽位数变成了2,分别是this、a和b共用槽位,因为a的作用域完了之后才定义的b,b可以重用a的空间。

code

code就是执行的代码指令,因为code_length是u4,理论可以执行2的32次方条指令,但虚拟机规范了一个方法最多只能有65535条指令。

exception_table

异常表,描述异常语句跳转

如:

LineNumberTable属性

LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对 应关系。是Code属性的属性,它的作用是断点调试跟踪,以及抛出异常时显示行号

如:

LocalVariableTable属性

LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,也是Code属性的属性,它的作用是关联源码时显示源码变量名,而不是arg1、arg2这种

如:

 

Exceptions属性

描述方法会抛出哪些异常,类似于throws声明

 

如方法声明了throws Exception

SourceFile属性

SourceFile属性用于记录生成这个Class文件的源码文件名称。如果不设置,抛异常时不会显示出错代码所属的文件名。

ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为变量赋值。只有final属性修饰的基本类型、String类型会自动加这个属性。

InnerClasses属性

InnerClasses属性用于记录内部类与宿主类之间的关联。

 

Deprecated与Synthetic属性

 

Deprecated表示方法已经过时,Synthetic表示此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的

如:

字节码指令

加载和存储指令

将一个局部变量加载到操作栈(load):iload、iload_<n>、lload、lload_<n>、fload、fload_ <n>、dload、dload_<n>、aload、aload_<n>。

 

将一个数值从操作数栈存储到局部变量表(store):istore、istore_<n>、lstore、lstore_<n>、 fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。

 

将一个常量加载到操作数栈(push、const、ldc):bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、 iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>

 

 

其中<n>表示一个数字,也就是局部变量的槽位slot,static方法从0开始,非static方法从1开始(0已经被this占用)

如:

 

    void test() {

        int a = 2;

        int b = a;

    }

对应字节码

0 iconst_2

1 istore_1

2 iload_1

3 istore_2

4 return

 

 

    static void test(int a, int b) {

        a = 1;

//        0 iconst_1

//        1 istore_0

        

        int c = 3;

//        2 iconst_3

//        3 istore_2

        

        b = 2;

//        4 iconst_2

//        5 istore_1

    }

 

运算指令

加法指令:iadd、ladd、fadd、dadd。

减法指令:isub、lsub、fsub、dsub。

乘法指令:imul、lmul、fmul、dmul。

除法指令:idiv、ldiv、fdiv、ddiv。

求余指令:irem、lrem、frem、drem。

取反指令:ineg、lneg、fneg、dneg。

位移指令:ishl、ishr、iushr、lshl、lshr、lushr。

按位或指令:ior、lor。

按位与指令:iand、land。

按位异或指令:ixor、lxor。

局部变量自增指令:iinc。

比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。

 

如:

 

    static void test() {

        int a = 2;

//         0 iconst_2

//         1 istore_0

        

        int b = 3;

//         2 iconst_3

//         3 istore_1

        

        int c = a + b;

//         4 iload_0

//         5 iload_1

//         6 iadd

//         7 istore_2

        

        c = a - b;

//         8 iload_0

//         9 iload_1

//        10 isub

//        11 istore_2

        

        c = a * b;

//        12 iload_0

//        13 iload_1

//        14 imul

//        15 istore_2

        

        c = a / b;

//        16 iload_0

//        17 iload_1

//        18 idiv

//        19 istore_2

        

        c = a % b;

//        20 iload_0

//        21 iload_1

//        22 irem

//        23 istore_2

        

        c = -a;

//        24 iload_0

//        25 ineg

//        26 istore_2

        

        c = a << 1;

//        27 iload_0

//        28 iconst_1

//        29 ishl

//        30 istore_2

        

        c = a | a;

//        31 iload_0

//        32 iload_0

//        33 ior

//        34 istore_2

        

        c = a & a;

//        35 iload_0

//        36 iload_0

//        37 iand

//        38 istore_2

        

        c++;

//        39 iinc 2 by 1

    }

 

类型转换指令

i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f

如:

 

    static void test() {

        int a = 2;

//         0 iconst_2

//         1 istore_0

        

        long b = (long)a;

//         2 iload_0

//         3 i2l

//         4 lstore_1

        

        double c = (double)a;

//         5 iload_0

//         6 i2d

//         7 dstore_3

        

        float d = (float)a;

//         8 iload_0

//         9 i2f

//        10 fstore 5

        

        int e = (int)b;

//        12 lload_1

//        13 l2i

//        14 istore 6

    }

 

对象创建与访问指令

创建类实例的指令:new。

创建数组的指令:newarray、anewarray、multianewarray。

如:

    static void test() {

        Object a = null;

//         0 aconst_null

//         1 astore_0

        

        a = new Test();

//         2 new #1 <com/test/Test>

//         5 dup

//         6 invokespecial #21 <com/test/Test.<init>>

//         9 astore_0

        

        int[] b = new int[15];

//        10 bipush 15

//        12 newarray 10 (int)

//        14 astore_1

        

        int[][] c = new int[2][3];

//        15 iconst_2

//        16 iconst_3

//        17 multianewarray #22 <[[I> dim 2

//        21 astore_2

    }

 

其中创建对象后调用dup原因是创建对象后会自动将对象地址入栈,而此时会自动调用init方法(构造函数),此方法会将地址出栈,再根据地址去调用,因此需要再入栈一份,传给用户使用。而创建数组对象不会自动调用init方法,所以不用dup

 

操作数栈管理指令

将操作数栈的栈顶一个或两个元素出栈:pop、pop2。

复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、 dup_x1、dup2_x1、dup_x2、dup2_x2。

将栈最顶端的两个数值互换:swap

控制转移指令

条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、 if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。

复合条件分支:tableswitch、lookupswitch。

无条件分支:goto、goto_w、jsr、jsr_w、ret。

如:

 

    static void test() {

        int a = 2;

//         0 iconst_2

//         1 istore_0

        

        if (a > 3) {

//             2 iload_0

//             3 iconst_3

//             4 if_icmple 12 (+8)

            

            a = 1;

//             7 iconst_1

//             8 istore_0

//             9 goto 14 (+5)

        } else {

            a = 3;

//            12 iconst_3

//            13 istore_0

        }

//        14 return

    }

 

 

 

    static void test() {

        Object a = null;

//         0 aconst_null

//         1 astore_0

        

        if (a == new Test()) {

//             2 aload_0

//             3 new #1 <com/test/Test>

//             6 dup

//             7 invokespecial #21 <com/test/Test.<init>>

//            10 if_acmpne 24 (+14)

            

            a = new Test();

//            13 new #1 <com/test/Test>

//            16 dup

//            17 invokespecial #21 <com/test/Test.<init>>

//            20 astore_0

//            21 goto 26 (+5)

        } else {

            a = null;

//            24 aconst_null

//            25 astore_0

        }

//        26 return

    }

方法调用和返回指令

invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分 派),这也是Java语言中最常见的方法分派方式。

invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对 象,找出适合的方法进行调用。

invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有 方法和父类方法。

invokestatic指令用于调用类方法(static方法)。

invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方 法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻 辑是由用户所设定的引导方法决定的。

 

方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括 ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和 areturn,另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初 始化方法使用。

如:

    static int test() {

        t1();

        // 0 invokestatic #21 <com/test/Test.t1>

        

        Test t = new Test();

//         3 new #1 <com/test/Test>

//         6 dup

//         7 invokespecial #24 <com/test/Test.<init>>

//        10 astore_0

        

        t.t2();

//        11 aload_0

//        12 invokevirtual #25 <com/test/Test.t2>

        

        return 2;

//        15 iconst_2

//        16 ireturn

    }

    

    static void t1() {}

    void t2() {}

    异常处理指令

异常处理指令jsr和ret已经废弃,改用异常表来处理。

同步指令

monitorenter和monitorexit两条指令来支持synchronized关键字的代码块同步。

如:

 

    void test() {

        Test t = new Test();

//         0 new #1 <com/test/Test>

//         3 dup

//         4 invokespecial #19 <com/test/Test.<init>>

//         7 astore_1

        

        synchronized (t) {

//             8 aload_1

//             9 dup

//            10 astore_2

//            11 monitorenter

            

            int a = 2;

//            12 iconst_2

//            13 istore_3

        }

//        14 aload_2

//        15 monitorexit

//        16 goto 22 (+6)

        

        // 异常表

//        19 aload_2

//        20 monitorexit

//        21 athrow

        

//        22 return

    }

 

其中进入同步方法是将同步对象的引用dup一次存到局部变量作为monitorenter和monitorexit使用的原因是防止它被篡改,复制之后的局部变量就只有monitorenter和monitorexit使用了。

 

 

synchronized方法同步不是通过同步指令实现的,而是通过方法的描述符access flag说明的,如:

 

    static synchronized void t2() {

        int a = 2;

    }

引用类型(弱引用等)

分为强引用、软引用、弱引用、虚引用

强引用:通过new等创建对象时的引用,不会被垃圾回收

软引用:当只有软引用时,在内存不足时会被垃圾回收

弱引用:与软引用类似,不过它是不定时扫描到,即使内存充足也会回收

虚引用:随时被回收,即使有引用也不能使用。

 

弱引用使用场景:

引用:

考虑下面的场景:现在有一个Product类代表一种产品,这个类被设计为不可扩展的,而此时我们想要为每个产品增加一个编号。一种解决方案是使用HashMap<Product, Integer>。于是问题来了,如果我们已经不再需要一个Product对象存在于内存中(比如已经卖出了这件产品),假设指向它的引用为productA,我们这时会给productA赋值为null,然而这时productA过去指向的Product对象并不会被回收,因为它显然还被HashMap引用着。所以这种情况下,我们想要真正的回收一个Product对象,仅仅把它的强引用赋值为null是不够的,还要把相应的条目从HashMap中移除。显然"从HashMap中移除不再需要的条目"这个工作我们不想自己完成,我们希望告诉垃圾收集器:在只有HashMap中的key在引用着Product对象的情况下,就可以回收相应Product对象了。显然,根据前面弱引用的定义,使用弱引用能帮助我们达成这个目的。我们只需要用一个指向Product对象的弱引用对象来作为HashMap中的key就可以了。

 

productA = new Product(...);

  WeakReference<Product> weakProductA = new WeakReference<>(productA);

 

 

jdk提供了线程的WeakHashMap,它的key就是弱引用,可以直接使用。

 

ThreadLocal中的ThreadLocalMap的Entry也是弱引用:

 

弱引用测试:

一段不固定的时间后就被回收了。

posted @ 2020-12-16 09:41  吴克兢  阅读(145)  评论(0)    收藏  举报