jvm深入理解

jvm与java体系结构

简介

随着java7的发布,java虚拟机的设计者通过jsr-292规范基本是现在java虚拟机平台上运行费java语言编写的程序.

java虚拟机根本不关心运行在内部的程序到低是使用何种编程语言编写的,它只关心字节码文件.也就是说java虚拟机拥有语言无关性,并不会单纯的与java语言终生绑定,值要其他语言的便以结果满足并包含java虚拟机的内部指令集,符号以及其他辅助信息,它就是一个有效的字节码文件,能够被虚拟机所识别并装在运行

概念

介绍

所谓虚拟机就是一台虚拟的计算机.它是一款软件,用来执行一系列虚拟的计算机指令.大体上,虚拟机可以分为系统虚拟机和程序虚拟机.

  • 大名鼎鼎的visual box和vmware就属于系统虚拟机,他们是对物理计算机的仿真,提供了一个可运行完整操作系统的平台
  • 程序虚拟机的典型代表就是java虚拟机,它专门执行单个计算机程序而设计,在java虚拟机中执行的指令我们称之为java字节码指令.

无论是系统虚拟机还是程序虚拟机,上面运行的软件都被限制于虚拟机提供的资源中

java虚拟机

java虚拟机是一台执行java字节码的虚拟计算机,它拥有独立的运行机制,其它运行的java字节码也未必由java语言编译而成

jvm平台的各种语言可以共享虚拟机带来的跨平台性,优秀的垃圾回收机器,以及可靠的即使编译器

java技术的核心就是jvm(java virtual machine),因为所有的java程序都运行在java虚拟机内部

作用:

java虚拟机就是二进制字节码的运行环境,负责装载字节码到内部,解释编译为对应平台上的机器指令执行.每一条java命令,java虚拟机规范中都有详细的定义,怎么取操作数,怎么处理操作数,结果放在哪里

特点

一次编译到处运行

自动内存管理

自动垃圾回收功能

结构

jvm是在操作系统之上的

hotspot vm是目前市面上高性能虚拟机的代表作之一

它采用解释器与即使编译器并存的架构

在今天java程序的运行性能脱胎换骨,语句达到了可以和c/c++程序一较高下的地步

代码执行流程

java源码经过java编译器生成字节码文件,java虚拟机执行字节码文件把字节码文件编译成机器指令.

架构模型

nava虚拟机输入的指令流基本上是一种基于栈的指令集架构,另一种指令集架构则是基于寄存器的指令集架构

基于栈的特点

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

避开了寄存器的分配难题:适用零地址指令方式分配

指令刘中的指令大部分是零地址指令,其执行过程依赖于操作栈.指令集更小,编译器更容易实现.

不需要硬件的支持,可以执行更好.更好的实现跨平台

基于寄存器的特点

典型的应用是x86的二进制指令集:比如传统的pc以及1andorid的Davlik虚拟机

指令集架构则完全依赖硬件,可以执性差

性能优秀和执行效率高

花费更少的指令去完成一项操作

在大部分情况下,1基于寄存器架构的指令集往往都是以地址指令,二地址指令和三地址指令为主,而基于栈式架构的指令集确实以零地址指令为主。

将如下指令编译成汇编指令

public class StackStruTest {
    public static void main(String[] args) {
        int i=2;
        int j =3;
        int k=i+j;
    }
}

会变成

0: iconst_2 //保存常量2
1: istore_1 //保存到索引为1的栈中
2: iconst_3 //设置常量3
3: istore_2 //保存到索引为2的位置
4: iload_1    //加载索引1的位置
5: iload_2    //加载索引2的位置
6: iadd    //求和
7: istore_3    //保存到索引为3的栈当中
8: return //返回
总结

由于跨平台性的设计,java指令都是根据栈来设计的.不同cpu平台架构不同,所以不能设计为基于寄存器的.优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令.

生命周期

启动

jvm启动是通过引导类加载器创建一个初始类来完成的,这个类是由虚拟机的具体实现指定的.

执行

一个运行中的jvm有一个清晰的任务:执行java程序

程序开始执行时才开始运行,程序结束时他就停止

执行一个所谓的java程序的时候,真真正正在执行的是一个叫做jvm的进程

退出

有如下的几种情况

  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误从而导致java虚拟机终止
  • 摸线程调用runtime类或system类的exit方法1,或runtime类的1halt方法,并且java安全管理器也允许这次exit或halt操作.

jvm发展历程

sun classicvm

在1996年,java1.0的时候,sum发布了一款商用java虚拟机,jdk1.4时候,sum公司发布了第一款名为sun classicvm的java虚拟机,它同时是世界上第一款商用虚拟机,jdk1.4时被完全淘汰

这款虚拟机只提供内置解释器

如果使用jit编译器,就需要外挂.但是一旦使用了JIT编译器,JIT就会接管虚拟机的执行系统.解释器就不再工作.解释器和编译器不能配合工作.

现在hotspot内置了此虚拟机

Exact vm

为了解决上一个虚拟机的问题,jdk1.2时,sum提供了此虚拟机

exact memory management:准确式内存管理

虚拟机可以指导内存中的某个位置的数据具体是什么类型

具备现代高性能虚拟机的出雏形

  • 热点探测
  • 编译器与解释器混合工作模式

