0
点赞
收藏
分享

微信扫一扫

JVM内存结构和垃圾回收

开源GIS定制化开发方案 2022-04-08 阅读 99
java

JVM

整体结构

在这里插入图片描述

在这里插入图片描述

java代码执行流程

在这里插入图片描述

存在二次编译

第一次是将java源码编译成字节码(非常严格的编译标准)

第二次是将字节码文件通过解释和JIT编译成机器指令 -->执行引擎的重要性就在此

类加载子系统

在这里插入图片描述

类加载子系统负责加载class文件,class文件在文件开头有特定的文件标识

ClassLoader只负责class的加载,能否运行由ExecutionEngine(执行引擎)决定

加载的类信息存放在方法区(元空间)

加载阶段

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

(2)将这个字节流代表的静态存储结构转化为方法区(元空间)的运行时数据结构

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

链接阶段

验证

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

主要于(1)文件格式验证(2)元数据验证(3)字节码验证(4)符号引用验证

准备

类变量(不包含final修饰的static字段)分配内存并设置该类变量的默认初始值,即零值

private static int a =1
在准备阶段,a=0
在初始化阶段,a=1

解析

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

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等

初始化阶段

初始化就是执行类构造器方法()的过程

此方法不需定义,是javac编译器自动收集类中静态类变量的赋值动作和静态代码块中的语句合并而来

若该类存在父类,JVM会保证子类的()执行前先执行父类的()

虚拟机必须保证一个类的()方法在多线程下被同步加锁(即类只被初始化一次,将类信息会保存在方法区(直接内存)做缓存)

类加载器

在这里插入图片描述

在这里插入图片描述

(1)引导类加载器(Bootstrap Class Loader)

使用C/C++语言实现的,嵌套在JVM内部

用来加载Java的核心类库,用于提供JVM自身需要的类

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

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

(2)扩展类加载器(Extension Class Loadr)

Java语言编写,派生于ClassLoader类

从java.ext.dirs系统属性指定目录中加载类库,或从JDK安装目录下jre/lib/ext子目录(扩展目录)下加载类库

如果用户创建的jar放在此目录下,也会自动由扩展类加载器加载

(3)系统类加载器(System Class Loadr)

Java语言编写,派生于ClassLoader类

负责加载环境变量classpath或系统属性java.class.path指定下的类库

该类加载是程序中默认的类加载器

(4)自定义加载器(User Defined Class Loader)

双亲委派机制

在这里插入图片描述

(1)一个类加载器收到类加载请求时,会先将这个请求委托给父类加载器去执行

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

(3)如果父类加载器可以完成加载,就成功返回。否则由子类加载器尝试加载

双亲委派机制避免类的重复加载,保护程序安全,防止核心API被随意篡改

其他

JVM中判断两个class对象是否为同一个对象的必要条件:

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

(2)加载这个类的ClassLoader必须相同

JVM必须知道一个类型是由启动加载器还是由用户加载器加载的

Java程序对类的使用方式:主动使用、被动使用

主动使用情况:

(1)创建类的实例

(2)访问某个类或接口的静态变量或对该静态变量赋值

(3)调用某个类的静态方法

(4)反射使用对象(Class.forName(“com.travelsky.bean”))

(5)初始化一个类的子类

(6)JVM启动时被表明为启动类的类

(7)JDK7提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果

除开以上情况,都看做是对类的被动使用,都不会导致类的初始化

运行时数据区

在这里插入图片描述

红色区域是进程(多个线程)共享的

灰色区域是每个线程私有的

例如:一个进程有5个线程 则存在5个程序计数器和栈 5个线程共用一个方法区和堆

内存是硬盘与CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时进行

在这里插入图片描述

每个JVM只有一个Runtime实例,即为运行时环境

线程

线程是一个程序里运行单元,JVM允许一个应用有多个线程并行执行

JVM里每个线程都与操作系统的本地线程直接映射。当一个Java线程准备好执行后,此时操作系统一个本地线程也同时创建,初始化成功后会调用Java线程中的run()方法。当Java线程执行终止后,本地线程也会回收

程序计数器(PC寄存器)

JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟

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

在这里插入图片描述

是一块很小的内存空间,也是运行速度最快的存储区域

每个线程都有它自己的程序计数器,是线程私有的,声明周期与线程一致

任何时间一个线程只有一个方法在执行(当前方法),程序计数器会存储当前线程正在执行的Java方法的JVM指令地址

PC寄存器是JVM中唯一一个不会发生OOM情况的区域

在字节码文件中:

在这里插入图片描述

使用PC寄存器存储当前线程执行地址的原因:JVM的字节码解释器需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令

