JVM的内存模型、对象创建、对象内存分配、对象内存回收、cms垃圾收集器


前言

本文主要介绍JVM的内存模型、JVM对象创建、JVM对象内存分配、JVM对象内存回收、垃圾回收算法


一、JVM内存模型是什么?

概念:内存模型也叫运行时数据区
在这里插入图片描述
内存模型有5个分区,根据线程是否共享可划分为:
线程私有: 程序计数器、本地方法栈、栈
线程共享:堆、方法区
各个区放了什么
程序计数器:字节码执行引擎,每执行一行代码,就更新程序计数器的值。
栈: 存放局部变量、动态链接、方法出口、操作数栈。栈的执行顺序是先进后出(FILO),即main方法是最后才执行。
本地方法栈:底层操作系统的函数
堆:存放对象信息,平常分配内存就是这块。
方法区:存放常量池、全局静态变量+类信息

二、 JVM内存参数如何设置?

分配内存的实战参数

在这里插入图片描述

java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar app.jar

在机器配置2核4G,推荐配置如下:

java -Xms1536M -Xmx1536M -Xmn512M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly

-Xms:堆的初始可用空间
-Xmx:堆的最大可用空间
-Xmn:新生代可用空间,即默认1/3的堆空间
-Xss:每个线程的栈大小
-XX:SurvivorRatio: eden区占整个年轻代空间的比例,默认为8,跟survivor刚好8:1:1
-XX:MetaspaceSize 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小),以字节为单位,默认是21M左右,它会动态调整阈值
-XX:MaxMetaspaceSize 设置元空间最大值,默认是-1.即不限制(或者说只受限于本地内存大小)。一般建议与-XX:MetaspaceSize设置成一样的值,因为调整元空间的大小需要full gc。
-XX:CMSInitiatingOccupancyFraction=75 和 -XX:+UseCMSInitiatingOccupancyOnly是配套使用的,-XX:CMSInitiatingOccupancyFraction=75是指老年代内存空间达到75%就会触发full gc

三、 对象的创建

在代码层面上,new关键词、对象克隆、对象序列化都能实现对象的创建。对象内包含静态变量、成员变量、常量。

类加载过程跟对象创建过程有什么关系?

对象创建之前就要进行类加载,然后再对对象进行初始化操作。如下流程图:
对象创建的主要流程如下:
类加载检查
类加载过程有如下几步:
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

  • 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  • 验证:校验字节码文件的正确性
  • 准备:给类的静态变量分配内存,并赋予默认值
  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用,下节课会讲到动态链接
  • 初始化:对类的静态变量初始化为指定的值,执行静态代码块
public class ClassLoad {


    static {
        System.out.println("加载代码块");
    }

    public ClassLoad(){
        System.out.println("加载无参构造器");
    }

    public void math(){
        System.out.println("math start");
        int a=1;
        int b=2;
        int c=a+b;
        System.out.println("math end");
    }

    public static void main(String[] args) {
        ClassLoad cl=new ClassLoad();
        cl.math();
    }
}
=====================================
加载代码块
加载无参构造器
math start
math end

四、 分配内存(堆空间分配)

划分内存的方法(内存分配跟垃圾回收机制有关,包含标记整理,标记清除等):

  • “指针碰撞”(Bump the Pointer)是JVM默认使用的方法,该方法下的堆内存排序是规整的,新创建的对象,会在相邻的位置分配空间。在垃圾回收机制中,通常采用标记整理将内存空间变得规整。
  • “空间列表”(Free List)该方法下的堆内存是无序的,JVM需要维护一张记录内存的列表,在垃圾回收机制中,通常是采用标记清除的算法。

划分内存的过程中,如果出现并发问题时,即多个线程抢占同一个内存空间,有两种解决方法:

  • CAS,抢不到就重试
  • 本地线程分配缓存(TLAB),指每个线程在堆中预先分配一小块内存,JDK8默认开启-XX:+UseTLAB,开启TLAB后,在初始化零值的过程中可以提前至TLAB分配时进行。这一步操作保证了对象的实际字段在JAVA代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

指针压缩

指针压缩是指8位压缩成4位,减少64位平台下内存的消耗。

对象内存分配

对象内存分配流程图:

在这里插入图片描述

对象栈上分配

Java中的对象都是在上进行分配的,只有开启逃逸分析(-XX:+DoEscapeAnalysis)和标量替换(-XX:+EliminateAllocations)后,JVM通过逃逸分析确定该对象不会被外部访问,则将该对象在栈上分配,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。

