昨天写到了运行时数据区的5个组件 还有三点:java类加载器,GC收集器(对象回收),执行引擎(执行字节码):一般对jvm的优化时用不到。回顾下:1、java源码通过class编译器编译成java 的class文件 2、通过java的类加载器把class文件加载到内存里面去 3、然后将class文件进行拆分成5大块,存在元数据空间存储java类加载器所加载的类的信息,包括创建类的对象和classloader对象的引用。也存储常量池信息(字面量(就是常量)和符号的引用:字符串的名称) 4、java堆的内容是从元数据空间里面拿,拿到对象信息,然后在java堆中开辟一块内存,然后将里面的对象进行设值操作。
5、java栈里面的内容也是从元数据空间里面拿的
6、程序计数器是和java栈配套使用的,用来计数程序的行号
7、本地方法栈只是和本地方法打交道,不和java代码打交道
8、java栈是由执行引擎来执行的,局部变量表,操作数栈,动态链接,返回地址等信息被称作为栈帧,这就是一个函数包含这几个部分,但是在java虚拟机里面被称作为栈帧
今天的内容深入了解:java堆的对象的创建和GC收集器对对象的回收,和对jvm的调优
java的对象在创建的时候它是从元数据空间里面去拿,我们需要知道对象从哪里拿?
上面的图是整体创建对象的流程,贴上去,有点不清晰,有需要的联系我。
先从创建对象开始:看下对象是如何创建的?
1、首先要生成这个对象,最普通的方式需要new指令,new关键在在java虚拟机中就是一个指令,告诉虚拟机帮创建一个对象。常量池:是对引用信息和常量信息的一个抽象,只是一种叫法,存的是类的名称,包括字段的引用,以及字段的名称都存在常量池里面。2、符号引用:符号引用是一个常量,就是字符串不变的量
3、判断上面的App3是否被加载 4、如果没有加载的话就通过类加载器加载class文件,把类的所有的数据放到元数据空间,常 量池只是对这些数据的概括,也会创建一个class对象 5、进行对象内存的分配,内存分配需要从元数据空间中拿数据,拿的时候需要知道内存有多大。就是定义的App.class文件有多大, 类比:数据库,首先定义的表要有多大 6、内存是二维的结构,而且是连续的,如果内存中什么对象都没有的话,这时就会使用指针碰撞:一般是新生代。如果内存不是连续的,而是杂乱无章的话,这时就会进行空闲列表:一般用在老年代 这两种结构的原因:因为要进行GC回收来决定的 7、如果用了指针碰撞的话,开辟了一块内存,就标识了私人空间,而这里是有并发的,是有多线程的,如果说正在给这个对象开辟一块内存的时候被另外一个线程给这东西给拿走了。这里会判断是否并发的创建了其他对象?就是两个对象的创建共用一个指针来创建 8、如果是的话,有两个手段来解决:①、cas失败重试,就是第一次失败的时候,记录下来,进行下一次的重试,无限的去尝试,直到其他线程空闲时,我才开始创建。②、TLAB(线程预先分配一个,本地分配线程缓冲) 不进行失败的尝试,而是我自己单独开辟一个原子性的线程空间,线程的局部变量 9、对象内存分配完成之后 10、初始化设置对象属性默认值(零值) 11、如果外面的对象被某个线程给锁住的话,就有一个线程的标识。当用某个数据的时候,需要考虑对象如何添加进去,才能够取出来用。这个阶段进行对象的设置:对象的类的实例 hashCode ,对象Gc分代年龄等信息(设置到对象头) 4、线程锁住的状态-记录线程的编号 上面的步骤在java虚拟机中才算创建成功了。12、默认的构造函数实现自定义的初始化-执行init方法 13、对象创建成功
二、知道对象内部的结构:
对象内存布局:1、对象头:
1.1 mark-word
1.1.1 hashcode
1.1.2 Gc分代年龄
1.1.3 线程编号
1.1.4 锁编号(一个对象被锁住了)
1.1.5 对象创建的时间戳
1.1.6 引用计数
。。。。
1.2 对象的类型引用(指向元数据区的一个引用)
2、对象实例数据
自定义的字段值
父类的字段值
。。。。。
3、对齐填充(填充的不是数据)
必须保证对象的大小都是8字节的整数倍(只是hotSpot虚拟机规定的了,其他的虚拟机不是这么规定的)。
上面的就是整个对象的内部的结构,资源收集的阶段
三、如何把对象取出来用(对象访问)
对象创建成功之后会返回一个引用,通过引用找到对象具体的地方,然后就可以拿出来用了
对象的访问有两种方式:引用就是指针就是地址,在java里面就是引用,在c语言看来它就是指针。在操作系统看来,它就是地址。所以有不同的角度有不同的思考,说的都是同一个东西。
第一种方式:直接指针访问:
在java栈本地变量表里面直接存的是指针,然后用指针指向java堆中真正存储对象实例的地方,对象类型引用也指向元数据空间里面的对象类型数据。通过对象实例.Class对象的时候获取一个Class对象,通过Class对象就可以获取到所有字段的信息,因为这些都是存储在元数据空间里面的,也可以获取到方法的信息。
第二种方式:间接(句柄)指针访问 可以把句柄理解为表,专业术语是句柄池,可以理解为一张表而已。为什么称之为池呢?因为它是不连续的。但是表的数据是连续的,但是也可以把它理解 为表。这时的指针指向的是句柄池里面的指针,然后句柄池里的指针指向实例池里的数据 通过句柄池做为一个中间人。第二个指针指向元数据空间里的对象类型数据的。也就是通过Class对象直接指向对象类型数据了
区别:第一个性能比较高,因为cpu的寻址只寻址一次,一般都是用第一种的直接访问既然性能高,但是时间换空间,它的扩展性肯定不好。第二种是空间换时间,扩展性肯定比第一种要好,所以说有利就有弊。扩展性好的在于当对象改变的话,只需要改变句柄池里的指针的指向就行了。一般的是在虚拟机要求性能不是很高的情况下可以使用间接指针访问,因为句柄池占用一部分内存,一般性能要求很高的情况下可以用直接指针访问,一般用在机顶盒。上面的两种是通过虚拟机看的,通java代码看不了的。hotspot使用的是直接访问的方案。其他的虚拟机还是看虚拟机的厂商,默认使用的是直接访问的方案。
对象的删除:
先说下:什么是内存的溢出:(无法添加对象) 举个生活中的例子:比如家里买了很多的水果,把肉吃了,把皮搞掉,这个皮就是垃圾了,比如垃圾桶只能装10个香蕉皮,发现有11个香蕉皮,最后一个丢在客厅,这个时候会有很多的蚊子过来。
可以把内存比喻成垃圾桶,多出来的东西在java中就叫做内存的溢出。解决办法:垃圾桶买大号的。所以在java中加内存条。在溢出的话,考虑人有问题了,在程序中就考虑程序有问题了。
什么是内存的泄露:(存的是无用的对象):
举个生活中的例子:本来只有10个香蕉皮,我天天不倒, 在程序中是内存数据还在里面,内存数据已经是没有引用了,居然还在内存里面 这时导致可用内存变小了。这时导致内存泄漏,因为存的是一些没有用的对象。正常的情况下在java中是内存溢出。可以看日志看是内存泄漏还是内存溢出。
既然有了垃圾了:该怎么去收集呢?如何和正常的对象有个区别呢?
引出一个新的概念:引用计数:我这里用1以上表示有用的资源,0代表无用的资源 如果app3=new App3();执行这个操作的时候就会在对象头的引用计数里面加1. 表示对象被引用了一次,方法执行之后就减1了,如果没有被引用的话,这时对象头里面的引用计数就为0。这时就会有可能产生内存泄漏的问题。在spring源码中这个引用是循环引用,这个时候又有一个循环引用出来:
这两个东西是一模一样的,没有什么区别。循环引用会导致内存泄漏,本来对象是无用了,还在占有.
如何去解决上面的问题呢?
1、可达性分析算法 Gc root:有了根之后,下面肯定会有链式的操作,由枝叶,树干 一般不再根的上面,对象都会被回收的。这就解决了循环引用的问题,这就是可达性分析算法。
什么称为链式呢?
上面的app3调用demo的方法,而在App3中还有对象的引用
这种就称之为循环的引用 那什么是根呢?
第一种:虚拟机栈中引用的对象
在下面,如果在主方法里面调用demo方法,而在demo方法中,通过app3调用demo方法,这个在demo方法就称之为根。这个根就称之为gc root引用
1、依据当前的引用往下找,找到在这个链上对象,我们就把它判断为资源,这时会遍历所有内存的对象,从根开始过滤找到所有的内存对象,依次找到上面所有的数据,和内存上面的对象进行对比过滤器的模式。
2、通过gc root找出来之后作为一个局部变量。然后把内存中对象取出来进行全部的遍历,然后和局部变量进行对比。如果不在这里的话,把这些对象进行标识一下,然后将这些对象开始进行回收。这时的标记可以用引用计数来标记。标记为0
第二种:静态变量也称之根:第三种:常量池中引用的变量
通过根来判断对象是否存活
GC怎么回收对象-垃圾回收的理念算法:
第一种会使用标记-清除算法 白色的代表:未使用的内存,黑色的代表需要被回收的对象
刚才通过可达性分析算法标注的对象需要被清理。绿色标注的对象代表以使用的对象就是存货的对象 ,不需要被清理的。我们需要把上面进行全局遍历,然后直接过滤,判断引用计数为0的就给它清除掉。所以叫做标记清除算法
1、在写内存分布的时候,要找到连续的分配的内存块才能够用指针碰撞,如果不是连续的内存块用空闲列表,可想而知空闲列表是很耗性能的。一个一个的去找是很耗时间的,提前触发gc的代价是导致所有的线程被中断,停掉。
2、gc回收也是线程,只不过是守护线程,在后台里面。线程处理cpu碎片的时候,所有的线程必须去等待。不管是Memeory GC还是Full GC都是把所有的线程终止掉,只是时间长短的问题。
3、效率不高的原因需要全盘扫描。
那么如何去解决碎片的问题和效率的问题呢?能不能不全盘扫描呢?
第二种算法:复制算法:内存空间分为:第一个是空闲的,第二个装内存有用的 这时的扫描只是扫描一半,提高了性能 1、先把存活的对象不删除,会减少第二次扫描。这时又会减少4分之三的时间 2、然后把存活的对象放到保留区域 3、然后把原先的地方全部清空,清空只需要一步 这时扫描的时间是第一个算法的4分之1.
第三个算法:标记整理算法
解决空间浪费的问题 就是在扫描的过程当中,也是全盘扫描只扫描一次,现在不分两个区域了
整理好也就是排序好之后一次性将需要被回收的对象给回收 所以空间和时间是相对的矛盾。第二种 算法虽然是时间换空间,第三种是用空间换时间。既然上面的三种GC算法都有缺点,可以把所有的优点都集中起来
就产生第四种算法:将对象进行分类:主要解决复制算法在复制大量存活对象的性能问题
1、在IBM公司里面调查过:对象的特点是98%的对象都是朝生夕死(这只是一个理论):就是说对象的寿命不是非常长的,就可以把这类的对象称之为新生代。然后把百分之2的对象称之为老年代。
2、在我们程序中百分之90的称之为新生代,百分之10的称之为老年代,因为有一个缓冲,在jvm里面把对象就分为两种类型-新生代,老年代 3、我们主要回收的是新生代的对象,GC的触发频率非常高 4、新生代使用的是复制算法,因为复制算法是4分之1扫描,在新生代有个比 例:8比1 将空间进行8比1分配,百分之90的对象都是需要被回收的,只有百分之10的对象才能逃掉,这个1又分成from to,1:1。总共 的比例是8比1 5、由于老年代不是经常回收的,所以GC的时候的频率是非常低的,同时要节省内存空间,标记清除算法和标记整理算法都是节省内存空间的,但是普遍使用的是标记整理算法。以空间换时间。
算法的使用:根据垃圾回收器来选用算法。
明天详细写下-GC的日志,参数配置,监控工具,如何去调优,今天就分享这里。。