JVM --- 笔记

虚拟机

java8API地址https://docs.oracle.com/javase/8/docs/api/

jvm8 地址https://docs.oracle.com/javase/specs/jvms/se8/html/

在这里插入图片描述

一、类加载子系统

在这里插入图片描述

(一)类的加载过程

1.加载阶段(将类以二进制流形式加载到JVM方法区中生成Class对象。)

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

(2)将字节流所代表的静态存储结构转为方法区的运行时数据结构。

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

2.链接阶段

(1)验证(Verify)

**确保字节流文件信息符合虚拟机规范,**保证类加载准确性,有格式、元数据、字节码验证等。

(2) 准备(Prepare)

为类变量分配内存,初始化零值。(Static修饰的变量)

这里不包含final修饰的static,因为final修饰的会在编译时就分配了。准备阶段显示初始化。

类变量分配在方法区,实例变量随对象一起分配在堆内存。这里不会为实例变量分配初始化。

(3)解析(Resolve)

将常量池中的符号引用转为直接引用。

3. 初始化

执行类构造器方法()的过程。此方法不需要定义,时虚拟机视角下的。此时会给类变量显示赋值。

一个类只能被加载一次。会被同步加锁。

(二)类加载器的分类

在这里插入图片描述

在这里插入图片描述

1.虚拟机自带的加载器

在这里插入图片描述

启动类加载器 (Bootstrap Class Loader)
  • 该类由c/c++实现。嵌套在jvm中。
  • 用来加载java的核心类库,用于提供JVM自身需要的类。
  • 并不继承java.lang.ClassLoader,没有父加载器。
  • 只加载java、javax、sun等开头的类(核心类库)
应用程序类加载器(System Class Loader)
  • java语言编写,由sun.misc.Launcher$AppClassLoader实现。

在这里插入图片描述

扩展类加载器(Extension Class Loader)
  • java语言编写。
用户自定义类加载器

什么情况下使用

  • 隔离加载类 不同框架整合在一起,中间件等,会存在类的冲突,使用该方式隔离框架之间、中间件之间隔离
  • 防止源码泄露。

(三) 类的双亲委派机制

在这里插入图片描述

1. 工作原理

类加载器收到类加载请求,首先向上找其父类,直到找到引导类,看起是否可以加载,不能则从引导类往下的子类进行尝试。

这样做的目的是类的加载首先去java核心类库中找是否有,核心类库的优先级最高,依次往下。

2. 优势

  • 避免类的重复加载。
  • 保护程序安全性,防止核心API被篡改。
    • eg:自定义的java.lang.String 是不会被加载的。
    • eg:自定义的java.lang.Maotao 加载会报错。自己定义和核心类库的包名一致,为了保护引导类加载器,会抛出安全异常。

3. 沙箱安全机制

保证核心API源代码的保护,引导类加载器只加载核心API,不加载自定义的与核心API同名或同包名的API。

(四)其他

jvm中比较两个calss是否为同一类,存在两个必要条件:

  • 类的完整类名必须一致,包括包名。
  • 类的类加载器必须一致。

二、 运行时数据区

在这里插入图片描述

灰色的PC程序计数器、本地方法栈、虚拟机栈是属于线程私有的。

红色的方法区和堆是属于进程的,同一个进程内的线程共享该内存。

线程

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

在Hotspot中,java线程与操作系统的本地线程是有直接映射关系。一一对应。

(一)PC程序寄存器

作用: 用来存储指向下一条指令的地址。行号的表示。

生命周期 : 线程私有,生命周期与线程一致。

执行:任何时间,一个线程只执行一个方法,程序计数器会保存当前正在执行方法的JVM指令地址。若执行本地方法则为空。

无OutMemoryError情况:Java虚拟机规范中唯一一个不会内存溢出的内存。

(二) 虚拟机栈

1. 栈的存储单元

由栈帧数据结构组成。一个栈帧代表一个方法。

