JVM笔记

本文详细探讨了Java对象的创建过程,包括类加载、内存布局、对象头的组成以及对象在内存中的存储。同时,讲解了运行时数据区的结构,如方法区、栈帧和垃圾回收机制,涉及不同垃圾回收算法如标记-清除、拷贝和压缩。此外,还讨论了对象分配、栈上分配策略以及垃圾回收器的组合,如PS、CMS等,并分析了GC相关问题和优化策略。

目录

4.3 对象的创建过程

4.4 对象在内存中的存储布局

1. 观察虚拟机配置

2.1 普通对象

2.2 数组对象

4.6 对象头

对象怎么定位

对象怎么分配

4.8 运行时数据区

方法区

4.9 栈帧

栈溢出

字节码分析

5.1 invoke指令

5.3 垃圾

如何找到垃圾

5.4 垃圾清除算法

1. 标记-清除

2. 拷贝

3. 标记压缩

5.5 堆内逻辑分区

5.6 栈上分配

5.7 对象何时进入老年代

常见的垃圾回收器

组合1:S+SO:不常用已被淘汰

组合2:PS+PO:生产环境默认组合

组合3:PN+CMS

PN vs PS

CMS

CMS的问题

算法

Card Table

G1

CollectionSet

RememberedSet

MixedGC

三色标记算法


JVM_阿狸男朋友的博客-优快云博客

4.3 对象的创建过程

>class loading:
    1. 将class文件load到内存
>class linking:
    1. Verification  验证文件是否符合JVM规定 CAFE BABE
    2. Preparation  静态成员变量赋默认值
    3. Resolution 将类、方法、属性等符号引用解析为直接引用,常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
>class initializing:
    1.调用类初始化代码 <clinit>,给静态成员变量赋初始值 (<clinit>是静态语句块,<init>是构造方法)
>开始new对象:
>申请对象内存
>对象成员变量赋默认值 (t=0)
>调用构造方法<init>
    1.成员变量按顺序赋初始值 (t=8)
    2.执行构造方法语句
        2.1 初始化super
        2.2 执行自己的构造方法语句

4.4 对象在内存中的存储布局

1. 观察虚拟机配置

java -XX:+PrintCommandLineFlags -version

C:\Users\Administrator>java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=400886336 
-XX:MaxHeapSize=6414181376 
-XX:+PrintCommandLineFlags 
-XX:+UseCompressedClassPointers 
-XX:+UseCompressedOops 
-XX:-UseLargePagesIndividualAllocation 
-XX:+UseParallelGC
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

2.1 普通对象

    1. 对象头:markword  8字节
    2. ClassPointer指针:指向xx.class文件在内存中的地址
        若虚拟机开启参数-XX:+UseCompressedClassPointers则占4字节,不开启为8字节
    3. 实例数据:如对象的成员变量int m=8中的m,String s = "xx",s指向另一个地址
        1. 基本类型:如int占4字节,byte占1字节
        2. 引用类型:如String或其他对象类型,若开启-XX:+UseCompressedOops 为4字节 不开启为8字节
           Oops: Ordinary Object Pointers
    4. Padding对齐,为保证对象总大小是8的倍数,进行位数补齐

2.2 数组对象

    1. 对象头:markword 8字节
    2. ClassPointer指针同上 4/8字节
    3. 数组长度:4字节
    4. 数组数据
    5. 对齐 8的倍数

public class T03_SizeOfAnObject {
    public static void main(String[] args) {
        System.out.println(ObjectSizeAgent.sizeOf(new Object()));//8+4+对齐4=16
        System.out.println(ObjectSizeAgent.sizeOf(new int[] {}));//8+4+4=16
        System.out.println(ObjectSizeAgent.sizeOf(new P()));// 32
    }

    //一个Object占多少个字节
    // -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
    // Oops = ordinary object pointers
    private static class P {
                        //8 _markword
                        //4 _class pointer
        int id;         //4
        String name;    //4
        int age;        //4
        byte b1;        //1
        byte b2;        //1
        Object o;       //4
        byte b3;        //1
    }
}

4.6 对象头

至少包括3位的锁信息,一位是否偏向锁,2位的锁标志位,GC的标记(分带年龄)

看对象的状态来真正分配这64位

1. hashcode部分

   31位hashcode->system.identityHashCode(...)
    按原始内容计算的hashcode,重写过的hashcode方法计算的结果不会放在这里
         如果对象没有重写hashcode方法,那么默认是调用os::random产生hashcode,可以调用System.identityHashCode获取
         os::random产生hashcode的规则为next_random=(16807seed)mod(2*31-1),因此可以使用31位存储;另外一旦生成了hashcode,JVM会将其记录在markword中
    当调用为重写的hashcode方法以及System.identityHashCode的时候
    GC年龄默认为15,原因是分带年龄占4位,最多到15