PC寄存器设定为线程私有原因:线程执行时需要抢占时间片,线程间是并发执行的,为了保证每个线程正在执行的当前字节码指令地址不出现相互干扰

在这里插入图片描述

虚拟机栈

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

每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应一次次的Java方法调用。是线程私有的

JVM对栈的操作:

(1)每个方法执行,伴随着进栈(入栈、压栈)

(2)执行结束后的出栈工作

栈不存在垃圾回收问题

使用**-Xss**参数来设置线程的最大栈空间(默认1024KB),栈的大小直接决定了函数调用的最大深度

线程上正在执行的每个方法都各自对用一个栈帧

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

栈帧内部结构

在这里插入图片描述

局部变量表

定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量

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

局部变量表所需的容量大小是在编译期确定下来的,在方法运行期间不会改变大小

局部变量表基本存储单元是Slot(变量槽)

在局部变量表里,32位以内的类型只占用一个slot(包括returmAddress类型),64位类型(long、double)占用两个slot

当前帧是由构造方法或实例方法创建,那么该对象引用this将会存放在index为0的slot处

栈帧中的局部变量表中的槽位是可以重复使用的(一个局部变量过了作用域,在作用域后声明的新的局部变量会复用该槽位)

成员变量:在使用前,都经历默认初始化赋值

​ (1)类变量:Linking的prepare阶段,给类变量默认赋值 --> initial阶段给类变量显示赋值

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

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

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

操作数栈

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

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

当一个方法刚开始执行时,一个新的栈帧随之被创建,该方法的操作数栈是空的

每一个操作数栈在编译期就明确了存储数值的深度

操作数栈并非采用访问索引的方式来进行数据访问

如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中

栈顶缓存:将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率

动态链接

在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中

动态链接:每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用

动态链接作用就是为了将这些符号引用转换为调用方法的直接引用

在这里插入图片描述

方法返回地址

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

方法的调用

在JVM中,将符号引用转换为调用方法的直接引用,如果是在编译期确定下来的就是静态链接,如果是在运行期确定下来 的就是动态链接

方法绑定机制:一个字段、方法或者类在符号引用被替换成直接引用的过程,仅仅只发生一次。静态链接对应早期绑定,动态链接对用晚期绑定

非虚方法:指方法在编译期就确定了具体调用且在运行时不可变。静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法

本地方法栈

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

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

本地方法接口

一个native修饰的方法就是一个Java调用非java代码的接口

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

在定义一个native method时,并不提供实现体(就像定义interface),因为实现体由非java语言在外面进行实现

核心概念

一个JVM实例只存在一个堆内存(大小是可调节的)

堆可以处于物理上不连续的内存空间,但在逻辑上应该被视为连续的

所有线程共享Java堆,为了提高并发性和保证数据安全,在堆上可以划分每个线程私有的缓冲区(TLAB)

几乎所有的对象实例以及数组都在堆上进行内存分配(可能逃逸分析后对象保存在栈上)

栈帧中保存的引用是用来指向对象实例或者数组在堆中的位置

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

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

内存细分

通过Java VisualVM,在插件找中安装Visual GC可以看见各个区域内存大小

在这里插入图片描述

JDK7

在这里插入图片描述

JDK8:年轻代 + 老年代 + 元空间

Java堆区用于存储Java对象实例,堆的大小在JVM启动时就已经设定好了

-Xms用来设置堆空间(年轻代+老年代)的起始内存大小

-Xmx用来表示堆空间(年轻代+老年代)的最大内存

Java堆区:年轻代(YoungGen)和老年代(OldGen)

在这里插入图片描述

默认情况下爱,年轻代与老年代占比为1:2,年轻代中eden与survior占比为8:1:1

其实在运行时想达到8:1:1,需要显示设置vm参数**-XX:SurvivorRatio=8**

在这里插入图片描述

几乎所有的Java对象都是在Eden区被new出来的

对象分配过程

(1)new对象先存放在Eden区

(2)在Eden区内存满了后,触发垃圾回收(YGC/Minor GC),此时会将Eden和Survivor区同时进行垃圾回收

(3)将Eden剩余对象移动到Survivor中(此时Eden区没对象),并记录age(是用来判断进入Old区的标记值)

(4)多次垃圾回收,Survivor0和Survivor1两者中来回交换对象并将对象age加1,需保证Survivor区有一个为空(为了GC时进行进行复制清除算法),当Survivor区中对象的age达到阈值(默认15),则进入Old

(5)在Old区存在不足时,会触发Major GC

(6)当Old垃圾回收后仍然无法进行对象保存,则产生OOM异常

注意:

1、Survivor区不会发生Minor GC,是在Eden触发Minor GC时同时进行Survivor的垃圾回收

2、Survivor0和Survivor1复制之后有交换,谁空谁是to(to表示下一次Eden幸存对象保存位置)

