Jvm

本文详细介绍了JVM内存结构,包括线程私有的程序计数器、虚拟机栈、本地方法栈和共享的Java堆、方法区。探讨了堆和栈的存储区别,以及类加载过程,包括加载、验证、准备、解析和初始化。此外,还阐述了双亲委派机制的工作原理,以及如何打破这一机制。最后,讨论了Object类的关键方法和JVM调优的基本概念。
摘要由CSDN通过智能技术生成

1.jvm内存结构

在这里插入图片描述
分析 JVM 内存结构,主要就是分析 JVM 运行时数据存储区域,运行时内存模型,分为线程私有和共享数据区两大类:

  • 线程私有–线程私有的数据区包含程序计数器、虚拟机栈、本地方法区。

  • 共享数据区–所有线程共享的数据区包含Java堆、方法区,在方法区内有一个常量池。

而 JVM 的优化问题主要在线程共享的数据区中:堆、方法区

程序计数器

程序计数器是一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器。字节码解释器工作时,通过改变程序计数器的值选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖程序计数器完成。

为了线程切换后能恢复到正确的执行位置,每个线程都需要有独立的程序计数器。由于每个线程的程序计数器是独立存储的,因此各线程之间的程序计数器互不影响,这类内存区域被称为线程私有的内存区域。

程序计数器是唯一不会出现 OutOfMemoryError 的内存区域。

虚拟机栈

和程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期与线程相同。

虚拟机栈描述的是 Java 方法执行的内存模型,每个方法被执行的时候会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。一个方法被调用直至执行完成的过程对应一个栈帧在虚拟机中从入栈到出栈的过程。

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

Java 虚拟机栈会出现两种异常。

  • 如果虚拟机栈不可以动态扩展,当线程请求的栈深度大于虚拟机所允许的深度时,将抛出 StackOverflowError 异常;

  • 如果虚拟机栈可以动态扩展,当无法申请到足够的内存时,将抛出 OutOfMemoryError 异常。

java堆

Java 堆是 Java 虚拟机管理的内存中最大的一块。Java 堆是被所有线程共享的内存区域,其目的是存放对象实例,几乎所有的对象实例都在堆中分配内存。比如存放由new创建的对象和数组。垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间。
Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。

  • 年轻代存储“新生对象”,我们新创建的对象存储在年轻代中。当年轻内存占满后,会触发Minor GC,清理年轻代内存空间。

  • 老年代存储长期存活的对象和大对象。年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储。老年代空间占满后,会触发Full GC。

注:Full GC是清理整个堆空间,包括年轻代和老年代。如果Full GC之后,堆中仍然无法存储对象,就会抛出OutOfMemoryError异常。
在这里插入图片描述

方法区

用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。更具体的说,静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中。常量池是方法区的一部分。
当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

JDK 1.8 将方法区彻底移除,取而代之的是元空间,元空间使用的是直接内存。
在这里插入图片描述

运行时常量池

常量池中存储编译器生成的各种字面量和符号引用。字面量就是Java中常量的意思。比如文本字符串,final修饰的常量等。符号引用则包括类和接口的全限定名,方法名和描述符,字段名和描述符等。

运行时常量池也受到方法区内存的限制,当常量池无法再申请到内存时将抛出 OutOfMemoryError 异常。
在这里插入图片描述
优点:常量池避免了频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。

2 局部变量表

栈帧中,由一个局部变量表存储数据。局部变量表中存储了基本数据类型(boolean、byte、char、short、int、float、long、double)的局部变量(包括参数)、和对象的引用(String、数组、对象等),但是不存储对象的内容。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。

局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型。对于64位的数据类型(long、double),JVM 会为其分配两个连续的变量槽来存储。

3堆和栈的存储

在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。

堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。