只在solaris平台短暂使用,马上就被hotspot vm代替了

hot sport

历史

最初由一家名为Logview Technologies的小公司设计

1997年,此公司被sum公司收购,2009年sum公司被甲骨文收购

jdk1.3时,hotsport vm称为默认虚拟机

目前

  • 不管是现在仍在广泛使用的jdk6,还是用例比较多的jdk8中,默认的虚拟机都是hotsport
  • sun/oracle jdk和openjdk默认的虚拟机
  • 其他俩个商用的虚拟机都没有方法区的概念
  • 从服务器,桌面到移动端,嵌入式都有应用
  • hotsport指的就是它的热点代码探测技术

    • 通过计数器找到最具编译价值的代码,出发时编译或栈上替换
    • 通过编译器与解释器协同工作,在最优化的程序响应时间与最佳性能中取得平衡
jrockit
  • 专注于服务器端应用

    • 它可以不太关注程序启动速度,因此JRockeit内部不包含解析器实现,全部代码都靠即使编译器编译后执行
  • 大量的商业基准测试显示,JRocket jvm是世界上最快的jvm
  • 优势全面的java运行时解决方案组合
j9
  • 也叫做IT4J,内部代号J9
  • 市场定位与HotSport接近,服务器端,桌面应用,嵌入式等多用途vm
  • 广泛应用于IBM的各种java产品
  • 目前有影响力的商用虚拟机之一
  • 2017年左右,IBM发布了开源的J9 vm,命名为openJ9,交给Eclipse基金会管理,也成为Eclipse Open J9
Azul vm
  • 前面三大高性能虚拟机使用在通用硬件平台上
  • 这里Azul vm和BEA Liquid vm是与特定硬件平台绑定软硬件配合的专有虚拟机
  • Azul vm是Azul Systems公司在hotSpot基础上进行大量改进,云星宇Azul Systems公司专有硬件Vega系统上的java虚拟机.
  • 每个Azul vm实例都可以管理至少数十个CPU和数百GB的硬件资源,并提供在巨大内存范围内实现可控的GC时间的垃圾收集器,专有硬件优化的线程调度等优秀特性
Liquid vm
  • 直接运行在hypervisor系统上
  • liquid vm是现在的jrockit ve,Liquid vm不需要操作系统的支持,或者说它本身实现了一个专用操作系统的必要功能,如线程调度,文件系统,网络支持等
  • 随着jrockit虚拟机终止开发,Liquid vm项目也停止了
Graal vm
  • 2018年四月 oracle发布了graal vm
  • graal vm在hotSpot vm基础上增强而成的跨语言全栈虚拟机,可以作为任何语言的运行平台使用.语言包括java,scala,groovy,kotlin,c,c++,js,ruby,py,R
  • 支持不同语言中混用对方的接口对象,支持这些语言使用以及编写好的本地库文件
  • 工作原理是将这些语言的源代码或源代码编译后的中间格式,通过解释器转换为能被graal vm接受的中间表示.graal vm提供Truffle根据集快速狗桨面向一种新语言的解释器,在运行时还能及时编译优化,获得比原生编译器更优秀的执行效率.

类加载器

简介

作用

  • 类加载器子系统负责从文件系统或者网络加载Class文件,class文件在文件开头有特定的文件标识
  • classloader只负责class文件的加载,至于是否可以运行,则有Excution Ehgine(执行引擎)决定
  • 加载的类信息存放于一块块称为方法区的内存空间.除了累的信息外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)

角色

class file 存在于本地磁盘上可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到jvm当中来,根据这个文件实例化出n个一模一样的实例

class file 加载到jvm中被称为dna元数据模板,放在方法区中

在.class文件->jvm->最终成为元数据模板,此过程就要一个运输工具(类加载器 class loader),扮演一个快递员的角色

类的加载过程

加载->链接->初始化

加载

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

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

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

补充

.class文件的加载方式

  • 通过本地直接加载
  • 通过网络获取:典型场景web applet
  • 从zip压缩包中读取,称为日后jar,war格式的基础
  • 运行时计算生成,使用最多的是动态代理
  • 从其他文件生成,典型场景:jsp应用
  • 从专有数据库中提取.class文件,比较少见
  • 从加密文件中获取,典型的防class文件被反编译的保护措施
链接
验证

目的在于确保class文件的字节流中包含的信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身安全.

主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证

准备

为类变量分配内存并且设置该类变量的默认初始值.不同类型不一样.

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

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

解析

将常量池内的符号引用转换为直接引用的过程

实际上解析操作往往会伴随jvm在执行完全初始化之后再执行

符号引用就是一组符号来描述锁引用的目标.符号引用的字面量形式

初始化
  • 初始化阶段就是执行类构造器方法<clinit>()过程
  • 此方法不需要定义,是javac编译器自动收集类中的所有类变量的复制动作和静态代码块中的语句合并而来.
  • 构造器方法中指令按语句在源文件中出现的顺序执行.
  • <clinit>()不同于类的构造器
  • 若该类有父类,jvm会保证子类的<Clinit>()执行前,父类的<clinit>()已经执行完毕
  • 虚拟机必须保证一个类<Clinit>()方法再多线程下被同步加锁(一个类只会被加载一次)

类加载器

