JAVA运行时数据区
https://segmentfault.com/a/1190000014395186?utm_source=tag-newest
https://www.jianshu.com/p/8a58d8335270
[外链图片转存失败(img-UUgoGZ9Y-1564641694255)(/home/alwaysdazz/图片/1710354522-5ad304bfcff05_articlex.png)]
栈:当一个方法被执行的时候会有一个栈帧进入栈内,当方法正常返回后或者返回异常后会出栈,栈帧包括局部变量表,局部变量表内存储的是方法的变量
public class StackFoo {
public static void foo(){
System.out.println("进入栈中");
foo();//无线递归调用自己
}
public static void main(String[] args) {
foo();//栈溢出 java.lang.StackOverflowError
/*
每当一个方法被执行的时候会有一个栈帧进入到栈内,当一个方法正常返回或返回异常都会出栈
栈帧包括局部变量表,操作数帧,动态链接,返回地址
局部变量表存储着方法需要用的所有变量,局部变量表所需要的空间在编译期间完成分配,后续不在改动
*/
}
}
堆:是jvm管理最大的一块内存,也是存储对象的地方,也是gc最常光顾的地方,java堆是线程共享区,此内存区域的唯一目的就是存放对象的实例,几乎所有的对象实例都在这里分配内存,存放new生成的对象和数组
方法区:方法区也是线程共享区,里面存放的是静态变量,常量和静态代码块,即编译器编译后的代码数据(类的信息,版本,字段,实现的方法和接口),方法区并不等于永久代,只有在hotspot虚拟机中为了减少代码管理内存将永久代实现了方法区,垃圾回收器在此回收效率极低,回收成本高,如果栈中对象引用方法区中的值都一样则只有一个实例(这里用的是hash去重),在这里的静态变量,常量和类的静态属性都是可以当做可达性分析法的roots的对象(扫描垃圾的算法)
JMM(JAVA Memory Model)内存模型
1.java的并发采用共享模型,线程间通过读写内存中公共状态进行通讯,多个线程之间是不能通过直接传递数据交互,他们之间的交互只能通过共享变量实现。
2.java内存模型规定所有变量都存储在主内存中,每个线程都有自己的工作内存
1)不同线程之间的数据无法直接访问对方的工作内存,线程间变量值的传递需要通过主内存来完成,当一个线程修改了此变量的副本时,则会将修改的变量副本flush刷新到主内存中
2)工作内存对应于虚拟机栈中,主内存对应于堆内存中的一部分实例
3)线程的工作内存中保存了被该线程使用到的主内存变量的副本,线程只能在工作内存中对变量副本修改而不能访问主内存中修改变量
3.java线程之间的通信由内存模型JMM(Java Memory Model)控制
1)线程之间的共享变量存放在主内存中
2)每个线程都有一个本地的工作内存,里面存储了主内存共享变量的副本
3)JMM通过控制每个线程的本地内存数据交互来保证程序员对内存的可见性
4.可见性,有序性
1)当一个共享变量在多个本地内存中有副本时,如果一个线程修改了此变量的副本,其他变量能看到修改后的值,此为可见性
2)保障线程的有序性(保障线程的安全)
[外链图片转存失败(img-WBxRsnhA-1564641694258)(/home/alwaysdazz/图片/1532709451-5ad313f437a51_articlex.png)]
在JVM内部,Java内存模型把内存分成了两部分:线程栈区和堆区
JVM中运行的每个线程都拥有自己的线程栈,线程栈包含了当前线程执行的方法调用相关信息,我们也把它称作调用栈。随着代码的不断执行,调用栈会不断变化。
[外链图片转存失败(img-qrniTkEb-1564641694259)(/home/alwaysdazz/图片/2019-07-31 21-37-18屏幕截图.png)]
堆区包含了Java应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)。不管对象是属于一个成员变量还是方法中的局部变量,它都会被存储在堆区,一个局部变量如果是原始类型,那么它会被完全存储到栈区。 一个局部变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区。对于一个对象的成员方法,这些方法中包含局部变量,仍需要存储在栈区,即使它们所属的对象在堆区。 对于一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区。Static类型的变量以及类本身相关信息都会随着类本身存储在堆区。
Java内存模型带来的问题
可见性问题
CPU中运行的线程从主存中拷贝共享对象obj到它的CPU缓存,把对象obj的count变量改为2。但这个变更对运行在右边CPU中的线程不可见,因为这个更改还没有flush到主存中:要解决共享对象可见性这个问题,我们可以使用java volatile关键字或者是加锁
[外链图片转存失败(img-kK3DyXsB-1564641694262)(/home/alwaysdazz/图片/2019-07-31 21-42-17屏幕截图.png)]
竞争对象(并发性)
线程A和线程B共享一个对象obj。假设线程A从主存读取Obj.count变量到自己的CPU缓存,同时,线程B也读取了Obj.count变量到它的CPU缓存,并且这两个线程都对Obj.count做了加1操作。此时,Obj.count加1操作被执行了两次,不过都在不同的CPU缓存中。如果这两个加1操作是串行执行的,那么Obj.count变量便会在原始值上加2,最终主存中的Obj.count的值会是3。然而下图中两个加1操作是并行的,不管是线程A还是线程B先flush计算结果到主存,最终主存中的Obj.count只会增加1次变成2,尽管一共有两次加1操作。 要解决上面的问题我们可以使用java synchronized代码块。
[外链图片转存失败(img-2qNibi0b-1564641694264)(/home/alwaysdazz/图片/2019-07-31 21-44-20屏幕截图.png)]
堆的内存划分:
[外链图片转存失败(img-6x0NgygG-1564641694266)(/home/alwaysdazz/图片/2948191696-5ad31617eb2bd_articlex.png)]
Java堆的内存划分如图所示,分别为年轻代、Old Memory(老年代)、Perm(永久代)。其中在Jdk1.8中,永久代被移除,使用MetaSpace代替。
1、新生代:
(1)使用复制清除算法(Copinng算法),原因是年轻代每次GC都要回收大部分对象。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。
(2)分为Eden、Survivor From、Survivor To,比例默认为8:1:1
(3)内存不足时发生Minor GC
2、老年代:
(1)采用标记-整理算法(mark-compact),原因是老年代每次GC只会回收少部分对象。
3、Perm:用来存储类的元数据,也就是方法区。
(1)Perm的废除:在jdk1.8中,Perm被替换成MetaSpace,MetaSpace存放在本地内存中。原因是永久代进场内存不够用,或者发生内存泄漏。
(2)MetaSpace(元空间):元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
4、堆内存的划分在JVM里面的示意图:
[外链图片转存失败(img-D1tiUGkG-1564641694267)(/home/alwaysdazz/图片/2866120826-5ad3196c98b32_articlex.png)]
GC垃圾回收:
垃圾回收的意义:写java应用时内存由java虚拟机管理(申请内存释放内存),在开发的过程中不需要注意这块内容,但高并发环节出现瓶颈的时候,java垃圾回收就是优化重要的一个环节。
我们的对象什么时候能变成一个垃圾(如何判断一个对象为垃圾对象)?
①.引用 计数法
②.可达性分析法
①引用计数法:在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就+1,当引用失效的时候,引用计数器的值就-1(将对象的置为空)
当对象循环互相引用的时候就不能都分析对象是否为垃圾对象,虽然引用计数法灵活性,但是缺陷很大,不可用。
package Text;
public class biaojiqingchusuanfa {
private Object instance;
public static void main(String[] args)
{
Main.m1=new Main();//在栈内创建指向堆的示例
Main.m2=new Main();//在栈内创建指向堆的示例
m1.instance=m2;//将堆内的m1的引用指向m2
m2.instance=m1;//将堆内的m2引用指向m1
m1=null;//将栈内的m1指向为空
m2=null;//将栈内的m2指向为空
System.gc();//手动调用垃圾回收器
}
}
② 可达性分析法:通过一系列“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不一定会成为可回收对象。进入DEAD状态的线程还可以恢复,GC不会回收它的内存。(把一些对象当做root对象,JVM认为root对象是不可回收的,并且root对象引用的对象也是不可回收的)
2、 以下对象会被认为是root对象:
虚拟机栈(栈桢中的本地变量表)中的引用的对象
方法区中的类静态属性引用的对象
方法区中的常量引用的对象
本地方法栈中JNI(Native方法)的引用的对象
3、 对象被判定可被回收,需要经历两个阶段:
(1) 第一个阶段是可达性分析,分析该对象是否可达
(2) 第二个阶段是当对象没有重写finalize()方法或者finalize()方法已经被调用过,虚拟机认为该对象不可以被救活,因此回收该对象。(finalize()方法在垃圾回收中的作用是,给该对象一次救活的机会)
4、 方法区中的垃圾回收:
(1) 常量池中一些常量、符号引用没有被引用,则会被清理出常量池
(2) 无用的类:被判定为无用的类,会被清理出方法区。判定方法如下:
5、 finalize():
(1) GC垃圾回收要回收一个对象的时候,调用该对象的finalize()方法。然后在下一次垃圾回收的时候,才去回收这个对象的内存。
(2) 可以在该方法里面,指定一些对象在释放前必须执行的操作。
二、 发现虚拟机频繁full GC时应该怎么办:
(full GC指的是清理整个堆空间,包括年轻代和永久代)
(1) 首先用命令查看触发GC的原因是什么 jstat –gccause 进程id
(2) 如果是System.gc(),则看下代码哪里调用了这个方法
(3) 如果是heap inspection(内存检查),可能是哪里执行jmap –histo[:live]命令
(4) 如果是GC locker,可能是程序依赖的JNI库的原因
三、常见的垃圾回收算法:
1、Mark-Sweep(标记-清除算法):
(1)思想:标记清除算法分为两个阶段,标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。
(2)优缺点:实现简单,容易产生内存碎片
2、Copying(复制清除算法):
(1)思想:将可用内存划分为大小相等的两块,每次只使用其中的一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉。
(2)优缺点:不容易产生内存碎片;可用内存空间少;存活对象多的话,效率低下。
3、Mark-Compact(标记-整理算法):
(1)思想:先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存。
(2)优缺点:不容易产生内存碎片;内存利用率高;存活对象多并且分散的时候,移动次数多,效率低下
4、分代收集算法:(目前大部分JVM的垃圾收集器所采用的算法):
思想:把堆分成新生代和老年代。(永久代指的是方法区)
(1) 因为新生代每次垃圾回收都要回收大部分对象,所以新生代采用Copying算法。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。
(2) 由于老年代每次只回收少量的对象,因此采用mark-compact算法。
(3) 在堆区外有一个永久代。对永久代的回收主要是无效的类和常量
5、回收的时机
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC),它们所针对的区域如下。
minor GC:只针对新生代区域的GC。
Major GC:清理整个老年代,当eden区内存不足时触发。
Full GC:清理整个堆空间,包括年轻代和老年代。当老年代内存不足时触发
由于年老代与永久代相对来说GC效果不好,而且二者的内存使用增长速度也慢,因此一般情况下,需要经过好几次普通GC,才会触发一次全局GC。
对象的创建
1、 Java对象创建过程:
(1)虚拟机遇到一条new指令时,首先检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经加载、连接和初始化。如果没有,就执行该类的加载过程。
(2)为该对象分配内存。
A、假设Java堆是规整的,所有用过的内存放在一边,空闲的内存放在另外一边,中间放着一个指针作为分界点的指示器。那分配内存只是把指针向空闲空间那边挪动与对象大小相等的距离,这种分配称为“指针碰撞”
B、假设Java堆不是规整的,用过的内存和空闲的内存相互交错,那就没办法进行“指针碰撞”。虚拟机通过维护一个列表,记录哪些内存块是可用的,在分配的时候找出一块足够大的空间分配给对象实例,并更新表上的记录。这种分配方式称为“空闲列表“。
C、使用哪种分配方式由Java堆是否规整决定。Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
D、分配对象保证线程安全的做法:虚拟机使用CAS失败重试的方式保证更新操作的原子性。(实际上还有另外一种方案:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,TLAB。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才进行同步锁定。虚拟机是否使用TLAB,由-XX:+/-UseTLAB参数决定)
(3)虚拟机为分配的内存空间初始化为零值(默认值)
[外链图片转存失败(img-I5UGWfWU-1564641694268)(/home/alwaysdazz/图片/20190204194536578.png)]
结果:user{name=null,age=0,genner=false}
对象的结构
例如这个对象的实例、对象的元数据信息、对象的Hash码、对象的GC分代年龄等信息,这些信息存放在对象的对象头中。
对象的定位访问的方式(通过引用如何去定位到堆上的具体对象的位置)
句柄:使用句柄的方式,Java堆中将会划分出一块内存作为作为句柄池,引用中存储的就是对象的句柄的地址。而句柄中包含了对象实例数据和对象类型数据的地址
使用句柄访问的好处是引用中存放的是稳定的句柄地址,当对象被移动(比如说垃圾回收时移动对象),只会改变句柄中实例数据指针,而引用本身不会被修改。
[外链图片转存失败(img-EIeJezxs-1564641694271)(/home/alwaysdazz/图片/1936499994-5ad31ed04297d_articlex.png)]
直接指针:使用直接指针的方式,引用中存储的就是对象的地址。Java堆对象的布局必须必须考虑如何去访问对象类型数据。
使用直接指针的好处,节省了一次指针定位的时间开销。
[外链图片转存失败(img-j5ZjVXhZ-1564641694273)(/home/alwaysdazz/图片/1299747618-5ad31edf7d168_articlex.png)]
JVM优化
对象什么时候进入老年代?
1.多次Minor GC将对象从enden到suriver然后是老年代
2.大对象指向存入老年代,一半多指数组这些需要连续内存的对象
3.当suriver区中相同年龄的对象占总数的一般多时则将该年龄段的放入老年区,无需等待gc年限
老年代什么时候发生gc? 1.调用system.gc 2.老年代空间不足 3.Minor GC空间分配担保失败
类加载机制
1.加载:查找并加载类的二进制数据(把class文件里面的信息加载到内存里面)
2.连接:把二进制数据文件放入到运行时内存中
1).验证:确保类加载的正确性。
2).准备:将其初始化Wie默认值
3).解析:把类中的符号引用转化为直接引用
3.初始化:为类的静态变量赋予正确的初始值
双亲委派机制
当一个类收到类加载器请求时,首先是通过父类加载,如果父类加载成功,则初始化子类,如果父类无法加载则子类自行查找本地方法进行加载,如果本地没有找到方法则爆出异常ClassNotFoundException
意义:提高系统的安全性,用户自定义的类加载器不可能加载,应该由父加载器加载可靠的类(如果用户自定义了恶意代码,系统加载器检查该代码不符合规范则不会继续加载)
本文深入探讨Java运行时数据区的结构与功能,包括栈、堆、方法区的特性,以及垃圾回收机制。详细解释了JMM内存模型,可见性、有序性问题,和垃圾回收算法,如复制算法、标记-整理算法等。此外,还介绍了对象的创建过程、对象的结构及定位访问方式。
319

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