引用变量是普通变量,定义时在栈中分配内存,引用变量在程序运行到作用域外释放。而数组&对象本身在堆中分配,即使程序运行到使用new产生数组和对象的语句所在地代码块之外,数组和对象本身占用的堆内存也不会被释放,数组和对象在没有引用变量指向它的时候,才变成垃圾,不能再被使用,但是仍然占着内存,在随后的一个不确定的时间被垃圾回收器释放掉。这个也是java比较占内存的主要原因,实际上,栈中的变量指向堆内存中的变量,这相当于指针。

4.java类加载过程

类加载的过程就是通过一个类的全限定名来获取描述此类的二进制字节流,将.class文件中的二进制数据读入到内存中,在这个过程中,将引用,基本类型等放在栈空间,将类变量,常量放在方法区,并且在堆中放一个java.lang.class的对象。

类的生命周期为:加载,验证,准备,解析,初始化,使用,卸载。

类加载过程包括加载、验证、准备、解析和初始化五个阶段。
在这里插入图片描述
1、加载
简单的说,类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例,这个Class对象在日后就会作为方法区中该类的各种数据的访问入口。
2、链接
链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备和解析三个阶段。

2.1验证
验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。

  • 格式验证:验证是否符合class文件规范
  • 语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法是否被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)
  • 操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)

2.2准备
为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值
2.3解析
将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)
3、初始化
将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作。

所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是方法,即类/接口初始化方法(又叫类构造器)。该方法的作用就是初始化类变量,使用用户指定的值覆盖之前在准备阶段里设定的初始值。任何invoke之类的字节码都无法调用方法,因为该方法只能在类加载的过程中由JVM调用。

如果父类还没有被初始化,那么优先对父类初始化,但在方法内部不会显示调用父类的方法,由JVM负责保证一个类的方法执行之前,它的父类方法已经被执行。
JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。

PS:方法clinit对于类或接口不是必须的,如果一个类中没有静态代码块,也没有静态变量的赋值操作,那么编译器不会生成。
在这里插入图片描述

5.双亲委派机制

在这里插入图片描述
了解双亲委派机制首先要了解类加载器,类加载的过程就是通过一个类的全限定名来获取描述此类的二进制字节流,将.class文件中的二进制数据读入到内存中。类加载器包括启动类加载器,扩展类加载器,应用程序类加载器,双亲委派机制就是,当类加载器接收到一个加载请求时,自己先不加载,先把加载的请求给父类即扩展类加载器,扩展类加载器也会给启动类加载器,当发现启动类加载器可加载的类中不包含此类即加载失败,则扩展类加载器加载,若依然找不到则由应用程序加载器加载,若依然加载不到,抛出ClassNotFoundException。
使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。

打破双亲委派机制

在这里插入图片描述

9.java 程序的初始化顺序

有父类优先父类,有接口优先接口,顺序为,接口-抽象类/父类-子类

静态变量,静态代码块,普通变量,普通代码块,构造方法。
在这里插入图片描述
在这里插入图片描述

9.1类的初始化时机

  • 创建类的实例
  • 访问类的静态变量(注意:当访问类的静态并且final修饰的变量时,不会触发类的初始化。),或者为静态变量赋值。
  • 调用类的静态方法(注意:调用静态且final的成员方法时,会触发类的初始化!一定要和静态且final修饰的变量区分开!!)
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。如:Class.forName("********");注意通过类名.class得到Class文件对象并不会触发类的加载。
  • 初始化某个类的子类
  • 直接使用java.exe命令来运行某个主类(java.exe运行,本质上就是调用main方法,所以必须要有main方法才行)。

10.Object类有什么方法

  • wait:导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法
  • toString:返回该对象的字符串表示
  • equals:用于确认两个对象是否相同
  • Hashcode:返回对象的哈希码值
  • NotifyAll:唤醒在此对象监视器上等待的所有线程。
  • Notify:唤醒在此对象监视器上等待的单个线程。
  • Finalize():当当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
  • Clone:创建并返回此对象的副本
  • getClass:返回此对象的运行时类

11.虚拟机栈和本地方法栈为什么是私有的

虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

12.jvm调优

工具、参数
场景处理

参考文章:
一文搞懂JVM内存结构
Java反射:框架设计的灵魂

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值