JVM—内存模型

目录

前言

内存模型

概述

 JVM的架构模型

JVM的生命周期

类加载子系统

类的加载器以及类加载过程

类的加载器的分类

关于ClassLoader

双亲委派机制

运行时数据区

概述

 线程

 程序计数器(pc寄存器)

虚拟机栈(java栈)

本地方法接口

本地方法栈

 堆

方法区

对象实例化内存布局与访问定位

对象的实例化

 对象的内存布局

 对象访问定位

 直接内存

执行引擎

概述


前言

jdk,jre,jvm的关系
JDK(Java Development Kit) 是 Java 语言的软件开发工具包(SDK)。 在JDK的安装目录下有一个jre目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib合起来就称为jre。
JRE(Java Runtime Environment,Java运行环境),包含JVM标准实现及Java核心类库。JRE是Java运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器和调试器)
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

由上图一目了然:
JDK是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。JRE是运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。JVM是整个java实现跨平台的最核心的部分,能够运行以Java语言写的程序。

java跨平台:java程序在编译成java字节码之后,可以在安装了jvm的不同操作系统中运行

 跨语言的平台:很多语言只要经过自己的编译器编译,而且满足jvm的相关规范,就能在java虚拟机上运行

内存模型

概述

虚拟机分为程序虚拟机和系统虚拟机.

程序虚拟机:它专门执行单个计算机程序执行而设计。如JVM.

系统虚拟机:对物理计算机的仿真。如VMWAre.

作用:就是二进制字节码的运行环境

特点:一次编译到处运行,自动内存管理,自动垃圾回收功能

多个线程共享方法区和堆,每一个线程独有一个java栈和本地方法和程序计数器。

执行引擎就是把字节码转换成机器码,可以供操作系统识别

在java程序编译成字节码文件时候称为前端编译,在执行引擎中的编译是后端编译。

java代码执行流程:

 JVM的架构模型

java编译器输入的指令流一种是基于栈的指令集架构,另外一种指令集架构基于寄存器的指令集架构

1.基于栈的指令集架构

优点:(1)不需要硬件支持,可移植性更好,更好实现跨平台的操作

           (2)指令流中的指令大部分是领地址指令,其执行过程依赖操作栈,编译器更容易实现

              (3)  设计和实现更简单,适用于资源受限的系统

            (4)指令流中的指令大部分是0地址指令,其执行过程依赖于栈。指令集更小,编译器容易实现。

缺点:(1)基于栈的指令集的缺点在于完成相同的操作,指令数量通常要比基于寄存器的指令集数量要多

2.基于寄存器的指令集架构

优点:(1)基于寄存器的指令集是直接由CPU来执行的,它是在高速缓冲区进行执行的,速度要快很多

缺点:(1)依赖硬件,可移植性差(因为寄存器的指令集架构跟系统的cpu息息相关,所以移植性会很差)

因为跨平台的设计:Java的指令都是根据栈来设计的(不同CPU架构不同,所以不能用寄存器)

市场上暂时发现的虚拟机Dalvik VM 的虚拟机是基于寄存器的指令集架构,其他的都是基于栈的指令集架构

JVM的生命周期

JVM的启动

JVM的执行

JVM的退出

类加载子系统

类的加载器以及类加载过程

类加载子系统作用:类加载器子系统负责从文件中或者网络中加载class文件,class文件在文件开头有特定的文件标识。

classLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。

加载:把class文件加载到内存之中

1.通过一个类的全限定名获取定义此类的二进制字节流

2.将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构

3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类各种数据的访问入口

补充:加载.class文件的方式

从本地系统直接加载

通过网络获取,典型场景:web Applet

从zip压缩包中读取,成为日后jar,war格式的基础

运行时计算生成,使用最多的是,动态代理技术

由其他文件生成,典型场景:jsp的应用

从专有数据库中提取.class

从加密文件中获取,典型的防Class文件被反编译的保护措施

链接

验证

目的在于确保Class文件字节流中包含信息符合该虚拟机的要求,保证类加载的正确性,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

准备

为类变量分配内存并且设置该类变量的默认值,即零值。

这里不包含用final修改的static,因为final在编译时候就开始分配了,准备阶段会显式初始化;

这里不会为实例变量分 配初始化,类变量会分配在方法区中,而实例变量是会随对象一起分配到java堆中。

为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中

解析

将常量池内的符号转换成直接引用的过程,直接引用就算指向目标的指针,相对偏移量或一个间接定位到目标的句柄

事实上,解析操作往往会伴随JVM在执行完初始化之后再执行。

 初始化

初始化阶段就是执行类构造器方法<clinit> ()的过程。

此方法不需定义,是javac编译器会自动收集类在的所有类变量(static修饰 )的复制动作和静态代码块中语句合并而来。

构造器方法中指令按语句在源文件中出现的顺序执行。

<clinit>()不同于类的构造器。 (<clinit>()<构造器是虚拟机视角下的<init>()>)

若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()以及执行完毕

虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁

类的加载器的分类

概述

JVM支持两种类型的类加载器,分别为引导类加载器和自定义类加载器。

从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是java虚拟机规范缺没有这么定义,而是所有派生于(直接或者间接继承)抽象类Classloader的类加载器都划分为自定义类加载器。

public class Ts {
    public static void main(String[] args) {
        //获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
        //获取上层:扩展类加载器
        ClassLoader ex = systemClassLoader.getParent();
        System.out.println(ex);//sun.misc.Launcher$ExtClassLoader@1b6d3586
        //获取上层
        ClassLoader boo = ex.getParent();
        System.out.println(boo);//null
//证明java核心类库都是引导类加载器加载
        ClassLoader classLoader = String.class.getClassLoader();
        System.out.println(classLoader);//null
    }
}

 启动类加载器(引导类加载器,Bootstrap classLoader)