当一个对象计算过identityHashCode之后,不能进入偏向锁状态

对象怎么定位

访问对象两种方式--句柄和直接指针_进阶的科技花园~-优快云博客_句柄和直接指针

  1. 句柄池
  2. 直接指针

对象怎么分配

4.8 运行时数据区

方法区

方法区存储class文件

1. Perm Space (<1.8)
   字符串常量位于PermSpace
   FGC不会清理
   大小启动的时候指定,不能变
2. Meta Space (>=1.8)
   字符串常量位于堆
   会触发FGC清理
   不设定的话,最大就是物理内存

4.9 栈帧

1. Frame - 每个方法对应一个栈帧
   1. Local Variable Table 局部变量表
   2. Operand Stack 操作数栈
      对于long的处理(store and load),多数虚拟机的实现都是原子的
      jls 17.7,没必要加volatile
   3. Dynamic Linking 动态链接
       https://blog.youkuaiyun.com/qq_41813060/article/details/88379473 
      jvms 2.6.3
   4. return address 返回地址
      a() -> b(),方法a调用了方法b, b方法的返回值放在什么地方

栈溢出

虚拟机栈中存储每个方法的栈帧,当方法结束后这个栈帧就释放了,如果方法一直不结束就会一直占用虚拟机栈空间

字节码分析

int i = 8:
    bipush 8: byte 8有符号扩展为int 8,压入main方法的操作数栈
    istore_1: 将int 8从操作数栈出栈,将局部变量表下标1位置赋值为8

i = i++:
    iload_1: 将局部变量表1位置的数取出,压入操作数栈[即完成读取i操作,i=8]
    iincr 1 by 1: 将局部变量表1位置的数+1[即完成i++操作,局部变量表中i=9]
    istore_1:将8弹出操作数栈,并赋给局部变量表1位置,最终局部变量表i=8
i = ++i:
    iincr 1 by 1: 将局部变量表1位置的数+1[即完成i++操作,局部变量表中i=9]
    iload_1: 将局部变量表1位置的数取出,压入操作数栈[即完成读取i操作,i=9]
    istore_1:将9弹出操作数栈,并赋给局部变量表1位置,最终局部变量表i=9

5.1 invoke指令

1. InvokeStatic
2. InvokeVirtual
3. InvokeInterface
4. InovkeSpecial
   可以直接定位,不需要多态的方法
   private 方法 , 构造方法
5. InvokeDynamic
   JVM最难的指令
   lambda表达式或者反射或者其他动态语言scala kotlin,或者CGLib ASM,动态产生的class,会用到的指令

5.3 垃圾

如何找到垃圾

1. 引用计数RC(reference count),不能解决循环引用的垃圾团

 

2. 根可达算法RS(root searching)

5.4 垃圾清除算法

1. 标记-清除

适合老年代回收

2. 拷贝

适合新生代回收

3. 标记压缩

5.5 堆内逻辑分区

 

5.6 栈上分配

5.7 对象何时进入老年代

常见的垃圾回收器

JDK诞生时产生Serial 
-> 为了提高回收效率诞生了PS 
-> 为了缩短stop the world的时长, 诞生并发回收器CMS
    CMS是1.4版本后期引入,CMS是里程碑式的GC,它开启了并发回收的过程,
    但是CMS毛病较多,因此目前没有任何一个JDK版本默认是CMS
-> 为了配合CMS,诞生了PN

组合1:S+SO:不常用已被淘汰

组合2:PS+PO:生产环境默认组合

10G内存 PS+PO停顿时间

组合3:PN+CMS

PN vs PS

CMS

CMS的问题

1. Memory Fragmentation【内存碎片化】

内存碎片化可通过设置压缩来解决,代价是这个压缩过程会损失响应时间

-XX:+UseCMSCompactAtFullCollection 每次FGC时都进行压缩

-XX:CMSFullGCsBeforeCompaction 默认为0 指的是经过多少次FGC才进行压缩

2. loating Garbage【浮动垃圾】

浮动垃圾是所有并发/增量标记算法(包括增量更新和SATB)的固有特性: 是并发 GC 为了减少停顿时间(STW)所付出的代价——牺牲部分回收的即时性换取应用吞吐量。浮动垃圾会在下一次 GC 中被回收。

三色标记法的局限:在并发标记阶段,之前被标记为黑色(已扫描)的对象突然变为不可达,但由于黑色对象不会被重新扫描,这些对象就成为浮动垃圾,这些垃圾在重新标记阶段也不会被识别为不可达,以为重新标记关注的是新增的引用,而不是减少的引用,这些垃圾最终不会被此次GC回收

