顶级架构师学习——第三阶段:深入JVM内核——原理、诊断与优化

1、JVM简介

JVM是Java Virtual Machine的简称,意为Java虚拟机,使用软件模拟Java 字节码的指令集。

2、JVM运行机制

JVM启动流程

JVM基本结构

 

1.PC寄存器

每个线程拥有一个PC寄存器,在线程创建时创建,指向下一条指令的地址,执行本地方法时,PC的值为undefined。

2.方法区

保存装载的类信息,包括类型的常量池、字段、方法信息、方法字节码等,通常和永久区(Perm)关联在一起。JDK6时,String等常量信息置于方法;JDK7时,已经移动到了堆。

3.Java堆

它和程序开发密切相关,应用系统对象都保存在Java堆中,所有线程共享Java堆。对分代GC来说,堆也是分代的。它是GC的主要工作区间。

4.Java栈

线程私有,栈由一系列帧组成(因此Java栈也叫做帧栈),帧保存一个方法的局部变量、操作数栈、常量池指针,每一次方法调用创建一个帧,并压栈。

// Java栈 – 局部变量表 包含参数和局部变量
public class StackDemo {

    public static int runStatic(int i,long l,float  f,Object o ,byte b){
        return 0;
    }


    public int runInstance(char c,short s,boolean b){
        return 0;
    }
}
// Java栈 – 函数调用组成帧栈
public static int runStatic(int i,long l,float  f,Object o ,byte b){
    return runStatic(i,l,f,o,b);
}
// Java栈 – 操作数栈  Java没有寄存器,所有参数传递使用操作数栈
public static int add(int a,int b){
    int c=0;
    c=a+b;
    return c;
}

/*  
 0:   iconst_0 // 0压栈
 1:   istore_2 // 弹出int,存放于局部变量2
 2:   iload_0  // 把局部变量0压栈
 3:   iload_1 // 局部变量1压栈
 4:   iadd      //弹出2个变量,求和,结果压栈
 5:   istore_2 //弹出结果,放于局部变量2
 6:   iload_2  //局部变量2压栈
 7:   ireturn   //返回
*/

// Java栈 – 栈上分配
class BcmBasicString{
    public void method(){    
        BcmBasicString* str=new BcmBasicString;    ....    delete str;
        // 堆上分配,每次需要清理空间
    }
    public void method(){   
        // 栈上分配,函数调用完成自动清理 
        BcmBasicString str;  
          ....
    }
}


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);
    }
}
// 使用-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC
// 输出结果 5
// 使用-server -Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:+PrintGC
// 输出结果 ……
// [GC 3550K->478K(10240K), 0.0000977 secs]
// [GC 3550K->478K(10240K), 0.0001361 secs]
// [GC 3550K->478K(10240K), 0.0000963 secs]
// 564

小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上;直接分配在栈上,可以自动回收,减轻GC压力;大对象或者逃逸对象无法栈上分配。

5.栈、堆、方法区交互

 

public class AppMain {// 运行时, jvm 把appmain的信息都放入方法区
    public static void main(String[] args) {// main方法本身放入方法区。
        Sample test1 = new Sample("测试1");  
        //test1是引用,所以放到栈区里,Sample是自定义对象应该放到堆里面
        Sample test2 = new Sample("测试2");
        test1.printName();
        test2.printName();
    }
}

public class Sample {//运行时, jvm 把appmain的信息都放入方法区
    private name;     
    //new Sample实例后,name引用放入栈区里,name对象放入堆里
    public Sample(String name) {
        this.name = name;
    }
    //print方法本身放入方法区里。
    public void printName() {
        System.out.println(name);
    }
}

6.内存模型

每一个线程有一个工作内存和主存独立,工作内存存放主存中变量的值的拷贝。当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作。每一个操作都是原子的,即执行期间不会被中断。对于普通变量,一个线程中更新的值,不能马上反应在其他变量中,如果需要在其他线程中立即可见,需要使用 volatile 关键字。

 