分类

jvm支持俩种类型的类加载器,分别为引导类加载器和自定义类加载器

从概念上将讲,自定义类加载器一般指有开发人员自定义的一类,类加载器,但是java虚拟机规范却没有这么定义,而是将所有派生与抽象类ClassLoader的类加载器都划分为自定义类加载器而是将所有派生于抽象类classLoader的类加载器都划分为自定义类加载器

无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终有三个,

引导类加载器(bootstrap classLoader)

启动类加载器(引导类加载器)

  • 这个类使用c/c++语言实现,嵌套在jvm内部
  • 用它来加载java的核心库,用于提供jvm自身需要的类
  • 并不集成子java.lang.ClassLoader没有父加载器
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器.
  • 出于安全考虑,bootstrap启动类加载只加载包名为java,javax,sum等开头的类
扩展类加载器(Extension ClassLoader)
  • java语言编写,派生于ClassLoader类
  • 类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从jdk安装目录jre/lib/ext目录下加载类库.如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载.
应用程序类加载器(APPClassLoader)
  • java语言编写,派生于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库
  • 该类加载是程序默认的类加载器,一般来说java应用的类都是由它来完成加载
  • 通过ClassLoader.getSystemClassLoader()方法可以获取到该类加载器
用户自定义类加载器

在javba日常开发中,类的加载几乎是由上述三种类加载器互相配合执行的,在必要时,我们还可以自定义类加载器,来制定类加载方式

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

  • 隔离加载类
  • 修改类的加载方式
  • 扩展加载资源
  • 防止源码泄露

关于CLassLoader类

ClassLoader类是一个抽象类,其后索引的类加载器都继承自ClassLoader,不包括引导类加载器

获取classloader的途径

  • 获取当前类的classloder:clazz.getClassLoader()
  • 获取当前线程上下文的ClassLoader:Thread.currentThread().getContextClassLoader()
  • 获取当前系统的ClassLoader:ClassLoader.getSystemClassLoader()
  • 获取调用者的ClassLoader:DriverManager.getCallerClassLoader()

双亲委派机制

简介

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

工作原理

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

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

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

优势

避免类的重复加载

保护程序安全,防止核心API被随意篡改

沙箱安全机制

自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件.这样就可以保证对java核心源码的保护,这就是沙箱安全机制

其他

相同

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

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

加载这个类的ClassLoader(只ClassLoader实例对象必须相同)

换句话说,在jvm中,即使在俩个类对象(class对象)来源于同一个class文件,被同一个虚拟机加载,但只要加载它们的classLoader实例对象不同,那么这俩个类对象也是不相等的

对类加载器的引用

jvm必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的.如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保留在方法区中.当解析一个类型到另一个类型的引用的时候,jvm需要保证这俩个类型的类加载器是相同的.

类的主动使用和被动使用

主动使用分七种情况

  • 创建类的实例
  • 访问摸个类或者接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射
  • 初始化一个子类
  • java虚拟机开机启动的时候表名为启动类的类
  • jdk7开始提供的动态语言支持

除了以上七种情况,其他使用java类的方式都被看做是对类的被动使用,都不会导致类的初始化.

运行时数据区

程序计数器(pc寄存器)

介绍

作用

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

概念