重新标记阶段的局限:重新标记阶段,如果a新增了到b的引用,则将a由黑色标为灰色,再次扫描a,如果a比较靠近root,那会比较耗时,这是他的缺点。重新标记阶段主要处理"漏标"(存活对象未被标记)问题,比如重新扫描a时就会把b由白色变成其他色,b就不会删除了。但是"错标"(已标记对象变为垃圾)问题解决不了,比如并发标记阶段a引用b,b已经标为黑色,但用户线程断开了a到b的引用,到重新标记阶段b是黑色的,不会被关注,在这次GC就不会被回收,成为浮动垃圾

标记与清理的时间差:CMS进行清理时,应用程序可能继续运行并生成新的垃圾对象,这些新对象无法在当前GC周期中被清理 

Concurrent Mode Failure 产生:
1.并发收集器无法在老年代用完之前完成垃圾对象的回收
    并发清理阶段会产生新的垃圾,这些垃圾就是浮动垃圾,如果老年代满了但是这些浮动垃圾还没来得及清理,这时请出SO单线程回收
2.老年代中无法满足升代所需的内存,产生PromotionFailed问题,则触发STW,用户线程全停止,专门进行垃圾回收
    CMS设计之初只为应付最多10G的内存,32G内存当old区满了,新生代年龄升级要往老年代放的时候,老年代满了
    这是CMS会召唤SO进行单线程的回收,由于内存过大,导致这个STW过程很慢
解决方案:
降低触发CMS的阈值,保持老年代有足够的空间 –XX:CMSInitiatingOccupancyFraction 92% 可以降低这个值,尽早触发CMS清理,让CMS保持老年代足够的空间

G1通过记录引用变化(STAB)和Remembered Set来调整对象着色,避免浮动垃圾 

算法

Card Table

由于做YGC时,需要扫描整个OLD区,效率非常低,所以JVM设计了CardTable, 如果一个OLD区CardTable中有对象指向Y区,就将它设为Dirty,下次扫描时,只需要扫描Dirty Card 在结构上,Card Table用BitMap来实现

 并发标记算法-三色标记算法

G1

吞吐量方面,G1比PS降低10%-15%,但响应时间/暂停时间可以降低到200ms

追求响应时间用G1,追求吞吐量用PS

CollectionSet

RememberedSet

MixedGC

如果G1产生FGC,你应该做什么?
    1. 扩内存
    2. 提高CPU性能(回收的快,业务逻辑产生对象的速度固定,垃圾回收越快,内存空间越大)
    3. 降低MixedGC触发的阈值,让MixedGC提早发生(默认是45%)

### 关于JVM的学习资源 对于希望深入学习 Java 虚拟机 (JVM) 的开发者来说,《黑马程序员 JVM 完整教程》是一份非常有价值的入门材料[^1]。这份笔记涵盖了 JVM 基础概念、内存模型以及性能调优等内容,适合初学者快速掌握 JVM 的核心知识点。 如果需要更深层次的理解,可以参考以下书籍: 1. **《深入理解 Java 虚拟机:Jvm 高级特性与最佳实践(第 2 版)》** 这本书详细讲解了 JVM 的内部机制,包括类加载过程、垃圾回收算法、性能优化策略等高级主题。它不仅提供了丰富的理论知识,还通过实际案例帮助读者解决常见的 JVM 性能问题。 2. **《实战 Java 虚拟机》** 此书侧重于 JVM 实战技巧,特别适用于那些希望通过真实项目经验提升技能的技术人员。书中包含大量实用工具和技术的应用场景分析。 3. **《深入 JAVA 虚拟机第二版》** 另一本经典著作,专注于 JVM 架构设计及其运行原理,能够为开发者的日常编码提供指导和支持。 除了上述书籍外,在线还有许多免费资源可供利用。例如官方文档总是最好的起点之一;另外像 Oracle 提供的各种技术白皮书也是不可多得的好资料。 需要注意的是,当涉及到并发编程时,了解重排序现象非常重要。因为即使代码按特定顺序编写出来,编译器或者处理器为了提高效率可能会改变它们的实际执行次序——只要这种变化不会影响单线程环境下的最终结果即可[^2]。 至于方法实现方面,则需确保函数签名中的返回类型同其逻辑体内的最后一句`return`表达式的类别相一致。否则将会引发编译错误[^3]。 以下是基于这些原则的一个简单示例展示如何判断一个数是否偶数并找出两个数值之间的较大者: ```java public class Example { public static void main(String[] args){ System.out.println(isEvenNumber(4)); // 输出true System.out.println(getMax(8,5)); // 输出8 } public static boolean isEvenNumber(int number ){ return ((number % 2)==0); } public static int getMax(int a,int b){ if(a>b) return a; else return b; } } ``` ####
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值