jvm 内存模型概述

jvm 内存模型概述_第1张图片一、类加载子系统

 

1、类加载的过程:装载、链接、初始化,其中,链接又分为验证、准备和解析

装载:加载class文件

 

验证:确保字节流中包含信息符合当前虚拟机要求

 

准备:分配内存,设置初始值

 

解析:将变量从符号引用改为直接引用

 

初始化:执行类构造器方法

 

 

 

2、类加载器的分类

加载阶段使用到类加载器,类加载器分为:由C++实现的引导类加载器和由java实现的自定义加载器

 

自定义加载器又分为三层类加载器:拓展类加载器,应用程序类加载器和系统类加载器

 

 

 

①、引导类加载器:启动类加载器,将lib目录下,或-Xbootclasspasth参数指定路径存放的,java虚拟机能识别的类库加载进虚拟机;

 

②、拓展类加载器:将\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库加载进虚拟机;

 

③、系统类加载器:应用程序类加载器:负责加载用户类路径上所有的类库,可以直接在开发中使用这个类加载器;

 

④、自定义类加载器:用户自定义的类。

 

3、双亲委派机制

java虚拟机对class文件采用按需加载的方式,即在需要使用到类时,才加载对应的类到内存中,并且使用双亲委派机制的模式进行加载:

 

①:当一个类加载器收到了类加载的请求,不会立刻自己加载,而是将请求委托给父类的加载器去加载;

 

②、当父类加载器还存在父类加载器,则继续往上依次委托,最终的委托到最顶层的启动类加载器;

 

③、如果父类的加载器可以完成类加载器,则成功返回;如果父类加载器无法完成加载,子类加载器才会尝试着自己去加载。

 

这就是双亲委派机制的加载过程。

 

双亲委派机制的优势:

可以避免类的重复加载;

 

保护程序的安全,避免核心的api被篡改。

 

4、沙箱安全机制

java代码被限定在虚拟机特定的运行范围中,并且严格限制代码对本地资源的访问,通过这样来保护代码的的有效隔离性,防止本地系统造成破坏,这就是沙箱安全机制。

 

沙箱主要限制系统资源访问,例如:cpu、内存,文件系统、网络。不同级别的代码相对这些资源的访问的限制也可以是不一样的。

 

二、运行时数据区

1、虚拟机栈

每个线程在创建的时候,都会创建一个虚拟机栈,虚拟机栈是属于线程私有的。

 

 

 

a、栈帧(Stack Frame)

虚拟机栈中会存放着一个一个的栈帧,一个栈帧对应着一个方法,用于支持方法的调用和方法执行的数据结构;

 

虚拟机栈帧中存放着方法的局部变量表、操作数栈、动态链接和方法返回地址,在编译代码时,栈帧中需要多大的局部变量表和多深的操作数栈都已经确定好了。

 

①局部变量表:是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。注意:在mian()方法中,第一个表中的第一个位置的参数是args;

 

局部变量表存放了编译器可知的各种基本数据类型,对象引用和方法返回地址;

 

局部变量表以变量槽为最小单位,每个槽可以存放32位的长度,即long类型和doubule类型占俩个变量槽,而其他的基本数据类型占一个变量槽。

 

②操作数栈:用于保存计算过程中的中间结果,同时作为计算过程中变量的临时存放空间;

 

③动态链接:指向运行时常量池中该帧所属方法的引用;包含这个引用的目的是为了支持当前方法的代码能够实现动态链接,如:invokedynamic指令。

 

在java源文件被编译到字节文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里,程序运行时将其加载进方法区的运行时常量池中;

 

如描述一个方法调用了另外的其他方法时,就是  通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用 转为调用方法的直接引用。

 

④方法返回地址:存放该方法在寄存器中的值,即是该方法的指令地址,方便执行引擎在执行完该方法后,回到该方法对应的指令行号,这样才能继续执行下去。

 

2、程序计数器