7.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);
    }
}
// 没有volatile的情况下,程序使用-server运行,将无法停止

 volatile不能代替锁,一般认为volatile比锁性能好(不绝对)。选择使用volatile的条件是:语义是否满足应用。

8.可见性

一个线程修改了变量,其他线程可以立即知道。保证可见性的方法:volatile;synchronized(unlock之前,写变量值回主存);final(一旦初始化完成,其他线程就可见)。

9.有序性

在本线程内,操作都是有序的;在线程外观察,操作都是无序的。(使用指令重排或主内存同步延时)

10.指令重排

线程内串行语义

破坏线程间的有序性 

class OrderExample {
    int a = 0;
    boolean flag = false;

    public void writer() {
        a = 1;                   
        flag = true;           
    }

    public void reader() {
        if (flag) {                
            int i =  a +1;      
            ……
        }
    }
}

线程A首先执行writer()方法,线程B线程接着执行reader()方法。线程B在int i=a+1 是不一定能看到a已经被赋值为1,因为在writer中,两句话顺序可能打乱。

class OrderExample {// 同步后保证有序性
    int a = 0;
    boolean flag = false;

    public synchronized void writer() {
        a = 1;                   
        flag = true;           
    }
    
    public synchronized void reader() {
        if (flag) {                
            int i =  a +1;      
            ……
        }
    }
}

指令重排基本规则

11.解释运行

解释执行以解释方式运行字节码,意思是:读一句执行一句 。

12.编译运行(JIT)

将字节码编译成机器码,直接执行机器码,运行时编译,编译后性能有数量级的提升,一般为10倍左右。

3、常用JVM配置参数

Trace跟踪参数

-XX:+printGC 可以打印GC的简要信息

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

-XX:+PrintGCTimeStamps 打印CG发生的时间戳

-Xloggc:log/filename.log 指定GC log的位置,以文件输出,帮助开发人员分析问题

-XX:+PrintHeapAtGC 每次一次GC后,都打印堆信息

-XX:+TraceClassLoading 监控类的加载

-XX:+PrintClassHistogram 按下Ctrl+Break后,打印类的信息,分别显示:序号、实例数量、总大小、类型

 

堆的分配参数

-Xmx –Xms 指定最大堆和最小堆,示例:-Xmx20m -Xms5m 

-Xmn 设置新生代大小

-XX:NewRatio 新生代(eden+2*s)和老年代(不包含永久区)的比值,示例:-XX:NewRatio4,4 表示新生代:老年代=1:4,即年轻代占堆的1/5

-XX:SurvivorRatio 设置两个Survivor区和eden的比,示例:-XX:SurvivorRatio8,8表示 两个Survivor :eden=2:8,即一个Survivor占年轻代的1/10

-XX:+HeapDumpOnOutOfMemoryError OOM时导出堆到文件

-XX:+HeapDumpPath 导出OOM的路径

-XX:OnOutOfMemoryError 在OOM时,执行一个脚本 "-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p“  当程序OOM时,在D:/a.txt中将会生成线程的dump,可以在OOM时,发送邮件,甚至是重启程序

小结:根据实际事情调整新生代和幸存代的大小,官方推荐新生代占堆的3/8,幸存代占新生代的1/10。在OOM时,记得Dump出堆,确保可以排查现场问题。

永久区分配参数

-XX:PermSize  -XX:MaxPermSize 设置永久区的初始空间和最大空间,他们表示一个系统可以容纳多少个类型

栈大小分配

-Xss 通常只有几百K,决定了函数调用的深度,每个线程都有独立的栈空间,局部变量、参数分配在栈上

