Egoistic_Flowers

JVM入门
JVM入门

fd29389f8af9c58c4d9527004b454ea1

JVM入门

常见面试题:

  • 谈谈对JVM的理解,java8虚拟机和之前有声明变化更新
  • 什么是OOM,什么是栈溢出StackOverFlowError?怎么分析?
  • JVM的常用调优参数有哪些?
  • 内存快照如何抓取,怎么分析Dump文件?
  • 谈谈JVM类加载器的认识

1 JVM的位置

  • JVM在系统之上运行,Java程序在JVM之上运行,JRE环境包含了JVM

JDK,JRE,JVM的关系图:

JDK

  • JDK是Java开发工具包,是Sun Microsystems针对Java开发员的产品。
  • JDK中包含JRE,在JDK的安装目录下有一个名为jre的目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre。

JRE

  • JRE是运行基于Java语言编写的程序所不可缺少的运行环境。也是通过它,Java的开发者才得以将自己开发的程序发布到用户手中,让用户使用。
  • JRE中包含了JVM,runtime class libraries和Java application launcher,这些是运行Java程序的必要组件。
  • JRE不包含开发工具,只针对用户

JVM

  • java虚拟机,它是整个java实现跨平台的最核心的部分,所有的java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行。
  • 只有JVM还不能成class的执行,因为在解释class的时候JVM需要调用解释所需要的类库lib,而jre包含lib类库。
  • JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行。编写的Java程序经过编译后生成的字节码可以被JVM识别,JVM为程序运行屏蔽了底层操作系统的差异。每个操作系统有不同版本的JVM

2 JVM体系结构

JVM整体结构:

java代码执行(1)

Java代码的执行流程:

java代码执行

Java架构模型

Java编译器输入指令流是一种基于栈的指令集构架

基于栈式构架:

  • 设计实现简单,适用于资源受限的系统
  • 避开寄存器分配问题,使用零地址指令方式分配
  • 指令流中的大部分指令是零地址指令,其执行过程依赖于操作栈,指令集更小,编译器更容易实现
  • 不需要硬件支持,更好实现跨平台

基于寄存器构架:

  • 典型应用:x86二进制指令集,传统PC及Android的Davlik虚拟机
  • 指令集架构完全依赖硬件,移植性差
  • 性能优秀执行效率高
  • 花费更少指令完成一项操作
  • 大部分情况指令集以一地址指令,二地址指令,三地址指令为主

指令格式:

  • 三地址指令:

    • 指令格式:OP A1 A2 A3 操作码:OP 第一操作数地址:A1 第二操作数地址:A2 结果地址:A3
    • 例子:(A1)OP(A2)->(A3) A1内数据与A2内数据进行某种操作,结果放到A3这个地址中
  • 二地址指令:

    • 指令格式:OP A1 A2 操作码:OP 第一操作数地址:A1 第二操作数地址:A2
    • 例子:(A1)OP(A2)->(A1) A1数据与A2数据进行某种操作,结果放到A1地址
  • 一地址指令:

    • 指令格式:OP A1 操作码:OP 第一操作数地址:A1
    • 例子:取反,+1 OP(A1)->(A1) , (PC)+1->(PC)
  • 零地址指令

    • 指令格式:OP 操作码
    • 例子:对堆栈进行操作

java指令

  • Java源码在运行之前都要编译成为字节码格式(如.class文件),然后由ClassLoader将字节码载入运行。在字节码文件中,指令代码只是其中的一部分,里面还记录了字节码文件的编译版本、常量池、访问权限、所有成员变量和成员方法等信息

javap指令查看字节码

  • -v输出附加信息
  • -l输出行号和本地变量表
  • -p显示所有类和成员
  • -c对代码进行反汇编

JVM生命周期

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

  • 虚拟机的执行:执行一个所谓的Java程序的时候,真正执行的是一个叫Java虚拟机的进程

  • 虚拟机的退出:

    • 程序正常执行结束
    • 执行过程遇到异常或错误而异常终止
    • 操作系统错误导致Java虚拟机进程终止
    • Runtime类或System类的exit方法、runtime类的halt方法,并且Java安全管理器允许这次exit或halt操作
    • JNI(Java Native Interface)规范描述了用JNI Invocation API来加载或卸载Java虚拟机时,Java虚拟机退出的情况

