内存模型以及分区,需要详细到每个区放什么。
方法区 用于存储以被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
堆 存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。
虚拟机栈 线程私有。每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
本地方法栈 虚拟机用到的Native方法服务
程序计数器 它是一块较小的内存空间,它的作用可以看做是当先线程所执行的字节码的信号指示器。
堆里面的分区:Eden,survival from to,老年代,各自的特点。
对象创建方法,对象的内存分配,对象的访问定位。
创建:new->检查这个指令参数是否能在常量池中定位到一个类的引用符号->判断这个类是否被加载、解析、初始化过->加载检查通过后,分配内存(指针碰撞,空闲列表)->对分配内存空间的动作进行同步处理(CAS配上失败重试、TLAB)->虚拟机需要将分配到的内存空间都初始化为零值->对对象进行必要的设置
内存分配:对象头(header)、实例数据(Instance Data)、对其填充(Padding)
对象访问: 句柄 直接访问;如果使用句柄访问的话,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体各自的地址信息。
GC的两种判定方法:引用计数与引用链。
对象死了么:引用计数算法;可达性分析算法(gc root)
java GC ROOTS的对象包含以下几种
虚拟机栈中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈种JNI(native方法)引用的对象GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路?
标记清楚:分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。该算法的缺点是效率不高并且会产生不连续的内存碎片。
把内存空间划为两个区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。优点:实现简单,运行高效。缺点:会浪费一定的内存。一般新生代采用这种算法
标记阶段与标记清除算法一样。但后续并不是直接对可回收的对象进行清理,而是让所有存活对象都想一端移动,然后清理。优点是不会造成内存碎片
新生代:复制算法 死得多 活的少
老年代:标记整理 活得多GC收集器有哪些?CMS收集器与G1收集器的特点。
JVM中的垃圾收集一般都采用“分代收集”,不同的堆内存区域采用不同的收集算法,主要目的就是为了增加吞吐量或降低停顿时间。
- Serial收集器:新生代收集器,使用复制算法,使用一个线程进行GC,串行,其它工作线程暂停。
- ParNew收集器:新生代收集器,使用复制算法,Serial收集器的多线程版,用多个线程进行GC,并行,其它工作线程暂停。使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。
- Parallel Scavenge 收集器:吞吐量优先的垃圾回收器,作用在新生代,使用复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间。使用-XX:+UseParallelGC开关控制使用Parallel Scavenge+Serial Old收集器组合回收垃圾。
- Serial Old收集器:老年代收集器,单线程收集器,串行,使用标记整理算法,使用单线程进行GC,其它工作线程暂停。
- Parallel Old收集器:吞吐量优先的垃圾回收器,作用在老年代,多线程,并行,多线程机制与Parallel Scavenge差不错,使用标记整理算法,在Parallel Old执行时,仍然需要暂停其它线程。
- CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于获取最短回收停顿时间(即缩短垃圾回收的时间),使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial Old进行内存回收,优先使用ParNew+CMS(原因见Full GC和并发垃圾回收一节),当用户线程内存不足时,采用备用方案Serial Old收集。
- GI收集器:cms的替代? 并行于并发 分代收集 空间整合 可预测的停顿
Minor GC与Full GC分别在什么时候发生?
- 为了分代垃圾回收,Java堆内存分为3代:新生代,老年代和永久代。
- 新的对象实例会优先分配在新生代,在经历几次Minor GC后(默认15次),还存活的会被移至老年代(某些大对象会直接在老年代分配)。
- 永久代是否执行GC,取决于采用的JVM。
- Minor GC发生在新生代,当Eden区没有足够空间时,会发起一次Minor GC,将Eden区中的存活对象移至Survivor区。Major GC发生在老年代,当升到老年代的对象大于老年代剩余空间时会发生Major GC。
- 发生Major GC时用户线程会暂停,会降低系统性能和吞吐量。
几种常用的内存调试工具:jmap、jstack、jconsole。
- jps:进程状态工具
- jmap:生成堆存储快照
- jstack:对战跟踪工具
- jconsole:一个可视化java见识和管理控制台
类加载的五个过程:加载、验证、准备、解析、初始化。
1.装载:
- (1).找到该类型的class文件,产生一个该类型的class文件二进制数据流(ClassLoader需要实现的loadClassData()方法)
- (2).解析该二进制数据流为方法区内的数据结构
- (3).创建一个该类型的java.lang.Class实例
- 在加载器的相关代码中可以看到,最终通过defineClass()创建一个Java类型对象(Class对象)。
2.验证:
class文件校验器需要四趟独立的扫描来完成验证工作,其中:第一趟扫描在装载时进行,会对class文件进行结构检查,如
(1).对魔数进行检查,以判断该文件是否是一个正常的class文件
(2).对主次版本号进行检查,以判断class文件是否与java虚拟机兼容。
(3).对class文件的长度和类型进行检查,避免class文件部分缺失或被附加内容。第二趟扫描在连接过程中进行,会对类型数据进行语义检查,主要检查各个类的二进制兼容性(主要是查看超类和子类的关系)和类本身是否符合特定的语义条件
(1).final类不能拥有子类
(2).final方法不能被重写(覆盖)
(3).子类和超类之间没有不兼容的方法声明
(4).检查常量池入口类型是否一致(如CONSTANT_Class常量池的内容是否指向一个CONSTANT_Utf8字符串常量池)
(5).检查常量池的所有特殊字符串,以确定它们是否是其所属类型的实例,以及是否符合特定的上下文无关语法、格式第三趟扫描为字节码验证,其验证内容和实现较为复杂,主要检验字节码是否可以被java虚拟机安全地执行。
第四趟扫描在解析过程中进行,为对符号引用的验证。在动态连接过程中,通过保存在常量池的符号引用查找被引用的类、接口、字段、方法时,在把符号引用替换成直接引用时,首先需要确认查找的元素真正存在,然后需要检查访问权限、查找的元素是否是静态类成员而非实例成员。
3.准备:
为类变量分配内存、设置默认初始值(内存设置初始值,而非对类变量真正地进行初始化,即类中声明int i = 5,但实际上这里是分配内存并设置初始值为0)
4.解析:
在类的常量池中寻找类、接口、字段、方法的符号引用,将这些符号引用替换成直接引用
5.初始化
对类变量赋予指定的初始值(这个时候int i = 5就必须赋予i以初值5)。这个初始值的给定方式有两种,一种是通过类变量的初始化语句,一种是静态初始化语句。而这些初始化语句都将被Java编译器一起放在方法中。
如前面所述,一个类的初始化需要初始化其直接超类,并递归初始化其祖先类,初始化是通过调用类的初始化方法完成的。此外,对于接口,并不需要初始化其父接口,而只需要执行该接口的接口初始化方法就可以了。双亲委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。
启动类加载器Bootstrap ClassLoader javahome/lib
↑
扩展类加载器Extension ClassLoader /lib/ext
↑
应用程序类加载器ApplicationClassLoader 用户类路径
↑
自定义类加载器
- 分派:静态分派与动态分派。
JVM过去过来就问了这么些问题,没怎么变,内存模型和GC算法这块问得比较多,可以在网上多找几篇博客来看看。
推荐书籍:《深入理解java虚拟机》