jvm运行内存划分+部分GC知识+锁

下图来自:https://blog.youkuaiyun.com/marvel__dead/article/details/69220153




在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace) 。符号引用存储在native heap中,字符串常量和静态类型变量存储在普通的堆区中
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
java8中移除了永久代,新增了元空间,其实在这两者之间存储的内容几乎没怎么变化,而是在内存限制、垃圾回收等机制上改变较大。元空间的出现就是为了解决突出的类和类加载器元数据过多导致的OOM(out of memory)问题

https://www.cnblogs.com/dolphin0520/p/3613043.html
程序计数器:
线程私有,字节码解释器通过改变个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转等基础功能都需要这个计数器。
java虚拟机栈:
线程私有,描述的Java方法执行执行的内存模型。每一个方法从调用直至执行完成的过程就是对应一个栈帧在虚拟机栈中入栈到出栈的过程。(平时说的堆栈中的栈就是这一块)
64位长度的long和double占用两个局部变量空间(slot),其余只占一个。局部变量表所需的内存空间在编译期间完成分配,运行时不会改变局部变量表的大小。
reference类型,对象引用:不等于于对象本身,可能是一个指向对象起始地址的引用指针,也可能是一个指向代表对象的句柄或其他与此对象相关的位置。
returnAddress类型:指向一条字节码指令的地址
迭代次数过多,栈帧太大等会导致栈内存不足。(可通过减少最大堆等方法增大栈的容量)
java堆:
GC的主要区域,存放对象实例。所有的对象实例以及数组都在堆上分配(现在不是这么“绝对”了)
可以在物理上不连续,只在逻辑上连续。
字符串常量池被移到了堆中了
可细分为新生代和老年代。
GC中的新生代和老年代:https://www.cnblogs.com/sunshisonghit/p/6694590.html
方法区:
方法区与Java堆一样,是各个线程共享的内存区域。
运行时常量池:方法区的一部分,class文件中的常量池,用于存放编译期生成的各种字面量和符合引用,在类加载后进入方法区的运行时常量池存放。具有动态性,运行时也可以将新的常量放入池中,如string的intern方法。
对象的创立:
1.首先检查这个指令参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有没有,先执行对应的类加载过程。
类加载检查通过后,虚拟机为新生对象分配内存。因为GC是否带有压缩整理功能不同,分为两种方法:
1.指针碰撞(bump the pointer):堆内存绝对规整,分配内存就把指向分界点的指针向空闲内存挪动一段距离。
2.空闲列表(free list):堆内存不规整,根据记录空闲内存块的表来分配内存。
内存分配的线程安全问题:两个解决方案。1.cas加失败重试;2.在java堆中预先分配一小块内存,成为本地线程分配缓冲(TLAB)。
JVM内存分配,新建对象(很详细):https://www.cnblogs.com/wangjzh/p/5258254.html
cas:compare and swap https://blog.youkuaiyun.com/mmoren/article/details/79185862
cas为乐观锁
乐观锁和悲观锁:https://www.cnblogs.com/qjjazry/p/6581568.html
下图详情:https://blog.youkuaiyun.com/leunging/article/details/80599282

对于什么时候会在常量池存储字符串对象,我想我们可以基本得出结论: 1. 显示调用String的intern方法的时候; 2. 直接声明字符串字面常量的时候,例如: String a = "aaa";3. 字符串直接常量相加的时候,例如: String c = "aa" + "bb"; 其中的aa/bb只要有任何一个不是字符串字面常量形式(包括new String("aabb")),都不会在常量池生成"aabb". 且此时jvm做了优化,不会同时生成"aa"和"bb"在字符串常量池中.
理解:如果相加不是全常量形式的字符串(“111”),是不会直接在常量池生成相应的常量的,new string会建立两个对象,一个在堆里一个在常量池里。相加的未在常量池里的字符串在intern之后会在常量池内建立指向堆的引用。(string == string.intern()),但如果在intern之前声明了相同的字符串常量,则会先在常量池内建立常量而无视后来的string的intern,这个时候string!=string.intern()
string.intern方法:
intern用来返回常量池中的某字符串,如果常量池中已经存在该字符串,则直接返回常量池中该对象的引用。否则,在常量池中加入该对象,然后 返回引用。
图来自第二个博客

