JVM(二)JVM内存模型深度剖析与对象分配机制
JVM内存模型深度剖析与对象分配机制一、背景铺垫 /** 0. cd 文件所在目录 1. 将java文件编译成字节码文件 javac Math.java -->Math.class 2. 运行 java Math */ public class Math { public int add(int a, int b) { return a + b; } public static void main(String[] args) { Math math = new Math(); System.out.println(math.add(1, 2)); } } 复制代码
我们都知道java语言的一大特性就是夸平台,我们日常写的代码都是编译成字节码文件,运行在jvm虚拟机中,在开始JVM内存模型之前请简单运行此案例,并记住他二、JVM整体结构及内存模型
如图所示,JVM运行时一共分为如下几块数据区堆 - 我们都知道对是用来存放对象,堆有分成不同的区域,新生代、老年代;其中新生代有分为Eden 、s0 、s1 区域,再触发gc的时候 新生代触发minor gc 老年代触发full gc
方法区 - 常量 + 静态变量 + 类信息栈 - 线程运行在栈上本地方法栈 - 运行 native 修饰的方法程序计数器 - 记录当前线程正在执行的字节码指令的地址.它可以用来支持线程的恢复和继续执行。当线程被切换到另一个线程时,当前线程的程序计数器的值会被保存下来,当该线程被再次切换回来时,它的程序计数器的值会被恢复。这个过程可以让线程在被中断后继续执行。字节码执行引擎 — java 代码的执行是有字节码执行引擎执行 的,每执行一行代码都会字节码执行引擎都会修改线程栈中程序计数器的值。
我们都知道java 线成是运行在栈上面的,每个线程启动时会在栈上分配一块区域,供线程使用。当一个方法被调用时,Java 虚拟机会为该方法创建一个栈帧,并将其压入栈顶。当方法执行完毕后,虚拟机会弹出该栈帧,将控制权返回给调用该方法的方法。栈针包含局部变量表、操作数栈、动态链接、方法出口局部变量表 — 用于存放方法运行过程汇总的局部变量操作数栈 — 当方法执行时,它会从字节码中读取指令,并根据指令类型从操作数栈中弹出相应数量的操作数,执行指令后将结果压回操作数栈中。动态链接 — 具体来说,当程序调用一个方法时,虚拟机会在类的方法表中查找方法的符号引用,并将其转化为直接引用方法出口 — 因为java调用 在栈上变化就是栈帧的进栈和出栈,当一个方法调用完成之后需要回到调用该方法的地方 继续执行三、java 中的对象
我们都知道 我们在日常使用java的时候 通过 new 关键字创建的对象 大部分情况下都存储在堆中 (有一些情况会触发栈上分配后续讲),那么我们的对象 是个什么样的结构呢,以及对象创建的流程是什么样的呢?对象的结构对象结构包含两个主要部分:对象头和实例数据。对象头:对象头是一个固定大小的数据结构,用于存储对象的元数据信息,包括对象的哈希码、类元数据指针、锁状态标志等。对象头的大小和内容可能因 JVM 实现而异。
实例数据:实例数据是对象存储的主要部分,它包含对象的属性或字段的值,以及可能包含一些对象的方法和其他操作。对象创建流程
类加载检查 - 当我们去new 关键字创建对象的时候 jvm会先检查 对应的类是否已经加载进jvm内存,如果未加载,会先进行加载内存分配,我们知道对象是有大小的,并且存储在堆中,当我们要new 一个对象在堆中需要一小块连续的区域来存放对象 ,那么这块就有两个小问题了,jvm分配内存有哪些方法指针碰撞法指针碰撞(Pointer Bumping)是一种内存分配算法,通常用于基于物理地址的内存管理系统中。这种算法利用指针在内存中的连续性,将内存分为两个区域:已分配和未分配。当需要分配一块新的内存时,指针会直接指向未分配区域的空闲内存,将指针向前移动所需的内存块大小,然后返回指针的旧值作为分配的内存的起始地址空闲列表空闲列表(Free List)是一种内存分配算法中常用的数据结构,用于管理可用的内存块,即还未被分配的内存块。空闲列表通常是一个链表,链表中的每个节点代表一个空闲的内存块,节点中存储了该内存块的大小和起始地址等信息jvm如何保证对象分配过程中多线程同时分配不出现内存泄露问题呢?CAS compare and swap本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)java 中由于多线程共享同一块内存区域,在对象分配时会存在争抢的线程,本地线程分配缓冲指的是 每个线程在创建的时候会在堆中开辟一块线程独享的区域,当线程需要分配内存时,JVM会尝试从该线程的TLAB中分配内存。如果TLAB中没有足够的空间,就会退化为普通的堆内存分配,此时需要加锁以保证线程安全初始化零值— 对象创建完成后会对对象中的变量进行初始化,注意这会初始化时各种类型的默认值,这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。设置对象头 — 设置对象头相关信息执行对象init 方法 — 即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。四、对象内存分配流程
栈上分配
我们都知道java的对象是被分配在栈上的,当我们的对象没有被引用的时候,会通过gc 回收掉,但是如果创建很多没有被引用的对象 的话会给gc造成一定的压力,在我们创建对象的过程中jvm会通过一中逃逸分析技术来判断我们创建的对象是否会逃逸处我们的方法 ,如果无法逃逸出去,并且空间足够就会实行栈上分配,这样做就会随着线程运行完直接销毁空间,减少gc压力逃逸分析public User save1(){ User user = new User(); user.setId(1); user.setName("admin"); //TODO 保存数据库 return user; } public void save2(){ User user = new User(); user.setId(1); user.setName("admin"); //TODO 保存数据库 } 复制代码
save1 方法中创建的对象,最后会返回,也就是说user 对象有可能会被方法外的变量引用,但是save2 方法中创建的对象,逃不出save2 方法, 当save2方法运行结束 user 对象就会成为垃圾对象,对于这样的对象我们其实可以把对象分配到栈上,随着方法的技术释放内存
JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优先分配在栈上(栈上分配),JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)
**标量替换:**通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认开启。
**标量与聚合量:**标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。
测试栈上分配/** * 栈上分配,标量替换 * 代码调用了1亿次createUser(),如果是分配到堆上,大概需要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 Test { public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i < 1000000000; i++) { createUser(); } System.out.println(System.currentTimeMillis() - start); } public static void createUser() { User user = new User(); user.setId(1); user.setName("admin"); } } 复制代码对象在Eden 区分配
我们知道,大对数对象都是在eden区分配,当eden 区空间不足的时候会触发一次minor gc 将对象已到s0s1 区,我们先来测试一下,在测试之前我们来看下 minor gc 和full gc 有什么不同minor gc 和full gc 的不同minor gc 是发生在新生代的gc 特点是 频繁,耗时短full gc 是发生早老年代 gc 一般回收新生代,老年代、方法区 特点是 不频繁,耗时长
Eden与Survivor区默认8:1:1
大量的对象被分配在eden区,eden区满了后会触发minor gc,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块survivor区,下一次eden区满了后又会触发minor gc,把eden区和survivor区垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空的survivor区,因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可
JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy
示例:public class GCTest { /** * -XX:+PrintGCDetails * * @param args */ public static void main(String[] args) { byte[] allocation1, allocation2/*, allocation3, allocation4, allocation5, allocation6*/; allocation1 = new byte[60 * 1024 * 1024]; // allocation2 = new byte[8 * 1024 * 1024]; } } Heap PSYoungGen total 76288K, used 65536K [0x000000076ab00000, 0x0000000770000000, 0x00000007c0000000) eden space 65536K, 100% used [0x000000076ab00000,0x000000076eb00000,0x000000076eb00000) from space 10752K, 0% used [0x000000076f580000,0x000000076f580000,0x0000000770000000) to space 10752K, 0% used [0x000000076eb00000,0x000000076eb00000,0x000000076f580000) ParOldGen total 175104K, used 0K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000) object space 175104K, 0% used [0x00000006c0000000,0x00000006c0000000,0x00000006cab00000) Metaspace used 3276K, capacity 4496K, committed 4864K, reserved 1056768K class space used 362K, capacity 388K, committed 512K, reserved 1048576K Process finished with exit code 0 我们看到 eden 分配 60M 的空间 此时 eden区已经站慢,我们在打开 allocation2 的注释,在在看gc 日志 [GC (Allocation Failure) [PSYoungGen: 65372K->608K(76288K)] 65372K->62048K(251392K), 0.0220770 secs] [Times: user=0.07 sys=0.02, real=0.02 secs] Heap PSYoungGen total 76288K, used 9455K [0x000000076ab00000, 0x0000000774000000, 0x00000007c0000000) eden space 65536K, 13% used [0x000000076ab00000,0x000000076b3a3ef8,0x000000076eb00000) from space 10752K, 5% used [0x000000076eb00000,0x000000076eb98020,0x000000076f580000) to space 10752K, 0% used [0x0000000773580000,0x0000000773580000,0x0000000774000000) ParOldGen total 175104K, used 61440K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000) object space 175104K, 35% used [0x00000006c0000000,0x00000006c3c00010,0x00000006cab00000) Metaspace used 3276K, capacity 4496K, committed 4864K, reserved 1056768K class space used 362K, capacity 388K, committed 512K, reserved 1048576K 我们看到,此时触发了一次minorgc 会把eden 区 60M数据向**Survivor 区移动,但是survior 区空间 不足,最后将数据移动到了老年代** 复制代码大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效长期存活的对象进入老年代
jvm 每进行一次minor gc 每有没回收掉的对象 年龄就会+1 (对象年龄在对象头中又一个字段表示),当对象年龄大于 Jvm 设定的值时 (默认是 15 cms 回收器 默认是 6),就是把对象从新生代 挪到老年代 ,对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。对象动态年龄判断机制
当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了.
例如
-XX:MaxTenuringThreshold = 15年龄为1 的对象 占 33%年龄为2 的对象 占 33%年龄为3 的对象占 34%
当Survivor 区满的时候,对象按年龄从小算起 年领1+年龄2+年龄N 对象,指导对象占比空间超过 50%,会将 大于该年龄的以上的对象移入老年代
正对以上面的案例,年领1+年龄2 的对象 > 50%, 此时就会把 年龄≥2 的对象提前移入老年代老年代空间担保机制
年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间
如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象)
就会看一个"-XX:-HandlePromotionFailure"(jdk1.8默认就设置了)的参数是否设置了
如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。
如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生"OOM"
当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生"OOM"
五、判断对象是否是垃圾对象的两种方法引用计数法
一个对象没被一个变量引用,对象的引用计数就会加1 当对象引用计数为0的时候,可以判断次对象是垃圾对象。但是这存在一个循环引用的问题public class CycleRefObject { public CycleRefObject instance = null; public static void main(String[] args) { CycleRefObject a = new CycleRefObject(); // a 对象引用计数此时为1 CycleRefObject b = new CycleRefObject(); // b 对象引用计数此时为1 b.instance = a; // a 对象引用计数此时 +1 变为2 a.instance = b; // b 对象引用计数此时 +1 变为2 a = null; // a 对象引用计数此时 -1 变为1 b = null; // b 对象引用计数此时 -1 变为1 // 当我们程序运行完,a b 对象引用计数 分别为1 无法做到清零,这就是我们常说的循环引用 } } 复制代码对象可达性分析算法
将**"GC Roots"** 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
六、Java 中的引用类型
java 中一共有四种引用类型 强引用、弱引用、软引用、虚引用强引用 : 普通变量的引用public static User user = new User(); 复制代码软引用:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存public static SoftReference user = new SoftReference(new User()); 复制代码弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用public static WeakReference user = new WeakReference(new User()); 复制代码虚引用 : 几乎不用七、总结jvm 内从模型一共分我堆、方法区、栈、本地方法栈、程序计数器对象分配内存空间有两种算法 指针碰撞、空连列表,保证对象分不出现并发问题采用的方法是CAS 和TLAB对象分配流程中 逃逸分析 标量替换以及栈上分配对象分配到Eden 区,Eden 区和Survior 区默认是 8:1:1对象进入老年代的几种机制 大对象直接进入老年代 长期存活的的对象进入老年代 对象动态年龄判断机制 老年代空间担保机制JVM 判断对象是否是垃圾算法的两种算法 ,引用计数和可达性分析算法Java 中常见的四种引用类型 强引用 软引用 弱引用 虚引用本文中出现的一些配置参数 -XX:+DoEscapeAnalysis 开启逃逸分析 (jdk 7 之后默认开始,关闭使用 -XX:-DoEscapeAnalysis) -XX:+EliminateAllocations 开启标量替换 (jdk 7 之后默认开始,关闭使用 -XX:-EliminateAllocations) -XX:+UseAdaptiveSizePolicy 默认开启,功能是jvm 会动态调整 Eden 和Survior 区8:1:1的比例 关闭使用-XX:-UseAdaptiveSizePolicy -XX:MaxTenuringThreshold 进入老年代对象年龄阈值 (默认是15,cms 垃圾回收器是 6) -XX:TargetSurvivorRatio Survior 区触发动态年龄判断机制的阈值(默认 50%) "-XX:-HandlePromotionFailure"(jdk1.8默认就设置了) 老年代空间担保机制 -XX:+PrintGCDetails 打印gc 详情 -XX:+PrintGC 打印Gc
澳大利亚泳装品牌Seafolly20172018时尚广告大片Seafolly再次在其2018年夏季活动中扮演了一张著名的面孔。德国模特托尼加恩(ToniGarrn)出现在澳大利亚埃斯佩兰斯海滩拍摄的阳光普照下的图像中。由西蒙厄普顿(Simo
底线方远的权力有多大?别看他只是副科级,权力却不输院长在底线这部剧里面,作为老干部的靳东这一次又饰演了一个干部方远,在剧情里面,方远可谓是大写的惨字,各种糊涂事都给碰到了,以至于我都怀疑靳东以前真的当过法官,这才让人觉得他那么真实,但
微信小游戏不止小把戏用户需求或可延展出更多可能性。文丨海克财经许俊浩爆火近20天后,微信小游戏羊了个羊时下仍在颇受大众关注之列。我们不妨对这款产品的演进轨迹稍作回溯。9月10日前后,羊了个羊在微信抖音
阿里国际站基础课详解L1买家点击占比详解L1买家点击占比有很多刚刚开始做阿里国际站的朋友还不了解,什么是L1买家点击占比,一般都显示在哪里,有那些作用,今天我和大家详细解析营销L1买家点击占比,下面一起来看一下吧!L
太牛了,阿里这份开发手册几乎涵盖了SpringCloud所有操作前言SpringCloudAlibaba为分布式应用开发提供了一站式解决方案。它包含开发分布式应用程序所需的所有组件,可以轻松地使用SpringCloud开发应用程序。使用Spri
KEFLSXII有源蓝牙音响,让你听见生活最近想在家里打造一个家庭影院,发现要想实现家庭影院自由,最重要的就是音质,请教了音乐发烧友的朋友,他向我推荐了KEFLSXII有源蓝牙音响,入手体验了以后,发现真香。它的外观简约时
央视直播冠军战!孙颖莎挑大梁,国乒连轰3个30,中日会师决赛世乒赛女团两场半决赛已经结束,国乒30战胜兄弟球队中国台北晋级决赛,日乒则是在半决赛30击败德国队,中日会师女团决赛,也是众望所归的结果。国乒与中国台北队之间的半决赛率先进行,孙颖
看似人畜无害实为跨国毒枭央视天网栏目为您讲述!深夜机场突降不速之客,物流寄递牵出隐秘毒线,是谁辗转千里铤而走险?是谁藏身境外暗中操控?跨境贩运必遭雷霆摧毁,幕后黑手终将法网难逃。CCTV12社会与法频道天网栏目以没有行李的旅客
克里米亚大桥被炸,战争升级!A股又要交修桥费?十一长假即将结束,影响节后A股的两个不确定性因素外围市场和俄乌战争,今天可以总结一下了。关于外围市场,跟持仓者的心情差不多,总体是一个冲高回落,激动过后恢复平静的过程。美股5个交易
焦点访谈看江河奔腾讲中国故事央视网消息(焦点访谈)一条大河波浪宽,风吹稻花香两岸,这首歌,相信大家都很熟悉,长江黄河松花江珠江这些大江大河,我们也从小就知道。江河奔腾,润泽万物,是生命之源生产之要生态之基。但
一个人最好的风水是管住自己有句俗话说与人为善,于己为善,与人有路,于己有退。你对生活释放以善意,生活也会回报你以善意。一个人生命中最大的财富,就是管好自己的脸色,管住自己的嘴巴,以及管理自己的内心。01hr