3、垃圾回收频繁在新生代(Eden)收集,很少在老年代(Old)收集,几乎不在永久区/元空间收集

特殊情况:

1、超大对象(超过Eden区大小),则判断Old区是否能保存。不能保存的话会在Old先进行FGC,回收后还放不下就OOM

2、在进行YGC后,对象超过Survivor区大小则直接晋升Old

内存分配策略

在这里插入图片描述

TLAB

(Thread Local Allocation Buffer)

由于堆区是线程共享区域,任何线程都可以访问到堆区的共享数据,会存在线程安全问题(加锁的话会影响分配速度)

JVM为每个线程分配了一个私有缓存区域(TLAB),包含在Eden区

在这里插入图片描述

默认情况下,TLAB内存仅占Eden的1%,但JVM是将TLAB作为内存分配的首选

逃逸分析

如果经过逃逸分析(Escape Analysis)发现一个new的对象实体没有逃逸出方法,那么可能被优化成栈上分配

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

(1)对象在方法中定义后,只在方法内部使用,则没有发生逃逸

(2)对象在方法中定义后,被外部方法引用(如作为方法返回值),则发生逃逸

//sb对象作为返回值被外部方法引用,任务发生了逃逸
public static StringBuffer create(String S1,String S2){
	StringBuffer sb = new StringBuffer();
	sb.append(s1);
	sb.append(s2);
	return sb;
}
//改进后对象没有发生逃逸,则可以优化成栈上分配空间
public static String create(String S1,String S2){
	StringBuffer sb = new StringBuffer();
	sb.append(s1);
	sb.append(s2);
	return sb.toString();
}
代码优化

(1)栈上分配

(2)同步省略(消除)

public void fun(){
	Object obj = new Object();
	synchronized(obj){
		syso(obj);
	}
}
//对obj加锁,但obj生命周期只在方法内,不会发生逃逸,则在JIT编译期优化成以下
public void fun(){
	Object obj = new Object();
	syso(obj);
}

(3)标量替换

标量:是指一个无法再分解成更小数据的数据,如Java基本数据类型

聚合量:可以再分解的数据称为聚合量,如Java中对象

class Point(){
	private int x;
	private int y;
}
public void fun(){
	Point point = new Point(1,2);
	syso(point.x + ","+point.y)
}
//进过逃逸分析发现,point对象不会被外界访问,在JIT编译器把这个对象分解成其成员变量来代替(没有对象则不分配内存)
public void fun(){
	int x = 1;
	int y = 2
	syso(x + ","+y)
}

方法区

方法区(Method Area)看做是一块独立于Java堆的内存空间(别名:Non-Heap非堆)

方法区用来加载类信息

方法区和Java堆一样是各个线程共享的内存区域

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

在这里插入图片描述

元空间与永久代区别:元空间不在虚拟机设置的内存中,而是使用本地内存

JDK7:

-XX:PermSize 设置初始化永久代空间大小

-XX:MaxPerSize 设置永久代最大空间大小

JDK8:

-XX:MetaspaceSize 初始化元空间大小(建议设置为相对较高的值)

-XX:MaxMetaspaceSize 最大元空间大小(一般为-1,表示没有限制)

在JDK8中,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆
在这里插入图片描述

永久代被元空间替代的原因:

(1)为永久代设置空间大小是很难确定的

(2)对永久代进行调优是很困难的

内部结构

方法区用于存储已被虚拟机加载的类型信息、常量、静态信息、即时编译器(JIT)编译后的代码缓存

类型信息:类的完整名称、父类或接口的完整名称、修饰符、方法信息(返回类型、参数列表、修饰符、字节码、操作数栈和局部变量表的大小、异常表)

方法区保存类信息时会同时保存每个类的classLoader,且classLoader也会记录都加载了哪些类

static修饰的静态变量被类的所有实例共享,即时没有类实例时也能访问

运行时常量池

字节码文件中内部包含了常量池(Constant Pool)

常量池中存储的数据类型:

(1)数量值

(2)字符串值

(3)类引用

(4)字段引用

(5)方法引用

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

方法区内部包含了运行时常量池(Runtime Constant Pool)

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

字节码文件中的常量池经过类加载器放入方法区后就称为运行时常量池

运行时常量池相较于class文件中常量池是具有动态性

对象实例化

创建对象的方式

在这里插入图片描述

创建对象的步骤

在这里插入图片描述

1、虚拟机遇见new指令,会先判断Metaspace的常量池汇总是否存在类的符号引用,若没有,则通过类加载器进行类加载并生成Class类对象

2、计算对象占用空间大小,在堆中划分一块内存给新对象

4、对象的默认初始化(零值初始化)

5、将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中

