JVM虚拟机

 

 

1.程序计数器

1.如果线程正在执行的是一个java方法,那么计数器记录的当前代码的行号确定执行哪一条字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都会依赖这个计数器完成

2.如果执行的native方法,计数器当中的内容应当是空
3.此内存区域在java的虚拟机规范当中是唯一一个没有规定OutOfMemoryError的区域  

2.JVM栈:

栈帧:一个栈帧随着一个方法的调用开始而创建,这个方法调用完成而销毁。局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是指向一个代表对象的句柄或者其他与此对象相关的变量)和retureAddress类型。其中64位长度的long和double类型的数据会占2个局部变量空间,其余的数据类型只占1个。

Java栈也称作虚拟机栈(Java Vitual Machine Stack),JVM栈只对栈帧进行存储,压栈和出栈操作。Java栈是Java方法执行的内存模型。下面我们来看一个Java栈图。

 

 

对于所有的程序设计语言来说,栈这部分空间对程序员来说是不透明的。

栈内存的大小可以有两种设置,固定值和根据线程需要动态增长。
JVM栈这个数据区可能会发生抛出两种错误。
1. StackOverflowError 出现在栈内存设置成固定值的时候,当程序执行需要的栈内存超过设定的固定值会抛出这个错误。
2. OutOfMemoryError 出现在栈内存设置成动态增长的时候,当JVM尝试申请的内存大小超过了其可用内存时会抛出这个错误。

3.本地方法栈

这块内存区域和虚拟机栈非常相似,他们的区别从名字就可以看出来:Java虚拟机栈是用来执行Java方法的,而本地方法栈是用来执行Native方法的。

4.内存

Java堆应该是虚拟机管辖范围内最大的一块内存区域。这块区域是被所有线程共享,在虚拟机一启动的时候就创建的,它的唯一目的就是存放对象实例,几乎所有对象的实例都需要到这里申请内存。

Java堆是垃圾回收期(GC)管理的主要区域,从内存回收的角度来看,现代的收集器都采用分代收集算法,所有这块区域又会细分为好几个区域:新生代、老年代;再细节的又会分为 EdenFrom SurvivorTo Survivor,下图展示了详细的区域:

 

新生代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大多数对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可用,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消亡的),这个GC机制被称为Minor GC或叫Young GC。注意,Minor GC并不代表年轻代内存不足,它事实上只表示在Eden区上的GC。

年轻代上的内存分配是这样的,年轻代可以分为3个区域:Eden区(伊甸园,亚当和夏娃偷吃禁果生娃娃的地方,用来表示内存首次分配的区域,再贴切不过)和两个存活区(Survivor 0 、Survivor 1)

  1. 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;
  2. 最初一次,当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);
  3. 下次Eden区满了,再执行一次Minor GC,将消亡的对象清理掉,将存活的对象复制到Survivor1中,然后清空Eden区;
  4. 将Survivor0中消亡的对象清理掉,将其中可以晋级的对象晋级到Old区,将存活的对象也复制到Survivor1区,然后清空Survivor0区;
  5. 当两个存活区切换了几次(HotSpot虚拟机默认15次,用XX:MaxTenuringThreshold控制,大于该值进入老年代,但这只是个最大值,并不代表一定是这个值)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。

 从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着当前还活着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的停止复制(Stopandcopy清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中)

 

年老代(Old Generation:对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC

Minor GC ,Full GC 触发条件 

Minor GC触发条件:当Eden区满时,触发Minor GC

Full GC触发条件:

1)调用System.gc时,系统建议执行Full GC,但是不必然执行

2)老年代空间不足

3)方法区空间不足。

4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存

5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

 

GC机制的基本算法是:分代收集

JVM调优,可以通过配置以下参数改变整个JVM堆的配置比例

