方法区是规范,永久代和元空间都是方法区的具体实现。
本地内存?也称为直接内存、操作系统内存、native memory
最小 20.75M、最大 256T 无限
JVM的程序计数器是程序模拟出来的,和操作系统的程序计数器(EIP)不一样
上图中绿框中的就是java中的程序计数,如上图当JVM 处理new操作时,会将3置入程序计数器(是3还是0???)
栈是线程私有的,创建线程时就会创建一个栈,栈的生命周期和线程同步;
栈中的变量不需要JVM进行GC回收,而是随着作用域的结束而释放,如一个方法执行完就会清理对应栈帧;
栈的大小和深度是固定的,在编译期就确定了。
栈包含多个栈帧,栈帧由方法调用创建,方法结束释放;
调用方法时,创建栈帧并压入栈顶。方法结束时栈会弹出栈顶栈帧。该栈帧会被jvm释放;
栈只会操作栈顶栈帧,不会同时操作多个栈帧;
栈帧组成:局部变量表、操作数栈、动态链接、返回地址
动态链接,存放的是这个方法在方法区的内存地址
恢复现场应该表达的更准确,a方法内调用b方法结束后,需要返回,这里要分两种情况,方法调用正常完成、方法调用异常完成。
每个栈帧内都包含一组局部变量列表。局部变量表长度从编译期就确定,存储在class文件的二进制表示中;
当调用类方法时,参数会依次传递到局部变量表从0开始的连续位置上;
当调用实例方法时,第0个局部变量永远存储的都是该实例方法所在对象的引用,即this,后续参数传递到1开始的连续位置。
知识点:非静态方法及构造方法中,局部变量表index=0的位置存放的都是this
顾名思义,操作数栈就是供字节码指令操作的数据栈。栈帧刚创建时,操作数栈是空的,一些字节码指令从局部变量表或对象实例的字段中复制常量或变量到操作数栈中,也有一些字节码指令可以从操作数栈中取走数据、操作数据或将操作结果重新入栈。调用其他方法时,操作数栈也用于准备调用方法的参数及接收方法的返回结果
堆,新生代和老年代默认分配比例是1/3
-Xms30M、-Xmx30M、-Xmn10M
把Java堆大小设置为30MB,不可扩展(防止内存抖动)。其中10M分配给新生代,另外20M分配给老生代。
新生代分Eden区、From区、To区,比例8/1/1
-XX:SurvivorRatio=8
来分配新生代各区的比例,设置为8,表示eden与一个survivor区的空间比例为8:1
堆的默认大小,最大是物理内存的1/64,最大是1/4
From区和To区会相互转换,???
内存担保机制 JVM内存分配担保机制
-XX:+HandlePromotionFailure
允许新生代收集担保,JDK1.5及以前内存担保默认关闭,1.6以后默认开启
内存担保是在JVM在内存分配的时候,新生代内存不足时,把新生代的存活的对象搬到老生代,然后新生代腾出来的空间用于为分配给最新的对象。这里老生代是担保人。在不同的GC机制下,也就是不同垃圾回收器组合下,担保机制也略有不同。在Serial+Serial Old的情况下,发现放不下就直接启动担保机制;在Parallel Scavenge+Serial Old的情况下,却是先要去判断一下要分配的内存是不是>=Eden区大小的一半,如果是那么直接把该对象放入老生代,否则才会启动担保机制
JVM中所有对象大小都按8字节倍数对齐,填充的那部分就是填充
巩固上述内容,现在看一下空对象的大小
空对象:没有任何普通属性的类生成的对象
- 开启指针压缩 16B
=8B(Mark Word) + 4B(Klass point) + 0B(数组长度) + 0B(实例数据) + 4B(对齐填充)- 未开启指针压缩 16B
=8B(Mark Word) + 8B(Klass point) + 0B(数组长度) + 0B(实例数据) + 0B(对齐填充)
开启(默认)/关闭指针压缩 -XX:+/-UseCompressedOops
通常64位JVM消耗的内存会比32位的大1.5倍,这是因为对象指针在64位架构下,长度会翻倍(更宽的寻址)。
对于那些将要从32位平台移植到64位的应用来说,多了1/2的内存占用
节省空间,提升jvm运行效率
-XX:+/-UseCompressedOops
(默认开启指针压缩)
oop:普通对象指针(ordinary object pointer),注意不是OOP(面向对象)
未开启指针压缩时,指针长度为8B,寻址单位是1B。
通过上文我们得知,jvm中对象的大小都是按8B对齐的,这能让我们有什么想法?
为什么我们不能将寻址单位扩大为对齐单位呢(想想,对齐单位的大小能调整吗?)
开启指针压缩时,指针长度为4B,寻址单位是8B(默认)
!!!!!希望能找到open jdk的实现逻辑来佐证一下,立个flag!!!!!!
思考1:开启指针压缩时,指针最大能表示的堆空间大小是多大?
指针压缩:指针8B长度(1B/单位) -> 4B长度(8B/单位)
8B * 8bit/B = 64bit 寻址长度
2^64 * 1B ??操作系统的寻址位只用了48位,最大支持2^48的寻址单位,jvm能超了这个?疑问?同上,有时间找open jdk源码看一下吧,这里有个flag
反正支持的寻址内存很大!
4B * 8bit/B = 32bit 寻址长度
2^32 * 8B = 32GB (32位操作系统支持最大4G内存,其寻址单位是1B,这里的单位是8B,那这里当然就是8*4G了)
哎呀,开启了指针压缩,堆支持的大小还变小了??
思考2:开启指针压缩时,若将堆内存大小设置成32G,会发生什么?
java -Xmx32g -XX:+PrintFlagsFinal hzw | grep -e UseCompressed
和下图命令同效果,因为默认开启指针压缩
上图除了Oop压缩以外,还发现一个ClassPointers?(普通对象指针和类指针)
什么现象?堆内存分配大于32G时,指针压缩设置失效了?为什么呢?
哈哈,我也不知道,我猜有两点
a. 8B指针寻址方式,内存范围更大,能超过32G;
b. (瞎猜)指针压缩的目的是防止新生代担保到老年代,从而导致频繁的fullGC。那你想想,这堆都分配了32G了,家里有钱何必再抠搜搜省这么点空间?
思考3:思考一下,能否通过开启指针压缩,然后扩大补齐单位从8B到16B,实现内存扩容支持?
我也不清楚了,就是有这个疑问(待确认)!
若扩大的补齐单位到16B的话,指令压缩支持内存是不是就能扩展到64G了,是否有jvm的参数控制这个?
-Xss
,指设定每个线程的堆栈大小
虚拟机栈默认大小为1M,如下
上图1024的单位是1k,下图演示通过
-Xss
指定栈大小为228
上例显示,1.6限定的最小堆栈大小为160k,1.8限定的最小堆栈大小为228k
思考1: 最小堆栈大小是怎么确定的?哪些因素影响?
??
下例通过无限递归产生栈溢出,通过变量记录下栈溢出时,最大的栈深度
思考:分析下上例栈溢出产生的原因
此例,栈溢出的原因:jvm虚拟机栈中,test()方法都会有一个栈帧,当test()方法内再调用test()方法时栈中就会再创建一个栈帧,当无限递归时,就会产生很多test()方法栈帧,直到给栈塞满,再创建栈帧时栈内存不够就发生栈溢出了。
思考:上例设置堆栈大小为228k,测试出栈深度为1515,那么每个test方法的栈帧大小大概是多少?
计算:(228 * 1024 / 1515)B
Java Memory Model,JMM规定了内存主要划分为主内存和工作内存两种。此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。
额外的图中lock也会锁住cpu3对主内存的read-load动作(StoreLoad),另外cpu2的store也会被锁住(StroeStore)直到后来缓存被抛弃
图中发现两个CPU可以同时执行read-load操作,从而才有后面故事的发生,若让count不能被同时read不就可以了?(synchronized来一波?)
balabala~说了一堆,我都不知道我在说啥,哈哈,看不懂的往后看,看完后面的回来再见
思考: volitaile的有序性和可见性底层实现和原理是什么?
- 看一下jvm在对volitaile变量进行写的时候做了什么
上图逻辑a.判断volatile b.store操作 c.调用storeload d.storeload方法中内联了一段汇编(实现了内存屏障)- storeload即java中的内存屏障,它是通过内联汇编实现的,如下(我也不懂,先假模假样的看看吧):
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
如上内联汇编后(摘自:深入理解java虚拟机):
LOCK用于在多处理器中执行指令时对共享内存的独占使用。它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效;另外还提供了有序的指令无法越过这个内存屏障的作用,也就解决了。
上面这些能理解这些吗? 理解好像又不理解?额~ 好吧,下面再补点货吧
扩展1:cpu的写操作、lock怎么引起他核无效化Cache的
- cpu的写操作有两中方式
a. 同步写:cpu输出数据直接保存到主内存中;
b. 异步写:cpu输出的数据线保存在缓存中,待cpu空闲时将缓存同步到主内存中。
异步写因为主内存的速度跟不上cpu的处理速度,所以通过高速缓存来提升cpu执行效率。虽然这个异步过程很快但是高压下这个在多核心处理同一变量时依然有问题(什么问题?自己想!),lock顺带解决了这个问题- lock的作用
上面“无法保证原子性示例图解”图中所示,lock通过锁住地址总线从而锁住目标内存区域的read并通过总线嗅探机制使其他cpu抛弃缓存。这就理解了上面提到的lock怎么解决cpu异步写的问题了
扩展2:cpu的缓存知识
- ALU:CPU计算单元,加减乘除都在这里算
- PC:寄存器,ALU从寄存器读取一次数据为一个周期,需要时间小于1ns
- L1:1级缓存,当ALU从寄存器拿不到数据的时候,会从L1缓存去拿,耗时约1ns
- L2:2级缓存,当L1缓存里没有数据的时候,会从L2缓存去拿,耗时约3ns
- L3:3级缓存,一颗CPU里的双核共用,L2没有,则去L3去拿,耗时约15ns
- RAM内存:当缓存都没有数据的时候,会从内存读取数据
- 缓存行:CPU从内存读取数据到缓存行的时候,是一行一行的缓存,每行是64字节(现代处理器)
注意:这里有个缓存行失效问题,会导致性能降低。举例描述就是:[volatile x, volatile y],因缓存行的存在,该数组内两个元素会一起加载到cup缓存中,当cup1对x变量操作并store时会触发lock(汇编),其他线程或者说cup2在操作y时就会因缓存失效而导致y也必须从主内存中重新加载,从而增加性能损耗,这就是缓存行失效。
思考:为什么volatile无法保证原子性?
见上文
疑问:lock(汇编)的范围是什么,StoreStore和StoreLord仅限当前变量?还是当前线程变量?
下文jvm规范能看出范围是控制在当前volatile变量中,具体细节不知道了,有其他补充资料的话再补货吧。。
该小节部分内容摘自:volatile如何保证并发编程中的可见性和有序性?原子性为何不行?