启动类加载器是一种虚拟机自带的加载器,这个类加载器使用c/c++语言实现,嵌套在JVM的内部。

它用来加载Java的核心类库(Java_HOME/jre/lib/rt.jar,resource.jar或sun.boot.class.path路径下的内容)

并不继承java.lang.calssloader,没有父加载器。

加载扩展类和应用类加载器,并指定为他们的父类加载器

出于安全考虑,Bootstrap启动类加载器只加载包名为java或者javax,sun等开头的类,就算是自定义的类,只要包名是以java或者javax或者sun开头就以启动类加载器加载该类,但是它加载的是核心类库中的类,如果核心类库中没有该自定义的类的名字,就会报错,这是一种保护机制。如果有就加载核心类库中的该类。这里其实就算一种沙箱安全机制

public class Ts {
    public static void main(String[] args) {
        System.out.println("-----------启动类加载那些路径下的API----------------");
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for(URL url: urLs){
            System.out.println(url);
        }
    }
}

扩展类加载器

扩展类加载器是一种虚拟机自带的加载器,使用java语言编写,派生于Classloader类

父类加载器为启动类加载器

从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK目录的JRE/LIb/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自带由扩展类加载器加载

public class Ts {
    public static void main(String[] args) {
        System.out.println("-----------扩展类加载器那些路径下的API----------------");
        String exDirs = System.getProperty("java.ext.dirs");
        for (String path :exDirs.split(";")){
            System.out.println(path);
        }
    }
}

应用程序类加载器(系统类加载器)

应用程序类加载器是一种虚拟机自带的加载器,Java编写

派生于ClassLoader类

父类加载器为扩展类加载器

它负责加载环境变量Classpath或者系统属性Java.class.path指定下的类库

该类加载是程序中默认的类加载器,一般来说Java应用的类都是由它来完成加载

用户自定义类加载器

为什么要用自定义类的加载器?

隔离加载类:在一个项目中某个中间件和我们项目中的某个框架的jar包中,存在一些类路径相同,类名相同,在这个时候就需要使用隔离加载类,就需要使用用户自定义类加载器。

修改类加载的方式:

扩展加载源:字节码的来源(上面有)

防止源码泄露: 防止字节码文件的反编译 我们对字节码进行加密,如果我们要运行,我们就需要解密,解密我们可以自己定义类加载器。

关于ClassLoader

ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自Classloader(不包括启动类加载器)。

ClassLoader的方法

方法名称描述
getparent()返回该类加载器的超类加载器
loadClass(String name)加载名称为name的类,,返回结果为java.lang.class类的实例
findClass(String name)查找名称为name的类,返回结果为java.lang.class类的实例
findLoadedClass(String name)查找名称为name的已经被加载过的类,返回结果为java.lang.class
defineClass(String name,byte[] b,int off, int len)吧字节数组b中的内容转换为一个java类,返回结果为java.lang.class类的实例
resolveClass(class<?> c)连接指定的一个java类

 获取ClassLoader途径

方式一:获取当前类的ClassLoader

clazz.getClassLoader()

方式二:获取当前线程上下文的ClassLoader

Thread.currentThread().getContextClassLoader()

方式三:获取系统类的ClassLoader

Classloader.getSystemClassLoader()

方式四:获取调用者的ClassLoader

DriverManager.getCallerclassLoader()

双亲委派机制

概述

Java虚拟机对Class文件时按需加载得方式,也就是说当需要某个类的时候才会将它的class文件加载到内存中生成Class对象。而且加载某个类的Class文件时,java虚拟机采用的是双亲委派模式,即把请求交给父类处理,它是一种任务委派模式。

工作原理

1.如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给自己的父类的加载器加载。

2.如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求将最终到达顶层的启动类加载器

3.如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

 应用场景:当程序执行时候,就开始类加载,看该类中加载的东西是什么,就选择不同的类加载器,就会有不同的结果,遵循双亲委派机制

public class Ts {
    @Test
    public void test() {
        ava.lang.String s = new ava.lang.String();
    }
    @Test
    public void test1(){
        java.lang.String s = new java.lang.String();
    }
}

 出现以下情况是因为双亲委派机制递归到了启动类加载器,启动类加载器加载了核心api的String,而核心Api的String没有main方法。

public class String {
    static {
        System.out.println("String类加载器的静态代码块");
    }

    public static void main(String[] args) {
        System.out.println("ss");
    }
}

 双亲委派机制的优势

1.防止类的重复加载:因为加载类时候,类找到合适的类加载器,就会返回成功,避免了类重复加载。

2.防止核心API被纂改。

自定义类:

java.lang.String:如果没有双亲委派机制,就会加载自定义类,导致无法加载核心APi

java.lang.Skthfsu:下面会报错的原因是核心Api会有自己的保护机制无法被纂改,禁止在该包名下自定义类

类加载器是启动类加载器,加载核心类库

public class Sfjof {
    public static void main(String[] args) {
        
    }
}

 沙箱安全机制

自定义String类,但是在加载自定义String类的时候会先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载JDK自带的文件(rt.jar包中java/lang/String.class),报错信息说没有main方法,就是因为加载rt,jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。

类的主动使用和类的被动使用

JVM中表示两个class对象是否为同一个类存在两个必要条件

1.类的完整类名必须一致,包括包名。