1.Java heap的大小(新生代+老年代

  Xms堆的最小值

  Xmx堆空间的最大值

2.新生代堆空间大小调整

  XX:NewSize新生代的最小值

  XX:MaxNewSize新生代的最大值

  XX:NewRatio设置新生代与老年代在堆空间的大小

  XX:SurvivorRatio新生代中Eden所占区域的大小

3.永久代大小调整

  XX:MaxPermSize

 

4.其他

   XX:MaxTenuringThreshold,设置将新生代对象转到老年代时需要经过多少次垃圾回收,但是仍然没有被回收

 

5.方法区(非堆内存):

【名词解析】
        >java堆一样,方法区是一块所有线程共享的内存区域。
        >保存系统的类信息,比如,类的字段,方法,常量池等。
        >方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出的错误
        >jdk1.6jdk1.7方法区可以理解为永久区。
        >jdk1.8已经将方法区取消,替代的是元数据区。
        >jdk1.8的元数据区可以使用参数XX:MaxMetaspaceSzie设定大小,这是一块堆外的直接内存,与永久区不同,如果不指定大小,默认情况下,虚拟机会耗尽可用系统内存

【参数设定】
        >jdk1.6jdk1.7的永久区可以使用参数XX:PermSize XX:MaxPermSize指定
        >XX:PermSize =5m 默认启动大小为5M
        >XX:MaxPermSize=64m 最大大小为64M
【异常】
        >jdk1.8元数据区内存溢出:java.lang.OutOfMemoryError:Metaspace

 

(类加载时候执行)

静态代码块>静态属性>静态方法>普通属性>普通代码块>构造方法>普通方法

(静态代码块静态属性代码顺序有关属性为默认值) –> (普通属性,普通代码块代码顺序有关属性为默认值)>构造方法

 

6.JVM类加载链接,初始化。

程序运行时,加载类主要经过3个阶段分别是类的加载,连接和初始化。分别介绍一下这三个过程

加载 –> 验证 > 准备 > 解析 > 初始化 > 使用 > 卸载

6.1 加载

类的加载指的是将类的.class文件中二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象用来封装类在方法区内的数据结构。【】

加载.class文件的方式

1、从本地系统中直接加载

2、通过网络下载.class文件

3、从zip,jar等归档文件中加载.class文件

4、从专有数据库中提取.class文件

5、将Java源文件动态编译为.class

类加载的最终产品是位于堆区中的class对象,Class对象封装了类在方法区内的数据结构,并向Java程序员提供了访问方法区内的数据机构的接口.
我们可以通过类名.class来获取一个类的类型的引用,通过new 类名().getClass()来获取一个实例变量的类的引用

 

类的加载机制

JDK1.2开始类加载采用父亲委托机制。除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当Java程序启动加载器加载某个类时,加载器会首先委托自己的父加载器去加载该类,若父加载器能加载,则由父加载器完成加载任务,否则才由自加载器去加载。

 

 

 

 

同时,所有能成功返回Class对象的引用的类加载器(包括定义类加载器,即包括定义类加载器和它下面的所有子加载器)都被称为初始类加载器。
假设loader1实际加载了Sample类,则loader1为Sample类的定义类加载器

6.2链接

类加载完成后就进入了类的连接阶段,连接阶段主要分为三个过程分别是:验证,准备和解析。在连接阶段,主要是将已经读到内存的类的二进制数据合并到虚拟机的运行时环境中去。

验证

这个阶段主要目的是保证Class流的格式是正确的。主要验证的内容包括:

1、文件格式的验证

    是否以0xCAFEBABE开头

    版本号是否合理

2、元数据的验证

    是否有父类

    是否继承了final

    非抽象类实现了所有抽象方法

3、字节码验证

    运行检查

    栈数据类型和操作码数据参数吻合

    跳转指令指定到合理的位置

4、符号引用验证

    常量池中描述类是否存在

访问的方法或字段是否存在且有足够的权

准备

这个阶段主要是为对象和变量分配内存,并为类设置初始值(方法区中

给基本类型赋值0false,引用类型为null

重点对于static类型变量在这个阶段会为其赋值为默认值,比如public static int v=5,在这个阶段会为其赋值为v=0,而对于static final类型的变量,在准备阶段就会被赋值为正确的值

PS

解析

在这个阶段会将符号引用转换成直接引用。
原来的符号引用仅仅是一个字符串,而引用的对象不一定被加载,直接引用只的是将引用对象的指针或者地址偏移量指向真正的对象,将字符串所指向的对象加载到内存中。

 

6.3,初始化

在这个阶段主要执行类的构造方法。并且先为静态变量赋值为初始值,执行静态块代码

(静态代码块静态属性初始化真正值,代码顺序有关属性为默认值) –> (普通属性,普通代码块代码顺序有关属性为默认值)>构造方法

 

类的初始化步骤 
1、假如这个类还没有被加载和连接,那就先进行加载和连接
2、假如类存在直接的父类,并且这个父类还没有被初始化,那就先初始化它的父类
3、假如类中存在初始化语句时,那就依次执行这些初始化语句。

类的初始化时机 
所有的Java类只有在对类的首次主动使用时才会被初始化。主动使用的情况有六中,其他情况都属于被动使用:
1、 创建类的实例
2、访问某个类或接口的静态变量,或者对该静态变量赋值
3、调用类的静态方法
4、反射(Class.fotName)
5、初始化一个类的子类
6、Java虚拟机启动时被标明为启动类的类(main方法所在的类)

注意:1、当Java虚拟机初始化一个类时,要求他的所有父类都已经被初始化,但是这条规则并不适合接口。在初始化一个类或接口时,并不会先初始化它所实现的接口。
2、只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用。如果静态方法或变量在parent中定义,从子类进行调用,则不会初始化子类。

实例1.1:

public class testStatic {

    public static void main(String args[]){

        Singleton singleton=Singleton.getInstance();

        System.out.println(singleton.count1);

        System.out.println(singleton.count2);

    }

}

 

class Singleton{

    private static Singleton singleton=new Singleton();

    public static int count1;

    public static int count2=0;

 

    private Singleton(){

        count1++;

        count2++;

    }

 

    public static Singleton getInstance(){

        return singleton;

    }

}

输出的结果

 

 

 

 实例1.2

class Singleton{

 

    public static int count1;

    public static int count2=0;

     private static Singleton singleton=new Singleton();//调换了位置

    private Singleton(){

        count1++;

        count2++;

    }

 

    public static Singleton getInstance(){

        return singleton;

    }

}

位置调换,结果为

 

 

分析原因:

PS按自上向下顺序来

主要演示静态变量的加载。

1、在main方法中调用静态方法单利模式创建类的实例时,是对类的主动使用,同时在类的初始化时,会执行类的构造方法,在第一种情况下,执行完构造方法时,
Java虚拟机会对类中的静态变量进行复制,所以按顺序执行count1没有被赋值,还是count1++=1,而count2=0所以又重新被赋值为0了。
2、而第二种情况,是先赋值,再执行构造方法,所以结果为1,1.这个小例子说明类在初始化时,类里面的静态变量赋值语句和构造方法执行时是有先后顺序的。

 

实例二:

class FinalTest {

   // public static final int x=new Random().nextInt(100); //需要在运行时赋值,所以需要进行初始化          第一种场景

   public static final int x=2; //在编译时已经确定,不需要进行初始化                                                     第二种场景

    static

    {

        System.out.println("static block");

    }

}

public class testfinal{

    static{

        System.out.println("作为启动类测试");

    }

    public static void main(String args[]){

        System.out.println(FinalTest.x);

    }

}

第一种场景:

 

第二种场景:

 

 

分析原因:

主要考虑,静态变量为final类型的情形。
1、两种情况都输出了”作为启动类测试”,这是因为这个静态块放在了main函数所在的类作为了启动类,所以这属于对类的主动使用,所以每次都会执行这个静态块。
2、第一种情况输出了”static block”是因为在声明x时是new Random.nextInt(100),而这句声明在编译时是不能确定x的具体的值的。所以在运行时,需要去计算x的值,这就相当于调用了类中的静态变量。所以会去初始化该类。
3、第二种情况没有输出”static block”是因为x=2,而这条语句编译器在编译时就能确定x的值,所以在运行时直接调用它的值就可以了,不需要去初始化这个类。所以不会去执行静态块。
以上就是Java类从加载,连接到初始化的整个过程。

 

posted @ 2020-03-07 22:01  漫漫人生路322  阅读(141)  评论(0编辑  收藏  举报