jvm中的程序`计数器,register的命名源于cpu寄存器,寄存器存储指令相关现场信息.cpu只有把数据转载到寄存器才能够运行.这里并非是广义上的物理寄存器,或许将其翻译为pc计数器(或指令集计数器会更加贴切(也成为程序钩子).jvm中的pc寄存器是对物理pc寄存器的一种抽象模拟.

  • 他是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快单存储区域
  • 在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的,声明周期与线程的生命周期保持一致
  • 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法.程序计数器会存储当前线程正在执行的java方法jvm指令地址,或者如果是在执行native方法,则是未指定的值.
  • 他是程序控制流的指示器,分支循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器完成.
  • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码命令.
  • 它是唯一一个在java虚拟机中没有规定任何OutOtMemoryError情况的区域也没有GC

问题

使用pc寄存器存储字节码地址有什么用呢?

为什么使用pc寄存器记录当前线程的执行地址呢?

因为cpu需要不停切换各个线程,这时候切换回来以后,就得指定从哪开始继续执行

jvm的字节码解释器就需要通过改变pc寄存器的值来明确下一条一个执行什么样的字节码指令

为什么pc寄存器被设定为线程私有?

为了能够准确的记录各个线程正在执行的当前字节码指令地址,最好的办法是微每一个线程都分配一个pc寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况.

每个线程创建后,都会产生自己的程序计数器和栈帧,程序计计时器在各个贤臣各之前互不影响.

cpu时间片

cpu时间片即cpu分配给各个程序的时间,每个线程被分配一个小时时间段,称作它的时间片.

在宏观上:我们可以同时打开多个应用程序,每个程序并行不违背,同时在运行

但在微观上,由于只有一个cpu,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行

虚拟机栈(java虚拟栈)

简介

有不少java开发人员一提到java内存结构就会粗粒度的将jvm中的内存区理解为仅有堆(heap),和java栈(stack)

栈是运行时的单位,而堆是存储单位

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

虚拟机栈是什么?

java虚拟机栈(java virtual machine stack)早期也叫做java栈.每个线程在创建时都会创建一个虚拟机栈,内部保存一个个的栈帧,是线程私有的

生命周期和线程一直

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

特点

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

jvm直接对java栈操作的只有俩个

  • 每个方法执行,伴随着进栈(入栈,压栈)
  • 执行结束后的出栈工作

对于栈来说不存在垃圾回收问题

先进后出

异常

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

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

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

默认是采用固定大小的虚拟机栈,linux是1024kb,macos是1024kbwindows取决于虚拟机内存大小,默认1024

栈的存储单位
  • 每个栈都有自己的线程,栈中的数据都是以栈帧的格式存在.
  • 在这个线程上正在执行的每个方法都鸽子对应一个栈帧
  • 栈帧是一个内存的区块,是一个数据集,维系着方法执行过程中的各种数据信息
运行原理

在一条活动线中,一个时间点上,值会有一个活动的栈帧.即只有当前正在执行的方法栈帧是有效的,这个栈帧被称为当前栈帧,与当前栈帧响相对应的方法就是当前方法,定义这个方法的类就是当前类

执行引擎运行的所有字节码指令值针对当前栈帧进行操作

如果在该方法中调用了其他方法,对应的新栈会被创建出来,放在栈的顶端,称为新的当前栈帧

注意

不同线程中所包含的栈帧是不允许存在互相引用的,即不可逆在一个栈帧之中引用另一个线程的栈帧.

如果当前方法调用了其他方法,方法返回之际,当前栈帧会回传此方法执行的结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧

java方法有俩种返回函数的方式,一种是正常的函数返回,使用return指令,另一种是抛出异常,不管使用哪种方式,都会导致栈帧被弹出。

栈帧的内部结构

每个栈帧中存储着

  • 局部变量表
  • 操作数栈(表达式栈)
  • 动态链接(指向运行时常量池的方法引用)
  • 方法返回地址(正常退出或者异常退出的定义)
  • 附加信息
局部变量表
  • 局部变量表也被称之位局部变量数组或本地变量表
  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括基本数据类型,对象引用,以及returnAddress(返回值)类型
  • 由于局部变量表是建立在线程的栈上,是线程私有数据,因此不存在数据安全问题
  • 局部变量表所需的容量大小是在编译期确定下来的,在方法运行期间是不会改变局部变量表的大小的
  • 局部变量表的变量只在当前方法调用中有效.在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程.当方法调用结束后,随着方法栈帧的销毁,局部变量也会随之销毁

slot

参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束

局部变量表,最基本的存储单位就是slot(变量槽)

局部变量里32为以内的类型只占用一个slot

在局部变量里,32位以内的类型只占用一个slot(包括returnAddress类型),63位(long和double)占用俩个slot

  • byte,sort,char,boolean在存储前辈转换为int
  • long和double则占据俩个slot

jvm会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值

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

如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可.(比如double和long类型的变量)

如果当前帧是由构造方法或者实例方法创建的,name该对象引用this将会存放在index为0的slot出,其余的参数按照参数表顺序继续排序.

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

静态变量和成员变量的对比

静态变量:类加载器的连接的准备阶段,给变量赋默认值 -->initial阶段

实例变量随着对象的创建,会在堆空间中分配实例变量空间,

局部变量:在使用前,必须要进行显示的赋值,否则编译不通过

补充说明

在栈帧中,与虚拟调优最为密切的部分就是前面提到的局部变量表.在方法执行时,虚拟机使用局部变量表完成方法的传递.

局部变量表中的变量也是重要的垃圾回收节点,只要局部变量表中直接或间接引用的对象都不会被回收

操作数栈

operand stack

每个独立的栈帧中除了包含局部变量表以外,还包含一个先进后出的操作数栈,也可以称之位表达式栈

操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈push/出栈pop


操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间

操作数栈就是jvm执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈式空的。

每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译器就确定好了,保存在方法的code属性中,为max_stac的值

栈中任何一个元素都可以是任意的java数据类型

32bit占用一个栈,64bit占用俩个栈


如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新pc寄存器下一条需要执行的字节码命令

操作数栈中的元素的数据类型必须与字节码指令的序列严格匹配,这是由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证

另外我们说java虚拟机的解释引擎是基于栈的执行引擎其中的栈指的就是操作数栈

栈顶缓存技术

前面提过,基于栈式架构的虚拟机锁使用的零地址指令更加紧凑,但完成一项操作的时候必然需要更多的入栈和出栈指令,这也同时意味着将需要更多的指令分派次数和内存读写次数.

由于操作数是存储在内存中的,因此频繁地执行内存读写操作必然会影响执行速度.为了解决这个问题,hotsport jvm的设计们提出了栈定缓存技术,将栈定元素全部缓存在物理cpu寄存器中,一次降低对于内存的读写次数,提升执行引擎的执行效率

动态链接

我每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用.包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic linking) 比如invokedynamic指令

在java源文件被编译到字节码文件中时,所有变量和方法引用都作为符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法直接调用.

常量池的作用就是为了提供一些符号和常量,便于识别指令

方法调用

在jvm中,将符号引用转换为调用方法的直接引用于方法的绑定机制相关

  • 静态链接

当一个字节码文件被装载进jvm内部时,如果被调用的目标方法在编译期可知,且在运行期保持不变时.这种情况下将调用的方法符号引用转换为直接饮用的过程称之位静态链接

  • 动态链接

如果被调用的方法在编译期间无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接.


对应的绑定机制为早期绑定和晚期绑定.绑定是一个字段,方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次

  • 早期绑定

早期绑定就是只被调用的目标方法如果在编译期可知,且运行期保持不变,即可将这个方法与所属的属性的类型进行绑定,这样以阿里,就明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符引用转换为直接引用

  • 晚期绑定

如果被调用的方法在编译器无法被确定下来,只能够在程序运行期根据实际的类绑定相关的方法,这种绑定方式也就被称之为晚期绑定


非虚方法

  • 如果方法在编译期间就确定了具体的调用版本,这个版本在运行时是不可变的.这样的方法称为非需方法.
  • 静态方法,私有方法,final方法实例构造器,父类方法都是非虚方法
  • 其他方法称为虚方法

子类对象的多态使用前提是,集成关系方法重写

方法重写的本质

1.找到操作数栈顶的第一个元素所执行的对象的实际类型记作C。

如果在过程结,如果不通过类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限的校验,如果有权限则返回这个方法的直接引用,(调用)查找过程结束。如果不通过,则返回java.lang.IllegalAccessError异常.

否则,按照继承关系依次向上去寻找父类

如果始终没有找到合适的方法,则抛出java.lang。AbstractMEhtoError异常

IllegalAccessError

程序试图访问或修改一个属性或调用一个方法,这个属性你没有权限访问。一般的,这个会引发编译器异常。这个的错误如果发生在运行时,就说明一个类发生了不兼容的改变

虚方法表

在面向对象的编程中,会频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据搜索合适的目标的话,就可能影响到执行效率。因此为了提高性能,jvm采用在类的方法区建立一个虚方法表(virtual method table)(非虚方法不会出现在表中)来实现。使用索引来代替查找。

每个类都有一个虚方法表,表中存放着各方法的实际入口

虚方法表什么时候被创建?

虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,jvm会把该类的方法表也初始化完毕。

方法返回地址

存放调用该方法pc寄存器的值

一个方法的结束,有俩种方式

  • 正常执行完成
  • 出现异常,非正式退出

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

当一个方法开始执行后,只有俩种方式可以退出这个方法

执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层方法的调用者,简称正常完成出口

一个方法在正常调用完成之后就近需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定.


在方法执行的过程遇到了异常,并且这异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出.简称异常完成出口.

方法执行过程中抛出异常时异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码.

本地方法栈

java虚拟机栈用于管理java方法的调用,而本地方法栈用于管理本地方法的调用.本地方法栈,也是线程私有的.

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

  • 如果线程请求分配栈容量超过本地方法栈允许的最大容量,java虚拟机会抛出一个StackOverflowError异常.
  • 本地房发展可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程的时候没有足够的内存去创建对应的本地房方法栈,那么java虚拟机将会抛出一个outofmemoryError异常.

它具体的做法是native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库.


当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界.它和虚拟机拥有同样的权限.

  • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区.
  • 它升值可以直接使用各本处理器中的寄存器
  • 直接从本地内存的堆中分配任意数量的内存

并不是所有jvm都支持本地方法。因为java虚拟机规范并没有明确要求本地方法栈的使用语言,具体实现方式,数据结构等。如果jvm产品不打算支持native方法,也可以无需实现本地方法栈。

在hotport jvm中,直接将本地方法栈和虚拟机栈合二为一。

核心概述

简介

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

java堆区在jvm启动的时候被创建,其空间大小也就决定了.市jvm管理最大一块空间

堆的大小是可以调节的

java

jvm规范规定,堆可以处于物理上不连续的内存空间中,单子啊逻辑上它应该被视为连续的.

所有的线程共享java堆,在这里还可以划分线程私有的缓冲区

几乎所有对象实例以及数组都应当在运行时分配到堆上.

在方法结束后堆中的对象不会被马上移除,仅仅在垃圾回收的时候,才会被移除

堆是GC执行垃圾回收的重点区域

内存细分

现代垃圾收集器大部分都给予理论设计,堆细分为

  • 1.7:新生带+老年代+永久带
  • 1.8:新生带+老年代+元空间
大小设置

java堆区用于存储java对象实例,那么堆的大小在jvm启动时就已经设定好了,大家可以通过选项"-Xmx"和"-XmX"来进行设置

  • "-Xms"用来表示堆区的起始内存,等价于-XX:InitialHeapSize
  • "-Xmx"则用于表示堆区的最大内存,等价于-XX:ManxHeapSize

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

通常会将-Xms和-XmX俩个参数配置相同的值,其目的是,为了能在java垃圾回收机制清理完堆区后不需要重新分配堆区的大小,从而提高性能.

默认情况下 -Xms和-XmX俩个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新计算计算堆区的大小,从而提高性能

默认情况下,初始内存大小:物理电脑内存大小/64

最大内存大小:物理电脑内存大小/4

查看设置的参数方法一:jps/jstat -gc 进程id

方式二:-xx:+PrintGCDetails

结构

年轻带和老年代

存储在jvm中的java对象可以被划分为俩类

  • 生命周期较短的对象,这类对象的创建和消亡都非常的迅速
  • 生命周期非常长,在某些极端的情况下还能与jvm的生命周期保持一致

java堆区进一步细分的话,可以划分为年轻代(YangGen)和老年代(OldGen)

其中年轻带又可以分为Eden空间和survivor空间,suivivor空间可以分为from区和to区

配置`新生代欲老年代在堆结构的占比