程序计数器,又称pc寄存器,存储下一条要执行的指令的地址,每个线程都有专属的pc寄存器。

 

程序执行时,会不停的切换不同的线程进行执行,有些线程可能执行一般,就被停止执行,转而执行其他线程,这时候pc寄存器会记录下知道到的指令行号,等到线程获取到资源重新执行时,会依据pc寄存器中的指令,继续往下执行。

 

3、堆

 

 

堆由新生代和老年代组成

 

新生代

新生代由Eden区(伊甸园区)和俩个Survior(幸存区),Survior0,Survior1,新生代一般用来存放新创建的对象。

 

①Endn区:几乎所有的对象都在Eden区中创建,即Eden是大部分对象产生的地方;

 

创建的对象有部分存在的周期很短,有些对象的存在周期很长,当Eden区的内存满了时,会触发Minor GC 进行垃圾收集;

 

②Survior区: 幸存区 ,幸存区划分为俩个区Survior0和Surivor1即,幸存区0和幸存区1,在进行垃圾收集时,哪个区为空则为Survior0;一般都是俩个区互相替换成为Survior0;

 

Survior0 和Survior1俩个区的存储空间一般为1:8的比例,但实际大多数情况都达不到这个比例,可以使用-XX:SurviorRatio参数进行设置。

 

老年代

老年代一般用来存放生存周期较长的对象,当老年代的存储空间不足时,会触发Major GC或Full GC进行垃圾收集。

 

新生代和老年的存储空间比例为1:2,可以通过 -XX:NewRatio 参数进行设置。

 

垃圾收集

在堆中,有三个垃圾收集算法:Minor GC、Major GC 和 Full GC

 

①Minor GC

新生代垃圾收集,只是新生代(Eden、Survior0和Survior1)的垃圾收集。

 

触发机制:当新生代中的Eden区的空间不足时触发(注意:Survior区空间不足时不会主动触发,Survior区只会被动触发机制)进行垃圾收集。因为大多数java对象都具备朝生夕灭的特性,所以Minor GC触发的非常频繁,一般回收的速度也比较快。

 

收集过程:a、新生代中,对于每个对象都有一个引用计数器,当Eden区的空间满了只有,只有还被引用的对象能存活不被清理,并且会转移到Survior0区,对象的引用计数器增加1,代表这个对象存活的年龄增大一岁;

 

b、重复a步骤,将存活的对象复制到S0区,存活年龄增大,已存在在S0区的对象也相对应的增加存活年龄;

 

c、当Eden区和S0区空间满时,会将S0中的对象转移到S1中,存活的对象相对应的存活年龄,然后Eden和S0区会被清空,存活的对象全部在S1中,并且对象的存在着不一样的存活年龄。此时S0会变成意义上的S1(区中为空),S1会变成意义上的S0;如此反复,空的区为S1,对象存活的数据区为S0。

 

d、当不断进行Minor GC知道对象的存活年达到阈值(通过-XX:MaxTenuringThreshold设置,默认是15),则达到阈值年龄的对象,会判定为生命周期较长的对象,会被promote到老年代;不断的进行Minor GC,也会不断有对象被promote到老年代中。

 

TLAB:TLAB是一块为了解决线程安全问题所设置的存储空间,属于新生代的Ede区,TLAB内存空间非常小,仅占Eden的1%,不是所有的对象的实例都能在TLAB中成功分配内存,但JVM确实是以TLAB作为分配内存的首选。

 

堆空间是所有线程共享的,但Eden中的TLAB是线程私有的,解决线程的安全问题。

 

Major GC

老年代垃圾收集,只进行老年代的垃圾收集。

一个对象如果很大,那么会直接进入老年代中,即老年代存在着大对象和存活周期长的对象。

 

触发机制:老年代的空间不足时,会尝试触发Minor GC 进行新生代的垃圾收集,如果触发后空间还是不足,则触发Major GC;

 