6、对象的显式初始化

对象内存布局

在这里插入图片描述

通过代码解释:

public class CustomerTest{
	public static void main(String[] args){
		Customer customer = new Customer();
	}
}
public class Customer{
	int id = 1001;
	String name;
	Account acc;
	{
		name = "匿名客户";
	}
	public Customer(){
		acc = new Account();
	}
}
public class Account{
	......
}

在这里插入图片描述

对象访问定位

Hotspot采用直接指针

在这里插入图片描述

执行引擎

概述

虚拟机的执行引擎是由软件自行实现的,能够执行那些不被硬件直接支持的指令集格式

在这里插入图片描述

执行引擎(Execution Engine)任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以

在这里插入图片描述

(1)执行引擎获取PC寄存器中保存的字节码指令来执行

(2)执行引擎通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中类型指针定位到目标对象的类型信息

Java代码编译和执行

在这里插入图片描述

橙色:javac编译器(前端编译器)

Java被称为半编译、半解释语言(绿色解释 蓝色编译)

解释器:JVM启动时对字节码采用逐行解释的方式执行

JIT编译器:JVM将源代码直接编译成和本地机器平台相关的机器语言

JVM运行方式:

当JVM启动时,解释器可以首先发挥作用,而不必等待即时编译期(JIT)全部编译完成后再执行,节省编译时间。随着时间推移,编译器发生作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更快程序执行效率

String

String声明为final ,不可被继承,实现了Serializable接口(支持序列化),实现了Comparable接口(支持排序比较)

jdk8:定义final char[ ] value来保存字符串数据

jdk9:改为byte[ ](节约空间)

String是代表不可变的字符序列(不可变性)

通过字面量的方式(区别于new)给字符串赋值,此时的字符串值声明在字符串常量池中

字符串常量池中是不会存储相同内容的字符串的

在jdk7之后,字符串常量池保存在堆中。调整原因①永久代permSize默认比较小②永久代垃圾回收频率低

字符串拼接操作

(1)常量与常量的拼接结果在常量池,原理是编译期优化

String s1 = "a" + "b" + "c";//等同于"abc"
String s2 = "abc"
sout(s1==s2)//true

(2)只要其中一个是变量,其结果就在堆中。变量拼接原理就是StringBuilder

//相当于在堆空间中new String(),其具体内容为拼接的结果
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + "b"
sout(s3==s4)//false

(3)一个字符串调用intern()方法,则判断字符串常量池中是否存在当前字符串值,若没有,则在字符串常量池中加载一份当前字符串值并返回地址

String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + "b"
String s5 = s4.intern();//s4此时值为"ab",调用intern()方法时发现字符串常量池中已存在该值(s3)
sout(s3==s5)//true

intern()使用

如果不是用双引号声明的String对象,可以使用String提供的intern()方法:从常量池中查询当前字符串是否存在,若不存在就将当前字符串放入常量池中

//问题1:new String("ab")会创建几个对象?
2个  对象1new关键字在堆空间中创建的
	 对象2:字符串常量池中的对象(通过字节码指令中 ldc <ab>//问题2:new String("a") + new String("b")会创建几个对象?
6个  对象1new StringBuilder()   用于拼接
	对象2new String()
    对象3:常量池中的"a"
    对象4new String()
    对象5:常量池中的"b"
深入剖析:  StringBuildertoString()
    对象6new String()
    注意:toString()方法调用,此时在字符串常量池中没有生成"ab"

关于intern()面试题:

main(String[] args){
	String s1 = new String("1");//new String()时在常量池中已经生成了"1"
	s1.intern();
	String s2 = "1";
	sout(s1 == s2)//false
    //此时s1为堆空间对象,s2为字符串常量池对象    故不相等
        
    String s3 = new String("1") + new String("1");//s3变量地址为new String("11"),但此时字符串常量池中没有"11"
    s3.intern();/**
    	调用intern()方法在常量池中生成"11" 
    	但是由于jdk版本中字符串常量池保存地址存在差异
    	jdk6:常量池在方法区,则在常量池中创建了一个新对象"11"
    	jdk7/8:常量池在堆中,前面s3已经在堆中创建了"11",此时为节省空间,不会创建新对象,而是创建一个指向堆空间中new String("11")的地址
    **/
    String s4 = "11";
    sout(s3 == s4)//jdk6:false     jdk7/8:true
    //jdk6:s3为堆空间对象,s为常量池对象   故不相等
    //jdk7/8:s3为堆空间对象,s4位常量池中保存的指向堆空间的地址    故相等
}

G1垃圾回收器可以实现堆上重复的String对象进行去重(-XX:UseStringDeduplication参数,需要手动开启)

垃圾回收

举报

相关推荐

0 条评论