public class TestStackDeep {
	private static int count=0;
	public static void recursion(long a,long b,long c){
		long e=1,f=2,g=3,h=4,i=5,k=6,q=7,x=8,y=9,z=10;
		count++;
		recursion(a,b,c);
	}
	public static void main(String args[]){
		try{
			recursion(0L,0L,0L);
		}catch(Throwable e){
			System.out.println("deep of calling = "+count);
			e.printStackTrace();
		}
	}
}
/*
递归调用
-Xss128K
deep of calling = 701
java.lang.StackOverflowError

-Xss256K
deep of calling = 1817
java.lang.StackOverflowError
*/

4、GC算法

GC的概念

Garbage Collection 垃圾收集,1960年 List 使用了GC,Java中,GC的对象是堆空间和永久区

GC算法

1.引用计数法

老牌垃圾回收算法,通过引用计算来回收垃圾。引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。

引用计数法的问题是引用和去引用伴随加法和减法,影响性能,并且很难处理循环引用。

 2.标记-清除法

标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

 3.标记-压缩法

标记-压缩算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。

4.复制算法

与标记-清除算法相比,复制算法是一种相对高效的回收方法。不适用于存活对象较多的场合,如老年代。将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收 。

 

分代思想

依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代。根据不同代的特点,选取合适的收集算法:少量对象存活,适合复制算法;大量对象存活,适合标记清理或者标记压缩。

可触及性

可触及的:从根节点可以触及到这个对象 。

可复活的:一旦所有引用被释放,就是可复活状态,因为在finalize()中可能复活该对象 。

不可触及的:在finalize()后,可能会进入不可触及状态。不可触及的对象不可能复活,可以回收。

public class CanReliveObj {
	public static CanReliveObj obj;
	@Override
	protected void finalize() throws Throwable {
	    super.finalize();
	    System.out.println("CanReliveObj finalize called");
	    obj=this;
	}
	@Override
	public String toString(){
	    return "I am CanReliveObj";
	}
    public static void main(String[] args) throws
         InterruptedException{
    obj=new CanReliveObj();
    obj=null;   //可复活
    System.gc();
    Thread.sleep(1000);
    if(obj==null){
        System.out.println("obj 是 null");
    }else{
        System.out.println("obj 可用");
    }
    System.out.println("第二次gc");
    obj=null;    //不可复活
    System.gc();
    Thread.sleep(1000);
    if(obj==null){
        System.out.println("obj 是 null");
    }else{
        System.out.println("obj 可用");
    }
}
// 得到的结果如下
/*
CanReliveObj finalize called
obj 可用
第二次gc
obj 是 null
*/

经验:避免使用finalize(),操作不慎可能导致错误。优先级低,何时被调用,不确定,何时发生GC不确定。可以使用try-catch-finally来替代它。

Stop-The-World

Stop-The-World是Java中一种全局暂停的现象。全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互。这种现象多半由于GC引起,如Dump线程、死锁检查、堆Dump等。

GC时为什么会有全局停顿?

类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。

STW的危害

长时间服务停止,没有响应,遇到HA系统,可能引起主备切换,严重危害生产环境。

public static class PrintThread extends Thread{
	public static final long starttime=System.currentTimeMillis();
	@Override
	public void run(){
		try{
			while(true){
				long t=System.currentTimeMillis()-starttime;
				System.out.println("time:"+t);
				Thread.sleep(100);
			}
		}catch(Exception e){
			
		}
	}
}
public static class MyThread extends Thread{
	HashMap<Long,byte[]> map=new HashMap<Long,byte[]>();
	@Override
	public void run(){
		try{
			while(true){
				if(map.size()*512/1024/1024>=450){
					System.out.println(“=====准备清理=====:"+map.size());
					map.clear();
				}
				
				for(int i=0;i<1024;i++){
					map.put(System.nanoTime(), new byte[512]);
				}
				Thread.sleep(1);
			}
		}catch(Exception e){
			e.printStackTrace();
		}
	}
}
// 使用下列命令启动线程
/*
-Xmx512M -Xms512M -XX:+UseSerialGC -Xloggc:gc.log -XX:+PrintGCDetails  -Xmn1m -XX:PretenureSizeThreshold=50 -XX:MaxTenuringThreshold=1
*/