2.加载这个类的classloader(指classloader实例对象)必须相同。比如说自定义和我们核心类库中的String的类加载器就不一样,一个是系统类,一个是启动类。所以他们不是一个类。

主动使用的情况

创建类的实例

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

调用类的静态方法

反射

初始化一个类的子类

java虚拟机启动时被标明为启动的类

JDK7开始提供的动态语言的支持

除以上七种情况都是类的被动使用,都不会导致类的初始化

运行时数据区

概述

一个进程(所有线程)共享方法区(堆外内存(永久代或者元空间,代码缓存))和堆,每一个线程都有独立的程序计数器,本地方法栈,虚拟机栈。

每一个JVM只有一个Runtime实例。即运行时环境,相当于内存结构中的运行时数据区 

 线程

 程序计数器(pc寄存器)

pc寄存器介绍

PC寄存器:用来存储指向下一条指令的地址,也就是即将执行的指令代码。由执行引擎读取下一条指令。

它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。
在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址。
它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。(即是记录你的线程执行到了哪里,如果线程执行权被抢走,当它抢回执行权继续执行时,程序计数器会告诉它从哪里继续开始执行)
字节码解释器工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

不存在gc,和oom

在pc寄存器中查看下一条指令,根据指令对操作数栈进行出栈入栈,指令要通过执行引擎翻译成机器指令

虚拟机栈(java栈)

概述   

内存中的堆与栈

栈:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储问题,即数据怎么放,放在哪?  

java虚拟机栈是什么?
java虚拟机栈,早期也叫java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次一次的java方法调用。

是线程私有的

生命周期

生命周期与线程一致

作用:主管java程序的运行,它保存方法的局部变量,部分结果,并参与方法的调用与返回。

栈的特点

栈是一种有效分配存储的方式,访问速度仅次于程序计数器。

对栈来说不存在垃圾回收问题,但是存在内存溢出问题

栈中可能出现的异常

java虚拟机规范允许java栈的大小是动态或者是固定不变的。

如果采用固定大小的java虚拟机栈,那每一个线程的java虚拟机栈容量可以在线程创建的时候就独立选定。如果线程请求分配的栈容量超过java虚拟机允许的最大容量。java虚拟机将会抛出一个StackoverflowError异常。

如果java虚拟机栈可以动态扩展,并且尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,虚拟机将会抛出OutofMenryError。

设置栈的大小

 栈中存储了什么?

每个线程都有自己的栈,栈中数据都是以栈帧的格式存在。

在这个线程上只在执行的每个方法都各自对应着一个栈帧

栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。 

栈帧的内部结构 

局部变量表

操作数栈

动态链接(指向运行时常量池的方法引用)

方法返回地址(方法正常退出或者异常退出的 定义)

一些附加信息

 局部变量表

定义为一个数字数组(但是基本存储结构是变量槽),主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括基本数据类型,对象引用,以及Returnadress类型

由于局部变量是建立在线程的栈上,是线程私有数据,因此不存在数据安全问题

局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的code属性的maxmum local variables数据项中。在方法运行期间是不会改变局部变量表的大小

关于SLot的理解

 局部变量表,最基本的存储单元是Slot(变量槽),在局部变量表里面,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long喝double)占用两个slot。

JVM会位局部变量表中的每一个Slot部分分配一个访问索引,通过这个索引就可以成功访问到局部变量表中指定的局部变量表

当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot上

如果需要访问局部变量表中一个64bit的局部变量值时。只需要使用前一个索引即可。因为64bit是有2个Slot,也就是涉及两个索引,4或者5.

 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放到index为0的slot处,其余的参数按照参数表顺序排列。

 栈帧中的局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量很可能会复用过期局部变量的槽位,而达到节省资源的目的

 变量的分类:按照数据类型分:1.基本数据类型2.引用数据类型

                       按照类中声明的位置:成员变量:在使用前,都经历过默认初始化赋值

                                                                            类变量(static):linking的prepare阶段:给类变量默认赋值--->initial阶段:给类变量显示赋值和静态代码块赋值

                                                                             实例变量 :随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值

                                                           局部变量:使用前必须显示赋值(就是要有赋值),否则编译不通过

操作数栈

19.JVM栈帧的内部结构-操作数栈(Operand stack)_simpleGq的专栏-CSDN博客_操作数栈

动态链接

 

 方法返回地址

PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。

一个方法结束有两种方式:

正常执行完成

出现未处理的异常,非常退出

无论通过那种方式退出,在方法退出后都返回到该方法的被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址给我们的执行引擎,即调用该方法的指令的下一条指令的地址,而通过异常退出的,返回地址是要通过异常表来确定,栈帧一般不会保存这部分信息。

一些附加信息

跟虚拟机实现相关的一些附加信息。

本地方法接口

什么是本地方法

Native Method就是一个java调用非java代码的接口。一个Native Method就是一个Java方法:该方法实现由非java语言实现。

本地接口的作用就是融合不同编程语言为java所用。 

在定义一个native method时,并不提供实现体(有点像定义一个java的interface),因为其实现体是由非java语言在外面实现

    public final native Class<?> getClass();

 为什么要使用Native Method

java使用起来非常方便,然而有些层次的任务用java实现非常不容易或者在意效率

与java环境外交互:有时候java应用需要与Java外面的底层环境进行交互这是主要原因

你可以想想java需要与底层系统,如操作系统或者某些硬件交换信息情况。本地方法正是这种交流机制,它为我们提供了一个非常简洁的接口,我们无需了解java应用之外的繁琐应用。

本地方法栈

java虚拟机栈用于管理java方法的调用,而本地方法栈是用于本地方法的调用。