/**
 * 栈上分配,标量替换
 * 代码调用了1亿次alloc(),如果是分配到堆上,大概需要1GB以上堆空间,如果堆空间小于该值,必然会触发GC。
 * 
 * 使用如下参数不会发生GC
 * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
 * 使用如下参数都会发生大量GC
 * -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
 * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
 */
 public class AllotOnStack {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    private static void alloc() {
        User user = new User();
        user.setId(1);
        user.setName("zhuge");
    }
}

对象如何进入老年代

大对象直接进入老年代

大对象是指需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数-XX:PretenureSizeThreshold(单位是字节)可以设置大对象的大小,如果对象超过设置大小会直接进入到老年代。这个参数只在serial和parner两个收集器下有效。

//在serial收集器下,设置大对象为1MB
-XX:PretenureSizeThreshold=1048576 -XX:+UseSerialGC
//在parnew收集器下,设置大对象为1MB
-XX:PretenureSizeThreshold=1048576 -XX:+UseParNewGC

总结:大对象直接进入到老年代,是为了避免大对象分配内存时的复制操作而降低效率。

长期存活的对象将进入老年代

不同的垃圾收集器,对象晋升到老年代的年龄阈值不一样。可以通过参数-XX:MaxTenuringThreshold来设置。

//启用serial收集器,设置年龄阈值为15,CMS收集器默认6
-XX:MaxTenuringThreshold=15 -XX:+UseSerialGC

对象动态年龄判断

在minor gc之后触发对象动态年龄判断,当放对象的Survivor区域里,一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代。

Survivor区域里现在有一批对象,年龄1+年龄2+年龄N的多个年龄对象总和超过Survivor区域的50%内存,则就把年龄n(含)以上的对象都进入老年代。

老年代空间分配担保机制

开启-XX:-HandlePromotionFailure参数(JDK1.8默认开启)

年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间
如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象)
就会看“-XX:-HandlePromotionFailure”的参数是否已设置,如果已设置,则会查看老年代的可用内存大小是否大于之前每次minor gc后进入老年代的对象的平均大小,如果老年代剩余可用空间小于平均大小,则触发full gc,否则进入minor gc。
minor gc之后判断老年代剩余可用空间是否小于进入老年代的存活对象大小,若是小于则触发full gc。否则gc结束。

在这里插入图片描述

五、 对象内存回收

对象内存回收方法

引用计数法

实现方式:给对象添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
优点:实现简单,效率高。
缺点:很难解决对象之间的相互循环引用的问题。

 Object instance=null;
    public static void main(String[] args) {
        ClassLoad c1=new ClassLoad();
        ClassLoad c2=new ClassLoad();
        c1.instance=c2;
        c2.instance=c1;
        c1=null;
        c2=null;
        
    }

可达性分析算法

实现方式:将“GC Roots”对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象

  • 哪些是GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等。
  • 常见引用类型:强引用、软引用、弱引用、虚引用

如何判断一个类是无用的类

方法区主要回收的是无用的类,那么如何判断一个类是无用的类呢?
类需要同时满足下面3个条件才能算是“无用的类”:

  • 该类所有的对象实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收(自定义加载器才有可能)
  • 该类对应的java.lang.class对象没有在任何地方被引用,无法在任务地方通过反射访问该类的方法。

六、垃圾收集器

有哪些垃圾回收算法?

复制算法、标记-清除、标记-整理

垃圾收集器与垃圾收集算法的关系

如果说垃圾收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。

垃圾收集器有哪些?

  • Serial(串行): 单线程的垃圾收集器,在年轻代采用复制算法,在老年代采用标记-整理算法
  • parallel(并行):就是serial收集器的多线程版本,适用于小内存4个G以内,但是不能跟cms配合使用,原因是? 年轻代采用复制算法,老年代采用标记-整理算法
  • parnew(-XX:+UseParNewGC): 跟parallel类似,但是parnew可以跟cms配合使用,
  • cms[concurrent mark sweep](-XX:+UseConcMarkSweepGC):适用于内存4-8G(含),减少STW的时间,提升用户体验。
  • g1(-XX:+UseG1CG): 针对配备多颗处理器及大容量内存的机器,8G内存以上
  • zgc:适用于几百G以上。
    jdk1.8默认的新生代和老年代 都是用parallel。
    总结:
    parallel收集器关注点是吞吐量(高效的利用cpu)。
    cms收集器和G1关注点是用户线程的停顿时间(提高用户体验)。
    在这里插入图片描述

写屏障代码

void oop_filed_store(oop* field, oop new_value){
    pre_write_barrier(field); //对应update_barrier_set_pre 写屏障-写前操作,实现SATB
	*field=new_value;
	post_write_barrier(field,new_value);//update_barrier_set  写屏障-写后操作,实现增量更新,CMS采用这种
}
void pre_write_barrier(oop* field){
	oop old_value=*field;
	remark_set.add(old_value);
}
void post_write_barrier(oop* field,oop new_value){
    remark_set.add(new_value);
}