 预期中每秒有10条输出,但我们可以看到红色标记部分存在着一定的停顿时间,这部分就是GC造成的STW现象。

5、GC参数

串行收集器

串行收集器是最古老,最稳定的收集器,效率高,但可能会产生较长的停顿。使用-XX:+UseSerialGC启动串行收集器。新生代、老年代使用串行回收算法,新生代使用复制算法,而老年代使用标记-压缩算法。

并行收集器 ParNew

-XX:+UseParNewGC 新生代并行,老年代串行。Serial收集器新生代的并行版本,采用复制算法,多线程任务,需要多核支持。可使用-XX:ParallelGCThreads 限制线程数量。

-XX:MaxGCPauseMills 最大停顿时间,单位毫秒,GC尽力保证回收时间不超过设定值

-XX:GCTimeRatio 0-100的取值范围 垃圾收集时间占总时间的比 默认99,即最大允许1%时间做GC

这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优 。

CMS收集器

Concurrent Mark Sweep 并发标记清除。标记-清除算法与标记-压缩相比,并发阶段会降低吞吐量。老年代收集器(新生代使用ParNew)。-XX:+UseConcMarkSweepGC启动CMS收集器。

CMS运行过程比较复杂,着重实现了标记的过程,可分为:

初始标记:根可以直接关联到的对象,速度快

并发标记(和用户线程一起):主要标记过程,标记全部对象

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

并发清除(和用户线程一起):基于标记结果,直接清理对象

特点:

尽可能降低停顿

会影响系统整体吞吐量和性能。比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半

清理不彻底。因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理

因为和用户线程一起运行,不能在空间快满时再清理。-XX:CMSInitiatingOccupancyFraction设置触发GC的阈值,如果不幸内存预留空间不够,就会引起concurrent mode failure 。

-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次整理 整理过程是独占的,会引起停顿时间变长

-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理

-XX:ParallelCMSThreads 设定CMS的线程数量

GC参数整理

-XX:+UseSerialGC:在新生代和老年代使用串行收集器

-XX:SurvivorRatio:设置eden区大小和survivior区大小的比例

-XX:NewRatio:新生代和老年代的比

-XX:+UseParNewGC:在新生代使用并行收集器

-XX:+UseParallelGC :新生代使用并行回收收集器

-XX:+UseParallelOldGC:老年代使用并行回收收集器

-XX:ParallelGCThreads:设置用于垃圾回收的线程数

-XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器

-XX:ParallelCMSThreads:设定CMS的线程数量

-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发

-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理

-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩

-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收

-XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收

-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收

使用jmeter监控性能

 6、类装载器

class装载验证流程

加载

装载类的第一个阶段。取得类的二进制流,转为方法区数据结构,在Java堆中生成对应的java.lang.Class对象。

链接 -> 验证

目的是为了保证Class流的格式是正确的。文件格式的验证:是否以0xCAFEBABE开头、版本号是否合理;元数据验证:是否有父类、继承了final类?、非抽象类实现了所有的抽象方法;字节码验证 (很复杂):运行检查、栈数据类型和操作码数据参数吻合、跳转指令指定到合理的位置;符号引用验证:常量池中描述类是否存在、访问的方法或字段是否存在且有足够的权限。

链接 -> 准备

分配内存,并为类设置初始值 (方法区中)。public static int v=1; 在准备阶段中,v会被设置为0,在初始化的<clinit>中才会被设置为1,对于static final类型,在准备阶段就会被赋上正确的值 public static final  int v=1; 

链接 -> 解析

符号引用(字符串引用对象不一定被加载)替换为直接引用(指针或者地址偏移量引用对象一定在内存)

初始化

执行类构造器<clinit>,子类的<clinit>调用前保证父类的<clinit>被调用,<clinit>是线程安全的

什么是类装载器ClassLoader

ClassLoader是一个抽象类

ClassLoader的实例将读入Java字节码将类装载到JVM中

ClassLoader可以定制,满足不同的字节码流获取方式

ClassLoader负责类装载过程中的加载阶段

ClassLoader的重要方法:

