目录
运行时数据区
- 包含哪几个区域?都用来存放什么东西?
程序计数器
jvm虽然号称多线程,但是实际是通过cpu切换实现的,但从A线程切换到B线程是,A线程会暂停,在切回A线程时,A线程上次执行到哪,程序计数器就是记录这些信息的。线程私有
栈
线程私有,每个java方法执行的时候都会对应创建一个栈,用来存放方法局部变量等信息。生命周期随方法的结束而结束。
本地方法栈
和栈基本一样,只是执行的java方法为native方法
堆
用来存放对象实例,是GC回收的主要地方,因为现在垃圾回收器都采用分代回收,所以从内存回收角度看堆又分为新生代、老年代、持久代,在细分Edon和from survivor和to survivor。
方法区
存放类加载信息、常量、静态变量。
运行时常量池:方法区的一部分,jvm加载类的时候不只有类信息,还有一种常量池,常量池用来存放编译期生成的各种字面量和符号引用。
对象
- 对象的创建过程?分配内存的方法?如何保证线程安全?
- 对象的内存布局?
- 对象的使用?
对象的创建过程?
首先去方法区判断是否有符号引用(是否已经被创建过),
然后判断是否被加载、解析、初始化过,
分配内存
分配内存的方法?
指针碰撞:如果内存规整,可以采用这种方法,分配内存异动指针指定哪些被使用哪些未使用。
空闲列表:当内存不规整时,需要单独维护一个列表记录哪些内存可用。
如何保证线程安全?
1、CAS
2、TLAB(工作内存)
对象的内存布局?
对象在内存的存储布局分3块:对象头、实例数据、对齐填充
对象头:包括两部分,一部分存储对象自身数据,如哈希码、GC分代年龄等。
另一部存储对象指向元数据的指针。如果是数组则还要存储数组长度。
实例数据:真正的对象实例数据
对齐填充:jvm要求对象必须是8的倍数
对象的使用?
对象的访问方式两种:句柄和直接指针
句柄:对象的实例信息和类信息都存储在句柄池。稳定
指针:速度快。现在jvm使用
OOM异常
https://www.jianshu.com/p/2fdee831ed03
介绍几种OOM异常是什么?发生原因?解决方案?
- java.lang.OutOfMemoryError:Java heap space
- java.lang.OutOfMemoryError:GC overhead limit exceeded
- java.lang.OutOfMemoryError:Permgen space
- java.lang.OutOfMemoryError:Metaspace
- java.lang.OutOfMemoryError:Unable to create new native thread
- java.lang.OutOfMemoryError:Out of swap space?
- java.lang.OutOfMemoryError:Requested array size exceeds VM limit
- Out of memory:Kill process or sacrifice child
如何判断对象是否可被回收
引用计数法和可达性分析算法
引用计数法:给对象设置一个计数器,当有一个引用时加1,为0时代表可回收。简答高效,但是存在相互引用的对象无法被回收的问题,因此现在回收器很少使用。
可达性分析算法:现在主流语言都使用这个算法,通过GC Root为根开始向下找,没有引用的为可回收的。
可作为GC Root对象的:栈中引用的对象和方法区中引用的对象
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怎么办?(分配担保)
标记整理:当每次回收掉的内存较少时,如果还采用复制算法则效率就低下,此时采用标记整理,适合老年代
分配担保:
新生代Minor GC会把Eden和from Survivor中存活对象复制到to Survivor,但当存活对象大于to survivor时,jvm首先查看老年代最大连续可用空间是否大于新生代空间总和,如果大于则进行Minor GC并且多余部分进入老年代。如果小于则查看HandlePromotionFailure设置值是否允许担保,不允许则直接full GC。如果允许则判断老年代连续最大可用空间是否大于历次进入老年代对象平均值,如果大于则minor GC,否则fullGC。
枚举根节点(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。
jvm类加载顺序
在创建一个对象实例时,是如何一步步的进行代码运行的呢,一般来说,顺序如下:
1.首先是父类的静态变量和静态代码块(看两者的书写顺序);
2.第二执行子类的静态变量和静态代码块(看两者的书写顺序);
3.第三执行父类的成员变量赋值
4.第四执行父类类的构造代码块
5.第五执行父类的构造方法()
6.执行子类的构造代码块
7.第七执行子类的构造方法();
总结,也就是说虽然客户端代码是new 的构造方法,但是构造方法确实是在整个实例创建中的最后一个调用。切记切记!!!
**先是父类,再是子类;
先是类静态变量和静态代码块,再是对象的成员变量和构造代码块–》构造方法。**
记住,构造方法最后调用!!!!成员变量优先构造代码块优先构造方法!!
面试时问垃圾回收相关时如何回答
1、内存分为新生代、老年代、持久代
2、如何判断对象是否存活
3、垃圾回收几种算法
4、新生代介绍(eden、survivor)、以及对象从新生代到老年代的过程
延伸: 分配担保、 oopmap
垃圾收集器
新生代: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
类,导致“病毒代码”被执行。