jvm内存结构

jvm的整体结构

class文件 —> 类加载子系统 ->jvm内存 -> 解释器,垃圾回收器,本地方法接口

 

类加载子系统

类加载子系统负责加载class文件。class 文件有特定的文件标识

类的加载过程

加载-> 验证 -> 准备 -> 解析 -> 初始化

( 链接 )

加载

  1. 通过一个类的全限定名获取二进制字节流
  2. 生成运行时的数据结构
  3. 在内存中生成class对象

验证 :检查class文件是否符合jvm规范

准备 :设置变量的初始值

解析 :将符号引用转换为直接引用。(符号引用是用一组符号表示引用的目标,直接引用是指向目标的指针)

初始化 :执行类构造器方法。(类构造器方法不同于构造器方法。它自动执行赋值动作和静态代码块的语句)

类加载器的分类

引导类加载器 : 加载java的核心库,加载包名为 java、javax、sun等开头的类

扩展类加载器 jre等包

自定义类加载器

为什么要自定义类加载器?

隔离加载类

防止源码泄露

双亲委派机制

jvm对class文件采取按需加载的方式,只有使用时才加载。在加载类时,将请求交给父加载器进行处理,如果父类加载器还存在父类加载器,向上递归,直到顶层。如果父类可以完成加载就成功返回,只有当父类加载器无法完成时,子加载器才会自己加载。

优势

避免类的重复加载

保护程序安全(其他人无法自定义Object类,这样可以保证对java核心源代码的保护,这就是沙箱安全机制)

在jvm中表示两个class对象是否为同一个类时存在两个条件:

  • 类的完整类型必须一致
  • 类加载器必须相同

运行时数据区结构

 

每个线程独有:程序计数器,本地方法栈,虚拟机栈

线程间共享:方法区,堆

程序计数器

pc寄存器用来存储指向下一条指令的地址。

虚拟机栈

每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应一次次java方法的调用。

栈中可能出现的异常

java栈的大小可以是动态的或固定不变的。

如果采用固定大小的虚拟机栈,线程请求分配的栈容量超过虚拟机栈的最大容量,报stackOverflowError异常

如果虚拟机栈可以动态扩展,在申请内存时无法申请到足够的内存,报OutifMemoryError异常

例:无限递归

通过 -Xss 命令来设置线程最大栈空间

栈中存储什么?

栈中的数据以栈帧的形式存在,每个方法都对应一个栈帧。

栈的操作只有两个,入栈和出栈。在一个时间点上,只会存在一个活跃的栈帧,这个栈帧成为当前栈帧。 与之对应的为当前方法,当前类。

java中有两种返回函数的方式,return 或 抛异常。但不论哪种方式,都会弹出栈帧。

栈的内部结构

局部变量表 :负责存储局部变量,局部变量表中的变量只在当前方法调用中有效。当调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

局部变量表最基本的单位时slot(变量槽),32位的类型占用1个slot,64位的占用两个。jvm会为局部变量表中的每一个solt分配一个索引,通过索引即可访问到变量的值。

栈帧中的局部变量表中的槽位是可重用的。

操作数栈 :在方法执行的过程中,往栈中写入数据或提取数据,主要用于保存计算过程的中间结果。

动态链接 :每个栈帧内部都包含着指向运行时常量池中该栈帧所属方法的引用。动态链接的作用就是将这些符号引用转换为直接引用。

方法返回地址

虚方法与非虚方法

如果在编译器就确定了具体的调用版本,则成为非虚方法。常见的非虚方法有:静态方法、私有方法、final方法 、实例构造器、父类方法。

反之则成为虚方法。

为了提高性能,jvm在类的方法区建立了一个虚方法表指定执行的方法。

 

 

本地方法栈

本地方法接口

一个本地方法就是一个java代码调用非java代码的接口。

为什么要使用本地方法?

有时java应用需要与Java外面的环境交互,这是本地方法存在的原因。

 

现代垃圾收集器大部分都基于分代收集理论设计

java7之前分为:新生代+老年代+永久代

java8之后分为:新生代+老年代+元空间