Major GC的速度一般会比Minor GC的速度慢10倍以上,STW的时间更长,如果触发后依旧空间不住,则报OOm错误。

 

一把只有CMS GC 会有单独收集老年代的行为,很多时候,Major GC和 Full GC都是混淆这是用触发。

 

收集过程:一般Major GC 和 Full GC 同时触发,收集整个堆。

 

Full GC

整堆收集,收集整个java堆和方法区的垃圾。

 

触发机制:a、调用System.gc()时,系统会建议执行Full GC,但不必然执行;

 

b、老年代空间不足时;

 

c、方法区空间不足时;

 

d、通过Minor GC后进入老年代的平均大小大于老年代的可用内存;

 

e、有Eden区、Survior space0(From space) 区向Survior space(To space)区复制时,对象大小大于 To space的可用空间大小,则对象复制到老年代,但对象大小又大于老年代可用空间时触发。

 

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

 

代码优化

堆不是分配对象存储的唯一选择,如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么可能被优化成栈上分配。

 

逃逸分析:分析一个对象的作用域,当一个对象在方法中定以后,对象只在方法内部使用,则认为没有发生逃逸;当一个对象定以后,它被外部方法所用(例如:return了变量或作为参数传递到了其他方法),则认为发生了逃逸。

 

栈上分配:没有发生逃逸的对象,可以分配到线程专属的栈上,随着方法执行的结束,栈空间也被移除,变量也被移除,无需进行GC。

 

代码优化(不发生逃逸分析的情况)

1、栈上分配;

 

2、同步省略:变量只有一个线程访问,取消其同步策略;

 

3、分离对象或标量替换

 

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

 

分离对象或标量替换就是将一个大的对象分解成多个标量存放在不连续的内存地址。

 

4、方法区 -- Metaspce 元空间

方法区和java对是一样的,是各个线程共享的区域,它用于存储已被加载类的信息,常量,静态变量,即使编译偶的代码等数据。

 

元空间的演进

 

 

5、本地方法栈

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

 

本地方法:java调用非java代码的接口,如操作系统或和某些硬件交换信息。

 

java应用对java外部的环境交互,这就是本地方法存在的原因。

 

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

 

三、执行引擎

执行引擎负责将字节吗指令解释/编译成对应平台上的本地机器指令,即将高级语言翻译成机器语言。

 

java编译和执行的过程:

 

 

java为半编译半解释型语言,java在执行代码的时候,会将解释和编译执行二者结合起来进行。

 

执行引擎获取到后,将由javac将源码编译成字节码.class文件后,在运行解释器最终转换为机器码(解释器)。

 

解释器:java中,对字节码采用逐行解释的方式执行(边解释边执行),将每个字节码文件中的内容翻译对应机器平台的机器语言。

 

JTI编译器:虚拟机将源代码直接编译成和本地机器平台相关的机器语言。

 

解释器和JIT并存

 

 

即使编译的触发:热点探测

名词解析:1、热点代码:被调用执行频率高的代码被称为热点代码;

 

​ 2、方法调用计数器:统计方法被调用的次数;

 

​ 3、回边计数器:统计循环执行体执行的循环次数。

 

目前HotSpot的热点探测是基于计数器的的热点探测;

 

热点探测:当一个方法的方法调用计数器或回边计数器的次数达到阈值时,会出发即时编译,将代码运行从解释器执行改为即使编译执行。

 

​ 方法计数器不是一直在增加的,在一段时间内,方法的调用次数一直没有变(半衰周期),这个方法的方法调用计数器就会减少一半,这个过程称为方法计数器热度的衰减。

 

为什么即使编译和解释器并存

JIT编译器要发挥作用,,需要将代码全部编译成本地指令,需要一定的时间(但编译后的代码执行效率高),所以在程序刚启动时,需要用解释器进行逐行编译成机器指令。所以,jvm对这种俩种是进行了取长补短。

 

四、对象创建

对象创

你可能感兴趣的:(jvm)