jvm深入理解

12 篇文章 9 订阅

一、类的生命周期

加载—>(验证+准备+解析)连接—>初始化—>使用—>卸载。

1、加载
  • 通过类的全限定名获取该类的二进制字节流。
  • 将二进制字节流所代表的静态结构转化为方法区的运行时数据结构。
  • 在内存中创建一个代表该类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
2、验证

确保当前Class字节流文件中包含的信息及内容不会危害到当前虚拟机。并符合规范

3、准备

为类的静态成员变量分配内存空间,并设置初始值 0或者false

4、解析

将常量池的符号引用替换为直接引用

5、初始化

执行所有静态代码块的动作以及为静态变量赋值操作

二、JVM运行时的数据区

1、堆(heap)

各个线程共享的内存区域,线程不安全❌

这块区域是 JVM 所管理的内存中最大的一块。堆是用来存放对象的内存空间,几乎所有的对象都存储在堆中。栈帧保存的引用地址指向区域

JAVA堆内存管理是影响性能主要因素之一。jvm调优主要关注的内存区域

同时也是垃圾回收重点区域

  1. JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。

  2. 年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。

  3. 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。

  4. 非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。

  5. 在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。
    元空间有注意有两个参数:

    MetaspaceSize :初始化元空间大小,控制发生GC阈值
    MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存
    
JVM堆内存常用参数
参数描述
-Xms堆内存初始大小,单位m、g
-Xmx(MaxHeapSize)堆内存最大允许大小,一般不要大于物理内存的80%
-XX:PermSize非堆内存初始大小,一般应用设置初始化200m,最大1024m就够了
-XX:MaxPermSize非堆内存最大允许大小
-XX:NewSize(-Xns)年轻代内存初始大小
-XX:MaxNewSize(-Xmn)年轻代内存最大允许大小,也可以缩写
-XX:SurvivorRatio=8年轻代中Eden区与Survivor区的容量比例值,默认为8,即8:1
-Xss堆栈内存大小
2、方法区
  1. 各个线程共享的内存区域,线程不安全❌
  2. 它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存…
  3. 方法区在 JVM 启动的时候创建,并且它的实际的物理内存空间和 Java 堆区一样都可以是不连续的。
  4. 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
  5. 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区的溢出,虚拟机同样会抛出内存溢出错误:OOM
  6. 关闭 JVM 就会释放这个内存区域。
  7. 在 jdk7 及以前,习惯上把方法区称为永久代。jdk8开始,使用元空间取代了永久代。到了 JDK 8 ,终于完全废弃了永久代的概念,改用与 JRockit,J9一样在本地内存中实现的元空间(Metaspace)来代替.
  8. 元空间不再虚拟机设置的内存当中,而是使用本地内存。

方法区的大小不必是固定的,jvm 可以根据应用的需要动态调整。

  • jdk7 及以前:

    • 通过

      -XX:PermSize 来设置永久代初始分配空间。默认值是20.75M
      
      -XX:MaxPermSize 来设定永久代最大可分配空间。32 位机默认是64M,64位机器模式是82M
      
    • 当 JVM 加载的类信息容量超过了这个值,会报异常 OutOfMemoryError:PermGen space.

  • jdk8 及以后:

      • 元数据大小可以使用参数

        -XX:MetaspaceSize 
        -XX:MaxMetaspaceSize 指定,替代上述原有的两个参数。
        
      • 默认值依赖于平台。windows 下,-XX:MetaspaceSize 是 21M, -XX:MaxMetaspaceSize 的值是 -1,即没有限制。

      • 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据发生异常,虚拟机一样会抛出异常 OutOfMemoryError:Metaspace

      • -XX:MetaspaceSize 设置初始的元空间大小。
        -XX:MetaspaceSize 值为21MB (默认)。
        

        这就是初始的高水位线,一旦触及这个水位线, Full GC 将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线取决于 GC 释放了多少空间。如果释放的空间不足,那么在不超过 MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。

      • 如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到 Full GC 多次调用。为了避免频繁的GC,建议将 -XX:MetaSpaceSize 设置为一个相对较高的值。

2.1、运行时常量池

运行时常量池就是将编译后的类信息放入方法区中,也就是说它是方法区的一部分。

运行时常量池用来动态获取类信息,包括:class文件元信息描述、编译后的代码数据、引用类型数据、类文件常量池等。

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

运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中。每个class都有一个运行时常量池,类在解析之后将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

运行时常量池相对于class文件常量池的另外一个特性是具备动态性,java语言并不要求常量一定只有编译器才产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。

3、虚拟机栈
  1. 线程隔离,线程安全✅
  2. 生命周期与线程同步
  3. 存放每一个栈帧(栈的特性,先进后出)StackFrame

​ 每一个栈帧包含局部变量表,操作数栈,动态连接,方法返回地址等信息

3.1、局部变量表

每一个栈帧对应的一个方法、由名称可知,局部变量则保存在该方法内保存的局部的基本数据类型变量(boolean,byte,char,short,float,long,double)、以及引用对象的地址。

具体的内容包含:方法的参数、方法内定义的本地变量、try-catch定义的异常

3.2、操作数栈(表达式栈Operand Stack)

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

当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这时方法的操作数栈是空的

操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问。

如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。

3.3、动态链接

如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期间调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。

  • 动态链接也可称为指向运行时常量池的方法引用
  • 每一个栈帧内部都包含一个指向运行时常量池中该线程所属方法的引用。

常量池:就是为了提供一些符号以及一些常量,用于指令的识别

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