本地方法栈,也是线程私有的。

允许被实现成固定或者可动态扩展的内存大小(在内存溢出方面是相同的)

如果采用固定大小的java虚拟机栈,那每一个线程的java虚拟机栈容量可以在线程创建的时候就独立选定。如果线程请求分配的栈容量超过java虚拟机允许的最大容量。java虚拟机将会抛出一个StackoverflowError异常。

如果java虚拟机栈可以动态扩展,并且尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,虚拟机将会抛出OutofMenryError。

本地方法是使用c语言实现

 本地方法栈中登记本地方法,然后动态链接调用相关本地方法库,在执行引擎执行时加载本地方法库。 

 堆

pc寄存器没有gc和oom

虚拟机栈没有gc有oom

本地方法没有gc有oom

堆有gc和oom 

方法区有GC和oom

堆的核心概述

一个JVM实例只存在一个堆内存,堆也是java内存管理的核心区域。 

java堆区在JVM启动时候就被创建,其空间就确定了。是jvm中最大的空间,堆的大小可以调节

堆的内存分配是不规律的

一个java程序代表一个进程,一个java实例对应一个堆空间。

java虚拟机规范认为,堆可以处于物理上不连续,逻辑上连续。

所有线程共享Java堆,在这里还可以划分线程私有的缓冲区(TLAB):就是把堆分成很多份,每个线程独占一份 .

"几乎"所有的对象以及数组都应该分配到堆上,在虚拟机栈中有个引用指向堆中数组(数组也是new出来的)以及对象。

在方法结束之后,引用没有了方法出栈了,堆中的对象不会被马上移除,仅在垃圾收集的时候才会被移除, 堆,是gc执行垃圾回收的重点区域

 堆内存细分

现代垃圾收集器大部分都是基于分代收集理论设计,堆空间细分为:

java7及之前版本堆内存逻辑上区分为三个部分:新生区+养老区+永久区

年轻代又分为eden space(伊甸园区),surviver(幸存者区):分为surviver 0(幸存者0区)(from)和surviver 1(幸存者1区)(to)

java8堆空间逻辑上区分为三个部分:新生区+养老区+元空间

年轻代又分为eden space(伊甸园区),surviver(幸存者区):分为surviver 0(幸存者0区)(from)和surviver 1(幸存者1区)(to)

约定:新生区=新生代=年轻代      养老区=老年区=老年代      永久区=永久代

为啥要说是逻辑上,因为永久区或者原空间实际上不属于堆空间,实际上属于方法区的落地实现。

 下图可以证明,实际上永久代或者原空间不属于堆,因为堆的大小是10m,最大的内存也是10m,老年代和年轻代加起来正好是10m.因此下面"-Xms"设置的是堆空间的大小,其实堆空间就是老年代+年轻代

 设置堆内存大小与oom

堆在JVM启动的时候就已经设定好了,大家可以通过选项"-Xms"和"-Xmx"来进行设置

"-Xms"用于表示堆区的其实内存,等价于-XX:InitialHeapSize

"-Xmx"则用于表示堆区最大内存,等价于-XX:MaxHeapSize

一旦堆区中内存大小超过"-Xmx"所指定的最大内存时候,将会抛出outofMemoryError异常。

通常会将-Xms和-Xmx两个参数配置相同的值,其目的就是未来能在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区大小,从而提高性能,在生产环境中,如果监视GC数据,您会注意到这是相对较短的时间段(通常少于一小时),JVM最终会将堆大小增加到-Xmx设置。每次JVM增加堆大小时,它都必须向操作系统请求额外的内存,这会花费一些时间(因此会增加GC命中时正在处理的所有请求的响应时间)

默认情况下,初始内存:物理电脑内存大小的64分之一

最大内存大小:物理电脑内存大小的四分之一

查看堆中设置参数

方式一:jps 查看进程     jstat -gc 进程id

 方式二:-Xms10m -Xmx10m -XX:+PrintGCDetails在run的edition中编辑

