《深入理解JAVA虚拟机》——学习笔记
Posted 知其然,后知其所以然
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《深入理解JAVA虚拟机》——学习笔记相关的知识,希望对你有一定的参考价值。
JVM内存模型以及分区
JVM内存分为:
1.方法区:线程共享的区域,存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
2.堆:线程共享的区域,存储对象实例,以及给数组分配的内存区域也在这里。
3.虚拟机栈:线程隔离的区域,每个线程都有自己的虚拟机栈,生命周期和线程相同。虚拟机栈描述方法执行的内存模型,以站栈帧为单位,每个栈帧存储和方法运行有关的局部变量表、操作数栈、动态链接、方法返回地址等信息。
4.程序计数器:线程隔离的区域,每个线程都有自己的程序计数器,存储程序当前执行的字节码的行号。
5.本地方法栈:线程隔离,和虚拟机栈类似,是虚拟机调用Native方法时使用的。
堆的分区,以及各个分区的特点:
Java堆是垃圾收集器管理的主要区域,按照分代收集算法的划分,堆内存空间可以继续细分为年轻代,老年代。年轻代又可以划分为较大的Eden区,两个同等大小的From Survivor,To Survivor区。默认的Eden区和Survivor区的大小比例为8:1:1,这个比例可以调节。在为新创建的对象分配内存的时候先将对象分配到Eden区和From Survivor区,在立即回收时,会将Eden区和Survivor区还存活的对象复制到To Survivor区中,如果To Survivor区的大小不能容纳存活的对象,会把存活的对象分配到老年区。总体来说,新创建的小对象会放在年轻代,年轻代的对象大多在下一次垃圾回收时被回收,老年代存储大的对象和存活时间长的对象。
对象的创建方法,对象的内存布局,对象的访问定位
对象的创建:
1.普通对象的创建过程:虚拟机遇到一条new指令时,首先检查这个指令的参数(类的类型)是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类时候已经被加载、解析、初始化过,如果没有要执行类加载过程。
2.数组对象的创建:虚拟机遇到一条newarray字节码指令会在内存中直接分配一块区域。
3.Class对象的创建:在虚拟机加载类的时候,通过类的全限定名获取此类的二进制字节流,再通过文件验证后把字节流代表的静态结构转化为方法区的运行时数据结构,并且在内存中生成一个代表这个类的Class对象,存在方法区中,作为这个类的各种数据的访问入口。
对象的内存布局:
对象在内存中的布局分为三块区域:对象头、实例数据、对齐填充
对象头:存储对象自身的运行时数据,包括哈希吗,GC分代年龄,锁状态标识,线程持有的锁,偏向线程ID,偏向时间戳等;对象头的另外一部分是类型指针,即对象指向它在方法区中的类元数据的指针,虚拟机通过这个指针来确定该对象是哪个类的实例。如果对象是一个数组,对象头还有一块用语记录数组长度的数据。
实例数据:对象真正存储的有效信息,是在类中定义的各种类型的字段内容。
对齐填充:虚拟机要求对象的大小必须是8字节的整数倍,对齐填充起占位符的作用,保证对象大小为8字节的整数倍。
对象的访问定位:Java程序通过栈上的引用数据操作堆中的具体对象,对象访问方式有两种:句柄访问,直接指针访问。
句柄访问:Java堆划分出一块区域用作句柄池,引用中存储对象的句柄地址,句柄中才实际包含着对象实例数据和对象类型数据各自的具体地址信息。
直接指针访问:栈中的引用直接指向对象在堆中的地址,对象在头数据中指向方法区中其类元数据的地址。
使用句柄的好处是引用中存储的是稳定的句柄地址,在对象被移动(垃圾回收导致对象的移动)时只会改变局并重的实例数据指针。使用直接指针访问的好处是速度更快。
垃圾回收的判定方法:引用计数法,引用链法
引用计数法:给对象添加一个引用计数器,有对象引用计数器加1,引用失效计数器减1,计数器为0表示对象不再被使用,可以被回收。
引用链法(可达性分析):通过GC Roots作为起点,当一个对象到到GC Roots没有任何引用链相连时,证明对象时不可用的。
可作为GC Roots的对象是虚拟机栈中引用的对象、本地方法栈中引用的对象、方法区中类静态属性引用的对象,方法区中常量引用的对象(执行上下文和全局性引用)
Java的四种引用类型及特点:
1.强引用:程序中普遍存在的,类似“String s=”hello wold””这类的引用,强引用的对象不会被回收。
2.软引用:有用但是非必须的对象在系统将要发生内存溢出之前会对软引用的对象进行垃圾回收,SoftReference类实现软引用。
3.弱引用:非必须的对象,被弱引用关联的对象只能存活到下一次垃圾收集发生之前。
4.虚引用:最弱的引用关系,不能通过虚引用取得对象的实例,为对象设置虚引用的唯一目的就是在这个对象被收集器回收时收到一个系统通知。
四种引用强度依次减弱,强软弱虚。
GC的三种收集算法的原理和特点,用途,优化思路
三种垃圾收集算法:复制算法,标记-清除算法、标记-整理算法
标记-清除算法:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。缺点:标记和清除两个过程效率都不高;标记清楚后会产生空间碎片,空间碎片导致分配较大对象时可能提前出发垃圾回收。
复制算法:将可用内存分为两个区域,每次只使用其中一块,当使用的那一块内存用完时,将还存活的对象复制到另外一块内存中,然后把已使用过的内存空间一次清理掉。优点:解决的空间碎片问题,实现简单。缺点:将内存缩小为两块,内存使用率不高。复制操作频繁效率变低。
标记-整理算法:可回收对象标记后,让所有存活的对象向一端移动,然后清理掉边界以外的内存。优点:不会产生空间碎片,比复制算法提高了内存空间利用率。
复制算法用在年轻代的垃圾回收中,标记整理和标记清除算法用在老年代垃圾回收的收集器中。
GC收集器有哪些?CMS和G1收集器的特点
GC收集器按照回收区域不同,新生代有Serial,Parnew,Paralell Scanvage,老年代有Serial Old,CMS,Parallel old,还有新生代老年代通用的G1;
Serial 和Serial old是早期jdk中发布的垃圾收集器,特点是都为单线程,新生代采用复制算法,老年代采用标记整理算法,两个垃圾收集器在工作的时候必须要停掉所有的用户线程,直到收集完成后才能回复用户线程,由于是单线程工作方式,没有线程交互的开销所以能够活的最高的单线程收集效率,使用在client模式下的虚拟机。
ParNew收集器是Serial收集器的多线程版本,是年轻代的垃圾收集器,可以和Serial old以及CMS老年代收集器搭配使用。Parnew在单CPU环境中的性能没有Serial好,因为单CPU环境下的多线程按照时间顺序串行执行,还要承担线程间交互的额外开销,不过在多cpu环境下,Parnew的性能就会好很多,是运行在server模式下的虚拟机首选的新生代收集器。
在jdk1.4时新推出的垃圾收集器是Parallel Scanvage 和对应的Parallel Old,新生代基于复制算法,老年代基于标记整理算法.Parallel Scanvage也是并行性的多线程收集器,它和Parnew 的区别在于两者的关注点不同。Parnew关注于减少垃圾回收时用户线程停顿的时间,而Parllel Scanvage 关注点事获得最大的吞吐量,也就是CPU运行用户代码与CPU总消耗时间的比值。停顿时间短适合于和用户有交互的程序,吞吐量高则可以高效的利用CPU时间,尽快完成运算任务,主要是和在后台运算不需要太多的交互任务。
Jdk1.5时推出了能够和用户线程并发执行的CMS收集器,CMS是老年代垃圾收集器。CMS是一种以获取最短回收停顿时间为目标的收集器,基于标记清除算法来实现。它的工作过程先后分为初始标记、并发标记、重新标记、并发清除四个步骤,其中初始标记和重新标记是需要停顿用户线程的,并发标记和并发清理过程是可以和用户线程并发执行的,在整体垃圾收集时间里,初始标记和重新标记所占的时间很少,重新标记阶段又是可以多个垃圾回收线程并行执行的,所以整体用户线程停顿的时间很短。CMS的缺点:对CPU资源敏感,CMS默认启动的垃圾回收线程数为(CPU数量+3)/4,在并发阶段由于占用用户线程导致应用变慢,cpu不足4个时候对用户程序影响很大;CMS无法处理在并发清理阶段新产生的垃圾,只有等下一次垃圾回收标记后才能清除;CMS基于标记清除算法会产生空间碎片,CMS的解决方式是在进行Full GC时开启内存整理,这一过程无法并发,延长了用户线程的停顿时间。
G1收集器是在jdk1.7时推出的用语新生代和来年代的垃圾收集器,面向server模式。G1把内存区域划分成多个大小相同的独立区域,G1跟踪每个区域里面垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的区域,这种收集策略可以在有限时间内获取尽可能高的收集效率。G1垃圾回收过程:初始标记(单线程,停顿)、并发标记(单线程,并发)、最终标记(多线程,并行,停顿)、筛选回收(多线程,并行,停顿)。
Minor GC和Full GC分别发生在什么时候?
当创建对象分配的内存空间不足时会启动一次Minor GC,收集新生代的Eden区和From Survivor区,把还存活的对象分配到To Survivor区,如果To Survivor区的空间不足以容纳存活的对象,会把存活的对象分配到老年代,如果老年代也没有足够的空间会启动一次Full GC。
类加载过程:加载、验证、准备、解析、初始化
虚拟机的类加载机制就是把描述类的数据从Class文件(或者其他途径)加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
加载:1.通过一个类的全限定名获取定义此类的二进制字节流2、将这个字节流所戴晓的静态结构转化为方法区的运行时数据结构3、在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。
验证:1、文件格式验证,保证输入的字节流在格式上符合Class文件的格式规范,保证输入的字节流能正确的解析,只有通过这个验证,字节流才会存储在方法区之内2、元数据验证,对类的元数据进行语义校验,保证类描述的信息符合Java语言规范。比如验证类的是否实现了父类或者接口中的方法等3、字节码验证,通过数据流和控制流的分析,确保类的方法符合逻辑,不会在运行时对虚拟机产生危害4、符号引用校验,发生在解析阶段,确保解析阶段将符号引用转化为直接饮用的正常执行。
准备:正式为类变量(static)分配内存,并设置类变量初始值(数据类型的零值),这些变量所使用的内存在方法区中分配。
解析:虚拟机将常量池内的符号引用转化为直接饮用,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
初始化:初始化阶段才真正执行类中定义的Java代码,初始化阶段是执行类构造器<init>方法的过程。<init>方法(类构造器)是由编译器自动收集类中的静态变量和静态代码块合并产生的。子类和父类的初始化过程优先级为:父类类构造器->子类类构造器->父类对象构造函数->子类对象构造函数。类中静态类变量和静态代码块是按照在类中定义的顺序执行的。
什么时候进行类的初始化?
JVM规定了有且仅有5中情况——对类进行主动引用,必须立即执行类的初始化。
1)、遇到new,putstatic,getstatic,invokespecial四条字节码指令的时候,如果没有进行类的初始化要立即初始化。这四条字节码指令对应的编程中的环境为:使用new关键字实例化对象,读取或设置类的静态变量,调用类的静态方法。
2)、使用java.lang.reflect包对类进行反射的时候,如果没有初始化要立即初始化。
3)、初始化一个类的时候,如果其父类没有进行初始化要先出发父类的初始化
4)、虚拟机启动的时候,main方法所在的主类会被虚拟机先初始化
5)、使用动态语言在lava.lang.invoke.MethodHandle实例最后的解析结果是REF_getdtatic,REF_putStatic,REF_invokeStatic的方法句柄,这个句柄对应的类没有被初始化需要先触发其初始化。
双亲委派模型:
类加载器的双亲委派模型是指从顶层到底层分别是启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器。类加载器之间的父子关系不是通过继承来实现,而是通过组合来实现。双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,首先把这个请求委派给父类加载器去完成,所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类反馈自己无法完成类加载请求的时候,自加载器才会尝试自己去加载。
使用双亲委派模型的好处:java类随着他的加载器一起具备了带有优先级的层次结构,最基础的类由顶层的类加载器加载,这样保证在程序中使用该类的地方使用的都是这同一个类。
以上是关于《深入理解JAVA虚拟机》——学习笔记的主要内容,如果未能解决你的问题,请参考以下文章