运行时数据区
程序计数器
jvm虽然号称多线程,但是实际是通过cpu切换实现的,但从A线程切换到B线程是,A线程会暂停,在切回A线程时,A线程上次执行到哪,程序计数器就是记录这些信息的。线程私有
栈
线程私有,每个java方法执行的时候都会对应创建一个栈,用来存放方法局部变量等信息。生命周期随方法的结束而结束。
本地方法栈
和栈基本一样,只是执行的java方法为native方法
堆
用来存放对象实例,是GC回收的主要地方,因为现在垃圾回收器都采用分代回收,所以从内存回收角度看堆又分为新生代、老年代、持久代,在细分Edon和from survivor和to survivor。
方法区
存放类加载信息、常量、静态变量。
运行时常量池:方法区的一部分,jvm加载类的时候不只有类信息,还有一种常量池,常量池用来存放编译期生成的各种字面量和符号引用。
对象的创建
给对象分配内存对象有两种方法:指针碰撞和空闲列表。
采用哪种方法由java堆是否规整决定,而java堆是否规整由垃圾回收器是否有压缩整理功能决定。
内存分配如何保证线程安全?
1、将分配动作进行同步处理
2、为每个线程单独划分一块线程内存(TLAB)。可以通过命令设置是否使用TLAB
内存分配完成后,接下来将分配到的内存化为零空间都初始值,保证对象实例字段在代码中可以不赋初始化值而直接使用。
设置对象头信息。
最后一步按照代码初始化实例字段的值。
对象的内存布局
对象在内存的存储布局分3块:对象头、实例数据、对齐填充
对象头:包括两部分,一部分存储对象自身数据,如哈希码、GC分代年龄等。另一部存储对象指向元数据的指针。如果是数组则还要存储数组长度。
实例数据:真正的对象实例数据
对齐填充:jvm要求对象必须是8的倍数
对象的访问定位
对象的访问方式两种:句柄和直接指针
句柄:对象的实例信息和类信息都存储在句柄池。稳定
指针:速度快。现在jvm使用
OOM异常
java堆溢出
对象不停创建,堆内存不够时发生。
-XX:+HeapDumpOnOutOfMemoryError参数会让jvm出现内存溢出异常时dump出快照
-Xmx和-Xms设置堆大小
解决思路:首先用工具分析dump,判断是泄漏还是溢出,如果泄漏找到GC Root引用链优化,如果是溢出,检查虚拟机堆参数,尝试减少对象生命周期
栈溢出
-Xss参数设置栈大小
方法区和运行时常量池溢出
-XX:PermSize和-XX:MaxPermSize参数设置方法区大小,同时间接限制运行时常量池大小。
如何判断对象是否可被回收
引用计数法和可达性分析算法
引用计数法:给对象设置一个计数器,当有一个引用时加1,为0时代表可回收。简答高效,但是存在相互引用的对象无法被回收的问题,因此现在回收器很少使用。
可达性分析算法:现在主流语言都使用这个算法,通过GC Root为根开始向下找,没有引用的为可回收的。
可作为GC Root对象的:栈中引用的对象和方法区中引用的对象
对象引用分为四种:
强引用:new 一个对象
软引用:当系统要发生内存溢出之前,将会回收该类型应用对象,如果内存还不够才会内存溢出
弱引用:弱引用对象只生存到下次垃圾回收之前,当垃圾收集工作时,无论内存是否够用都会回收掉
虚引用:最弱的引用,唯一目的:在对象被回收时会收到一个系统通知
finalize()方法
因为有些内存java无法回收:比如native发放开辟的内存、socket或者文件的释放等,可以使用finalize方法去释放(类似c++的函数)
finalize执行过程:GC的时候,发现对象不可达,首先判断是否覆盖finalize方法,未覆盖则执行回收,弱覆盖则判断finalize方法是否被执行过,弱未执行过,则将对象标记放入F-queen队列中,稍后会由低优先级的Finalizer线程执行这个队列,执行完毕后GC会再次判断对象是否可达,弱不可达则回收,可达则对象复活。
有不确定性,不能保证该方法何时执行或是否被执行,不建议使用。
垃圾收集算法
标记清除、复制、标记整理
标记清除:低效、产生大量内存碎片
复制:会牺牲内存。现在虚拟机的新生代采用这种方法,新生代分1个Eden和两个Survivor,比例8比1,每次使用Eden和一个Survivor,发生GC的时候会把Eden和使用着的Survivor中对象复制到另一个Survivor,然后清空Eden和使用着Survivor,最后对调Survivor。适合GC存活对象较少时。新生代每次都会回收掉大量内存,所以适合这种方法。
Eden和Sur比例为8:1,所以新生代内存使用率达到百分之90,但是存在一个问题,当GC时存活对象超过百分10怎么办?
分配担保:对象直接通过分配担保机制进入老年代
标记整理:当每次回收掉的内存较少时,如果还采用复制算法则效率就低下,此时采用标记整理,适合老年代
枚举根节点(oopMap)
https://my.oschina.net/u/1757225/blog/1583822
每次GC之前都要枚举根节点来判断哪些对象可以被回收,但是如果每次都全量准确检查的话效率很慢。
为了解决这个问题,最初保守式GC:在进行GC的时候,会从一些已知的位置(GC Roots)开始扫描内存,扫描到一个数字就判断他是不是可能是指向GC堆中的一个指针(这里会涉及上下边界检查(GC堆的上下界是已知的)、对齐检查(通常分配空间的时候会有对齐要求,假如说是4字节对齐,那么不能被4整除的数字就肯定不是指针),之类的。)
个人理解这个并没有做是否指向堆对象的检查,这种方式会把很多可被回收的对象保留下来。
现在jvm采用精准式GC:在类加载和编译的时候,jvm就会把对象内是什么类型的数据记录下来存放到oopmap中,这样每次枚举根节点的时候就会直接判断是否为指针。
对象是随时变化的,每次都更新oopmap不行,采用安全点的概念,每个安全点会生成一批oopmap。
每次到安全点要进行生产oopmap的时候,会在安全点设立一个标识,然后各个线程到达安全点时会检查该标识,然后挂起本线程,然后生产oopmap。
空间分配担保
新生代Minor GC会把Eden和from Survivor中存活对象复制到to Survivor,但当存活对象大于to survivor时,jvm首先查看老年代最大连续可用空间是否大于新生代空间总和,如果大于则进行Minor GC并且多余部分进入老年代。如果小于则查看HandlePromotionFailure设置值是否允许担保,不允许则直接full GC。如果允许则判断老年代连续最大可用空间是否大于历次进入老年代对象平均值,如果大于则minor GC,否则fullGC。
jvm类加载顺序
在创建一个对象实例时,是如何一步步的进行代码运行的呢,一般来说,顺序如下:
1.首先是父类的静态变量和静态代码块(看两者的书写顺序);
2.第二执行子类的静态变量和静态代码块(看两者的书写顺序);
3.第三执行父类的成员变量赋值
4.第四执行父类类的构造代码块
5.第五执行父类的构造方法()
6.执行子类的构造代码块
7.第七执行子类的构造方法();
总结,也就是说虽然客户端代码是new 的构造方法,但是构造方法确实是在整个实例创建中的最后一个调用。切记切记!!!
**先是父类,再是子类;
先是类静态变量和静态代码块,再是对象的成员变量和构造代码块–》构造方法。**
记住,构造方法最后调用!!!!成员变量优先构造代码块优先构造方法!!
垃圾回收机制:
1、内存分为新生代、老年代、持久代
2、如何判断对象是否存活
3、垃圾回收几种算法
4、新生代介绍(eden、survivor)、以及对象从新生代到老年代的过程
延伸: 分配担保、 oopmap
垃圾收集器:总共7个
新生代:Serial(串行)、ParNew(并行)、Parallel Scavenge(并行)
老年代:cms(并发)、Serial old(串行)、Parallel Old(并行)
都可以的G1
了解一下cms原理:https://blog.csdn.net/liuwenbo0920/article/details/53886431
cms标记清除,产生内存碎片
双亲委派模型:类加载器有很多种,自定义类加载器会继承jvm的加载器,当需要加载一个类的时候,会尝试用父类加载器去加载,如果可以继续尝试父类的父类,以此类推。
意义:黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。