JVM初识

2019-03-10

零 class文件详解

class文件一 8位(一个字节)为基础单位的二进制,各个数据项目紧凑的排列在文件中。中间没有人恶化分隔符。

根据jvm规范,class文件以一种类似于C语言结构体的伪代码来存储数据,这种伪代码只有两种数据类型:无符号数和表。

无符号数属于基本的数据类型,以 u1\u2\u4\u8分别表示1、2、4、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照utf8编码的字符串。

表是由多个无符号数或者其他表作为数据项构成的符号数据类型。所有表都习惯性的以_info结尾

class字节码分析:

{

    魔数:前四个字节:)0xCAFEBABE

    class文件版本号:又四个字节 //从这里记为4

    次版本号:第5、6两个字节

    主版本号:第7、8两个字节 //版本可以向前兼容但不能向后兼容

    常量池入口:{

        数量一般不固定,所以入口放置一个 u2类型的数据,代表常量池的容量计数值,这个计数从1开始。

    }

    常量池:{

        主要放置两大类常量:字面量Literal和符号引用Symbolic Reference。

        字面量比较接近java语言层面的常量该奶奶,如文本字符串、声明为final的常量值等。

        符号引用属于编译原理方面的概念,包括下面三类常量{类和接口的全限定名,字段的名称和描述符,方法的名称和描述符}



 

        class文件只保存各个方法和字段,不保存它们的最终内存信息(需要虚拟机运行时从常量池获得符号引用在类创建时或者运行时再解析、翻译到具体的内存中)

    }

}

一 java平台四个步骤

1.java编程语言 .java格式的源代码

2.编译后的字节码 .class格式的字节码

3.jvm 对字节码进行验证、装载

4.字节码被解释进虚拟机,被解释执行 最终变成机器码进行处理

二 jvm是什么

jvm是运行在计算机上的假想的计算机,运行在操作系统之上不与硬件直接交互。它有自己的一套字节码指令集,寄存器,栈,垃圾回收,堆和存储方法域

jvm定义的数据类型

1.原始类型

{数字类型,boolean类型,returnAddress:指向指令的指针}

2.引用类型

{类的类型,数组类型,接口类型}

三 jvm运行原理

java字节码文件被类加载器装载,进入内存,被执行引擎解释执行,最终变成机器码进行处理。

四 类加载器 类加载相关

加载过程有

1.装载loading:{

    负责找到二进制字节码并加载至JVM中,JVM通过类名、类所在的包名、ClassLoader完成类的加载。

    因此,标识一个被加载了的类:类名 + 包名 + ClassLoader实例ID。

}

2.连接linking:{

    负责对二进制字节码的格式进行校验、初始化装载类中的静态变量以及解析类中调用的接口。

链接又包含三块内容:验证、准备、解析。

    1)验证,文件格式、元数据、字节码、符号引用验证;

    2)准备,为类的静态变量分配内存,并将其初始化为默认值;

    3)解析,把类中的符号引用转换为直接引用

}

3.初始化:{

    负责执行类中的静态初始化代码、构造器代码以及静态属性的初始化,以下六种情况初始化过程会被触发。

    1. 创建类的实例,也就是new的方式

    2.访问某个类或接口的静态变量,或者对该静态变量赋值

    3.调用类的静态方法

    4.反射(如Class.forName(“com.shengsiyuan.Test”))

    5.初始化某个类的子类,则其父类也会被初始化

    6.Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类

}

类加载器:{

    从java开发人员角度来看,类加载器可以大致分为以下三类:

    启动类加载器:Bootstrap ClassLoader{

        负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,

        或被-Xbootclasspath参数指定的路径中的并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。

        启动类加载器是无法被Java程序直接引用的。

    }

    扩展类加载器:Extension ClassLoader{

        该加载器由sun.misc.Launcher$ExtClassLoader实现,

        它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),

        开发者可以直接使用扩展类加载器。

    }

    应用类加载器:Application ClassLoader{

        该类加载器由sun.misc.Launcher$AppClassLoader来实现,

        它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,

        如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

    } 

}

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:

1、在执行非置信代码之前,自动验证数字签名。

2、动态地创建符合用户特定需要的定制化构建类。

3、从特定的场所取得java class,例如数据库中和网络中



 

双亲委派模式--当类加载器收到了类加载的请求{

    当JVM加载一个类的时候,下层的加载器会将任务给上一层类加载器,上一层加载检查它的命名空间中是否已经加载这个类,如果已经加载,直接使用这个类。如果没有加载,继续往上委托直到顶部。检查之后,按照相反的顺序进行加载。如果Bootstrap加载器不到这个类,则往下委托,直到找到这个类。一个类可以被不同的类加载器加载。

    1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,

    而是把类加载请求委派给父类加载器`ExtClassLoader`去完成。

    2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,

    而是把类加载请求委派给`BootStrapClassLoader`去完成。

    3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),

    会使用`ExtClassLoader`来尝试加载;

    4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,

    如果 `AppClassLoader`也加载失败,则会报出异常ClassNotFoundException。    

}