 - public Class<?> loadClass(String name) throws ClassNotFoundException 载入并返回一个Class

 - protected final Class<?> defineClass(byte[] b, int off, int len) 定义一个类,不公开调用

 - protected Class<?> findClass(String name) throws ClassNotFoundException loadClass回调该方法,自定义ClassLoader的推荐做法

 - protected final Class<?> findLoadedClass(String name) 寻找已经加载的类

JDK中ClassLoader默认设计模式 – 协同工作

 在查找类的时候,先在底层的Loader查找,是从下往上的。Apploader能找到,就不会去上层加载器加载。

使用-Xbootclasspath/a:D:/tmp/clz设定搜索路径。

双亲模式的问题

顶层ClassLoader,无法加载底层ClassLoader的类

解决:Thread. setContextClassLoader() 上下文加载器,是一个角色,用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题。基本思想是:在顶层ClassLoader中,传入底层ClassLoader的实例。

static private Class getProviderClass(String className, ClassLoader cl,
        boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
{
    try {
        if (cl == null) {
            if (useBSClsLoader) {
                return Class.forName(className, true, FactoryFinder.class.getClassLoader());
            } else {
                cl = ss.getContextClassLoader();
                if (cl == null) {
                    throw new ClassNotFoundException();
                }
                else {
                    return cl.loadClass(className); //使用上下文ClassLoader
                }
            }
        }
        else {
            return cl.loadClass(className);
        }
    }
    catch (ClassNotFoundException e1) {
        if (doFallback) {
            // Use current class loader - should always be bootstrap CL
            return Class.forName(className, true, FactoryFinder.class.getClassLoader());
        }
......
/*
代码来自于
javax.xml.parsers.FactoryFinder
展示如何在启动类加载器加载AppLoader的类
上下文ClassLoader可以突破双亲模式的局限性
*/

双亲模式是默认的模式,但不是必须这么做。Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent。OSGi的ClassLoader形成网状结构,根据需要自由加载Class。

破坏双亲模式例子:先从底层ClassLoader加载

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // First, check if the class has already been loaded
    Class re=findClass(name);
    if(re==null){
        System.out.println(“无法载入类:”+name+“ 需要请求父加载器");
        return super.loadClass(name,resolve);
    }
    return re;
}
protected Class<?> findClass(String className) throws ClassNotFoundException {
    Class clazz = this.findLoadedClass(className);
    if (null == clazz) {
        try {
            String classFile = getClassFile(className);
            FileInputStream fis = new FileInputStream(classFile);
            FileChannel fileC = fis.getChannel();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            WritableByteChannel outC = Channels.newChannel(baos);
            ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
             省略部分代码
            fis.close();
            byte[] bytes = baos.toByteArray();

            clazz = defineClass(className, bytes, 0, bytes.length);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return clazz;
}

热替换

当一个class被替换后,系统无需重启,替换的类立即生效。

7、性能监控工具

linux系统

uptime:系统时间 运行时间 例子中为7分钟 连接数 每一个终端算一个连接 1,5,15分钟内的系统平均负载 运行队列中的平均进程数

top:可以知道哪个程序占CPU最多 

 vmstat:可以统计系统的CPU,内存,swap,io等情况

pidstat:可以细致的观察进程,监控CPU、监控IO、监控内存等等。

windows系统

 任务管理器

perfmon:Windows自带多功能性能监控工具,在命令行下输入perfmon启动。

Process Explorer:需要下载,能显示进程间的关系等等。

pslist:命令行工具,可用于自动化数据收集,显示java程序的运行情况。

java自带的工具

jps:列出java进程,类似于ps命令。参数-q可以指定jps只输出进程ID,不输出类的短名称;参数-m可以用于输出传递给Java进程(主函数)的参数;参数-l可以用于输出主函数的完整路径;参数-v可以显示传递给JVM的参数。

jinfo:可以用来查看正在运行的Java应用程序的扩展参数,甚至支持在运行时,修改部分参数;-flag <name>:打印指定JVM的参数值;-flag [+|-]<name>:设置指定JVM参数的布尔值;-flag <name>=<value>:设置指定JVM参数的值。

jmap:生成Java应用程序的堆快照和对象的统计信息

jstack:打印线程dump。-l 打印锁信息;-m 打印java和native的帧信息;-F 强制dump,当jstack没有响应时使用

JConsole:图形化监控工具,可以查看Java应用程序的运行概况,监控堆信息、永久区使用情况、类加载情况等

Visual VM:Visual VM是一个功能强大的多合一故障诊断和性能监控的可视化工具

实战分析——程序卡死

 

 

 

 

实战分析——死锁 

 

 

 8、java堆分析

内存溢出(OOM)的原因

1.占用大量堆空间,直接溢出

解决方法:增大堆空间,及时释放内存

2.永久区溢出

解决方法: 增大Perm区,允许Class回收

3.Java栈溢出:这里的栈溢出指,在创建线程的时候,需要为线程分配栈空间,这个栈空间是向操作系统请求的,如果操作系统无法给出足够的空间,就会抛出OOM

解决方法: 减少堆内存,减少线程栈大小

4.直接内存溢出:ByteBuffer.allocateDirect()无法从操作系统获得足够的空间

解决方法: 减少堆内存,有意触发GC

MAT(Memory Analyzer)使用基础

 

它的功能十分强大,不再一一列举 。下面介绍一下上面说到的浅堆和深堆。

浅堆:一个对象结构所占用的内存大小。3个int类型以及一个引用类型合计占用内存3*4+4=16个字节,再加上对象头的8个字节,因此String对象占用的空间,即浅堆的大小是16+8=24字节。对象大小按照8字节对齐。浅堆大小和对象的内容无关,只和对象的结构有关。

深堆:一个对象被GC回收后,可以真实释放的内存大小,只能通过对象访问到的(直接或者间接)所有对象的浅堆之和 (支配树)。

9、Class文件结构

access flag

Flag Name

Value

Interpretation

ACC_PUBLIC

0x0001

public

ACC_FINAL

0x0010

final,不能被继承.

ACC_SUPER

0x0020

是否允许使用invokespecial指令,JDK1.2后,该值为true

ACC_INTERFACE

0x0200

是否是接口

ACC_ABSTRACT

0x0400

抽象类

ACC_SYNTHETIC

0x1000

该类不是由用户代码生成,运行时生成的,没有源码

ACC_ANNOTATION

0x2000

是否为注解

ACC_ENUM

0x4000

是否是枚举

Flag Name

Value

Interpretation

ACC_PUBLIC

0x0001

public

ACC_PRIVATE

0x0002

private

ACC_PROTECTED

0x0004

protected

ACC_STATIC

0x0008

static

ACC_FINAL

0x0010

final

ACC_SYNCHRONIZED

0x0020

synchronized

ACC_BRIDGE

0x0040

编译器产生 桥接方法

ACC_VARARGS

0x0080

可变参数

ACC_NATIVE

0x0100

native

ACC_ABSTRACT

0x0400

abstract

ACC_STRICT

0x0800

strictfp

ACC_SYNTHETIC

0x1000

不在源码中,由编译器产生

 attribute

名称

使用者

描述

Deprecated

field method

字段、方法、类被废弃

ConstantValue

field

final常量

Code

method

方法的字节码和其他数据

Exceptions

method

方法的异常

LineNumberTable

Code_Attribute

方法行号和字节码映射

LocalVaribleTable

Code_Attribute

方法局部变量表描述

SourceFile

Class file

源文件名

Synthetic

field method

编译器产生的方法或字段

 class文件结构例子

 

 

10、JVM字节码执行

javap

class文件反汇编工具 

简单字节码执行示例

 

 

 

 

 

常用字节码

1.常量入栈

aconst_null null对象入栈

iconst_m1   int常量-1入栈

iconst_0      int常量0入栈

iconst_5

lconst_1      long常量1入栈

fconst_1      float 1.0入栈

dconst_1     double 1.0 入栈

bipush        8位带符号整数入栈

sipush         16位带符号整数入栈

ldc               常量池中的项入栈

2.局部变量压栈

xload(x为i l f d a) 分别表示int,long,float,double,object ref

xload_n(n为0 1 2 3)

xaload(x为i l f d a b c s) 分别表示int, long, float, double, obj ref ,byte,char,short,从数组中取得给定索引的值,将该值压栈 iaload 执行前,栈:..., arrayref, index 它取得arrayref所在数组的index的值,并将值压栈 执行后,栈:..., value

3.出栈装载入局部变量

xstore(x为i l f d a) 出栈,存入局部变量

xstore_n(n 0 1 2 3) 出栈,将值存入第n个局部变量

xastore(x为i l f d a b c s) 将值存入数组中 iastore:执行前,栈:...,arrayref, index, value;执行后,栈:... ;将value存入arrayref[index]

4.通用栈操作(无类型)

nop

pop 弹出栈顶1个字长

dup 复制栈顶1个字长,复制内容压入栈

5.类型转换

i2l i2f l2i l2f l2d f2i f2d d2i d2l d2f i2b i2c i2s等(i2l:将int转为long;执行前,栈:..., value;执行后,栈:...,result.word1,result.word2;弹出int,扩展为long,并入栈)

6.整数运算

iadd ladd isub lsub idiv ldiv imul lmul iinc等

7.浮点运算

fadd dadd fsub dsub fdiv ddiv fmul dmul等

8.对象操作指令

new getfield putfield getstatic putstatic

9.条件控制

ifeq  如果为0,则跳转 (ifeq 参数 byte1,byte2 value出栈 ,如果栈顶value为0则跳转到(byte1<<8)|byte2 执行前,栈:...,value 执行后,栈:...)

ifne  如果不为0,则跳转

iflt   如果小于0 ,则跳转

ifge  如果大于0,则跳转

if_icmpeq 如果两个int相同,则跳转

10.方法调用

invokevirtual invokespecial invokestatic invokeinterface xreturn(x为 i l f d a 或为空)

ASM

Java字节码操作框架,可以用于修改现有类或者动态产生新类

JIT

字节码执行性能较差,所以可以对于热点代码编译成机器码再执行,在运行时的编译, 叫做JIT Just-In-Time。基本思路是,将热点代码,就是执行比较频繁的代码(热点代码 hot spot code),编译成机器码。

public class JITTest {

    public static void met(){
        int a=0,b=0;
        b=a+b;
    }
    
    public static void main(String[] args) {
        for(int i=0;i<1000;i++){
            met();
        }
    }
}
// 使用下列参数启动测试
/*
-XX:CompileThreshold=1000
-XX:+PrintCompilation
*/
// 输出如下
/*
56    1             java.lang.String::hashCode (55 bytes)
56    2             java.lang.String::equals (81 bytes)
57    3             java.lang.String::indexOf (70 bytes)
60    4             java.lang.String::charAt (29 bytes)
61    5             java.lang.String::length (6 bytes)
61    6             java.lang.String::lastIndexOf (52 bytes)
61    7             java.lang.String::toLowerCase (472 bytes)
67    8             geym.jvm.ch2.jit.JITTest::met (9 bytes)
*/

 相关参数

-Xint 解释执行

-Xcomp 全部编译执行

-Xmixed 默认,混合

扫描关注我们的各种:落饼枫林,与我们一起进步~~

 

猜你喜欢

转载自blog.csdn.net/qq_39391192/article/details/83037267