在方法调用结束后,必须返回到该方法最初被调用时的位置,程序才能继续运行,所以在栈帧中要保存一些信息,用来帮助恢复它的上层主调方法的执行状态。方法返回地址就可以是主调方法在调用该方法的指令的下一条指令的地址。

方法返回地址存放调用该方法的PC寄存器的值

方法结束有两种方式:正常退出和抛异常退出。

方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址

方法异常退出时不会给他的上层调用者产生任何返回值

4、本地方法栈

线程安全,线程隔离✅

本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的作用是非常相似的。主要区别是 本地方法栈是为虚拟机使用到的 Native 方法服务。

Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库(相当于是C/C++暴露给java的接口)

而本地方法栈则主要是用户处理native的方法

5、pc寄存器(程序计数器)
  1. 线程隔离,线程安全✅
  2. pc寄存器的内存占用非常小。可以忽略不计
  3. 程序执行时,保存正在执行的字节码地址
  4. 是唯一个不会出现OOM(OutofMemeryError)的区域

​ 在单线程情况下,可以看出pc寄存器是可以可有可无的,因为即使没有pc寄存器。程序也是顺序执行,即使遇到方法调用、分支调用,程序也会跳转到指定指令顺序执行完成。

​ 但是现实情况是,不可避免的会多线程多协同的完成任务。而jvm的多线程是按照时间轮片来实现 cpu通过给每个线程分配cpu时间片,不停地切换线程的状态来实现多线程(感觉同时执行是因为时间片较短)。而当cpu需要再次从挂起处继续指令时,需要获取pc寄存器的当前执行的地址。由此可以看出pc寄存器无法线程共享。

三、垃圾收集算法

1、引用计数法

每个对象添加一个引用计数器,当该对象添加一个引用时计数器+1。引用失效时计数器-1。当计数器数为0时,执行垃圾回收

引用计数器有一个严重的问题,无法处理循环引用的问题,导致不知道是否是垃圾还是存活类似A->B->A的问题,会造成内存泄漏。

2、标记复制算法

**GC 复制算法(Copying GC)**是 Marvin L. Minsky 在 1963 年研究出来的算法。就是只把某个空间里的活动对象复制到其他空间,把原空间里的所有对象都回收掉

需要提供原空间两倍大小的内存

3、标记清除算法

**标记清除(Mark and Sweep)**是最早开发出的GC算法(1960年)。它的原理非常简单,首先从根开始将可能被引用的对象用递归的方式进行标记,然后将没有标记到的对象作为垃圾进行回收。

4、标记整理算法

其原理主要是:分为两个阶段,第一个阶段与标记-清理算法一样,先从根节点标记哪些是被对象引用的,第二阶段将所有存活的对象压缩移动到内存的另一端,按顺序排放,最后清除所有边界以外的空间。

四、主流HotSpot 垃圾收集器

1.新生代的收集器包括:
  • Serial 【/ˈsɪriəl/】 复制算法

    优点是简单高效,它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。

  • PraNew 复制算法

    Serial的多线程版本

  • Parallel Scavenge 收集器-复制算法

  • (并行回收)

    该收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

2.老年代的收集器
  • Serial Old 标记整理

    它同样是一个单线程(串行)收集器,使用标记整理算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。

  • Parallel Old 标记赋值算法

    与新生代PraNew类似,该收集器更注重吞吐量。吞吐量是指在同样的回收时间内,吞吐量越高,所执行的用户代码越多

  • CMS 标记-清除算法

    在jdk1.7后被G1代替。JVM 团队设计出 G1 收集器的目的就是取代 CMS 收集器,因为 CMS 收集器在很多场景下存在诸多问题,缺陷暴露无遗

    1. CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4
    2. CMS在并发标记垃圾时,程序仍在运行,随着程序运行,不断会有新的垃圾出现。这些垃圾在标记过程之后,无法被立即的清除,需要等到下次gc清除
    3. 标记清除算法会产生非常多不连续的内存碎片
3.回收整个Java堆(新生代和老年代)
  • G1收集器

    G1 是一款面向服务端应用的垃圾收集器,它没有新生代和老年代的概念,而是将堆划分为一块块独立的 Region。当要进行垃圾收集时,首先估计每个 Region 中垃圾的数量,每次都从垃圾回收价值最大的 Region 开始回收,因此可以获得最大的回收效率。

    每个 Region 都有一个 Remembered Set,用于记录本区域中所有对象引用的对象所在的区域,进行可达性分析时,只要在 GC Roots 中再加上 Remembered Set 即可防止对整个堆内存进行遍历。

使用命令查看jdk虚拟机使用的垃圾收集器

java -XX:+PrintCommandLineFlags -version
#或者
java -Xlog:gc* -version

五、JVM四种不同的引用类型

String obj = new String("test")   //强引用
SoftReference<String> softReference = new SoftReference<>(str); //创建一个软引用关联该对象
WeakReference<String> weakReference = new WeakReference<>(str); //创建一个弱引用关联该对象
ReferenceQueue<String> objectReferenceQueue = new ReferenceQueue<>(); //创建一个引用队列
PhantomReference<String> phantomReference = new PhantomReference<>(str,objectReferenceQueue); //创建一个虚引用指向该对象,并关联引用队列
1、强引用

使用new关键词实例化出来并赋值引用的。该引用不可能会被垃圾收集器回收,会随着jvm的停止而回收

2、软引用

当出现内存不足时,会被gc回收

3、弱引用

只要垃圾收集器触发,则会被回收

4、虚引用

回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

languageStudents

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值