默认-XX:NewRatio=2表示新生代栈1,老年代占2,新生代占整个堆的1/3,老年代2/3

可以修改-XX:NewRatio=4,表示新生代栈1,老年代占4,新生代占整个堆的1/5


在hotspot中Eden空间和另外俩个Survivor空间缺省所占的比例是8:1:1

开发人员可以通过选项--XX:SurvivorRatio调整这个比例.

几乎所有的java对象都是在Eden区被new出来的(可能会有eden区装不下的东西)

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

  • IBM公司专门的研究表明80%的新生代对象都是招生暮死的

可以使用选项-Xmn设置新生代最大内存大小,这个参数一般默认值就可以了

分配过程
概述
  • new的对象放伊甸园区.此区有大小限制
  • 当伊甸园的空间被填满时,程序有需要创建对象,jvm垃圾回收期将对伊甸园区进行垃圾回收,将伊甸园区中不再被其他对象所引用的对象进行销毁.再加载新的对象放到伊甸园区
  • 然后伊甸园区中的剩余对象移动到survivor0区
  • 如果再出发垃圾回收1,此时上次幸存下来的放到survivor0区的,如果没有回收,就会放到survivor1区
  • 如果再经历垃圾回收,此时会重新放回survivor1区
  • 啥时候能去老年代呢?可以设置参数默认是15此

    • -XX:MaxTenuringThreshold=<N>进行设置