如何解决跨代引用问题

记忆集与卡表,两者关系类比Java中的Map与HashMap的关系。
年轻代和老年代都存在跨代引用问题,
比如:老年代整个内存空间被划分为一个个卡页(card page),里面包含多个对象,只要有一个对象的字段存在跨代指针,则对应的卡表(card table)的元素标识变成1,表示该元素dirty,把dirty的元素加入GC roots里进行可达性扫描。

七、JVM调优工具

jinfo

jinfo -flags : 这条命令会展示jar启动时的JVM参数。
jinfo -sysprops : 这条命令会展示系统属性

jstack

用jstack+ 进程号 可以查找死锁。命令如下:

jstack <pid>

用jstack+线程号 查看线程堆栈信息

jstack <pid>|grep -A 10 <线程id的十六进制>H,可在top视图内查看每个线程的内存情况。

jmap

jmap -histo pid 查看当前系统运行的情况
jmap -heap 查看堆内存信息

jmap -dump:format=b,file=重命名.hprof 导出堆内存信息
也可以设置内存溢出自动导出dump文件

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./ (路径)

通过jvisualvm命令工具导入.hprog的dump文件,进行分析

jstat (调优最重要的命令)

jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。
jstat -gc pid <间隔时间(毫秒)> <次数> ,此命令是垃圾回收统计。

jstat -gc 12345 1000 10 //针对12345进程,每秒统计一次,总共统计10次

如何通过这些工具来优化JVM

  • 通过jstat -gc 1000 10(每隔1秒执行1次命令,共执行10次) 观察系统的YGC(年轻代垃圾回收次数)、YGCT(年轻代垃圾回收消耗时间)、FGC(老年代垃圾回收次数)、FGCT(老年代垃圾回收消耗时间)、GCT(垃圾回收消耗总时间)等关键数据是否有异常,比如FGC > YGC 或 频繁YGC和FGC等。如果是FGC > YGC异常情况,可能是老年代担保分配机制的参数有问题。如果是频繁YGC,则先估算年轻代对象增长的速率。
  • jstat -gc pid 1000 10(每隔1秒执行1次命令,共执行10次),通过观察EU(eden区的使用量)开估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率从1秒上升到1分钟,甚至更长的时间来观察。注意,一般系统都有高峰期和日常期,所以需要再不同的时间分别估算不同情况下对象增长速率。
  • 知道了年轻代eden每秒增加的对象,就能根据eden区的大小推算出YGC大概多久触发一次。然后调整eden和survivor的大小。

总体优化思路就是尽量让每次YGC后的存活对象小于survivor区域的50%(对象动态年龄判断)。【1.加大eden区的空间 2.降低长期存活对象的年龄阈值】
尽量减少Full gc的频率。【1.设置大对象的大小。2.检查老年代空间分配担保机制(设置不好会出现1次minor gc,2次full gc)】

如何分析GC日志

java -jar -Xloggc:./gc-%t.log -XX:+PrintGCDetails -XX:+PrintGCDataStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M app.jar

-Xloggc:./gc-%t.log : 表示gc的时候,在指定路径输入gc日志文件,这个路径也可以是绝对路径(/usr/gc/)
-XX:+PrintGCDetails : 表示打印Gc 详细信息
-XX:+PrintGCDataStamps : 表示打印GC日志的系统时间
-XX:+PrintGCTimeStamps : 表示打印每个GC事件的时间戳
-XX:+PrintGCCause : 表示打印导致垃圾收集事件发生的原因
-XX:+UseGCLogFileRotation:表示启用垃圾收集日志文件的轮转功能,启用此参数后,JVM在当前工作目录下创建一个名为gc.log的初始GC日志文件,并在达到一定大小或一定时间间隔后,将当前日志文件重命名为gc.log.<序号>(gc.log.0)
-XX:NumberOfGCLogFiles=10 : 表示垃圾收集日志文件轮转10次,该参数跟-XX:+UseGCLogFileRotation配合使用,表示最大能保留垃圾收集日志文件为10,当GC日志文件达到最大数量时,旧的GC日志文件将被覆盖或删除。
-XX:GCLogFileSize=100M : 表示每个垃圾收集文件大小为100M。

借助工具gceasy(https://gceasy.io),上传gc文件,通过可视化界面展示GC情况。

总结

本文主要讲解
1.JVM的内存模型的概念和内存模型内各个分区的概念以及所存储的数据和JVM的参数设置。
2.区分对象创建过程和类加载过程的关系
3.对象内存分配的方法和对象内存分配节省空间的方法,以及内存分配流程及减少full gc的优化手段。
4.对象内存回收的方法
5.cms的垃圾收集器
6.JVM调优工具

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值