其中年轻代又可划分为 eden+s1+s0

堆空间大小的设置

-XX:+PrintFlagsInitial 查看所有参数的默认初始值

-Xmn:设置新生代的大小

-xms: 堆起始内存

-xmx :堆的最大内存 ,通常将xms和xmx设置相同的值,为了能够在gc之后不需要重新分隔计算堆区的大小,提高性能。

新生代与老年代的占比

默认 -XX:newRatio=2 ,新生代占1 ,老年代占2,新生代占整个堆 的1/3

Eden和另外两个 survivor 空间所占比例是 8:1:1

对象分配过程

1.new对象先放eden区,大小有限制

2.当eden区填满时,进行minor gc ,将eden区不使用的对象销毁,再加载新的对象。eden区,survivor放不下的大对象直接晋升至老年代。

3.将eden区的剩余对象放入s0区

4.再次触发垃圾回收,从s0区放到s1区

5.再次回收,从s1区到s0区

6.经历过15次之后,去养老区

7.在养老区满了触发major gc

8.在major gc之后无法进行对象的保存,产生OOM

 

minor gc 收集年轻代

major gc 收集老年代

full gc 收集整个堆和方法区

 

full gc 触发机制

  1. 调用 System.gc()
  2. 老年代空间不足
  3. 方法区空间不足
  4. 通过minor gc 后进入老年代的平均大小大于老年代的可用内存
  5. 由 eden ,s0 向 s1区复制时,对象大于to区可用的内存 ,会把对象转移到老年代,且老年代的可用内存小于该对象的大小。

为什么要分代?

不同对象生命周期不同,大部分对象是临时对象。

TLAB

jvm为每个线程分配了一个私有的缓存区域,包含在eden区内,提高分配内存的效率。

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

开启了逃逸分析后,如果对象没有逃逸出方法的话,就可能被优化称栈上分配。 随着方法的结束,栈空间被移除,变量也消失。

但是无法保证逃逸分析的性能消耗小于它的优化。

 

方法区

方法区是一块独立于java堆的内存空间,是线程共享的,大小可固定可扩展。

如果系统定义了太多的类,导致方法区溢出,也会抛出异常。OutOfMemoryError:PermGen space 或者Metasoace。

在jdk7以前,方法区称为永久代。jdk8开始,元空间取代了永久代。

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

 

方法区主要存储什么?

存储已被虚拟机加载的类型信息,常量,静态变量等。

 

运行时常量池与常量池

方法区内部包含了运行时常量池

字节码文件内部包含了常量池

 

一个有效的字节码文件包括了魔数,版本信息,字段,方法,接口信息,还包括了常量池表(其中包括对类型,方法等的符号引用)。

为什么需要常量池?

一个java源文件编译后的字节码文件大小有限,不能存储所有的数据,这时可以存到常量池中,它包含了指向常量池的引用。

常量池,可以看作是一张表,用于存放编译器生成的各种符号引用。虚拟机指令根据这张表找到要执行的类名,方法名等。这部分将在类加载后存放到方法区的运行时常量池中。

 

方法区演进的细节

jdk1.6以前 有永久代,静态变量存放在永久代上

jdk1.7 有永久代,字符串常量池,静态变量移除,保存在堆中

jdk1.8 以后 无永久代。类型信息、字段、方法、常量保存在本地内存中,但字符串常量池,静态变量仍在堆。

为什么永久代要被元空间替换?

1.为永久代设置空间大小是很难的。如果在运行过程中需要动态加载的类过多很容易造成内存溢出。

2.对永久代调优是很难的。

字符串常量池为什么要调整?

因为永久代的回收效率很低,只有full gc时才触发。而开发时会有大量字符串被创建,回收效率低,导致永久代不足。

 

创建对象的步骤

  1. 判断对象对应的类是否加载、链接、初始化
  2. 为对象分配内存
  3. 处理并发安全问题
  4. 初始化分配的空间,为属性赋默认值
  5. 设置对象的对象头
  6. 执行init()方法初始化

执行引擎

将字节码指令解释为对应平台上的机器指令。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值