http://www.cnblogs.com/Kidezyq/p/8040338.html
https://blog.youkuaiyun.com/seu_calvin/article/details/52291082
Volatile、synchronized两者的区别联系
1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
3.volatile仅能实现变量的修改可见性,不能保证原子性(线程A修改了变量还没结束时,另外的线程B可以看到已修改的值,而且可以修改这个变量,而不用等待A释放锁,因为Volatile 变量没上锁);而synchronized则可以保证变量的修改可见性和原子性。
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞和上下文切换。
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
6.在使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原子操作。当变量的值由自身决定时,如n=n+1、n++ 等,volatile关键字将失效。只有当变量的值和自身无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile。
7.“锁是昂贵的”,谨慎使用锁机制。
↑:https://blog.youkuaiyun.com/baidu_39299382/article/details/80329962
GC和内存分配
判断对象是否存活:
1.引用计数法:对象添加一个引用计数器,有一处引用就加一。无法解决循环引用的问题,主流jvm中没有选择这个方法。
2.可达性分析算法:GC Roots的对象作为起点开始搜索,走过的路径为引用链(reference chain),如果一个对象到GC Roots没有任何引用链相连接时,则证明此对象不可用。主流
GC Roots包括:虚拟机栈(栈帧中的本地变量表)中引用的对象;方法区中类静态属性引用的对象;方法区中常量引用的对象;本地方法栈中JNI(Native方法)引用的对象。

引用:强引用,软引用(内存要溢出时对这些进行会回收),弱引用(只能活到下一次GC),虚引用(无实际影响,唯一目的是对象被回收时收到一个系统通知)
软引用和弱引用适合用来保存那些可有可无的缓存数据。虚引用最大作用在于跟踪对象回收,清理被销对象的相关资源。
weakhashmap时弱引用的一种典型应用,可以作为简单的缓存表解决方案。
可达性分析算法中不可达的对象,会首先处于“缓刑”阶段,需要至少经历两次死亡标记过程:没有引用,第一次标记,然后进行筛选,是否需要执行finalize()方法,如果没有覆盖finalize方法或者已经执行过则第二次标记。需要finalize执行时,首先放到F-Queue队列中,jvm会执行这个方法,但不会等待其结束(防止死循环),finalize可以使对象逃脱死亡,重新与引用链上的任何一个对象建立关联就行。

对象在内存中存储的布局可以分成三块:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头包括:Mark Word(上图) 和 Class Metadata Address(对象指向它的类元数据的指针,虚拟机通过这个指针来确定该对象是哪个类的实例)
https://blog.youkuaiyun.com/smileiam/article/details/80364641
- 偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。cas更改对象头
- 轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。cas更改对象头,无互斥量,通过自旋锁等待
- 重量级锁:有实际竞争,且锁竞争时间长。有互斥量,状态的转换,所以锁之间转换很消耗资源
锁可以升级,但是不能降级
由于它是互斥的,那么它的操作成本就非常的高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。
轻量级锁,不需要申请互斥量(mutex),通过cas和自旋锁实现。如果自旋锁一直无法获得资源,则膨胀为重量级锁。
偏向锁的竞争结果:根据持有偏向锁的线程是否存活
1.如果不活动,偏向锁撤销到无锁状态,再偏向到其他线程
2.如果线程仍然活着,则升级到轻量级锁
https://blog.youkuaiyun.com/u010648018/article/details/79750608

本文介绍了JVM运行内存划分,包括程序计数器、Java虚拟机栈、Java堆、方法区等,还阐述了对象创立过程及内存分配的线程安全问题。同时讲解了Volatile和synchronized的区别联系,以及GC和内存分配相关知识,如判断对象存活的方法、对象存储布局和锁的类型等。
945

被折叠的 条评论
为什么被折叠?