总结

针对幸存者s0,s1的总结:复制完之后进行内存的交换,谁是空的区域谁就是to区,另一个就是from区

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

特殊情况

gc

jvm在执行gc的时候,并非每次都对上面三个内存区域进行回收(新生代,老年代,方法区),大部分时候回收的都是新生代.

针对hotsport vm的实现,它里面的gc按照回收区域又分为俩大类型:一种是部分收集(Partial Gc),一种是整堆收集(Full GC)

  • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
  • 老年代收集(Major GC/old GC):只是老年代的垃圾收集

    • 目前只有CMS GC会有单独收集老年代的行为
    • 注意,很多时候,Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收
  • 混合收集:(Mixed GC)收集整个新生代以及老年代的垃圾收集,目前只有G1 GC会有这种行为

整堆手机(Full GC)收集整个java对和方法区的垃圾收集.

触发机制

当年轻带空间不足的时候就会触发Minor GC,这里年轻代指的是1Eden代满,Survivor满不会引发GC.每次Minor GC会清理年轻带的内存

因为java对象大多具备招生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快.这一定义即清晰又易于理解

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

老年代Major GC

指发生在老年代的GC,对象从老年代消失时,我们说Major GC或Full GC发生了

出现了Major GC经常会伴随着至少一次的Minor GC.(但不是绝对的,在Parallel Scavenge收集器的收集策略里就有- 直接进行major GC的策略选择过程,也就是在老年代空间不足时,会先尝试触发Minor GC.如果之后空间还不足,则触发MAjor GC)

Major GC的速度一般会比Minor GC慢十倍以上,STW的时间更长

如果Major GC后内存还不足,就报OOM了

full GC触发机制

触发Full GC执行的情况有下五种

  • 调用System.gc()时,系统建议执行FullGC,但是不必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后进入老年代的平均大小大于老年代可用内存
  • eden区,suivivor 0向suivivor1区复制时,对象大小大于to区可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

full GC是开发或调优中尽量要避免的.这样暂停时间会短一些

对象分配过程TLAB

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

由于对象的实例创建在jvm中非常繁琐,因此在并发环境下从堆中划分内存空间是线程不安全的,为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度

什么是TLAB

从内存模型而不是垃圾收集的角度,对Edne区域继续进行划分,jvm为每个线程分配了一个私有缓存区域,它包含在Eden空间内.

多个线程同时分配内存时,使用tlab可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为快速分配策略

据所知所有OpenJDK衍生出来的JVM都提供了TLAB的设计


尽管不是所有的对象实例都能在TLAB中成功分配内存,但jvm确实是将tlab作为内存分配的首选.

在程序中开发人员可以通过选项-XX:UserTLAB设置是否开启TLAB空间

默认情况下,TLAB空间的内存非常小,仅栈有整个Eden区空间的1%,当然我们可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小

一旦对象在TLAB空间分配内存失败时,JVM就会尝试通着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存.

NEAnMQ.md.png

扩展

在jvm中,对象是在java堆中分配内存的,这是一个普遍的常识,但是有一种特殊情况,那就是如果结果逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就看被优化成栈上分配1.这样就无需在堆上分配,也无需进行垃圾回收了.这也是最常见的堆外存储技术.

逃逸分析

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

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

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

逃逸分析的基本行为就是分析对象动态作用域

  • 当一个对象方法被定义后,对象只在方法内部调用,则认为没有发生逃逸
  • 当一个对象在方法中被定义后,它被外部方法引用,则认为发生逃逸

如何快速判断是否发生了逃逸分析,大家就看new的对象是否在外部被引用

