JVM与Java体系结构
指令集架构
Java编译器输入的指令流基本上是以一种基于栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构。
反编译命令
javap -v class文件名
JVM的生命周期
Java虚拟机的启动时通过引导类加载器(bootstrap class loader)创建一个初始类来完成的,这个类是由虚拟机的具体实现来完成的。
JVM发展历程
Sun Classic VM
Exact VM
JRockit
J9
…………
类的加载过程
加载(Loading)
- 通过一个类的全限定类名获取定义此类的二进制字节流
- 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
(此处的方法区具体看jdk版本了,jdk7及以前叫永久代,jdk8及以后叫元数据/元空间)
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
加载.class文件的方式
- 从本地系统中直接加载
- 通过网络获取,典型场景:Web Applet
- 从gzip压缩包中读取,成为日后jar、war格式的基础
- 运行时计算生成,使用最多的的是:动态代理技术
- 由其他文件生成,典型场景:JSP应用
- 从转悠数据库中提取.class文件,比较少见
- 从加密文件中虎丘,典型的防Class文件被反编译的保护措施
验证(Verify)
- 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
- 主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证
准备(Prepare)
- 为类变量分配内存并且设置该类变量的默认初始值(即零值)
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
解析(Resolve)
- 将常量池内的符号引用转换为直接引用的过程
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。
初始化(Initalization)
- 初始化阶段就是执行类的构造器方法<clinit>()的过程。
- 此方法不需定义,是javac编译器自动收集中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 构造器方法中指令按语句在源文件中出现的顺序执行
- <clinit>()不同于类的构造器(关联:构造器是虚拟机视角下的<inti>())
- 若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。
- 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁。
类的加载器分类
- JVM支持两种类型的类加载器,分别为引导类加载器(Bootstarp ClassLoader)和自定义类加载器(User-Defined ClassLoader。
- 从概念上讲,自定义类加载器一般指的是程序中由开发人员自定义的一类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器(相当于除了引导类加载器以外,其他的全都是自定义类加载器,扩展类加载器、系统类加载器也属于自定义加载器)。
- 无论类加载器如何划分,在程序中我们最常见的类加载器始终只有3个:
用户自定义类默认使用系统类加载器进行加载,Java核心类库都是使用引导类加载器进行加载
引导类加载器(启动类加载器,Bootstrap ClassLoader)
- 这个类加载使用c语言实现,嵌套在JVM内部
- 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
- 并不继承自java.lang.ClassLoader,没有父类加载器
- 加载扩展类加载器和系统类加载器,并制定为他们的父类加载器
- 处于安全考虑,引导类加载器值加载包名为java、javax、sun等开头的类
扩展类加载器(Extension ClassLoader)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所制定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在子目录下,也会自动由扩展类加载器加载。
系统类加载器(应用程序加载器,App ClassLoader)
- Java语言编写,由sun.misc.Launcher$AppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 他负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类加载器是程序中默认的类加载器,一般来说,Java应用的类都是由他来完成加载的
- 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
用户自定义类加载器
在Java的日常开发中,类的加载几乎是由上述3中类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
为什么要自定义类加载器?
- 隔离加载类
- 修改类的加载方式
- 扩展加载源
- 防止源码泄漏
获取ClassLoader的方式
双亲委派机制
Java虚拟机对class文件采用按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存中生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派机制,即把请求交由父类处理,他是一种任务委派模式。
优势
- 避免类的重复加载
- 保护程序安全,防止核心API被篡改
判断两个class对象是否为同一个类的必要条件
- 类的全限定类名(包含包名)必须一致
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
Java程序中对类的使用方式:主动使用和被动使用
主动使用,又分为七种情况:
- 创建类的实例
- 访问某个列或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(比如:Class.forName(类名))
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
- JDK7开始提供的动态语言支持:
java.lang.invoke.MehtodHandle实例的解析结果
REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
运行时数据区概述及线程
程序计数器(pc寄存器、pc register)
pc寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
- 它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域。
- 在JVM规范中,每个线程都有他自己的程序计数器,是线程私有的,生命周期和线程生命周期保持一致。
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前正在执行Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefined)
- 他是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
两个常见问题(面试题)
使用pc寄存器存储字节码指令地址有什么用?/为什么使用pc寄存器记录当前线程的执行地址?
因为cpu需要不停的切换各个线程,这时候切换回来以后就得知道接着从哪开始继续执行。
jvm字节码解释器就需要通过改变pc寄存器的值来明确下一条应该执行什么样的字节码指令。
pc寄存器为什么被设定为线程私有?
我们知道所谓的多线程在一个特定的时间段内智慧执行其中一个线程的方法,cpu会不停的做任务切换,这样必然导致经常中断和恢复,如何保证分毫无差呢?为了能够准确的记录各个线程正在执行的当前字节码地址,最好的办法自然是为每一个线程都分配一个pc寄存器。这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
由于cpu时间片轮限制,众多线程在并发执行的过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,智慧执行某个线程中的一条指令。
虚拟机栈(Java栈)
概述
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
优点是跨平台、指令集小、编译器容易实现,缺点是性能下降、实现同样的功能需要更多的指令。
Java虚拟机栈是什么?
Java虚拟机栈(Java Virtual Machine Stack),早起也叫作Java栈。
每个线程在创建时都会床架一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。
是线程私有的
生命周期
生命周期和线程一致
作用
主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用与返回。
特性
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
- JVM直接怼Java栈的操作只有两个:
-
- 每个方法执行时的进栈(入栈、压栈)
- 执行结束后的出栈
- 对于栈来说不存在垃圾回收问题
栈中可能出现的异常
Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
- 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。
- 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建相应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。
设置栈大小:
格式:-Xss大小(默认字节,也可以接k/K、m/M、g/G)
例子:-Xss256k
栈的存储单位
栈中存储什么?
- 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在的。
- 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
栈帧的内部结构
局部变量表
- 局部变量表也被称作局部变量数组或本地变量表。
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
- 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
字节码指令查看工具使用教学:尚硅谷宋红康JVM全套教程(详解java虚拟机)_哔哩哔哩_bilibili
关于slot的理解
- 参数值的存放总是在局部变量表数组的index0开始,到数组长度-1的索引结束。
- 局部变量表,最基本的存储单元是slot(变量槽)
- 局部变量表中存放编译器克制的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
- 在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long、double)占用两个slot。
-
- byte、short、char在存储前被转换为int,boolean也被转换为int(0表示flase,非0表示true)。
- long和double则占据两个slot。
- JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
- 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上
- 如果需要访问局部变量表中的一个64bit的局部变量值时,只需要使用前一个索引即可(比如:访问long或double类型变量占两个slot 4和5,访问的时候使用索引4)
- 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
- 栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从未达到节省资源的目的。
补充说明
- 在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
- 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
操作数栈(Operand Stack)
- 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
- 操作数占就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
- 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值
- 栈中的任何一个元素都是可以任意的Java数据类型。
-
- 32bit的类型占用一个栈单位深度
- 64bit的类型占用两个栈单位深度
- 操作数栈并非采用访问索引的方式来进行数据访问,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令
- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证
- 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
动态链接(Dynamic Linking)
- 指向运行时常量池的方法引用
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Liking)。比如invokedynamic指令
- 在Java源文件被编译到字节码文件中时,所有变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
方法返回地址
存放该方法的pc寄存器的值
一些附加信息
方法的调用
- 过程
-
- 静态链接
- 动态链接
- 方式
-
- 早期绑定
- 晚期绑定
虚方法和非虚方法
动态类型语言和静态类型语言
- 静态类型语言:编译期检查类型
- 动态类型语言:运行期检查类型
面试题
本地方法栈
Java虚拟机栈用于管理Java方法的调用,而本地房发展用于管理本地方法的调用
本地方法栈也是线程私有的
- 允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
-
- 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。
- 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个OutOfMemoryError异常。
- 本地方法是使用C语言实现的。
- 它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。
- 当某个线程调用一个本地方法时,他就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
-
- 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
- 他甚至可以直接使用本地处理器中的寄存器
-
- 直接从本地内存的堆中分配任意数量的内存
- 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的实用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
- 在Hostspot JVM中,直接将本地方法栈和虚拟机栈合二为一。
堆
- 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
- Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
-
- 堆内存的大小是可以调节的(Xmx:最大堆大小,-Xms:初始堆大小,-Xmn:年轻代大小)
- 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上他应该被视为连续的。
- 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)
- 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap si the run-time data area from which memory for all class instances and arrays is allocated)
-
- 我要说的是:“几乎”所有的对象实例都在这里分配内存。——从实际实用角度看的。
- 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或数组在堆中的位置。
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
- 堆,是GC(Garbage Collection, 垃圾收集器)执行垃圾回收的重点区域。
设置堆空间大小
设置初始堆空间大小:-Xms字节数
eg.:-Xms1048576 或 -Xms1m ...
设置最大堆空间大小:-Xmx字节数
eg.:-Xmx1048576 或 -Xmx1m ...
年轻代与老年代
配置新生代与老年代在堆结构中的占比
- 默认-XX:NewRatio=2,表示新生代占1、老年代占2(新生代占整个堆的1/3)
- 可以修改(例)-XX:NewRatio=4,表示新生代占1、老年代占4,新生代占整个堆的1/5
年轻代
- 年轻代可以划分为Eden、survivor0(from)、survivor1(to)
- 在HotSpot中,Eden和另外两个Survivor空间缺省占比是8:1:1,开发人员可以通过-XX:SurvivorRatio调整比例。但是如果不手动设置为8,使用默认设置的话,用工具查看参数确实是SurvivorRatio=8,但是查看容量的时候显示的不是8:1:1,而是6:1:1的内存
- 几乎所有的Java对象都是在Eden区被new出来的(如果new的对象Eden放不下,就直接进入老年区了)
- 可以使用-Xmn设置新生代最大内存大小(一般不设置)
对象分配与回收过程
尚硅谷宋红康JVM全套教程(详解java虚拟机)_哔哩哔哩_bilibili
尚硅谷宋红康JVM全套教程(详解java虚拟机)_哔哩哔哩_bilibili
逃逸分析
堆是分配对象存储的唯一选择吗? 不是
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需再堆上分配内存,也无需进行垃圾回收了。这也是最常见的堆外存储技术。
逃逸判断的是创建的对象,而不是变量:
A a = new A();
判断的是new A()
public class AText{
public A obj;
public A newA1(){ //逃逸
return obj==null? new A(): obj;
}
public void newA2(){ //未逃逸
A a = new A();
}
public void getNewA1(){ //逃逸,因为newA1()中new出来的实体是逃逸的
A a = newA1();
}
public void getNewA2(){ //未逃逸
A a = newA2();
}
}
方法区
堆栈方法区的交互
方法区的理解
设置方法区大小与OOM
- jdk7及以前
-
- 通过-XX:PermSize设置永久代初始分配空间大小,默认20.75M
- 通过-XX:MaxPermSize来设定永久代最大分配空间大小。32位机默认64M,64位机器默认82M
-
- 当超出最大值时,会报OutOfMemoryError:PermGen space
- jdk8及以后
-
- 通过-XX:MetaspaceSize设置元空间初始分配空间大小
-
-
- windows下默认21M
-
-
- 通过-XX:MaxMetaspaceSize设置元空间最大分配空间大小
-
-
- windows下默认-1,即没有限制
-
-
- 当超出最大值时,会报OutOfMemoryError:Metaspace
- 如何解决OOM
方法区内部结构
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存……
运行时常量池
- 常量池VS运行时常量池
-
- 方法区:内部包含运行时常量池
- 字节码文件:内部包含常量池
字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息就是常量池表,包括各种字面量和对类型、域、方法的符号引用。
- 运行时常量池(Runtime Constant Pool)是方法区的一部分
- 常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区额运行时常量池中。
- 在加载类和接口道虚拟机后,就会创建相应的运行时常量池
- JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项向数组项一样,是通过索引访问的。
- 运行时常量池中包含多种不同的常量,包括编译器就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换位真实地址。
-
- 运行时常量池相对于Class文件常量池的另一个重要特征是:具备动态性。
- 运行时常量池类似于传统编程语言中的符号表(symbol table),但是他所包含的数据却比符号表更加丰富一些。
- 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛出OutOfMemoryError异常。
方法区细节演示
方法区垃圾回收
大厂面试题
对象实例化信息
对象创建方式
- new
- Class.newInstance()
- Constructor.newInstance(XXX)
- clone()
- 反序列化
- 第三方库Objenesis
对象创建步骤
- 判断对象相应的类是否加载、链接、初始化
- 为对象分配内存
-
- 如果内存规整
-
-
- 指针碰撞
-
-
- 如果内存不规整
-
-
- JVM需要维护一个列表
- 空闲列表分配
-
- 处理并发安全问题
-
- 采用CAS配上失败重试保证更新的原子性
- 每个线程预先分配一块TLAB
- 初始化分配到的空间
-
- 所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用
- 设置对象的对象头
- 执行<init>方法进行初始化
对象的内存布局
- 对象头
-
- 运行时元数据
-
-
- HashCode
- GC分代年龄
-
-
-
- 锁状态标志
- 线程持有的锁
-
-
-
- 偏向线程id
- 偏向时间戳
-
-
- 类型指针
-
-
- 指向类元数据InstanceKlass,确定该对象所属的类型
-
-
- 如果是数组,还需记录长度
- 实例数据
-
- 说明:他是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括继承来的和自己的)
- 规则:
-
-
- 相同宽度的字段总是被分配在一起
- 父类中定义的变量会出现在子类之前
-
-
-
- 如果CompactFields参数为true(默认为true),子类的窄变量可能插入到父类的空隙
-
- 对齐填充
-
- 不是必须的,也没什么特别含义,仅仅起到占位符作用
实例访问的两种方式
- 句柄指针
- 直接指针(hotspot使用)
执行引擎
概述
是JVM核心的组成部分之一,充当了高级语言的翻译官
编译和执行过程
什么是解释器和JIT编译器
解释器:
当JVM启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译成为对应平台的本地机器指令执行。
JIT(Just In Time Complier)编译器:
就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。
为什么Java叫做半解释型半编译型语言
因为执行引擎在执行字节码文件的时候既可以使用解释器,又可以使用即时编译器。
垃圾回收
概述
- 什么是垃圾?
-
- 垃圾是指在程序运行中没有任何指针指向的对象
- 如果不清理垃圾会怎么样
-
- 垃圾会一直保存到程序运行结束,被保留的空间无法被其他对象使用,甚至可能导致内存溢出
- 为什么需要GC
-
- 如果不进行垃圾回收,内存迟早会被消耗完(和钱一样)
- 清除内存里的记录碎片,以便JVM将整理出的内存分配给新的对象
- GC的作用区域
-
- 方法区
- 堆
- 几种GC的区别
-
- Minor GC:年轻代垃圾回收
- Young GC:就是minor GC
-
- Major GC:老年代垃圾回收(有的时候、有的人、会把Major GC和Full GC等价看待,当做回收整个堆)
- Old GC:老年代垃圾回收
-
- Full GC:回收新生代和老年代
- Mixed GC:收集整个年轻代和老年代。只有G1有这个模式
垃圾回收的相关概念
System.gc()
- 在默认情况下,通过System.gc()或者Runtime.getRuntime.gc()的调用,会显式的触发Full GC,同事对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
- 然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用。
- JVM实现着可以通过System.gc()调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的无需手动触发,否则就太过于麻烦啦。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()。
内存溢出
- 内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一。
- 由于GC一直在发展,所以一般情况下,除非应用程序占用的内存增长素的非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现OOM的情况。
- 大多数情况下,GC会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的Full GC操作,这时候会回收大量的内存,供应用程序继续使用。
- javadoc中对OOM的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。
没有空闲内存的情况:
- Java虚拟机的堆内存设置不够
-
- 比如内存泄漏问题、也有可能是堆的大小设置不合理
- 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
内存泄漏(Memory Leak)
也称作"存储渗漏"。严格来说,只有对象不会再被程序又到了,但是GC又不能回收他们的情况,才叫内存泄漏。
但实际情况很多时候一些不大好的实践会导致对象的生命周期变得很长甚至导致OOM,可也以叫做宽泛意义上的"内存泄漏"
尽管内存泄漏并不会立即引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终导致OOM出现,导致程序崩溃。
注:这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。
举个栗子:
- 单例模式
单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
- 一些提供close的资源(外部资源)未关闭导致内存泄漏
数据库连接(dataSource.getConnection)、网络连接(socket)、和io连接必须手动close,否则是不能被回收的。
不要举循环引用的例子,因为这种是引用计数算法的,然而Java用的不是引用计数算法
Stop The World
简称STW,指的是GC事件发生过程中,会产生应用程序的卡顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿成为STW。
- 可达性分析算法中枚举根节点会导致所有Java执行线程停顿。
-
- 分析工作必须在一个能够确保一致性的快照中进行
- 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
-
- 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确属性无法保证
- 被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少STW的发生。
- STW事件和采用哪款GC无关,所有的GC都有这个事件。
- 哪怕是G1也不能完全避免STW情况发生,只能说垃圾回收器越来越有限,回收效率越来越高,尽可能的缩短了暂停时间。
- STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
- 开发中不要用System.gc(),会导致STW的发生。
垃圾回收的并行与并发
- 并发(Concurrent)(一个咖啡机一队人)
-
- 一段时间中,有几个程序都处于启动状态,并不是真正意义的同时执行,只是在一段时间内多个线程来回切换(但是CPU处理的速度快,让用户感觉十多个应用程序同时在运行)
- 并行(Parallel)(多个咖啡机多队人)
-
- 当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互补抢占CPU资源,可以同时进行
- 决定因素不是CPU数量,而是CPU核心数,哪怕一个CPU多个核心也可以并行
在垃圾收集器的语境中,并行相关概念可以理解为如下释义:
- 并行:指多条垃圾回收线程并行工作,但此时用户线程仍处于等待状态。
-
- 如ParNew、Parallel、Scavenge、Parallel old
- 串行:
-
- 相对于并行的概念,单线程执行
- 如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收。回收完再启动程序的线程。
- 并发:用户线程与垃圾收集线程公示执行(不一定是并行的,可能会交替执行)
-
- 垃圾回收线程在执行时不会停顿用户程序的执行
- 用户程序在运行,垃圾收集线程在另一个CPU运行
-
- 如CMS、G1
安全点和安全区域
安全点(SafePoint)
程序执行时并非在所有地方都停下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为安全点
安全区域(SafeRegion)
安全区域实质在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。
强软弱虚引用
强引用
尚硅谷宋红康JVM全套教程(详解java虚拟机)_哔哩哔哩_bilibili
软引用
尚硅谷宋红康JVM全套教程(详解java虚拟机)_哔哩哔哩_bilibili
弱引用
尚硅谷宋红康JVM全套教程(详解java虚拟机)_哔哩哔哩_bilibili
虚引用
尚硅谷宋红康JVM全套教程(详解java虚拟机)_哔哩哔哩_bilibili
垃圾回收的相关算法
- 标记阶段
-
- 引用计数算法
- 可达性分析算法
- 清除阶段
-
- 标记-清除算法
- 复制算法
-
- 标记-压缩算法
引用计数算法
- 引用计数算法对每个对象保存一个整形的引用计数器属性,用于记录对象被应用的情况
- 对于一个对象A,只要有任何一个对象引用了A,A的引用计数器就+1,;当引用失效后,引用计数器就-1。只要A的引用计数器值为0,即表示对象A没有在被使用,可以进行回收
- 优点
-
- 实现简单,垃圾对象便于识别,判定效率高,回收没有延迟
- 缺点
-
- 他需要单独的字段存储计数器,增加了存储空间的开销
- 每次赋值都需要更新计数器,增加了时间开销
-
- 无法处理循环引用情况(这是致命的,导致在Java的垃圾回收器中没有使用这类算法)
可达性分析算法
也有人称为:根搜索算法、追踪性垃圾收集
- 相对于引用计数算法而言,可达性分析算法不近同样具备实现简单和执行高效等特点,更重要的是该算法可以有效的解决在引用计数算法中循环引用的问题,防止内存泄漏发生。
- 相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集
在Java中,GC Roots包括以下几类元素
- 虚拟机栈中引用的对象
-
- 比如:各个线程被调用的方法中使用到的参数、局部变量……
- 本地方法栈内JNI(通常说的本地方法)引用的对象
- 方法区中类静态属性引用的对象
-
- 比如:Java类的引用类型静态变量
- 方法区中常量引用的对象
-
- 比如:字符串常量池里的引用
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用
-
- 基本数据类型对应的Class对象、一些常驻的异常对象(如NullPointerException、OutOfMemoryError)、系统类加载器。
- 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存……
小技巧:
由于Root采用栈方式存放变量指针,所以如果一个指针它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那他就是一个Root。
注意:
- 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话,分析结果的准确性就无法保证。
- 这点也是导致GC进行时必须"Stop The World"的一个重要原因。
-
- 及时号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
对象的finalization机制
- Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
- 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法
- finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字、数据库连接……
- 由于finalize()方法存在,java对象存在三种状态
-
- 可触及的:从根节点开始,可以到达这个对象
- 可复活的:对象的所有引用都别释放,但是对象有可能在finalize()中复活
-
- 不可触及的:对象的finalize()被调用,并且没有复活。不可触及的对象不可能被复活,因为finalize()只会被调用一次
MAT与JProfiler的GC Roots溯源
MAT是一款功能强大的Java堆内存分析器,用于查找内存泄漏以及查看内存消耗情况。
- 获取dump文件
-
- 使用jmap命令
- 使用JVisualVM导出
标记清除算法(Mark-Sweep)
执行过程:
- 标记:标记可达对象(非垃圾对象)
- 清除:对堆中对象遍历,如果没有标记,清除该对象
- 缺点
-
- 效率不高
- 在进行GC时候,需要停止整个应用程序,降低用户体验
-
- 清理出来的空闲内存是不连续的,会产生内存碎片
- 何为清除
-
- 所谓的清除不是真正置空,而是把需要清除对象地址保存到地址列表中,下次创建新对象判断垃圾的未知空间是否足够,如果够,就存放
复制算法
- 核心思想
-
- 将内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象赋值到未被使用的内存块中,之后清除折能够在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收
- 优点
-
- 没有标记和清除过程,实现简单,运行高效
- 复制过去以后保证空间连续性,不会出现碎皮问题
- 缺点
-
- 需要两倍的内存空间
- 需要维护region之间对象引用关系,时间空间开销都不小
标记-压缩算法(标记-整理、Mark-Compact)
执行过程:
- 标记:标记可达对象(非垃圾对象)
- 压缩:将标记对象重新整理,清除未标记对象
- 优点
-
- 消除了标记-清除算法中,内存区域分散的缺点
- 消除了复制算法中,内存减半的高额代价
- 缺点
-
- 效率上低于复制算法
- 移动对象时,如果对象被其他对象引用,则需调整引用的地址
-
- 移动过程中,需要全程暂停用户应用程序(STW)
清除阶段三种算法比较
分代收集算法
并不是一个算法,只是综合多种收集算法,根据对象存活时间不同使用不同的收集算法。
- 年轻代
-
- 复制算法
- 老年代
-
- 标记-清除和标记-整理算法混合实现
增量收集算法
基本思想:
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,知道垃圾收集完成。
总的来说增量手机算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。
- 缺点
-
- 使用这种方式,由于在垃圾回收过程中,间断性的还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
分区算法
一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的没存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
分带算法将按照对象的生命周期长短划分为两个部分,分区算法将整个堆空间划分为连续的不同小区间。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
垃圾回收器
概述
按线程数分:
串行垃圾回收器
并行垃圾回收器
按工作模式分:
并发式垃圾回收器
独占式垃圾回收器
按碎片处理方式分:
压缩式垃圾回收器
非压缩式垃圾回收器
按工作内存区间分:
年轻代回收器
老年代回收器
评估GC的性能指标
- 吞吐量:运行用户代码的时间占总运行时间的比例(总=程序+回收)
- 垃圾收集开销:1-吞吐量
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间
- 收集频率:相对于应用程序的执行,收集操作发生的频率
- 内存占用:Java堆区所占的内存大小
- 快速:一个对象从诞生到被回收所经历的时间
不同分类的垃圾回收器
垃圾收集器的组合关系
- 连线表示都能搭配使用
- 红线 jdk8中过时 jdk9中移除
- 绿线 jdk14中过时 还没有移除
- 绿框CMS jdk9中过时 jdk14中删除
如何查看默认垃圾收集器
- -XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)
- 使用命令行指令:
jinfo -flag [相关垃圾回收器参数] [进程ID]
Serial回收器(串行)
垃圾收集器名称 | 起止版本 | 收集位置 | 串/并行 | 使用算法 | 机制 |
serial | 年轻代 | 串行 | 复制算法 | Stop The World | |
Serial Old | 老年代 | 串行 | 标记-压缩算法 | Stop The World |
- Serial收集器是最基本的、历史最悠久的垃圾收集器。JDK1.3之前回收新生代的唯一选择。
- Serial收集器座位HotSpot中Client模式下的默认新生代垃圾收集器
- Serial收集器采用复制算法、串行回收和STW机制的方式执行内存回收
- 除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial old收集器。Serial old收集器同样也采用了串行回收和STW机制,只不过内存回收算法使用的是标记-压缩算法。
-
- Serial old是运行在Client模式下默认的老年代的垃圾回收器
- Serial old在server模式下主要用途有两个:
-
-
- 与新生代的Parallel Scavenge配合使用
- 座位老年代CMS收集器的后备垃圾收集方案
-
- 它智只会使用一个线程去完成垃圾收集工作
- 它进行垃圾回收的时候,必须暂停其他所有工作线程,知道它结束(STW)
ParNew回收器(并行)
垃圾收集器名称 | 起止版本 | 收集位置 | 串/并行 | 使用算法 | 机制 |
ParNew | ?---jdk8 | 年轻代 | 并行 | 复制算法 | Stop The World |
- 如果说Srial GC是年轻代中的单线程垃圾收集器,那么ParNew则是Serial的多线程版本。
-
- Par是Parallel的缩写,New代表处理的是新生代
- ParNew收集器除了采用并行回收的方式执行内存回收外,和Serial收集器之间几乎没有任何区别。同样采用复制算法、STW机制。
- ParNew是很多JVM运行在Server模式下新生代的默认垃圾收集器
Parallel Scanvenge回收器(并行)
高吞吐量
垃圾收集器名称 | 起止版本 | 收集位置 | 串/并行 | 使用算法 | 机制 |
Parallel Scavenge | 年轻代 | 并行 | 复制算法 | Stop The World | |
Parallel Old | 1.6--- | 老年代 | 并行 | 标记-压缩算法 | Stop The World |
- 与ParNew收集器不同的是,ParNew目标是达到一个可控制的吞吐量,也被称作吞吐量优先的垃圾收集器。
- 自适应调节策略也是Parallel Scavenge与ParNew的一个重要区别
高吞吐量主要适合在后台运算而不需要太多交互的任务。例如批量处理、订单处理、工资支付、科学计算的应用程序。
CMS回收器(并发)
低延迟
- CMS(Concurrent-Mark-Sweep)
- 是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
- CMS的关注点是尽可能缩短垃圾收集时用户线程的停顿。
垃圾收集器名称 | 起止版本 | 收集位置 | 串/并行 | 使用算法 | 机制 |
ParNew | jdk1.5---1.9---14 | 老年代 | 并发 | 标记-清除算法 | Stop The World |
- 初始标记:仅仅标记出GC Roots能直接关联到的对象。速度非常快
- 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,耗时较长但不需要停顿用户线程,垃圾收集线程可以与用户线程一起并发运行
- 重新标记:修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录
- 并发清除:清理删除掉标记判断的已经死亡的对象,释放内存空间
缺点
- 会产生内存碎片
- 对cpu资源非常敏感
- 无法处理浮动垃圾
G1回收器
目标是在延迟可控的情况下,获得尽可能高的吞吐量。
垃圾收集器名称 | 起止版本 | 收集位置 | 串/并行 | 使用算法 | 机制 |
G1 | jdk1.9 | 年轻+老年代 | 并行+并发 | 复制算法 | Stop The World |
- G1是一个并行回收器,他把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使用Region来表示Eden、幸存者0、幸存者1区、老年代……
- 有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
优点
- 并行与并发
-
- 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW。
- 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况。
- 分代收集
-
- 从分代上看,G1依然属于分代型垃圾回收器,她会区分年轻代和老年代,年轻代依然有Eden、Survivor区。但从堆的结构上看,它不要求整个Eden区、Survivor或者老年代都为连续的,也不再坚持固定大小和固定数量。
- 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
-
- 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收期,或者工作在年轻代,或者工作在老年代。
- 空间整合
-
- CMS:"标记-清除"算法、内存碎片、若干次GC后进行一次碎片整理
- G1将内存划分为一个个Region,内存回收是以Region为基本单位的。Region之间是复制算法,但是整体上实际可看作是标记-压缩算法,朗胡总算法都可以避免内存碎片。
- 可预测的停顿时间模型(即:软实时soft real-time)
-
- 这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
- 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收范围,因此对于全局停顿情况的发生也能得到较好的控制
-
- G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
- 想比于CMS,G1未必能够坐待CMS在最好情况下的延时停顿,但是最差情况要好很多
G1的使用场景
- 面向服务端应用,针对具有大内存、多处理的机器。(在普通大小的堆里表现并不惊喜)
- 最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案
- 用来替换掉jdk1.5中的CMS收集器。以下情况时,G1优于CMS
-
- 超过50%的Java堆被活动数据占用
- 对象分配频率或年代提升频率变化很大
-
- GC停顿时间过长(长于0.5-1秒)
- HotSpot垃圾收集器里,除G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程
Region
分为Eden、Survivor、Old、Humongous四种,大对象放在Humongous存放
G1回收垃圾过程主要包括如下三个环节
- 年轻代GC(Young GC)
- 老年代并发标记过程(Concurrent Marking)
- 混合回收(Mixed GC)
- 如果需要,单线程、独占式、高强度的Full GC还是继续存在的。他针对GC的评估失败提供了一种失败保护机制,即强力回收。导致的原因可能有两个:
-
- Evacuation的时候没有足够的to-space来存放晋升的对象
- 并发处理过程完成之前空间耗尽
总结
怎样选择垃圾回收器
Java垃圾收集器的配置对于JVM优化来说是一个很重要的选择,选择合适的垃圾收集器可以让JVM的性能有一个很大的提升。
- 怎么选择垃圾收集器?
- 优先调整堆的大小让JVM自适应完成。
- 如果内存小于100M,使用串行收集器
- 如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
- 如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择
- 如果是多CPU、追求地停顿时间,需要快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器。官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。
GC日志分析
见:尚硅谷宋红康JVM全套教程(详解java虚拟机)_哔哩哔哩_bilibili
内存分配与垃圾回收的常用参数
-XX:+PrintGC 输出GC日志。类似:-verbose:gc
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PringGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如2020-9-29T14:53:45.123+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-XXloggc:[路径地址] 日志文件的输出路径
日志内容说明
- "GC" 和"Full GC" 说明了这次垃圾收集的停顿类型,如果有"Full"则说明GC发生了"Stop The World"
- 使用serial收集器在新生代的名字是Default New Generation,因此显示的是"DefNew"
- 使用ParNew收集器在新生代的名字会变成"ParNew",意思是"Parllel New Generation"
- 使用Parallel Scavenge收集器在新生代的名字是"PSYoungGen"
- 老年代的收集和新生代道理一样,名字也是收集器决定的……
- 使用G1收集器的话,会显示为"garbage-first heap"
- Allocation Failuer
-
- 表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了
- [PSYoungGen: 5986K->696K(8704K)] 5986K->704K(9216K)
-
- 中括号内:GC回收前年轻代大小,回收后大小,(年轻代总大小)
- 括号外:GC回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总和)
- user代表用户态回收耗时,sys内核态回收耗时,rea实际耗时。由于多核的原因,时间总和可能会超过real时间
大厂面试题
其他
直接内存
- 不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域
- 直接内存是在Java堆外、直接向系统申请的内存区间
- 来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存
- 通常,访问直接内存的速度会优于Java堆,即读写性高
-
- 因此处于性能考虑,读写频繁的场合可能会考虑使用直接内存
- Java的NIO库允许Java程序使用直接内存,用于数据缓冲区
- 也可能导致OutOfMemoryError
- 缺点
-
- 分配回收成本高
- 不受JVM内存回收管理
- 如果不指定,默认与堆的最大值-Xmx参数值一致
String Table
String基本特性
- 使用""引起来表示
- String是final的,不可被继承
- String的存储
-
- jdk8是字符数组char[]
- jdk9是字节数组byte[]
- 字符串常量池中是不会存储相同内容的字符串的
字符串拼接操作
- 常量与常量拼接结果在常量池,原理是编译期优化
- 常量池中不会存在相容内容的常量
- 只要其中有一个是变量(非final),结果就在堆中。原理是StringBuilder
- 如果拼接的结果调用itern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
intern()的使用
- 不同jdk版本使用的区别
-
- jdk1.6
-
-
- 将字符串对象尝试放入字符串常量池中
-
-
-
-
- 如果池中有:不放入,返回池中已有对象的地址
- 如果池中没有:会把此对象复制一份,放入池中,并返回池中的对象地址
-
-
-
- jdk1.7
-
-
- 将字符串对象尝试放入字符串常量池中
-
-
-
-
- 如果池中有:不放入,返回池中已有对象的地址
- 如果池中没有:会把对象引用地址赋值一份,放入池中,并返回池中的引用地址
-
-
- 创建字符串时对于字符串常量池的分析
-
- String s = "a"+"b"; 池中会生成
- String s = "ab"; 池中会生成
-
- String s = new String("ab"); 池中会生成
- String s = new String("a")+new String("b"); 池中不会生成
针对大量创建相同值的字符串的时候,使用intern()方法会节省很大内存空间
机器码、指令、汇编语言
机器码
- 各种采用二进制编码方式表示的指令,叫做机器指令码。
- cpu可直接读取运行,因此和其他语言相比,执行速度最快。
- 机器指令与CPU紧密相关,所以不同种类的CPU所对应的机器指令也就不同。
指令
- 由于机器码只有0、1,可读性太差,于是人们发明了指令。
- 指令就是把机器码的0、1简化成对应的指令(一般为英文缩写,如mov,inc等),可读性稍好
- 由于硬件平台不同,执行统一操作对应的机器码也有可能不同,所以不同的硬件平台的同一种指令(如mov)对应的机器码也可能不同。
指令集
- 不同的硬件平台,各自支持的指令是有差别的。因此每个平台所支持的指令,称为对应平台的指令集。
- 如常见的:
-
- x86指令集,对应的是x86架构的平台
- ARM指令集,对应的是ARM架构的平台
汇编语言
- 由于指令的可读性还是太差,于是人们又发明了汇编语言。
- 在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数地址。
- 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。
由于计算机只认识指令码,所以汇编语言编写的程序还必须翻译成机器指令码,计算机才能识别和执行