双亲委派模型的意义:{

    系统类防止内存中出现多份同样的字节码

    保证Java程序安全稳定运行

    可见性限制:下层的加载器能够看到上层加载器中的类,反之则不行,委派只能从下到上。

    不允许卸载类:类加载器可以加载一个类,但不能够卸载一个类。但是类加载器可以被创建或者删除。    

}

五 jvm的内存分配--基于堆栈的体系结构

PC寄存器:对于java程序中运行的每个线程,PC寄存器存储当前指令的地址

栈:对于每个线程 都会分配一个栈 来存储局部变量,方法参数和返回值

堆:所有线程共享的内存和存储对象{类实例和数组},对象的释放由垃圾收集器管理.物理内存不要求连续,逻辑上连续就可以了。

方法区(Metadata):对于每个加载的类 存储方法代码和符号表(例如对字段或方法的引用),以及常量池。

分配对象优先在新生代分配,但是有些情况会直接在老年代分配:{

    1.分配的对象大小 大于 Eden空间

    2.Eden空间剩余内存不足,且要分配对象内存大小 大于 Eden空间的一半

}



 

--HotSpot

程序计数器:{

    一块较小的内存空间,当前线程 所执行的字节码的行号指示器。在任何一个确定的时刻每个处理器都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置吗,每个线程都需要有一个独立的 程序计数器。每个线程间计数器互不影响}{如果执行的是本地方法则值为空}



 

java虚拟机栈:{

    描述的是 java方法执行时的内存模型(非native方法)

    每个方法在执行的同时都会创建一个栈帧用于存储 局部变量表,操作数栈,动态链接,方法出口等信息。

    局部变量表:{

        存放了编译器可知的各种基本类型、对象引用、returnAddress类型。

        局部变量表所需的内存空间在编译的时候就完成了分配,当进入一个方法时不会再改变局部变量表的大小。

    }

    returnAddress:{

        指向了一条字节码指令的地址。

    }

}

本地方法栈



 

堆Heap:{

    被所有线程共享

    jvm启动时创建

    目的就是为了存放 java实例:jvm规范描述 所有对象实例和数组都要在堆上分配

    从内存回收角度来看现在的收集器基本都采用分代收集的算法,所以java堆还可以细分为新生代和老年代

    新生代:{

        Eden空间:8/10

        from survivor:1/10

        to survivor:1/10

    }

    新生代和老年代比例为1:2

    JVM每次只是用Eden和一块Sruvivor区域来为对象服务,无论什么时候都有一块Survivor区域是空闲的。因此新生代实际可用内存只能有90%的新生代空间。

    Survivor的存在意义就是减少被送到老年代的对象,进而减少Major GC的发生。

}

方法区Metadata:{

    元空间不在虚拟机中,而是使用本地内存。//字符串如果存在永久代中容易出现性能问题和内存溢出。

    因此默认情况下 元空间大小仅受本地内存吸纳之,

    也是各个线程共享的内存区,用来存储已经被虚拟机加载的 类,常量,静态变量,即时编译器编译后的代码等数据。这个区域的内存回收主要是针对常量池的回收和堆类型的卸载。

    运行时常量池

}

直接内存:{



 

}

从new A说起:{

    虚拟机遇到new后 先检查new指令的参数能不能在常量池中定位到一个类的符号引用,并且坚持这个符号引用代表的类是否被加载、解析和初始化过。如果没有,就必须先进行类的加载过程。

}

对象在内存中的布局:{

    存储布局可看成三部分:{

        对象头:存储对象自身的运行时数据{

            运行时数据{

                哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程id,偏向时间戳

            }

            类型指针{

                对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的

            }

        }

        实例数据:{

            字段{

                父类继承字段,子类字段

            }

        }



 



 

    }

}

六 垃圾回收

Garbage Collection,GC,释放垃圾占用的空间,防止内存泄漏。有效的使用可以使用的内存,堆内存堆中已经死亡或者长时间没有使用的对象进行清除和回收。

垃圾判断算法:判断对象生死的算法{

    1.引用计数法

    2.可达性分析算法

}

垃圾回收算法

一些垃圾收集器:{

先说分代收集算法:{

}

}



 

七 从一个demo开始

0.

class Test{

    public static void main(String[] args){

        int i=0;

        while(i<10){

            System.out.println(i);

        }

    }

}

 java HelloApp run virtual machine 

1.传进来三个字符串组成的数组,试图执行Test类的main方法,发现Test类没有加载。于是虚拟机使用ClassLoader试图寻找这个二进制类文件。

2.类被装载后还不能调用main方法,要先对这个类进行链接和初始化。链接包括三个阶段:检验、准备和解析。然后执行类中声明的静态初始化函数。一个类被初始化前它的父类必须被初始化。