JVM笔记一内存与垃圾回收篇
学习流程图解:
类加载器子系统
1、类的加载过程
加载–>连接(linking)(验证、准备、解析)–>初始化
加载(loader)
- 通过一个类的全限定类名获取定义此类的二进制字节流
- 将这个字节流所代表的静态储存结果转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口
验证(verify)
准备(prepare)
- 为类变量分配内存开且设置该类变量的默认初始值,即零值。
- 这里不包含用final修饰的static,因为final在编 译的时候就会分配了,准备阶段会显式初始化。
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
解析(resolve)
- 将常量池内的符号引用转换为直接引用的过程。
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class_ info、 CONSTANT Fieldref info、 CONSTANT Methodref_ info等
初始化(Initialization)
- 初始化阶段就是执行类构造器方法< clinit > ()的过程。
- 此方法不需定义,是javac编译 器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 构造器方法中指令按语句在源文件中出现的顺序执行。
- < clinit > ()不同于类的构造器。(关联:构造器是虚拟机视角下的< init >())
- 若该类具有父类,JVM会 保证子类的< clinit > ()执行前,父类的< clinit > ()已经执行完毕。
- 虚拟机必须保证- -个类的< clinit > ()方法在多线程下被同步加锁。
2、类加载器的分类
虚拟机自带的加载器
1、启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部,不能直接获取到
- 它用来加载Java的核心库(JAVA HOME/jre/lib/rt.jar、resources. jar或sun . boot. class.path路径下的内容) ,用于提供JVM自身需要的类
- 并不继承自java.lang.classLoader,没有父加载器。
- 加载扩展类和应用程序类加载器,并指定为他们]的父类加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
2、扩展类加载器( Extension ClassLoader)
- Java语言编写,由sun . misc. LauncherSExtClassLoader实现。
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java. ext. dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
3、应用程序类加载器(系统类加载器,AppClassLoader )
- java语言编写,由sun.misc. Launcher$AppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java. class.path指定路径下的类库
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过ClassLoader #getSystemClassLoader ()方法可以获取到该类加载器
4、自定义加载类
**ClassLoader类,它是-一个抽象类,其后所有的类加载器都继承自ClassLoader (不包括启动类加载器) **
3、双亲委派机制
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
代码演示
创建一个StringTest类
package com.wlq.java1
/**
* 这里并不会执行StringTest的输出,说明这里请求委派给了父类加载器,
* 刚好发现引导加载器刚好可以加载这个java.lang.String,就被引导加载器执行了
*/
public class StringTest{
public static void main(String[] args){
java.lang.String str = new java.lang.String();
System.out.println("hello,wlq");
}
}
在src下创建包结构:java.lang
在java.lang下创建一个String类
package java.lang;
public class StringTest{
static{
System.out.println("我是自定义的String类的静态代码块");
}
/**会报错,错误:在类java.lang.String中找不到main方法.因为这里会委派给引导类加载器,
* 而引导类加载器中的核心API中常用的字符串没有main这个方法,所以会报错
*/
public static void main(String[] args){
System.out.println("hello,String");
}
}
沙箱安全机制:
自定义string类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String . class),报错信息说没有main方法,就是因为加载的是rt. jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
双亲委派机制的优点:
- 避免类的重复加载
- 保护程序的安全,防止核心API被随意篡改
4、其他
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
- 类的完整类名必须- -致,包括包名。
- 加载这个类的ClassLoader (指ClassLoader实例对象)必须相同。
换句话说,在JVM中,即使这两个类对象(class对象)来源同一一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
对类加载器的引用:
JVM必须知道–个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
类的主动使用和被动使用
Java程序对类的使用方式分为:主动使用和被动使用。
- 主动使用,又分为七种情况:
1、创建类的实例
2、访问某个类或接口的静态变量,或者对该静态变量赋值
3、调用类的静态方法
4、反射(比如: Class. forName (“com. atguigu. Test”) )
5、初始化一个类的子类
6、Java虚拟机启动时被标明为启动类的类
7、JDK 7开始提供的动态语言支持:java.lang. invoke . MethodHandle实例的解析结果REF getStatic、 REF putStatic、 REF invokeStatic句柄对应的类没有初始化,则初始化 - 除了以上七种情况,其他使用java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
运行时数据区(Runtime Data Area)
线程
- 线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行。
- 在Hotspot JVM里, 每个线程都与操作系统的本地线程直接映射。 当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收。
- 操作系统负责所有线程的安排调度到任何一个可用的cpu上。一旦本地线程初始化成功,它就会调用Java线程中的run()方法。
后台系统的主要线程
虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型包括"stop-the-world"的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。
周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一-般用于周期性操作的调度执行。
GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。
编译线程:这种线程在运行时会将字节码编译成到本地代码。
信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理。
1、程序计数器(PC寄存器)
JVM中的程序计数寄存器(Program Counter Register) 中,Register 的命名源于cpu的寄存器,寄存器存储指令相关的现场信息。cpu只有 把数据装载到寄存器才能够运行。这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
- 它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。
- 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一-致。
- 任何时间一一个线程都只有一一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefined)。
- 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 字节码解释器工作时就是通过改变这个计数器的值来选取下-"条需要执行的字节码指令。
- 它是唯一一个在Java虚拟机规范中没有规定任何OutOtMemoryError情况的区域。
作用:
PC寄存器用来存储指 向下一条指令的地址, 也即将要执行的指令代码。由执行引擎读取下一条指令。
使用举例:
int i = 10;
int j = 20;
int k = i + j;
String str = "abc";
System.out.println(i);
System.out.println(k);
面试常问问题
使用PC寄存器存储字节码指令地址有什么用呢? 为什么使用PC寄存器记录当前线程的执行地址呢?
- 因为cpu需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
- JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一-条应该执行什么样的字节码指令。
pc寄存器为什么会被设定为线程私有?(每个线程都会有自己的一个PC寄存器)
cpu时间片
2、虚拟机栈
内存中的栈和堆:
栈是运行时的单位,而堆是存储的单位。即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。|
- java虚拟机栈是什么?
Java虛拟机栈(Java Virtual Machine Stack) ,早期也叫Java栈。每个线程在创建时都会创建-一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame) ,对应着一次次的Java方法调用。
➢是线程私有的
- 生命周期
生命周期和线程一致。|
- 作用
栈的优点
栈中可能出现的异常
- Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
➢如果采用固定大小的Java虚拟机栈,那每- -个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。
➢如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError 异常。
设置栈的最大深度(举例)
/**
* 演示栈中的异常:StackOverflowError
*
* 默认情况下:count=11420
* 设置栈的大小:-Xss256k:count=2465
*/
public class StackerrorTest{
private static int count = 1;
public static void main(String[] args){
System.out.println(count);
count++;
main(args);
}
}
栈中储存什么
- 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame) 的格式存在。
- 在这个线程上正在执行的每个方法都各自对应-一个 栈帧(stack Frame) 。
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
复习:
OOP的基本概念:类,对象
类中的基本结构:field(属性,字段,域)、method
栈执行原理
- JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
- 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame) ,与当前栈帧相对应的方法就是当前方法(CurrentMethod),定义这个方法的类就是当前类(Current Class)
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
- 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
- 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
- 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
- Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
栈桢的内部结构
- 局部变量表(Local variables)
- 操作数栈(operand stack) (或表达式栈)
- 动态链接(Dynamic Linking) ( 或指向运行时常量池的方法引用)
- 方法返回地址(Return Address) (或方法正常退出或者异常退出的定义)
- 一些附加信息
1、局部变量表
- 局部变量表也被称之为局部变量数组或本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference) ,以及returnAddress类型。
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
- 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。 在方法运行期间是不会改变局部变量表的大小的。
- 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
- 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
Slot
- 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
- 局部变量表,最基本的存储单元是Slot (变量槽)
- 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
- 在局部变量表里,32位以内的类型只占用一个slot (包括returnAddress类型),64位的类型(long和double)占用两个slot。
➢byte 、short 、char 在存储前被转换为int,boolean 也被转换为int,0表示false,非0表示true。
➢long和double则占据两个Slot。
slot重复利用
- 栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
public void test(){
int a = 0;
{
int b = 0;
b = a + 1;
}
//变量c使用之前已经销毁的变量b占据的slot的位置
int c = a+1;
}
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
2、操作数栈
- 每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈,也可以称之为表达式栈。
- 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push) /出栈(pop)
➢某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。
➢比如:执行复制、交换、求和等操作, - 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
- 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
- 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max stack的值。
- 栈中的任何一个元素都是可以任意的Java数据类型。
➢32bit的类型占用一个栈单位深度
➢64bit的类型占用两个栈单位深度 - 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访 问。
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一-条需要执行的字节码指令。
- 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的
就是操作数栈。
代码演示:
byte i =15;
int j = 8;
int k =i + j;
局部变量表 locals = 4 ,操作数栈最大深度 stack = 2
3、动态链接(或指向运行时常量池的方法引用)
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking) 。比如: invokedynamic指 令
- 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference) 保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
理解图解
为什么需要常量池呢?
常量池的作用,就是为了提供一些符号和常量,便于指令的识别。
方法的调用(内部机制)
对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding) 。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
方法的调用,虚方法和非虚方法
非虚方法::
方法的调用:方法重写的本质
java语言中方法重写的本质
- 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。
- 如果在过程结束;如果不通类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过,则返回java.lang.IllegalAccessError异常。
- 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出java. lang .AbstractMethodError异常。
IllegalAccessError介绍:
程序试图访问或修改- -个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。
虚方法表
- 在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table) (非虚方法不会出现在表中)来实现。使用索引表来代替查找。
- 每个类中都有一-个虚方法表,表中存放着各个方法的实际入口。
- 那么虚方法表什么时候被创建?
虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
4、方法返回地址
- 存放调用该方法的pc寄存器的值。
- 一个方法的结束,有两种方式:
➢正常执行完成
➢出现未处理的异常,非正常退出 - 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
5、一些附加信息
- 栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。
面试常问问题
- 举例栈溢出的情况? (StackOverflowError)
比如写一个递归,运行了七千次,需要的内存远远地超出了栈的大小,就会出现StackOverflowError异常,可以通过-Xss设置栈的大小
- 调整栈大小,就能保证不出现溢出吗?
- 分配的栈内存越大越好吗?
不,因为内存是固定的,增大某个栈的大小,会导致其它线程的栈所分配的内存变小。
- 垃圾回收是否会涉及到虚拟机栈?
不会
- 方法中定义的局部变量是否线程安全?
何为线程安全:
3、本地方法栈
- Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
- 本地方法栈,也是线程私有的。
- 允许被实现成固定或者是可动态扩展的内存大小。( 在内存溢出方面是相同的)
➢如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟将会抛出一-个stackOverflowError 异常。
➢如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出-一个outofMemoryError 异常。 - 本地方法是使用C语言实现的。
- 它的具体做法是Native Method Stack中 登记native方法,在Execution Engine 执行时加载本地方法库。
- 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虛拟机拥有同样的权限。
➢本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
➢它甚至可以直接使用本地处理器中的寄存器
➢直接从本地内存的堆中分配任意数量的内存。 - 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要只本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
- 在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一
4、堆
1、堆的核心概述
- 对于一个进程来说,堆和方法区是唯一的;一个进程对应一个JVM实例,JVM实例中有一个运行时数据区,运行时数据区里有独立的一个堆和方法区。每个进程又包含很多线程,每个线程都有自己的程序计数器、本地方法栈、虚拟机栈,每个线程共享运行时数据区里独立的一个堆和方法区。
- 一个JVM实例(线程)只存在一个堆内存,堆也是Java内存管理的核心区域。
- Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
➢堆内存的大小是可以调节的。 - 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
- 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(ThreadLocal Al location Buffer, TLAB) 。
- 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area fromwhich memory for all class instances and arrays is allocated )
➢我要说的是:“几乎”所有的对象实例都在这里分配内存。一从实际
使用角度看的。 - 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
- 堆,是GC ( Garbage Collection, 垃圾收集器)执行垃圾回收的重点区域
内存细分
堆内存可视化工具:
jvisualvm
, 在jdk目录下有,或者cmd运行
2、设置内存大小与OOM
- 1、设置堆空间大小的参数
-xms用来设置堆空间(年轻代+老年代)的初始内存大小,等价于-XX: InitialHeapSize
-X是jvm的运行参数
ms是memory start
-Xmx用来设置堆空间(年轻代+老年代)的最大内存大小,等价于-XX :MaxHeapSize - 2、默认堆空间的大小
初始内存大小:物理电脑内存大小/ 64
最大内存大小:物理电脑内存大小/ 4 - 3、手动设置: - xms600m -Xmx600m
开发中建议将初始堆内存和最大的堆内存设置成相同的值。 - 4、查看设置的参数:方式一:jps / jstat -gc 进程id
方式二: -XX: +PrintGCDetails
/**
* OOM异常演示
*/
public class OOMTest {
public static void main(String[] args) {
ArrayList<Picture> list = new ArrayList<>();
while(true){
while(true){
try {
Thread.sleep(20);
}catch (InterruptedException e){
e.printstacktrace();
}
list.add(new Picture(new Random().nextInt(1024*1024)));
}
}
}
}
class Picture{
private byte[] pixels;
public Picture(int length){
this.pixels = new byte[length];
}
}
3、年轻代与老年代
- 存储在JVM中的Java对象可以被划分为两类:
➢一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
➢另外一-类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持-致。 - Java堆区进- -步细分的话,可以划分为年轻代(YoungGen)和老年代(OldGen)
- 其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区)。
- 在HotSpot中,Eden空间 和另外两个Survivor空间缺省所占的比例是8:1:1(实际并不是,默认开了自适应内存分配策略)
- 当然开发人员可以通过选项“-XX:SurvivorRatio"调整这个空间比例。比如-XX: SurvivorRatio=8
- 几乎所有的Java对象都是在Eden区被new出来的。
- 绝大部分的Java对象的销毁都在新生代进行了。
➢IBM公司的专门研究表明,新生代中80号的对象都是“朝生夕死”的。 - 可以使用选项"-Xmn"设置新生代最大内存大小
➢这个参数-般使用默认值就可以了。
下面参数实际开发一般不会调用
4、图解对象分配过程
为新对象分配内存是一件非 常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。
- new的对象先放伊甸园区。此区有大小限制。
- 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC), 将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
- 然后将伊甸园中的剩余对象移动到幸存者0区。
- 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
- 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
- 啥时候能去养老区呢?可以设置次数。默认是15次。
可以设置参数: -XX:MaxTenuringThreshold=进行设置。 - 在养老区,相对悠闲。当养老区内存不足时,再次触发GC: Major GC, 进行养老区的内存清理。
- 若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生00M异常
针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to.
关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。
复杂情况
可视化理解
常用调优工具
- JDK命令行
- Ecl ipse : Memory Analyzer Tool
- Jconsole|
- Vi sua lVM
- jprofiler
- Java Flight Recorder
- GCViewer
- GC Easy
5、Minor GC、Major GC、Full GC
JVM在进行GC时,并非每次都对上面三个内存(新生代、老年代;元空间)区域一起回收
的,大部分时候回收的都是指新生代。
针对HotSpotVM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集
(Partial GC),一种是整堆收集(Full GC)
- 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
➢新生代收集(Minor GC / Young GC) ;只是新生代(Eden\S0,S1)的垃圾收集
➢老年代收集(Major GC / 0ld GC);只是老年代的垃圾收集。
★ 目前,只有CMS GC会有单独收集老年代的行为。
★ 注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
➢混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
★目前,只有G1 GC会有这种行为 - 整堆收集(Fu11 GC):收集整个java堆和方法区的垃圾收集。
- 年轻代GC(Minor GC) 触发机制:
➢当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次 Minor GC会清理年轻代的内存。)
➢因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频,繁,一般回收速度也比较快。这一定义既清晰又易于理解。
➢Minor GC会引发STW, 暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。 - 老年代GC (Major GC/Full GC) 触发机制:
➢指发生在老年代的GC, 对象从老年代消失时,我们说“Major GC”或“Fu11 GC”发生了。
➢出现了Major GC,经常会伴随至少一次的Minor GC (但非绝对的,在ParallelScavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程)。v也就是在老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发Major GC
➢Major GC的速度- -般会比Minor GC慢10倍以上,STW的时间更长。
➢如果Major GC后,内存还不足,就报00M了。 - Full GC触发机制:
(后面细讲)
触发Full GC执行的情况有如下五种: - 调用System.gc()时,系统建议执行Fu1l GC, 但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、survivor spacee (From Space) 区向survivor space1 (ToSpace)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
说明:full gc是开发或调优中尽量要避免的。这样暂时时间会短一些 。
6、堆空间分代思想
- 经研究,不同对象的生命周期不同。709-998的对象是临时对象。.
➢新生代:有Eden、两块大小相同的Survivor (又称为from/to, s0/s1)构成,to总为空。
➢老年代:存放新生代中经历多次GC仍然存活的对象。 - 其实不分代完全可以,分代的唯- -理由就是优化GC性能。如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
7、内存分配策略
针对不同年龄段的对象分配原则如下所示:
- 优先分配到Eden
- 大对象直接分配到老年代
➢尽量避免程序中出现过多的大对象 - 长期存活的对象分配到老年代
- 动态对象年龄判断
➢如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。 - 空间分配担保
-XX : HandlePromotionFailure
8、为对象分配内存:TLAB
什么是TLAB ?
- 从内存模型而不是垃圾收集的角度,对Eden区 域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
- 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
TLAB的再说明
- 据我所知所有OpenJDK衍生出来的JVM都提供了TLAB的设计。
- 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。
- 在程序中,开发人员可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间。
- 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1号,当然我们可以通过选项“-XX: TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。
- 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
小结堆空间的参数设置
- -XX: +PrintFlagsInitial :查看所有的参数的默认初始值
- -XX:+PrintFlagsFinal : 查看所有的参数的最终值(可能会存在修改,不再是初始值)
具体查看某个参数的指令: jps: 查看当前运行中的进程
jinfo -flag SurvivorRatio进程id - -xms:初始堆空间内存(默认 为物理内存的1/64 )
- -Xmx:最大堆空间内存(默认为物理内存的1/4)
- -Xmn:设置新生代的大小。(初始值及最大值)
- -XX:NewRatio:配置新生代与老年代在堆结构的占比
- -XX:SurvivorRatio:设置新生代中Eden和Se/S1空间的比例
- -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
- -XX: +PrintGCDetails:输出详细的GC处理日志
打印gc简要信息:③-Xx: +PrintGC② -verbose:gc - -XX:HandlePromotionFailure:是否设置空间分配担保
空间分配担保问题
在发生Minor GC之前,
虚拟机会检查老年代最大可用的连续空间是否大于新生代所有 对象的总空间。
如果大于,则此次Minor GC是安全的
如果小于,则虚拟机会查看-XX: HandlePromotionFailure设置值是否允许担保失败。
➢如果HandlePromotionFailure=true, 那么会继续检查老年代最大可
用连续空间是否大于历次晋升到老年代的对象的平均大小。
如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;
如果小于,则改为进行一次Full GC。
如果HandlePromotionFailure=false, 则改为进行一次Full GC。
在JDK6 Update24之后(JDK7),HandlePromotionFailure参数不会再影响到虛拟机的空间分配担保策略,观察OpenJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC, 否则将进行Full GC。
9、堆是分配对象的唯一选择吗?
- 在《深入理解Java虚拟机》中关于Java堆内存有这样一- 段描述:随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
- 在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis) 后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
- 此外,前面提到的基于OpenJDK深度定制的TaoBaoVM,其中创新的GCIH (GCinvisible heap) 技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。
逃逸分析概述
- 如何快速的判断是否发生了逃逸分析,大家就看new的对象实体是否有可能在方法外被调用。
- 如何将堆上的对象分配到栈,需要使用逃逸分析手段。
- 这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
- 通过逃逸分析,Java Hotspot编 译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
- 逃逸分析的基本行为就是分析对象动态作用域:
➢当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
➢当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。 - 没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除。
参数设置
在JDK 6u23版本之后,HotSpot中默认就已经开启了逃逸分析。
如果使用的是较早的版本,开发人员则可以通过:
➢选项“-XX:+DoEscapeAnalysis"显式开启逃逸分析
➢通过选项“-XX: +PrintEscapeAnalysis"查看逃逸分析的筛选结果。
结论:
开发中能使用局部变量的,就不要使用在方法外定义。
逃逸分析:代码优化
使用逃逸分析,编译器可以对代码做如下优化:
一、栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
二、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
三、分离对象或标量替换。有的对象可能不需要作为一一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在cpu寄存器中。
参数-XX: +E1 iminateallocations:开启了标量替换(默认打开),允许将对象打散分配在栈上。
10、本章小结
- 年轻代是对象的诞生、成长、消亡的区域,-一个对象在这里产生、应用,最后被垃圾回收器收集、结束生命。
- 老年代放置长生命周期的对象,通常都是从Survivor区域筛选拷贝过来的Java对象。当然,也有特殊情况,我们知道普通的对象会被分配在TLAB_上;如果对象较大,JVM会 试图直接分配在Eden其他位置上;如果对象太大, 完全无法在新生代找到足够长的连续空闲空间,JVM就会直接分配到老年代。
- 当GC只发生在年轻代中,回收年轻代对象的行为被称为MinorGC。当GC发生在老年代时则被称为MajorGC或者FullGC。 一般的,MinorGC 的发生频率要比MajorGC高很多,即老年代中垃圾回收发生的频率将大大低于年轻代。
5、方法区
1、栈、堆、方法区的交互关系
2、方法区的理解
- 方法区(Method Area)与Java堆 - -样,是各个线程共享的内存区域。
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Jav堆区一样都可以是不连续的。
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误: java. lang . outofMemoryError:PermGen space 或者j ava. lang. OutO fMemoryError: Metaspace
- 关闭JVM就会释放这个区域的内存。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久.代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。
方法区在哪里
《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap (非堆),目的就是要和堆分开。所以,方法区看作是-块独立于Java堆的内存空间。
3、设置方法区的大小与OOM
- 方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。
- jdk7及以前:
➢通过-XX:PermSize来设置永久代初始分配空间。默认值是20.75M
➢-XX:MaxPermSize来 设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
➢当JVM加载的类信 息容量超过了这个值,会报异常OutOfMemoryErrorPermGenspace。 - jdk8及以后:
➢元数据区大小可以使用参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定,替代上述原有的两个参数。
➢默认值依赖于平台。windows’下,-XX:MetaspaceSize是21M, -XX :MaxMetaspaceSize的值是-1,即没有限制。
➢与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可系统内存。如果元数据区发生溢出,虚拟机一样会拋出异OutOfMemoryError: Metaspace
➢一XX : Me taspaceSize:
设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的-XX :MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
➢如果初始化的高水位线设置过低,上 述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一一个相对较高的值。
如何解决OOM
1、要解决00M异常或heap space的异常, 一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer) 对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(MemoryLeak)还是内存溢出(Memory Overf1ow)。
2、如果是内存泄漏,可进一-步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置。
3、如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-xms) ,与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
4、方法区的内部结构
方法区储存什么
《深入理解Java虚拟机》书中对方法区(Method
Area)存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
类型信息
对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于interface或是java . lang .object,都没有父类)
- 这个类型的修饰符(public, abstract, final的某个子集)
- 这个类型直接接口的一个有序列表
域信息
- JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
- 域的相关信息包括:域名称、域类型、域修饰符(public, private,protected, static, final, volatile, transient的某个子集)
方法信息
- 方法名称
- 方法的返回类型(或void)
- 方法参数的数量和类型(按顺序)
- 方法的修饰符(public, private, protected, static, final, synchronized, native, abstract的- 一个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract和native,方法除外)
- 异常表( abstract和native方法除外)
➢每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引non-final的类变量
运行时常量池
- 运行时常量池( Runtime Constant Pool) 是方法区的一部分。
- 常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。.
- 运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
- JVM为每个已加载的类型(类或接口)都维护-一个常量池。池中的数据项像数组项一样,是通过索引访问的。
- 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到.运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
➢运行时常量池,相对于Class文件常量池的另一-重要特征是:具备动态性。- 运行时常量池类似于传统编程语言中的符号表(symbol table) ,但是它所包含的数据却比符号表要更加丰富一些。
常量池
- 方法区,内部包含了运行时常量池。
- 字节码文件,内部包含了常量池。
- 要弄清楚方法区,需要理解清楚ClassFile,因为加载类的信息都在方法区。
- 要弄清楚方法区的运行时常量池,需要理解清楚ClassFile中的常量池。
- 几种在常量池内存储的数据类型包括:
数量值、字符串值、类引用、字段引用、方法引用
小结:常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型为什么要用常量池
一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字 节码需要数据
支持,通常这种数据会很大以至于不能直接存到字节码里,换另- -种方式,可以存到常量池,
这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池,之前有介绍。
5、方法区使用举例
6、方法区的演进细节
Hotspot中方法区的变化:
永久代为什么会被元空间替换?
- 随着Java8的到来,HotSpot VM中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间( Metaspace )。
- 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。
- 这项改动是很有必要的,原因有:
1、为永久代设置空间大小是很难确定的。在某些场景下,如果动态加载类过多,容易产生Perm区的00M。比如某个实际web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
2、 对永久代进行调优是很困难的。
StringTable为什么要调整
jdk7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才会触发。而full gc是老年代的空间不足、永久代不足时才会触发。这就导致StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
7、方法区的垃圾回收
有些人认为方法区(如HotSpot虛拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java 虛拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期的ZGc收集器就不支持类卸载)。
一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前Sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
- 先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用。字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:
➢1、类和接口的全限定名
➢2、字段的名称和描述符
➢3、方法的名称和描述符 - HotSpot虛拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。
- 回收废弃常量与回收Java堆中的对象非常类似。
- 判定一个常量是否“废弃”还是相对简单,而要判定-一个类型是否属于“不再被使用
的类”的条件就比较苛刻了。需要同时满足下面三个条件:
➢该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
➢加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如Osgi、JSP的重加载等,否则通常是很难达成的。
➢该类对应的java. lang. Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 - Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象- -样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose: class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查 看类加载和卸载信息
- 在大量使用反射、动态代理、cglib等字节码框架,动态生成JSP以及Osgi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
8、总结
9、常见面试题
- 百度
三面:说一下JVM内存模型吧,有哪些区?分别干什么的?- 蚂蚁金服:
Java8的内存分代改进 JVM内存分哪几个区,每个区的作用是什么?
一面::JVM内存分布/内存结构?栈和堆的区别?堆的结构?为什么两个survivor区?
二面:Eden和Survior的比例分配- 小米:
jvm内存分区,为什么要有新生代和老年代- 字节跳动:
二面: Java的内存分区
二面:讲讲jvm运行时数据库区 什么时候对象会进入老年代?- 京东:
JVM的内存结构,Eden和Survivor比例。
JVM内存为什么要分成新生代,老年代,持久代。
新生代中为什么要分为Eden和Survivor。- 天猫:
一面: Jvm内存模型以及分区,需要详细到每个区放什么。
一面: JVM的内存模型,Java8做 了什么修改- 拼多多:
JVM内存分哪几个区,每个区的作用是什么?- 美团:
java内存分配
jvm的永久代中会发生垃圾回收吗?
一面: jvm内存分区,为什么要有新生代和老年代?
对象的实例化内存布局与访问定位(栈、堆、方法区怎么粘合到一起的)
1、对象的实例化
测试对象实例化的过程
①加载类元信息;②为对象分配内存;③处理并发问题;④属性的默认初始化(零值初始化)
⑤设置对象头的信息;⑥属性的显式初始化、代码块中初始化、构造器中初始化
给对象的属性赋值的操作:
①属性的默认初始化;②显式初始化;③代码块中初始化;④构造器中初始化
2、对象的内存布局
大场面试题
美团:
对象在JVM中是怎么存储的?
对象头信息里面有哪些东西?
蚂蚁金服:
二面: java对象头里有什么
3、对象的访问定位
直接内存
- 不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
- 直接内存是在Java堆外的、直接向系统申请的内存区间。
- 来源于NIO,通过存在堆中的Di rectByteBuf fe r操作Native内存
- 通常,访问直接内存的速度会优于Java堆。即读写性能高。
➢因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。
➢Java的NIO库允许Java程序使用直接内存,用于数据缓冲区 - 也可能导致OutOfMemoryError异常
- 由于直接内存在Java堆外,因此它的大小不会直接受限于- Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
- 缺点
➢分配回收成本较高
➢不受JVM内存回收管理 - 直接内存大小可以通过MaxDi rectMemorySize设置
- 如果不指定,默认与堆的最大值-Xmx参数值一致
/**
* 查看直接内存的占用与释放
*/
public class BufferTest {
private static final int BUFFER = 1024 * 1024 * 1024;//1GB
public static void main(String[] args){
//直接分配本地内存空间
ByteBuffer byteBuffer = ByteBuffer . allocateDirect(BUFFER);
System.out.println("直接内存分配完毕,请求指示! ");
Scanner scanner = new Scanner(System. in);
scanner.next( );
System.out . println("直接内存开始释放! ");
byteBuffer = nu1l;
System.gc();
scanner.next() ;
}
}
本地方法接口
什么是本地方法
- 简单地讲,一个Native Method就是-一个Java调用非Java代码的接口。一个Native Method是这样一个Java方法: 该方法的实现由非Java语言实现,比如C。这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如C++中,你可以用extern "C"告知C++编译器去调用-一个C的函数。
- “A native method is a Java method whose implementation is
provided by non-java code.” - 在定义一一个native method时,并不提供实现体(有些像定义一个Javainterface),因为其实现体是由非java语言在外面实现的。
- 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。
- java中被native修饰的方法就是本地方法
标识符native可以与所有其它的java标识符连用,但是abstract除外。
为什么要使用Native Method
- 与Java环境外交互:
有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。你可以想想Java需要与–些底层系统,如操作系统或某些硬件交换信息时的情况。本地方法正是这样-种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。 - 与操作系统交互:
JVM支持着Java语言本身和运行时库,它是Java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用Java实现了jre的与底层系统的交互,甚JVM的一些部分就是用C写的。还有,如果我们要使用一些Java语言本身没有提封装的操作系统的特性时,我们也需要使用本地方法。 - Sun’ s Java
Sun的解释器是用C实现的,这使得它能像- -些普通的C一样与外部交互。jre大部分是用Java实现的,它也通过一些本地方法与外界交互。例如:类java.lang. Thread的setPriority()方法是用Java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用win32 SetPriority() API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用。
现状
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等。
执行引擎
1、执行引擎概述
- 执行引擎是Java虚拟机核心的组成部分之:一。
- “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
- JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之_上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。
- 那么,如果想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。
- 1、执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。
- 2、每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。
- 3、当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。
- 4、从外观上来看,所有的Java虛拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果。
2、java代码编译和执行过程
问题:什么是解释器( Interpreter),什么是JIT编译器?
- 解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
- JIT (Just In Time Compiler) 编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。
问题:为什么说Java是半编译半解释型语言?
- JDK1.0时代,将Java语 言定位为“解释执行”还是比较准确的。再后来,Java也发展出可以直接生成本地代码的编译器。
- 现在JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。
3、机器码、指令、汇编语言
1、机器码
- 各种用二进制编码方式表示的指令,叫做机器指令码。开始,人们就用它采编写程序,这就是机器语言。
- 机器语言虽然能够被计算机理解和接受,但和人们的语言差别太大,不易被人们理解和记忆,并且用它编程容易出差错。
- 用它编写的程序- -经输入计算机,cpu直接读取运行,因此和其他语言编的程序相比,执行速度最快。
- 机器指令与cpu紧密相关,所以不同种类的cpu所对应的机器指令也就不同。
2、指令
- 由于机器码是有0和1组成的二进制序列,可读性实在太差,于是人们发明了指令。
- 指令就是把机器码中特定的0和1序列,简化成对应的指令(-般为英文简写,如mov,
inc等),可读性稍好 - 由于不同的硬件平台,执行同一个操作,对应的机器码可能不同,所以不同的硬件平
台的同一种指令(比如mov),对应的机器码也可能不同。
3、指令集
4、汇编语言
- 由于指令的可读性还是太差,于是人们又发明了汇编语言。
- 在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号(Label)代替指令或操作数的地址。
- 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。
➢由于计算机只认识指令码,所以用汇编语言编写的程序还必须翻译成机器指令码,计算机才能识别和执行。
5、高级语言
- 为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言。高级语言比机器语言、汇编语言更接近人的语言
- 当计算机执行高级语言编写的程序时,仍然需要把程序解释和编译成机器的指令码。完成这个过程的程序就叫做解释程序或编译程序。
6、字节码
- 字节码是-种中间状态(中间码)的二进制代码(文件),它比机器码更抽象,需要直译器转译后才能成为机器码
- 字节码主要为了实现特定软件运行和软件环境、与硬件环境无关。
- 字节码的实现方式是通过编译器和虚拟机器。编译器将源码编译成字节码,特定平台,上的虚拟机器将字节码转译为可以直接执行的指令。
➢字节码的典型应用为Java bytecode。
4、解释器
- JVM设计者们的初衷仅仅只是单纯地为了满足Java程序实现跨平台特性,因此避免采用静态编译的方式直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法。
- 解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
- 当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。
解析器分类
- 在Java的发展历史里,一共有两套解释执行器,即古老的字节码解释器、现在普
遍使用的模板解释器。 - 字节码解释器在执行时通过纯软件代码模拟字节码的执行,效率非常低下。
- 而模板解释器将每一条字节码和一个模板函数相关联,模板函数中直接产生这
条字节码执行时的机器码,从而很大程度上提高了解释器的性能。
现状
- 由于解释器在设计和实现上非常简单,因此除了Java语言之外,还有许多高级语言同样也是基于解释器执行的,比如Python、Perl、Ruby等。但是在今天,基于解释器执行已经沦落为低效的代名词,并且时常被一-些C/C+ +程序员所调侃。
- 为了解决这个问题,JVM平台支持一种叫作即时编译的技术。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。
- 不过无论如何,基于解释器的执行模式仍然为中间语言的发展做出了不可磨灭的贡献。
5、JIT编译器
概念解释:
- Java语言的“编译期” 其实是一段“不确定”的操作过程,因为它可能是指一个前端编译器(其实叫“ 编译器的前端”更准确一 些)把. java文件转变成.class文件的过程;
- 也可能是指虚拟机的后端运行期编译器(JIT编译器,Just In Time Compiler) (把字节码转变成机器码的过程。
- 还可能是指使用静态提前编译器(AOT 编译器,Ahead Of Time Compiler)直接把. java文件编译成本地机器代码的过程。
- 前端编译器: Sun的Javac、 Eclipse JDT中的增量式编译器(ECJ)
- JIT编译器: HotSpot VM的C1、 C2编译器
- AOT编译器: GNU Compiler for the Java(GCJ) 、Excelsior JET。
热点代码及探测方式
- 当然是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。关于那些需要被编译为本地代码的字节码,也被称之为“热点代码”,JIT编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能。
- 一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为OSR (On StackReplacement)编译。
- ==一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准?==必然需要一个明确的阈值,JIT编译器才会将这些“热点代码”编译为本地机器指令执行。这里主要依靠热点探测功能。
- 目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。
- 采用基于计数器的热点探测,HotSpot VM将会为每-一个方 法都建立2个不同类型的计数器,分别为方法调用计数器( Invocation Counter) 和回边计数器(BackEdge Counter) 。
➢方法调用计数器用于统计方法的调用次数
➢回边计数器则用于统计循环体执行的循环次数
方法调用计数器
- 这个计数器就用于统计方法被调用的次数,它的默认阈值在Client模式下是1500次,在Server 模式下是10000 次。超过这个阈值,就会触发JIT编译。
- 这个阈值可以通过虚拟机参数- -xX :Compi leThreshold来人为设定。
- 当一个方法被调用时,会先检查该方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值。如果已超过阈值,那么将会向即时编译器提交-一个该方法的代码编译请求。
热度衰减
- 如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay) ,而这段时间就称为此方法统计的半衰周期(Counter Half Life Time )
- 进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数-xX: -UseCounterDecay来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。
- 另外,可以使用-xx: CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。
回边计数器
HotSpot设置程序执行方式
- 缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。如下所示:,
➢-Xint: 完全采用解释器模式执行程序;
➢-Xcomp: 完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行。
➢-Xmixed:采用解释器+即时编译器的混合模式共同执行程序。
HotSpot中JTI分类
在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和ServerCompiler,但大多数情况下我们简称为C1编译器和C2编译器。开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:
- -client: 指定Java虚拟机运行在Client模式下,并使用C1编译器;
➢C1 编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度。 - -server:指定Java虚拟机运行在Server模式下,并使用C2编译器。
➢C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高。
C1和C2编译器不同的优化策略:
- 在不同的编译器上有不同的优化策略,C1编译器上主要有方法内联,去虚拟化、冗余消除。
➢方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程
➢去虚拟化:对唯- -的实现类进行内联
➢冗余消除:在运行期间把一 些不会执行的代码折叠掉 - C2的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在C2上有如下几种优化:
➢标量替换:用标量值代替聚合对象的属性值
➢栈上分配:对于未逃逸的对象分配对象在栈而不是堆
➢同步消除:清除同步操作,通常指synchronized
总结
- 一般来讲,JIT编译出来的机器码性能比解释器高。
- C2编译器启动时长比C1编译器慢,系统稳定执行以后,C2编译器执行速度远远快于C1编译器。
java代码执行分类
- 第一种是将源代码编译成字节码文件,然后在运行时通过解释器将字节码文件转为机器码执行
- 第二种是编译执行(直接编译成机器码)。现代虚拟机为了提高执行效率,会使用即时编译技术(JIT,Just In Time) 将方法编译成机器码后再执行
- HotSpotVM是目前市面上高性能虚拟机的代表作之一。它采用解释器与即时编译器并存的架构。在Java虚拟机运行时,解释器和即时编译器能够相互协作,各自取长补短,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。
- 在今天,Java程序的运行性能早已脱胎换骨,已经达到了可以和C/C++程序一较高下的地步。
为什么还要用解释器呢?
- 有些开发人员会感觉到诧异,既然HotSpot VM中已经内置JIT编译器了,那么为什么还需要再使用解释器来“拖累”程序的执行性能呢?比如JRockit VM内 部就不包含解释器,字节码全部都依靠即时编译器编译后执行。
- 首先明确:
当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间。但编译为本地代码后,执行效率高。 - 所以:
尽管JRockit VM中程序的执行性能会非常高效,但程序在启动时必然需要花费更长的时间来进行编译。对于服务端应用来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一-个平衡点。在此模式下,当Java虛拟器启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。 - 同时,解释执行在编译器进行激进优化不成立的时候,作为编译器的“逃生门”。
StringTable
1、String的基本特性
- String:字符串,使用-一对""引起来表示。
➢String s1 = “atguigu”;//字面量的定 义方式
➢String s2 = new String (“hello”) ; - String声明为final的,不可被继承
- String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示String可以比较大小
- String在jdk8及以前内部定义了final char[] value用于存储字符串数据。jdk9时改为byte []
- String:代表不可变的字符序列。简称:不可变性。
➢当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
➢当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
➢当调用String的replace ()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。 - 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
- 字符串常量池中是不会存储相同内容的字符串的。
➢使用-XX: StringTableSi ze可设置StringTable的长度
➢String的String Pool是 一个固定大小的Hashtable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突,导致链表很长影响调用String.intern时性能下降
➢Jdk8开始, 设置StringTable的长度的话,1009是可设置的最小值。
2、String的内存分配
- 在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
- 常量池就类似–个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种。
➢直接使用双引号声明出来的String对象会直接存储在常量池中。
比如: String info = “atguigu. com”;
➢如果不是用双引号声明的String对象,可以使用String提供的
intern()方法。这个后面重点谈 - Java 6及以前,字符串常量池存放在永久代。
- Java 7中Oracle 的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内。
➢所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
➢字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用String. intern ()|。 - Java8元空间,字符串常量在堆
3、String的基本操作
Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须是指向同一个String 类实例。
应用场景举例
4、字符串拼接操作
- 1.常量与常量的拼接结果在常量池,原理是编译期优化
- 2.常量池中不会存在相同内容的常量。
- 3.只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
- 4.如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
代码理解
public static void main(String[] args) {
String s1 = "a"+"b"+"c"; //编译器优化:等同于“abc”
String s2 = "abc"; //一定是存放在字符串常量中,将此值赋值给S2
/**
* 最终.java编译成.class,再执行.class
* String s1 = "abc" 编译器优化
* String s2 = "abc"
*/
System.out.println(s1 == s2); //ture
System.out.println(s1.equals(s2)); //ture
}
拼接中带有变量
public static void main(String[] args) {
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE"+"hadoop";
//如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体的内容拼接结果为:javaEEhadoop
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4); //ture
System.out.println(s3 == s5); //false
System.out.println(s3 == s6); //false
System.out.println(s3 == s7); //false
System.out.println(s5 == s6); //false
System.out.println(s5 == s7); //false
System.out.println(s6 == s7); //false
//intern():判断字符串常量池中是否存在javaEEhadoop值, 如果存在,则返回常量池中javaEEhadoop的地址;
//如果字符串常量池中不存在javaEEhadoop,则在常量池中加载一-份javaEEhadoop, 并返回次对象的地址。
String s8 = s6.intern();
System.out.println(s3 == s8); //ture
}
详细解析
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
/**
* 如下的s1 + s2 的执行细节:(s 是随便定义的)
* ① StringBuilder s = new StringBuilder();
* ② s.append("a")
* ③ s.append("b")
* ④ s.toString() -->相当于 new String("ab")
*
补充:在jdk5. 0之后使用的是stringBuilder,在jdk5. 0之前使用的是StringBuffer
*/
String s4 = s1 + s2;
System.out.println(s3 == s4); //false
}
注意:
public static void main(String[] args) {
/**
* 1、字符串拼接操作不一定使用的是StringBuilder
* 如果拼接符号左右两边都是字符串常量或常量引用,则依然使用编译器优化,既非StringBuilder方式
* 2、针对于final修饰类、方法、基本数据类型、引用数据类型的量结构时,能使用上final尽量使用final
*/
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4); //ture
}
拼接操作和append操作的效率对比
s = s + "a";
s = s.append("a");
体会执行效率:通过stringBuilder 的append()的方式添加字符串的效率要远高于使用string的字符串拼接方式!
详情:
- ①stringBuilder的append()的方式:自始至终 中只创建过一个stringBuilder的对象使用string的字符串拼接方式:创建过多个StringBuilder和String的对象
- ②使用string的字符审拼接方式:内存中由于创建了较多的stringBuilder和string的对象, 内存占用更大;如果进行GC,需要花费额外的时间
- 改进空间:在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值highlevel的情况下,建议使用构造器实例化:StringBuilder s = new stringBuilder(highLevel);//new char[highLevel]
5、intern()的使用
- 如果不是用双引号声明的String对象,可以使用string提供的intern方法: intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
- 比如: String myInfo = new String(“I love atguigu”) . intern() ;
- 也就是说,如果在任意字符串上调用String. intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下 列表达式的值必定是true:
(“a” + “b” + “c”) . intern() == “abc” - 通俗点讲,Interned String就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)
如何保证变量s指向的是字符串常量池中的值
- 方式一:
String s = “shkstart”;//字面量定义的方式 - 方式二:调用intern()
String s = new string( “shkstart”). intern();
String s = new StringBuilder( “shks tart”). tostring(). intern();
面试题
1、new String(“ab”)会创建几个对象
两个,看字节码
一个对象是: new关键字在堆空间创建的
另一个对象是:字符串常量池中的对象。字 节码指令: ldc
2、new String(“a”) + new String(“b”)呢
对象1:new StringBuilder()
对象2:new String(“a”)
对象3:常量池中的"a"
对象4:new String(“b”)
对象5:常量池中的"b"
深入剖析:StringBuilder 的 toString():
对象6:new String(“ab”)
强调:toString()的调用,在字符串常量池中,没有生成"ab"
public static void main(String[] args) {
String s = new String("1");
s.intern(); //调用此方法之前,字符串常量池中已经存在了"1"
String s2 = "1";
System.out.println(s == s2); //jdk6:false jdk7/8:false
String s3 = new String("1") + new String("1");//s3 变量记录的地址为:new String("11")
//执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
s3.intern(); //在字符串常量池中生成"11" 如何理解:jdk6:创建了一个新的对象"11",也就有新的地址。
// jdk7/8:此时常量池中并没有创建"11",而是创建一个指向堆空间的new String("11")的地址
String s4 = "11"; //s4.变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11 "的地址
System.out.println(s3 == s4); //jdk6:false jdk7/8:ture
}
上题扩展:
public static void main(String[] args) {
//StringIntern.java中练习的扩展
String s3 = new String("1") + new String("1");
//执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
String s4 = "11";
String s5 =s3.intern();
System.out.println(s3 == s4); //false
System.out.println(s5 == s4); //ture
}
总结String中intern()的使用:
- jdk1.6中,将这个字符串对象尝试放入串池。
➢如果串池中有,则并不会放入。返回已有的串池中的对象的地址
➢如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址 - Jdk1.7起,将这个字符串对象尝试放入串池。
➢如果串池中有,则并不会放入。返回已有的串池中的对象的地址
➢如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址
6、StringTable的垃圾回收
7、G1中的String去重操作
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。