Heap
 PSYoungGen      total 2560K, used 1831K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 89% used [0x00000000ffd00000,0x00000000ffec9ed8,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
 Metaspace       used 3233K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

OOM举例与说明

在如下程序运行时,设置堆最大内存,和启动时堆内存的大小为600m,并在Jvisual的visual gc可视化插件中观察,幸存者0区,幸存者1区,伊甸园区,老年代区的变化,最后oom。

public class T1 {
    public static void main(String[] args) {
        ArrayList<picture> list = new ArrayList<>();
        while(true){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(new picture(new Random().nextInt(1024*1024)));
        }
    }
}
class picture{
    private  byte[] p;

    public picture(int length) {
        this.p = new byte[length];
    }
}

 新生代和老年代的各种参数设置

存储在JVM中的java对象可以被划分为两类对象:

一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速

另一类对象的生命周期却非常长,在某些极端情况下还能够与JVM的生命周期保持一致

 配置新生代与老年代在堆结构的占比。一般不建议修改这个默认参数,除非我们的程序中,我们认为确实有很多的生命周期特别长的对象,我们就把老年代的占比增大。 

默认 -XX:NewRatio=2 表示新生代占1,老年代占2

可以修改 -XX:NewRatio=4 新生代占1,老年代占4

在Hotspot中,eden空间和另外两个Survivor空间缺省所占比例为8:1:1.但是不是本身就是,需要设置-xx:survivorRatio = 8;

当然开发人员都可以通过选项“-xx:survivorRatio”调整这个空间比例

几乎所有的java对象都是Eden区被new出来,为什么说是几乎,因为如果这个对象非常大,伊甸园区都装不下。

绝大部分java对象的销毁都在新生代进行  

 -XX:-UserAdaptiveSizePolicy:关闭自适应的内存分配策略

-Xmn:设置新生代的空间大小

对象分配的过程

1.new对象先放在伊甸园区。此区有大小限制。

2.首先当伊甸园空间满的时候,程序又需要创建对象,JVM的垃圾回收器对伊甸园区进行垃圾回收,对伊甸园垃圾回收的同时对顺便幸存者区也进行垃圾回收。当幸存者区中一区或者2区满的时候不能进行垃圾回收,而是直接把幸存者区中的对象放入老年代。(幸存者区只能使用一个),将伊甸园中的不在被引用的对象进行销毁,再加载新的对象进入伊甸园区

3.然后将伊甸园区中剩余的对象移动到幸存者区

                                                                                            from                               to

 4.如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。

                                                                                          from                              to                          5.如果再次经历垃圾回收,此时就会重新放回     幸存者1区

啥时候能去养老区呢?可以设置次数。默认是15次。   

可以设置参数: -XX:MaxTenuringThreshold=<N>进行设置。                                            

                                                                                                      to                      from

6.在养老区,相对悠闲。当养老区内存不足时,再次触发GC:Major GC

7.若养老区执行Majorgc之后依然无法保存对象,就oom异常

from,to那个是from那个是to不好说,谁空谁是to

关于垃圾回收:频繁在新生区回收,很少在养老区回收,几乎不在永久区/元空间回收 

 对象分配的特殊情况

 FullGC和MajorGC有区别

就是当对象过大时候可以直接晋升老年代,如果在垃圾回收机制之后还是放不下,就oom

常用的调优工具

JDK命令行:jps,JvisualVM等

visualVM

Jprofiler

Minor GC(YGC),Full GC(FGC),Major GC(MGC)

一个程序属于一个进程,一个进程有多个线程,用户线程是执行程序的,gc线程是垃圾回收,当垃圾回收是会停止用户线程,从而导致,系统的吞吐量和性能下降,减少gc就让用户线程较少的被干扰。

我们要重点关注Full  GC ,Majar GC,因为他们让用户线程暂停的时间多。 

JVM进行GC时,并非每次都对上面三个内存(新生代,老年代;方法区)区域一起进行回收,大部分是对新生代进行回收,大部分的新生代对象朝生夕死。

争对HotSpor VM,GC按照区域回收分类,分为两个类型,一种是部分收集,一种是整堆收集

部分收集:不是完整对整个java堆进行收集垃圾。其中分为:

                  新生代收集(minor GC/Young GC):只是年轻代的垃圾收集

                  老年代收集(Major GC/old GC):只是老年代的垃圾收集

                          目前只有CMS GC会单独收集老年代

                          注意,很多时候Major GC会和Full GC混肴使用,要看是整堆回收还是老年代回收

                  混合收集(Mix GC):收集整个新生代以及部分老年代的垃圾收集

                            目前,只有G1 GC会有这种行为

整堆收集(Full GC):收集整个JAVA堆和方法区的垃圾收集

分代式GC策略的触发条件

年轻代GC(Minor GC)触发机制:

当Eden代满时候,会触发GC,顺便也会GCsurvivor,但是当survivor满不会引发GC。

因为JAVA对象大多数具备朝生夕灭,所以Minor GC非常频繁,一般回收速度快。

Minor GC会引发STW,暂停其他用户线程,等垃圾回收结束,用户线程才恢复。

STW:用户线程生产垃圾对象,而垃圾回收线程回收垃圾,如果俩个同时并行,一个标记对象为垃圾,一个生产垃圾,那么就会一直无法回收,因此需要暂停用户线程。-

  • Major GC
    • 出现major gc经常伴随至少一次的Minor GC,因为经历过minorgc,才能有major gc(但非绝对的,在Paraller Scavenge 收集器的收集策略中就有直接进行Major GC的策略选择过程),
    • 也就是说在老年代空间不足时,会先尝试触发Minor GC,如果之后空间还不足,会触发Major GC。
    • Maror GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。
    • 如果Major GC后,内存还不足,就会报OOM.
  • Full GC
    • 调用System.gc()时。系统建议执行Full GC,但是不必然执行
    • 老年代空间不足
    • 方法区空间不足
    • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
    • 由Eden区,幸存者0区向幸存者1区复制时,对象大小大于1区可用内存,则把该对象转存到老年代,且老年代的可用内存大小小于该对象大小。
    • 注意:full GC是开发或调优中尽量要避免的,这样STW会短一些

堆空间的分代思想

实际上堆是一个完全二叉树的数组对象,看起来是连续存储,其实并不是,其实是分散存储。

 为什么需要把java堆分代?不分代就不能正常工作?

         其实不分代完全可以,分代的唯一理由是优化GC性能。如果没有分代,那所有的对象都在一起。GC的时候 要找到那些对象没用,这样就会对堆的所有区域进行扫描。但是很多对象都是朝生夕死,如果分代的话,把新建的对象放到某个地方,当GC的时候先把这块存储“朝生夕死”对象进行回收,这样就会腾出很大的空间

内存分配策略

      如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能够在Survivor区容纳的话,将会被移动到survivor中,并将对象年龄通过年龄计数器设置为1.如果不能容纳,容纳不下就晋升为老年代,如果老年代还是容纳不下就Major GC,Major GC之后还是容纳不下就oom(内存溢出)。当Survivor区每熬过一次minor GC,年龄就增大一岁,当年龄增大到一定程度(每个JVM的默认程度都有所不同),就会被晋升到老年代。

对象晋升老年代的年龄阈值,可以通过选项 -XX:MaxTenuringThreshold来设置。

针对不同年龄段对象分配原则如下所示:

优先分配到Eden

大对象直接分配到老年代(尽量避免程序中出现过多的大对象)

长期存活的对象分配到老年代

动态对象年龄判断

如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

空间分配担保

-XX:HandlePromotionFailure

TLAB(Thread Local Allocation Buffer)

堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据

由于对象实例的创建在JVM中非常复杂,因此在并发的条件下堆空间的线程是不安全的,为了避免多个线程操作同一个地址,就需要加锁机制,进而影响分配速度,因此就搞了一个TLAB空间在Eden区,但是只占eden区的百分之一,非常小,为每个线程分配内存,但是如果对象过大那么就不用TLAB了,直接使用加锁的机制,当然我们也可以设置是否开启TLAB空间:-XX:UseTLAB

,我们也可以设置TLAB空间所占用Eden空间的百分比大小:-xx:TLABWasteTargetPercent

堆空间的参数设置

官网:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#BGBCIEFC

-XX:+PrintFlagsInitial:查看所有的参数的默认初始值

-XX:+printFlagsFinal:查看所有参数的最终值

-Xms:初始堆内存空间(默认物理内存的1/64)

-Xmx:最大堆空间内存(默认物理内存的1/4)

-Xmn:设置新生代的大小。(初始值及最大值)

-XX:NewRatio:配置新生代与老年代在堆结构占比,默认2

-XX:survivorRatio:设置新生代中Eden和s0/s1空间的比例

-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄

-XX:+printGCDetails:输出详细的GC处理日志

打印GC简要信息:1。-XX:PringtGC 2.-verbose:gc

-XX:handlePromotionFailure:是否设置空间分配担保

 堆是存储对象的唯一选择吗

随着JIT编译器发展与逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化,所有对象分配到堆上也没有那么绝对。

1.如果经过逃逸分析,一个对象没有逃逸出方法,那么可能被优化成栈上分配。无需堆上分配,也无需进行垃圾回收

2.基于OPenJDK深度定制的TaoBao VM,其中创新的GCIH(GC invisible heap)技术实现0ff-heap,将生命周期较长的java对象从heap中移到heap外,并且GC不能管理GCIH内部的JAVA对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。  

 逃逸分析概述

如何将堆上的对象分配到栈,需要使用逃逸分析的手段。

这是一种可以有效减少java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法

通过逃逸分析,java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否将这个对象分配到堆上。

所谓的逃逸分析,其实就是看方法中的对象是否可以在其他地方可以被使用。

 没有发生逃逸的对象,可以分配到栈上面,随着方法的执行的结束,栈空间被移除

在Java代码运行时,通过JVM参数可指定是否开启逃逸分析,

-XX:+DoEscapeAnalysis : 表示开启逃逸分析

-XX:-DoEscapeAnalysis : 表示关闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis

 逃逸分析:代码优化

1.栈上分配:将堆分配转换为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会发生逃逸,对象可能是栈分配的候选,而不是堆分配。 

JIT编译器在编译期间根据逃逸分析的结果,如果发现一个对象并没有逃逸出方法的话,就可能会优化为栈上分配。分配完成之后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量也会被回收。这样就不需要垃圾回收。

2.同步省略。如果一个对象被发现只能从一个线程被访问,那么对于这个对象的操作可以不考虑同步。

3.分离对象或者标量替换 。

方法区

栈,堆,方法区的关系

 方法区的理解

 在《java虚拟机规范》中明确说明:“尽管所有方法区在逻辑上属于堆的一部分”,但是对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap,目的就是要和堆分开,所以方法区看作是一块独立于Java堆的内存空间。加载的类放在方法区。 

方法区与java堆一样,是各个线程共享的内存区域。

方法区在JVM启动的时候被创建,并且它实际的物理内存空间中和java堆区一样都是可以不连续的

方法区的大小和堆空间一样,可以选择固定大小或者可扩展

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang,outofmenmory:permgem space(java7)java.lang.outofmenmoryerror:Metaspace

关闭JVM会释放这个区域内存

HotSpot中方法区的演进过程

JDK7及以前,习惯上把方法区,称为永久代。JDK8开始元空间取代永久代

方法区的大小设置

可以选择固定大小或者可扩展和压缩

JDK7及以前:通过-XX:PermSize来设置永久代初始分配空间。默认值20.75M

-XX:MaxpermSize来设定永久代最大可分配空间。32位机器默认是64M,64位机器是82M,如果超出就会OOm

JDK8及以后:

元数据区大小可以使用参数-XX:MetaspaceSize和MaxMetaspaceSize指定初始空间和最大空间

默认初始空间是21M,最大空间默认是-1,也就是没有限制 ,与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有系统内存,如果元数据区发生溢出,虚拟机一样会抛出OutOFmemoryError:metaspace

-XX:MetaspaceSize100M  -XXMaxMetaspaceSize100M(max一般不指定)

-XX:MetaspaceSize:设置初始的元空间大小

XX:Metaspace的值为21M。这就是初始的高水位线,一旦触及这个水位线,full GC将会被触发并卸载没用的类(也就是对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线取值取决于GC后释放l多少元空间。如果释放的空间不足,那么不超过maxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。

如果初始化高水位线设置过低,上述高水位线调整情况会发生很多次,通过垃圾回收机制可以观察到full GC多次调用,为了避免多次调用GC,建议将初始值设置成为一个较高值

方法区的内部结构

在不同的JDK版本中字符串常量池存放的位置有一些变化

它由于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后代码缓存

在方法区中应该保存类加载器,因为这个类会记录它是被那个类加载器加载到方法区,

 类型信息

对每个加载的类型(类class,接口interface,枚举enum,注解annotion),JVM必须在方法区存储以下类型信息:

1.这个类型的完整有效名称(全名=包名.类名)

2.这个类型直接父类的完整有效名称(interface或者object)

3.这个类型的修饰符

4.这个类型直接接口的一个有序列表

 域信息(Field)

 方法信息

non-final的类变量

静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分。

类变量被类的所有实例共享,即使没有类实例时可以访问它。

 补充说明:全局常量:static final

这种在编译的时候就赋值了。

运行池常量池

方法区,内部包含了运行时常量池。把编译之后的常量池类加载到方法区之后就叫做运行池常量池。

字节码文件,内部包含了常量池。

方法区的使用

程序计数器保存的是下一条指令的地址,然后被执行引擎读取,程序计数器由虚拟机栈中的方法返回地址改变。

public class Me {
    public static void main(String[] args) {
        int x = 500;
        int y = 100;
        int a = x/y;
        int b = 50;
        System.out.println(a+b);
    }
}

javap -v -p Me.class > test.txt进行反编译

Classfile /C:/Users/Administrator/Desktop/online-shopping-mall-ssm-vue/untitled/out/production/untitled/Me.class
  Last modified 2021-12-4; size 568 bytes
  MD5 checksum 68f812fbd6f5a5d096d30698a33e4d60
  Compiled from "Me.java"
public class Me
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#24         // java/lang/Object."<init>":()V
   #2 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #27.#28        // java/io/PrintStream.println:(I)V
   #4 = Class              #29            // Me
   #5 = Class              #30            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               LMe;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               x
  #18 = Utf8               I
  #19 = Utf8               y
  #20 = Utf8               a
  #21 = Utf8               b
  #22 = Utf8               SourceFile
  #23 = Utf8               Me.java
  #24 = NameAndType        #6:#7          // "<init>":()V
  #25 = Class              #31            // java/lang/System
  #26 = NameAndType        #32:#33        // out:Ljava/io/PrintStream;
  #27 = Class              #34            // java/io/PrintStream
  #28 = NameAndType        #35:#36        // println:(I)V
  #29 = Utf8               Me
  #30 = Utf8               java/lang/Object
  #31 = Utf8               java/lang/System
  #32 = Utf8               out
  #33 = Utf8               Ljava/io/PrintStream;
  #34 = Utf8               java/io/PrintStream
  #35 = Utf8               println
  #36 = Utf8               (I)V
{
  public Me();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:所对应的真实行号。
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LMe;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=1
         0: sipush        500
         3: istore_1
         4: bipush        100
         6: istore_2
         7: iload_1
         8: iload_2
         9: idiv
        10: istore_3
        11: bipush        50
        13: istore        4
        15: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_3
        19: iload         4
        21: iadd
        22: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        25: return
      LineNumberTable:
        line 3: 0
        line 4: 4
        line 5: 7
        line 6: 11
        line 7: 15
        line 8: 25
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      26     0  args   [Ljava/lang/String;
            4      22     1     x   I
            7      19     2     y   I
           11      15     3     a   I
           15      11     4     b   I
}
SourceFile: "Me.java"

 方法区的演进过程

1.只有hotSpot才有永久代。

2.Hotspot中方法区的变化:

JDK1.6及以前

有永久代,静态变量放在永久代
jdk1.7有永久代,但是已经逐步去除永久代,字符串常量池,静态变量移除,保存在堆中
jdk1.8及以后无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但是字符串常量池,静态变量仍在堆中

 之前jdk6及以前和jdk7都是放在虚拟机内存,而jdk8是元空间放在本地内存中,虚拟机内存和本地内存存在一个映射

为什么字符串常量池和静态变量为什么需要改到堆中

   在JDK7中将StringTable放在了堆空间中。因为永久代的回收效率低,在full gc的时候才会触发。而full Gc是老年代的空间不足,永久代不足时候才会触发,这导致了StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代空间不足。放到堆空间中能够及时回收内存

方法区的垃圾收集

这个区域的回收效果比较令人难以满意,尤其类型的卸载,但是又需要卸载,不然会导致内存泄漏

方法区的垃圾收集主要回收两个部分:常量池中废弃的常量和不在使用的类型

方法区内常量池之中主要存放两大类常量:字面量和符号引用

1.类和接口的全限定名称

2.字段方法和描述符号

3.方法的名称和描述符号

只有常量池中的常量没有被引用就可以被回收

对象实例化内存布局与访问定位

对象的实例化

创建对象的方式

1.new

2.Class的newInstance()

3.Constructor的newInstance(Xxx)

4.使用clone()

5.使用反序列化

6.第三方库Objenesis

对象的创建

 1.默认初始化 2. 显示初始化  3.代码段初始化 4.构造器中初始化

public class Me {
    //显示赋初值
    int id = 100;
    String name;
    Accout accout;
    //代码段赋初值
    {
        name = "ss";
    }
//构造函数赋初值
    public Me(Accout accout) {
        this.accout = accout;
    }
}
class Accout{
    
}

 对象的内存布局

 对象访问定位

 句柄访问

 直接指针(hotSpot)

 直接内存

原空间就是使用的是直接内存,然后在方法区有个指针指向元空间,方法区的落地实现

执行引擎

概述

执行引擎是java虚拟机核心组成部分之一,能执行那些不被硬件直接支持的指令集格式 。JVM的主要作用就是负责转载字节码到其内部,但是字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部仅仅包含只是一些能够被JVM所识别的字节码指令,符号表,以及其他辅助信息。如果要一个java程序运行起来,执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,jvm的执行引擎就是将高级语言翻译为机器语言的译者。

执行引擎的作用过程:

(1)执行引擎在执行的过程中究竟需要执行什么样的字节码完全依赖于pc寄存器。

(2)每当执行完一项指令之后,pc寄存器就会更新下一条需要被执行的指令地址。

(3)当方法在执行的过程中,执行引擎就可能通过执行局部变量表中的对象引用定位到堆中的对象实例,通过对象头中的元数据指针定位到目标对象的类型信息。

java代编译和执行的过程

橙色的是由前端编译器完成

 java是半解释半编译的语言,什么是解释器?什么是JIT编译器?

 解释器:当java虚拟机启动时候会根据预定义规范对字节码采用逐行解释的方式执行。

JIT编译器:虚拟机将源代码直接编译成和本地机器相关的机器语言。

在HotSpot中,解释器和即时编译器并存的架构,解释器和即时编译器能够互相协作,各自取长补短,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行的时间。

为什么还要保存解释器来拖累程序的性能?

当程序启动后,解释器可以马上发挥作用,马上就拿着字节码逐行解释执行,解释速度快,

即时编译器需要编译成本地指令之后才能执行,但是执行效率高。在hotSpot虚拟机中,解释器首先发挥作用,而不必等待即时编译器全部编译完成,这样可以省去许多不必要的编译时间,而且随着程序时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以获取更高的程序执行效率。同时,在解释执行在编译器进行激进优化不成立的时候,作为编译器的“逃生门”。

概念解释:前端编译器:java源程序变成.class文件

SUN公司的javac

 后端运行期编译器:字节码变成机器码的过程

c1和c2编译器,分别为Client Compiler和Server Compliler,

-client:指定java虚拟机在client模式下运行,并使用C1编译器:c1编译器只是对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度。

-server:指定java虚拟机运行在Server模式下,并使用C2编译器:耗时长的优化,以及激进的优化,但是代码执行效率高

静态提前编译器:直接把.java文件编译成本地机器代码的过程

Graal编译器

热点代码及探测方式

当然是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。JIT编译器在运行的时候会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升java程序性能

一个被多次调用的方法,或者一个方法体内部循环体都可以被称为“热点代码”,因此都可以通过JIT编译器编译为本地机器指令,这种编译方式发生在方法的执行过程中,因此也被称之栈上替换,简称OSR

一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准?

这里必须要有一个明确的阈值,JIT编译器才会将这些代码编译为本地机器指令,这里主要依靠热点探测功能。

目前HotSpot VM所采用的热点探测是基于计数器的热点探测,HotSpot VM将会为每一个方法都创建2个不同类型的计数器,分别为方法调用计数器和回边计数器

方法调用计数器用于统计方法的调用次数

这个计数器就是用于统计方法被调用的次数,它默认阈值在client模式下时1500次,在server模式下1000次。超过阈值,就会触发JIT编译

这个阈值可以通过虚拟机参数-XX:CompileThreshold来人为设定。 

当方法被调用的时候,首先判断该方法是否被编译,如果被编译就直接执行,如果没有就方法调用计数器加1,然后判断方法调用计数器和回边计数器值之和是否超过方法调用计数器的阈值,如果超过阈值就用即时编译器编译,如果没有就用解释器解释方法执行。

热度衰减

如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间内方法被调用的次数。当超过一定的时间限度,如果方法调用的次数仍然不足以让它交给即时编译器编译,那这个方法的调用计数器就会减少一半,这个过程称为方法调用计数器热度的衰减,而这段时间就叫半衰周期

进行热度衰减的动作时在进行虚拟机进行垃圾收集时候顺便进行的,可以使用虚拟机参数来关闭热度衰减 -XX:-UseCounterDecay来关闭热度衰减

-XX:CounterHalffilfeTime参数来设置半衰周期的时间,单位是秒。

回边计数器用于统计循环体执行的循环次数

 为什么是半解释半编译?

因为在java中既可以用解释器,也可以用JIT编译器。在java虚拟机运行时,解释器和即时编译器并存的架构。在java虚拟机运行时,解释器和即时编译器能相互协作,各自补长取短,尽力选择合适的方式权衡编译本地代码的时间和直接解释执行代码的时间。

机器码

各种用二进制编码方式表示的指令,叫做机器指令码。开始,人们就用它编写指令,这就是机器语言。虽然机器语言容易被计算机理解和接受,但是和人们的语言差别太大,不容易被接受或者理解记忆,并且用它编程容易出错。不同类的cpu所对应的机器指令也就不同。

指令

由于机器码是0和1组成的二进制序列,可读性实在太差,于是人们发明了指令。指令就是把机器码中特定的0和1序列,简化成对应的指令

由于不同的硬件平台所对应的指令不同,因此有了指令集

指令集

不同硬件平台,各自支持的指令,是有差别的,因此每个平台所支持的指令,称之为指令集

HotSpot VM可以设置程序执行方式

缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员根据具体的应用场景,设置通过命令显示地java虚拟机指定在运行时到底是采用完全解释还是采用即时编译器

-Xint:完全采用解释器模式执行程序;

-Xcomp:完全采用即时编译器模式执行程序。如果即时编译器出现问题,解释器会介入执行

-Xmixed:采用解释器+即时编译器混合模式共同执行

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值