2. 栈帧的组成
  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法返回地址
  • 附加信息。
2. 栈的运行原理

在这里插入图片描述

3. 局部变量表

在这里插入图片描述变量类型分类
按照数据类型分类 : 基本数据类型、引用数据类型
按照变量声明位置: 成员变量(类变量和实例变量)使用前都进行默认初始化赋值、局部变量。

5. 操作数栈

long 和double数据类型占两个栈单位深度。
在这里插入图片描述

(三)堆内存

堆内存分为:新生代、老年代。二者内存默认占比1:2。

新生代:Eden区、from区、to区。三者内存的默认占比 8:1:1.

堆内存中有一个TLAB区是线程私有的,内存很小,对象分配内存优先在TLAB中,放不下在去Eden区,更大的则去老年代。

对象可分配的内存空间:堆内存和栈内存。

什么时候分配在栈内存中?

通过内存逃逸分析,将未逃逸的对象在栈上分配。

(四)逃逸分析代码优化(栈上内存分配)

在这里插入图片描述

1.逃逸分析

对象的作用域仅在当前发方法中有效。

默认逃逸分析是打开的。只有Server模式的虚拟机才有,64位虚拟机默认是Server的。

开启逃逸分析的命令:-XX:+DoEscapeAnalysis

关闭逃逸分析的命令:-XX:-DoEscapeAnalysis

发生逃逸的情况:将对象return返回到,调用的方法外的对象等。可能被方法外使用的对象都被视为逃逸。

栈上内存分配的优点:1. 执行时间短,2.没有发生GC。

2. 标量替换

允许将对象拆分成标量,分配在栈上(就是当对象未发生逃逸,使用局部变量替换对象属性)。

默认标量替换是开启的。

开启标量替换的命令:-XX:+EliminateAllocations

关闭标量替换的命令:-XX:-EliminateAllocations

标量:无法在分解的数据。

聚合量:可分解的数据。

java中的基本数据类型就是标量,类就是聚合量。

oracle的Hotspot虚拟机中的逃逸分析采用的就是标量替换,其实对象还是分配在堆中。

(五)方法区

1. 方法区的介绍

与java堆一样,也是线程共享的内存区域。

与java堆一样,方法区在jvm启动时创建,可以是不连续的内存空间。

与java堆一样,大小可以选择固定或者可扩展的。

方法区的大小决定系统可以保存多少个类。堆大小决定系统可以保存多少个对象。

方法区在逻辑上属于堆的一部分,但是其简单实现可能不会选择进行垃圾回收或者压缩。对于Hotspot虚拟机,方法区被称为非堆。

在jdk1.7之前hotspot采用虚拟机中设置的内存(又被称为永久代),容易出现oom,其受限于虚拟机内存。jdk1.8开始采用元空间代替永久代,元空间是系统内存,不占用虚拟机设置的内存,不容易出现oom,其受限于本地内存。

jdk1.7之前的设置虚拟的方法区大小命令:-XX:MaxPermSize=10m -XX:MaxPermSize=10m。虚拟机默认方法区大小32位机是64M,64位机是82M.超过这个最大值则会抛出OOM。

jdk1.8开始设置虚拟机的方法区大小命令:-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m。默认值依赖平台系统,window下是21M,-XX:MaxMetaspaceSize默认是-1,没有大小限制。

MetaspaceSize建议设置的大一些。一旦方法区的分配大小触及MetaspaceSize的值,就会在方法区触发Full GC对没有用的类进行回收(这些类的类加载器不再存活)。若MetaspaceSize设置过低,会导致频繁进行FULL GC。建议根据实际项目需求,检测其类多少,设定MetaspaceSize较高一些。

2 .方法区的内存结构

用来存储虚拟机加载了的类型信息(类信息、接口、注解、枚举等)、运行时常量池、静态变量、即时编译器JIT编译后的代码缓存等。