3 类加载器

  • 类加载器的作用:将class文件字节码加载到内存中,将静态数据转换成方法区的运行时数据结构,在堆区生成Java.lang.Class对象,作为方法区中类数据的访问入口

java类加载器

类加载器种类

  • 根类加载器:JVM自带类加载器,装载核心类库,C++编写,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
  • 扩展类加载器:它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现。
  • 系统(应用)类加载器:它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。

java类加载器(1)

类加载过程

  • 1.类的加载:将类的class文件读入内存,并为之创建一个java.lang.Class对象,此过程由类加载器完成

  • 2.类的链接:将类的二进制数据合并到JRE中

    • 验证:确保类的信息符合JVM规范

      • 文件格式验证:验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理
      • 元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范
      • 字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的
      • 符号引用验证:主要是针对符号引用转换为直接引用的时候,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题
    • 准备:为static变量分配内存,并设置默认值0,在方法区中

    • 解析:将常量池的符号引用(常量名)替换为直接引用(地址)

  • 3.类的初始化:JVM负责类初始化

    • 执行类构造器方法,类构造器自动收集所有变量的赋值静态代码块中的语句合并(不是对象构造器)
    • 初始化一个类的时候,如果发现父类没有初始化,会先将父类进行初始化
    • JVM保证每个类的类构造器方法在多线程环境正确加锁和同步

类加载时机

类主动引用(发生类初始化)

  • JVM启动,初始化main方法所在的类
  • new一个类的对象
  • 调用类的静态成员变量或静态方法
  • 使用反射方式对类进行反射调用
  • 初始化一个类会先初始化它的父类

类的被动引用(不发生类的初始化)

  • 访问一个静态作用域,只有声明这个域的类会被初始化(子类用父类的静态方法,子类不会初始化)
  • 通过数组定义引用,不会触发类初始化
  • 引用常量不会触发类初始化(链接阶段已经进入常量池)

类加载机制

  • 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

  • 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。JVM垃圾回收机制会回收Class对象。

  • 双亲委派机制:

    • 先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。
    • 双亲委派机制可以保证类不被重复加载,父类加载器加载过类,子类就不用加载类了;双亲委派机制保证安全性,自定义一个java.lang.Integer的类,类加载器会先让父类加载器进行加载,父类已经加载该类,或者父类会在自己的工作目录下加载核心类,子类加载器都不会加载这个自定义的类,防止了核心库被篡改。

类加载器的加载路径

  • 根加载器主要加载/jre/lib/rt.jar,jre/目录下的核心库
  • 扩展加载器主要加载/jre/lib/ext/下的jar包
  • 应用类加载器加载CLASSPATH路径下的包,包括java项目中的.class路径

4 沙箱安全机制

java的安全模型的核心就是java沙箱,沙箱机制就是将java代码限定在JVM特定运行范围中,严格限制代码对本地资源的访问

最新的安全机制实现,则引入了域 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示 最新的安全模型(jdk 1.6)

20181022103108566