结论:开发中能使用局部变量的,就不要使用在方法外定义。

逃逸分析在jdk7默认开启

栈上分配

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

最常见的栈上分配场景:在逃逸分析中,已经说明了.分别是给成员变量赋值,方法返回值,实例引用传递

同步省略

如果一个对象只能从一个线程访问到,那么对于这个对象的操作可以不考虑同步

线程同步的代价是非常高的,同步的后果是降低并发和性能

在动态编译同步代码块的时候,JIT可以借助逃逸分析来判断同步块所使用的锁对象是否只能被一个线程访问而没有被发布到其他线程.如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步.这样就能大大提高并发性和性能.这个取消同步的过程就叫同步省略,也叫锁消除.

标量替换

有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分或全部,可以不存储在堆中,存在栈中.

标量(Scalar)是指一个无法再分解成更小的数据的数据.java中的原始数据类型就是标量.

相对的,那些还可以分解的数据叫做聚合量,java中对象就是聚合量,因为他可以分解成其他聚合量和标量.

在jit阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含若干个成员变量来代替,这个过程就是标量替换

标量替换默认是打开的

本地方法区

从县城共享与否的角度来看

简介

jvm虚拟机规范中明确说明:尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩.但对于hotspotjvm而言,方法区还有一个别名叫做non-heap,目的就是要和堆分开.

所以方法区看做是一块独立于java堆的内存空间

方法区和java堆一样是各个线程共享的区域.

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

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

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

  • 加载大量的jvmjar包:tomcat部署的工程过多,或者大量的生成反射类

关闭jvm就会释放这个区域内存

hotspot中方法区的演进

在jdk7以前,习惯上把方法区称为永久代.jdk8开始,使用元空间取代了永久代

本质上,方法区和永久带并不等价,仅是对hotspot虚拟机而言的.jvm规范对如何实现方法区不做统一要求.列入BEA JRockit/IBM J9不存在永久带的概念

现在来看,当年使用永久带,不是好的主意,导致java程序更容易oom

而到了jdk8,终于完全废弃了永久代的概念,改用Jrokit,j9一样本地内存中实现元空间来代替

元空间的本质和永久代类似,都是jvm规范中方法区的实现.不过元空间与永久带最大的区别在于,元空间不在虚拟机内存中,而是使用本地内存.永久带,元空间二者并不只是名字变了,内部结构也调整了.

根据jvm规范规定,如果方法区无法满足新的内存分配需求时,将抛出oom异常

设置

方法区的大小不必是固定的,jvm可以根据应用的需要动态调整

jdk8以后

元数据区大小可以使用俩个参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定,替代上述原有的俩个参数

默认值依赖平台.windows下-XX:metaSpaceSize是21M,-XXMaxMetaspaceSize的值是-1,即没有限制

于永久带不同,如果不指定大小,默认情况下,虚拟机会耗尽所有可用的系统内存.如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace

-XX:MetaspaceSize:设置初始元空间的大小.对于一个64位的服务器来说,某默认的-XX:MetaspaceSize的值为21MB.这就是初始的高水位线,一旦触及这个水位线,Full GC会被处罚并且卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置.新的高水位先的值取决于GC后释放了多少元空间.如果释放的空间不足,那么在不超过MAxMetaspaceSize时,适当提高该值.如果释放空间过多,则适当降低该值.

如果初始化的高水位线设置过低,上述高水位线的调整情况会发生很多次.通过垃圾回收器的日志可以观察到Full Gc多次调用.为了避免频繁的GC,建议将XX:MetaspaceSize设置为一个相对较高的值

OOM

要解决OOM异常或堆的异常,一般的手段是通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否必要的,也就是西安分清楚是出现了内存泄露还是内存溢出.

如果是内存泄露,可以进一步通过工具查看泄露对象GC Roots的引用链.于是就能找到泄露对象是通过怎么样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的.掌握了泄露对象的类型信息,以及GC Roots引用链的信息,就可以比较准确定位出泄露代码的位置