常量池和运行时常量池

常量池:是字节码中的一张表,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型、字面量等类型。虚拟机位每个已加载的类型(类或者接口)都维护一个常量池表,该表中数据项和数组一样,通过索引去访问。

运行时常量池:类加载器将字节码中常量池加载到方法区,将间接引用和符号引用转为直接引用,就是运行时常量池。

3.方法区的演进历程

在这里插入图片描述

4. 常量池的演变过程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.永久代为什么被元空间替代
  • 永久代的大小很难确定,当类或者jar包加载过多,会导致永久代内存不足,出现OOM。
  • 永久代调优比较难,出现full GC比较影响性能。
  • 元空间不属于虚拟机,故不会出现GC和OOM,内存大小受限于物理机大小。
6.为什么将字符串常量池和静态变量放入堆中

JDK1.7之前将字符串常量池放在永久代,永久代回收效率低,只有在full GC时才会触发,儿full GC只有在老年代不足、永久代不足时才会触发。

我们开发过程中会有大量创建字符串,回收效率低导致永久代内存不足,所以把其放到堆中能及时回收(放在新生代)。

静态变量随Class实例对象放在堆中,实例变量随实例对象放在堆中,局部变量随方法放在栈帧中的局部变量表中。

7.方法区的垃圾回收

方法区常量池中的垃圾回收:只要常量没有被任何地方引用,就可以被回收。

方法区中类型信息的垃圾回收:不在被使用的类,就可以被回收。但是需要满足三个苛刻的条件(1.该类所有实现对象都被回收,即堆中不存在该类的对象和其子类的对象;2.该类的类加载器被回收(通常很难实现);3.该类的Class没有在任何地方引用,不能在任何地方有通过反射访问该类的方法)。

如何解决OOM

OOM不同于Exception,OOM可能是代码没有问题,是虚拟机内存分配大小的问题。

分析OOM的问题,一般是heap space堆内存,需要先判断是内存泄漏还是内存溢出。

  1. 内存泄漏:通过工具查看泄漏对象到GC root的引用链。找到泄漏对象的类信息和其引用链就可以准确定位出泄漏代码的位置。
  2. 内存溢出:就是堆的内存不足,需要检查虚拟机的堆参数(-Xmx和 -Xms)与机器物理机相比是否还可以调大。也可以查看代码是否存在某些对象的生命周期过长,尝试减少程序运行期的内存消耗(能不需要静态就不需要static修饰,能在方法内定义就不要在方法外声明)。

在这里插入图片描述

三、对象的实例化

(一)对象创建的方式

  1. new 是最常见的方式。
  2. 通过Class的newInstance():反射的方式,只能调用空参的构造器,必须是public的,条件比较苛刻。
  3. 通过Constructor的newInstance():反射的方式,可以调用空参、有参的构造器,权限没有要求。
  4. 使用clone方式:需要实现Cloneable接口,实现clone()方法(浅拷贝)。
  5. 使用反序列化方式:可以从文件、网络中获取一个对象的二进制流,反序列化为java对象。

(二)对象的内存布局

在这里插入图片描述实例对象在堆中主要有两部分,对象头(运行时元数据、类型指针),实例数据。

(三)对象的访问定位

java的对象保存的堆上,访问定位通过栈帧上局部变量表中的引用变量(记录了对象的内存地址)去访问。
对象的两种访问方式
在这里插入图片描述

方法一:句柄访问,在堆内存中会专门开辟一个空间,用来存放句柄(映射栈帧中引用变量和堆中对象的地址)。该方式的缺点(浪费内存,访问效率低),优点(在内存回收,整理内存空间时,移动对象,堆栈帧中引用变量的映射地址不需要改变,栈帧中引用变量的映射地址比较稳定。)
在这里插入图片描述

方法二:直接地址,栈帧中引用变量的映射地址就是堆中对象的内存地址。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值