组成沙箱的基本组件:

  • 字节码校验器(bytecode verifier):确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。

  • 类装载器

    (class loader):其中类装载器在3个方面对Java沙箱起作用

    • 它防止恶意代码去干涉善意的代码;
    • 它守护了被信任的类库边界;
    • 它将代码归入保护域,确定了代码可以进行哪些操作。

  虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。

  类装载器采用的机制是双亲委派模式。

  • 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;

  • 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。

  • 存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。

  • 安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。

  • 安全软件包

    (security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:

    • 安全提供者
    • 消息摘要
    • 数字签名
    • 加密
    • 鉴别

5 Native关键字

在调用Thread().start()方法时发现底层只声明了方法,没有实现

  • native关键字:说明Java的作用范围实现不了,需要调用底层的c语言库
  • 定义了native关键字会进入本地方法栈,调用本地方法接口(JNI)
  • JNI的作用:扩展Java的使用,融合不同语言为Java用
  • Java在内存中标记了一块区域:本地方法栈:Native Method Stack ,用来登记native方法
  • 目前native方法一般用于操作硬件,调用其他语言可以使用其他进程通信的方法(socket,http...)

6 PC寄存器

  • 当前线程所执行的字节码的行号指示器(与CPU底层PC寄存器类似,跳转,中断,切换线程保护程序运行的位置)
  • 如果线程执行的Java方法,计数器记录正在执行的虚拟机字节码的指令的地址
  • 如果正在执行的本地方法,这个计数器值则应为空
  • 每个线程有一个独立的程序计数器,线程之间互不影响
  • 运行时数据区中唯一不会出现OOM的区域,没有垃圾回收

7 方法区

方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊的方法,如构造函数,接口代码也在这里定义,所有定义方法的信息都保存在此区域,此区域属于共享区间。

静态变量,常量,类信息(构造方法,接口定义)、运行时的常量池存在方法区中,但是实例变量在堆中

方法区储存的数据

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

  • 类型信息

    • 对于每个加载的类型(类Class,接口Interface,枚举Enum,注解annotation)

    • JVM必须在方法区中存储以下类型信息:

      • 类的完整有效名称(全名=包名.类名)
      • 类型直接父类的完整有效名,对于interface或Object没有父类
      • 类型的修饰符,public,abstract,final
      • 这个类型直接接口的一个有序列表
  • 域信息(类属性,类变量)

    • 域的相关信息,以及域的声明顺序

    • 相关信息:

      • 域名称
      • 域类型
      • 域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
  • 方法信息

    • 方法名称

    • 方法的返回类型

    • 方法参数的数量和类型

    • 方法的修饰符

    • 方法的字节码

    • 局部变量表及大小

    • 异常表:

      • 每个异常处理的开始位置,结束位置,代码处理在程序计数器中的偏移地址,被捕获的异常类的常量池索引
  • 全局常量(static final)

  • 常量池

    • 运行时将常量池加载到方法区,就是运行时常量池

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

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

      • 数量值
      • 字符串值
      • 类引用
      • 字段引用
      • 方法引用

java方法区

方法区内常量池中主要存放的两大类常量

  • 字面量

    • 比较接近Java语言层次的常量概念,如文本字符串,被声明为final的常量值等
  • 符号引用

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

9 栈

JVM直接对JAVA栈的操作只有两个:入栈,压栈

栈不存在垃圾回收,但是存在OOM

堆与栈

栈:先进后出

  • 栈是运行时的单位,而堆是存储的单位,栈解决程序如何执行,如何处理数据。堆解决的是数据存储问题,即数据怎么放,放在哪里。

栈的存储单位

  • 每个线程都有自己的栈,栈中的数据以栈帧格式存储
  • 线程上正在执行的每个方法都各自对应一个栈帧
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各个数据信息
  • 先进后出,后进先出
  • 一条活动的线程中,一个时间点上,只会有一个活动的栈帧,这个称为当前栈帧,对应方法是当前方法
  • 如果方法中调用了其他方法,对应的新的栈帧会被创建出来,放在顶端,成为新的当前帧

栈运行原理

  • 不同线程中包含的栈帧不允许存在相互引用

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

  • Java方法有两种返回方式

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

栈帧结构

  • 局部变量表:

    • 存储方法参数,定义在方法体内部的局部变量,数据类型包括各类基本数据类型,对象引用,以及return address类型(return address为jsr,jsr_w和ret指令服务,少使用)

    • 容量大小是在编译期确定下来的

    • 基本数据类型(8种),引用类型(reference),return address 类型

    • 最基本的存储单元是slot:32位占用一个slot,64位类型(long和double)占用两个slot

      • 每个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
      • 5ed986f4f346fb1712e10524
      • 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this,会存放在index为0的slot处
    • 方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁

    • 与性能调优关系最密切的部分,就是局部变量表,方法执行时,虚拟机使用局部变量表完成方法的传递

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

  • 操作数栈:

    • 操作数栈的最大深度在编译的时候写入到方法Code属性的max_stacks数据中
    • 操作数栈的每一个元素可以是任何的Java数据类型,32位占一个容量
    • 方法开始执行时,操作数栈是空的,随着方法执行,会从局部变量表或对象实例中写入到操作数栈,随这计算的进行,将栈的元素返回局部变量表或方法的调用者5ed98b6b6376891862184dd7
    • 操作数栈并非采用访问索引方式进行数据访问,而是只能通过标准的入栈、出栈操作完成一次数据访问
    • 栈顶缓存技术:由于操作数是存储在内存中,频繁的进行内存读写操作影响执行速度,将栈顶元素全部缓存到物理CPU的寄存器中,依此降低对内存的读写次数,提升执行引擎的执行效率
  • 动态连接:

    • 一个方法调用其他的方法,需要将方法的符号引用转换成内存地址的直接引用
    • 符号引用存在于方法区的运行时常量池
    • 指向运行时常量池中的方法的符号引用
    • 静态解析:编译时符号引用转换成直接引用
    • 动态连接:在运行时转换为直接引用
  • 方法返回:

    • 一种是正常的函数返回,使用return指令

    • 正常返回出口:

      • 调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址
    • 另外一种是抛出异常,不管哪种方式,都会导致栈帧被弹出

    • 异常退出:

      • 返回地址是通过异常表来确定,栈帧中一般不会保存这部分信息
  • 附加信息:

    • 虚拟机规范运行具体的虚拟机实现增加一些规范中没有的信息到栈帧中,例如调试信息,这部分信息取决于不同虚拟机的实现

方法调用

符号引用:

image-20211206182510496

静态链接:

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

动态链接:

如果被调用的方法无法再编译期被确定下来,只能在运行期将调用的方法的符号引用转为直接引用,这种引用转换过程具备动态性,因此被称为动态链接

方法的绑定:

绑定是一个字段、方法、或者类在符号引用被替换为直接引用的过程。仅仅发生一次。

  • 早期绑定:被调用的目标方法如果再编译期可知,且运行期保持不变
  • 晚期绑定:被调用的方法在编译期无法被确定,只能够在程序运行期根据实际的类型绑定相关的方法

虚方法:

Java中任何一个普通方法都具备虚函数的特征(运行期确认,具备晚期绑定的特点)

  • 非虚方法:如果方法在编译期就确定了具体的调用版本,则这个版本在运行时是不可变的。这样的方法称为非虚方法

    • 静态方法,私有方法,final方法,实例构造器,父类方法都是非虚方法

虚方法在调用时根据实际类型查找vtable虚方法表完成绑定

10 JVM虚拟机

  • Sun公司 HotSopt
  • BEA JRockit
  • IBM J9VM

学习基于HotSpot

11 堆

  • 堆(Heap),一个JVM只有一个堆内存,堆大小可以调节

  • Java堆区在JVM启动的时候即被创建,其空间大小也就确认了

  • 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(TLAB)

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

  • 堆是GC执行垃圾回收的重点区域,主要在伊甸园区和养老区

  • 堆空间细分为:

    • 新生区

      • Eden区

        • Survivor区 0,1
    • 老年区

    • 元空间(Java8之后)

      • 元空间不是与堆连续物理内存,而是使用本地内存,本地内存足够就不会出现OOM

      • 元空间与方法区:

        • 元空间是方法区的一种实现方式
        • 方法区是JVM规范中的逻辑区域
        • 元空间是HotSpot(JDK1.8)对方法区这个规范的实现方式

对象分配一般过程

  • 1.new的对象先放在Eden区,此区有大小限制
  • 2.当创建新对象,Eden空间填满时,会触发Minor GC,将Eden不再被其他对象引用的对象进行销毁。再加载新的对象放到Eden区
  • 3.将Eden中剩余的对象移到幸存者0区
  • 4.再次触发垃圾回收,此时上次幸存者下来的与放在幸存者0区的,如果没有回收,就会放到幸存者1区
  • 5.再次经历垃圾回收,又会将幸存者重新放回幸存者0区,依次类推
  • 6.达到年龄转移到老年区,默认是15次(可设置),超过15次,则会将幸存者区幸存下来的转去老年区

YGC简单理解:

YGC

  • 触发YGC,幸存者区就会进行回收,不会主动进行回收
  • 超大对象eden放不下,就要看Old区大小是否可以放下
  • old区也放不下,需要FullGC(MajorGC)

几种垃圾回收

  • Minor GC(Young GC):

  • 当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了,Survivor满不会引发GC。

  • 每次MinorGC会清理年轻代(Eden )的内存

  • Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快

  • MinorGC会引发STW

  • FullGC:

  • 触发机制:

    • 调用System.gc时,系统建议执行Full GC,但是不必然执行
    • 老年代空间不足
    • 方法区空间不足
    • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
    • 由Eden区、survivor space1(From Space)区向survivor space2(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
    • 当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载
  • Old GC:只清理老年代空间的 GC 事件,只有 CMS 的并发收集是这个模式

  • Mixed GC:清理整个新生代以及部分老年代的 GC,只有 G1 有这个模式

堆空间分代思想

其实不分代也可以,分代的理由是优化GC性能

内存分配策略

  • 优先分配到Eden

  • 大对象直接分配到老年代

  • 动态对象年龄分配:

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

    • 在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间,是否大于新生代所有对象的总空间

    • 如果大于,则此次MinorGC是安全的

    • 如果小于,则查看设置是否允许担保失败

      • 是,会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小

        • 大于,则尝试进行一次MinorGC,但是这次MinorGC依然是有风险的
        • 小于,则改为进行一次FullGC
      • 否,则改为进行一次FullGC

    • jdk6update24之后

      • 只要老年代的连续空间大于新生代对象总大小,或者历次晋升的平均大小,就会进行MinorGC
      • 否则进行FullGC

为对象分配内存TLAB

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

  • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。

  • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度

  • TLAB

    • 从内存模型而不是垃圾收集的角度,对Eden区域进行划分,JVM为每个线程分配了一个私有缓存区域,包含在Eden空间中
    • TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。从这一点看,它被翻译为 线程私有分配区 更为合理一点
    • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们将这种内存分配方式成为快速分配策略
    • 当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB,而在老TLAB里的对象还留在原地什么都不用管,它们无法感知自己是否是曾经从TLAB分配出来的而只关心自己是在eden里分配的
    • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存

2211121

堆是分配对象的唯一选择吗

  • 随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术,将会导致一些微秒变化,所有对象分配到堆上渐渐变得不那么绝对了
  • 有一种特殊情况,如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配,这样无需堆上分配,也不需要垃圾回收了,也是最常见的堆外存储技术

逃逸分析概述:

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

    • 当一个对象在方法中定义后,对象只在方法内部使用,则认为没有发生逃逸
    • 当一个对象在方法中被定义后,它被外部方法引用,则认为发生逃逸,例如作为调用参数传递到其他地方中
    • 将堆分配转为栈分配,如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配

12 堆内存调优

堆空间的参数设置

  • -XX:+PrintFlagsInitial:查看所有参数的默认值
  • -XX:+PrintFlagsFinal:查看所有参数的默认初始值
  • -Xms:初始堆空间内存(默认为物理内存的1/64)
  • -Xmx:最大堆空间内存(默认物理内存的1/4)
  • -Xmn:设置新生代的大小(初始值、最大值)
  • -XX:NewRatio:配置新生代与老年代在堆结构的占比
  • -XX:SurvivorRatio:设置新时代中Eden和S0/S1的空间比例
  • -XX:MaxTenuringThreshold:设置新时代Survivor最大年龄
  • -XX:+PrintGCDetails:输出详细的GC处理日志
  • -XX:+PrintGC:打印GC简要信息
  • -XX:HandlePromotionFailure:设置空间分配担保(JDK6update24前)

14 GC垃圾回收

哪些内存需要回收

  • 在Java内存运行时区域的各个部分中,堆和方法区这两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理,我们平时所说的内存分配与回收也仅仅特指这一部分内存。

怎么定义垃圾

垃圾是指运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾

标记算法

引用计数算法

  • 被对象引用了就+1,引用失效就-1,0表示不可能再被使用,可进行回收
  • 任何时刻计数器为零的对象就是不可能再被使用的

可达性分析算法:

  • 当前主流的商用程序语言的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

img

垃圾回收算法

分代收集理论:

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(GenerationalCollection)的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

  • 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  • 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

标记-清除算法(标记-清除-压缩):

  • 最早出现也是最基础的垃圾收集算法是“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
  • 解决内存碎片问题加入了压缩的步骤

标记-复制算法:

  • 为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费太多。

 

 

 

 

 

 

posted on 2022-03-03 17:15  Egoistic_Flowers  阅读(43)  评论(0)    收藏  举报