如果不存在内存泄露,换句话说就是内存中的对象确实还都必须存活着,那就当应检查虚拟机的堆参数,(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否UC你在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序的运行期的内存消耗.

内部结构

存储

方法区用于存储被虚拟机加载的类型信息,常量,静态变量,及时编译器编译后的代码缓存等.

类型信息

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

  • 这个类型的完整有效名称(全名=包名.类名)
  • 这类型直接父类的完整有效名(对于interface或者是java.lang.Object,都没有父类)
  • 这个类型的修饰符(public,abstract,final的某个子集)
  • 这个类型直接接口的一个有序列表
域(Field)信息

jvm必须在防区中保存类型的所有域的相关信息以及域的声明顺序

域的相关信息包括:域名称,域类型,域修饰符(public ,private,protected,static,final,volatile,transient的某个子集)

方法信息

jvm必须保存所有方法的以下信息,同域信息一样包括声明顺序

  • 方法名称
  • 方法的返回类型(或void)
  • 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
  • 方法的字节码(bytecodes),操作数栈,局部变量表及大小(abstract和native方法除外)
  • 异常表:每个异常处理的开始位置和结束位置,diamante处理在程序计数器中的偏移地址,呗捕获的异常类的常量池索引(abstract和native方法除外)
non-final变量

静态变量和类关联在一起,随着类的加载而加载,它们称为类数据在逻辑上的一部分.类变量被类的所有实例所有共享,即使没有类实例时你也可以访问它.

全局常量

被声明为final的类变量处理方法则不同,每个全局常量在编译的时候就会被分配了

运行时常量池

  • 方法区,内部包含了运行时常量池.
  • 字节码文件,内部包含了常量池.
常量池

一个有效的字节码文件中除了包含类的版本信息,字段,方法以及接口等描述信息外,还包含了一项信息俺就是常量池表,包含各种字面量和对类型域和方法的符号引用

常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息

运行时常量池

运行时常量池是方法区的一部分

常量池表时class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中.

运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池.

jvm为每个已加载的类型(类或接口)都维护一个常量池.池中的数据像数组一样,是通过索引访问的

运行时常量池中包含多种不同的常量,包括编译期就已经确定的数值字面量,也包括到运行期解析后才能获得的方法或者字段的引用.此时不再是常量池中的符号地址了,这里替换为真实地址.

运行时常量池,相对于class文件常量池的另一重要特性是:具备动态性

运行时常量池类似于传统编程语言中的符号表,但是它所包含的数据却比符号表要更加丰富一些

当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则jvm会抛出OutOfMemoryError异常

版本变化

首先明确只有hotspot才有永久代.

BEA Jrockit,IBM,J9等来说,是不存在永久代的概念的.原则上如何实现方法区属于虚拟机实现细节,并不要求统一

1.6之前有永久带,静态变量存放在永久代上
1.7之前有永久带,但逐步去永久代,字符串常量池,静态变量移除,保存在堆中
1.8及之后无永久带,类型信息,字段方法,常量保存在本地内存的元空间,但字符串常量池,静态变量仍在堆

改动的原因如下

  • 永久带设置空间大小是很难确定的

在某些场景下,如果动态加载类过多,容易缠身Perm区的oom.比如某个实际web工程中,因为功能点比较多,在运行过程中,要不断的动态加载很多类,经常出现致命错误

而元空间和永久带的最大区别在于:元空间并不在虚拟机中,而是使用本地内存.因此默认情况下,元空间的大小受本地内存限制.

  • 对永久代进行调优是很难的

方法区的垃圾收集主要回收俩部分内容:常量池中废弃的常量和不再使用的类型

为了防止堆空间的溢出,导致频繁的full gc,导致stop the world

StringTable

字符串常量池为什么要调整?

jdk7中将Stringtable放到了堆空间中.英文永久带的回收效率很低,在ful gc的时候才会触发.而full gc是老年代的不足,永久带的不足时才会触发.

这就导致StringTable回收效率不高.而我们开发中会有大量的字符串被创建,我回收效率很低,导致内存不足,放到堆里能及时回收.

方法区gc

有些人认为方法区是没有垃圾回收行为的,其实不然.jvm规范对方法区的约束是非常宽松的,提到过可以不要求在方法区实现垃圾回收.事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在

一般来说这个秋雨的回收比较难令人满意,尤其是类型的卸载,条件相当苛刻.但这部分区域的回收又是又是有必要的.

方法区的垃圾收集主要回收俩部分内容:常量池中废弃的常量和不再使用的类型


先来说说方法区内常量池之中主要存放俩大类常量:字面量和符号引用.字面量比较接近java语言层次的常量概念,如文本字符串,被声明为final的常量值等.而符号引用则属于编译原理方面的概念,包括下面三个变量

  • 类和接口的全限定名称
  • 字段的名称和描述符
  • 方法的名和描述符

hotspot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收.

与废弃常量与回收java堆中的对象非常类似

判定一个常量是否废弃还相对简单,而要判定一个类型是否属于不再被使用的类,的条件就比较苛刻了.需要同时满足下面三个条件

  • 该类的所有实例都已经被回收,也就是java堆中不存在该类及其任何派生子类的实例
  • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGI,JSP的重加载等,否则通常是很难达成的
  • 改类对应java.lang.class对象没有在任何地方呗引用,无法再任何地方通过反射访问该类方法

jvm被允许满足上述三个条件的无用类进行回收,这里说的仅仅是允许,而不是和对象一样,没有引用了就必然会进行回收.关于是否要对类型进行回收,hotspot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class

以及-XX:+TraceClass-Loading,-XX:TraceClassUnLoading查看类加载和卸载信息

在大量使用反射,动态代理,GCLib等字节码框架,动态生成JSP以及OSGI

本地方法

简介

简单讲一个,一个native method就是一个java调用非java代码的接口.一个native method是一个1这样的1java方法:该方法的实现由非java语言实现,比如C.这些特征非java所特有,很多其它的编程语言都有这个机制.

本地接口的作用是融合不同编程语言为java所用,它的初衷是融合c/C++程序.

为什么使用native Method

  • 有时候应用需要与java外面的环境交互,这是本地方法存在的主要原因
  • 与操作系统能够更好的进行交互.使用一些java语言本身没有提供封装的操作系统特性时,我们也需要使用本地方法
  • sum解释器是用c实现的,这使得它能像普通的c一样与外部交互
Last modification:July 6, 2020
如果觉得我的文章对你有用,请随意赞赏