jvm原理与性能调优

文章目录

一、JVM内存结构

1.运行时数据区

2.直接内存

二、JVM中的对象

1.对象的创建

2.对象的内存布局

3.对象的访问定位

三、垃圾回收算法和垃圾回收器

1. 如何判断对象是已死

2.分代回收理论

3.垃圾回收算法

4.垃圾收集器

四、JVM执行子系统

1.Class文件结构

2.类加载机制

3.类加载器

4.双亲委派模式

五、JVM性能优化

1.内存溢出

2.内存泄露

3.JDK提供的优化工具


一、JVM内存结构

jvm原理与性能调优_第1张图片

1.运行时数据区

        虚拟机栈

        线程运行时,在执行每个方法时都会打包成一个栈帧,存储了局部变量表、操作数栈、动态链接方法出口等信息,然后放入栈中。方法的执行对应着栈帧出栈的过程。栈的大小默认为1M,可通过参数-Xss调整大小,如-Xss256k。

        本地方法栈

        与虚拟栈类似,执行native方法。

        程序计数器

        当前线程执行的字节码行号指示器,各线程之间独立存储。分支、循环、跳转、异常和线程恢复都依赖于它。此内存区域是唯一一个不会出现内存溢出情况的区域。

        方法区

        用于存储已经被虚拟机加载的类信息、常量、静态变量等数据。可通过参数(JDK1.8以后)-XX:MetaspaceSize, -XX:MaxMetaspaceSize调节,如-XX:MaxMetaspaceSize=3M。

        堆

        几乎所有的对象分配都在这里,也是垃圾回收的主要区域。可通过以下参数调节:-Xms:堆的最小值;-Xmx:堆的最大值;-Xmn:新生代的大小;-XX:NewSize:新生代最小值;-XX:MaxNewSize:新生代最大值;例如-Xmx256m堆划分为新生代和老年代,新生代又分为Eden区、Survivor1(from)区、Survivor2(to)区。

2.直接内存

        调用native函数直接分配的堆外内存,使用直接内存避免了JAVA堆与native堆来回复制数据,能够提高效率。默认与-Xmx 参数值相同为 100M。可以通过-XX:MaxDirectMemorySize 来单独设置直接内存的大小。

二、JVM中的对象

1.对象的创建

对象创建的过程

1)检查加载

        检查类是否加载,如果没有则执行类加载过程。

2)分配内存

        根据方法区的类信息分配内存空间大小。

        分配内存的方式:

        指针碰撞

        如果堆中的内存空间是规整的,所有用过的内存放在一边,空闲的内存放在另外一边,中间放着一个指针作为分界点的指示器,那么分配内存就是将指针向空闲的那边挪出与对象大小相等的距离。

        空闲列表

        如果堆中的内存空间不是规整的,已使用的内存和空闲内存互相交错,虚拟机就必须维护一个列表,记录哪些内存块是可用的,分配内存时从列表中找出一块足够大的空间分配给对象实例,并更新列表。

        内存分配的线程安全解决方案:      

        CAS机制

        虚拟机采用CAS+失败重试的方式保证更新操作的原子性。

        分配缓冲

        把内存分配的动作按照线程划分到不同的空间中进行。

 3)初始化

        内存分配完成之后,虚拟机虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,这步操作保证对象的实例字段可以不赋初始值就可以直接使用。

 4)设置

        虚拟机需要对对象进行必要的设置,如对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄(这些信息放在对象的对象头中)。

 5)对象初始化

        执行构造方法,按照程序员的意愿对对象进行初始化。

2.对象的内存布局

        对象在堆内存中的存储布局划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

        对象头

        包含两类信息,第一类用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁的状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据的长度在32位和64位虚拟机中分别为32比特和64比特,称为Mark Word。另一部分是类型指针,即对象指向它的类型元数据的指针,JAVA虚拟机通过这个指针来确定该对象是哪个类的实例。

        实例数据

        是对象真正存储的有效信息,即我们在程序代码里定义的各种类型字段的内容,无论是从父类继承的,还是子类定义的都必须记录下来。 对齐填充 不是必然存在的,也没有特别含义,仅仅起着占位符的作用。因为任何对象的大小都必须是8字节的整数倍。

3.对象的访问定位

        JAVA程序需要通过栈上的reference数据(指向对象的引用)来操作堆上的具体对象,主流的访问方式主要有使用句柄和直接指针两种。

        句柄

        JAVA堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据和类型数据的地址信息。

        直接指针

        reference中存储的是对象的内存地址。

        使用句柄访问最大的好处是reference中存储的是稳定句柄地址,在对象被移动时(垃圾回收),只会改变句柄中的实例数据指针。 使用直接指针访问最大的好处是速度快,节省了一次指针定位的开销。 JAVA中对象的访问非常频繁,HotSpot主要使用直接指针访问。

三、垃圾回收算法和垃圾回收器

1. 如何判断对象是已死

        引用计数

        给对象添加一个引用计数器,当对象增加一个引用时计数器加1,引用失效时计数器减1,任何时刻计数器为0的对象就是不可能再使用的对象。

        优点:原理简单、效率高。 缺点:很难解决对象之间相互引用的问题。     

        可达性分析

        通过一系列称为GC Roots的根对象作为起始节点,根据引用关系向下搜索,如果某个对象到GC Roots没有任何引用链相连,则这个对象是不可能再被使用的。

        可作为GC Roots的对象包括:

        1)虚拟机栈中引用的对象,参数、局部变量、临时变量。

        2)本地方法栈(native方法)中引用的对象。

        3)方法区中常量引用的对象。

        4)方法区中静态属性引用的对象。

        5)虚拟机内部引用的对象,如基本数据类型对应的Class对象、常驻异常对象、系统类加载器等。

        6)被同步锁(synchronized)持有的对象。

2.分代回收理论

        在确定了哪些对象需要进行垃圾回收之后,jvm就要开始垃圾回收的工作了。那么jvm是如何进行垃圾回收的呢?

        根据程序实际运行的情况,jvm关于垃圾回收有2条假说(即2条经验法则):

        1)弱分代假说:绝大多数对象的生命周期都很短,绝大多数的对象都是朝生夕灭的。

        2)强分代假说:熬过越多次垃圾收集过程的对象越难以消亡。

        根据这两条假说,提出了分代回收理论。分代回收理论简单来说就是可以根据对象的年龄(年龄指的是熬过垃圾回收的次数),划分不同的区域,分区存储回收不同年龄的对象。

        jvm的堆被分成至少两个区域,新生代(young)和 老年代(old)。新生代存放才创建的对象,老年代存放少量存活的对象。

3.垃圾回收算法

        标记-清除

        首先标记所有需要回收的对象,在标记完成后,统一回收掉所有标记的对象(也可以标记存活对象,回收未标记的对象)。 缺点:执行效率不稳定(随着对象数量增长而降低)、内存空间碎片化。

        标记-复制(简称复制算法)

        将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存用完了,就将还存活的对象复制到另一块,然后再把已使用过的内存空间一次清理掉。

        缺点:内存缩小为原来的一半。

        商用虚拟机大多采用这种算法回收新生代,大多数对象是朝生夕死的(约98%对象熬不过第一轮收集),因此并不需要按照1:1来划分新生代的内存空间。

        Appel式回收将新生代划分为一块较大的Eden区和两块较小的Survivor区,每次分配内存只使用Eden区和其中一块Survivor区, 发生垃圾回收时,将Eden区和Survivor区中仍然存活的对象一次性复制到另外一块Survivor区上,然后清理掉Eden区和使用过的Survivor区。HotSpot虚拟机默认Eden和Survivor的大小比例为8:1,只有一个Survivor空间,即10%的新生代会被浪费。任何时候都没法保证每次回收后只有少于10%的对象存活,当Suvivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(大多是老年代)进行分配担保。

        标记-整理

        标记-复制算法在对象存活率较高时要进行较多的复制操作,效率会降低。如果不想浪费50%的空间,就需要额外的空间进行分配担保,所以老年代不直接使用标记-复制算法。

        标记-整理算法的标记过程与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象向内存空间的一端移动,然后清理掉边界以外的内容。

        移动存活对象并更新所以引用这些对象的地方是一个极为负重的操作,必须全程暂停用户程序(称为Stop The World)才能进行。

4.垃圾收集器

         Serial

        最基础的单线程工作的收集器。在进行垃圾收集时,必须暂停所有的工作线程,直到收集结束。 新生代采用复制算法,老年代采用标记-整理算法。

        Serial Old

        Serial收集器的老年代版本。

        ParNew

        实质上是Serial收集器的多线程版本,除了使用多线程外,其余的行为包括可控的参数、收集算法、STW、对象分配规则、回收策略等都与Serial完全一致。

        Parallel Scavenge

        是一款新生代的、基于复制算法实现的多线程收集器。它的目标是达到一个可控的吞吐量。 吞吐量= 运行用户代码时间/(运行用户代码时间+垃圾收集时间) 提供了2个参数用户精确控制吞吐量: 控制最大垃圾收集停顿时间 –XX:MaxGCPauseMills(大于0的毫秒值) 设置吞吐量大小 –XX:GCTimeRatio(大于0小于100的整数)。

        Parallel Old

        Parallel Scavenge的老年代版本。

        CMS(Concurrent Mark Sweep)

        一种以获取最短回收停顿为目标的收集器,基于标记-清除算法实现的。

        整个收集过程分为四个阶段:

        1)初始标记(CMS initial mark)

        2)并发标记(CMS concurrent mark)

        3)重新标记(CMS remark)

        4)并发清除(CMS concurrent sweep)

        初始标记、重新标记需要STW。初始标记只标记一下GC Roots直接关联的对象,速度很快。并发标记阶段从GC Roots直接关联的对象开始遍历整个对象图,过程较长、但不需要STW。重新标记是为了修正并发标记阶段因用户线程继续运行导致标记产生变动的那部分对象的标记记录,停顿时间比初始标记稍长,但远比并发标记时间短。并发清除阶段,清理掉标记阶段已经死亡的对象。

        优点:并发收集、低停顿。

        缺点:对处理器资源敏感(多线程占用CPU资源)、无法处理浮动垃圾、产生空间碎片。 G1(Garbage First) Java7 update 4之后引入的一个新的垃圾回收器。

        G1

        是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。它的设计目标是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。

        把堆内存空间划分为多个大小相等的独立区域(Region),每一个区域都可以根据需要扮演新生代的Eden区、Survivor区或者老年代空间,还有一类特殊的humongous区域,专门用来存储大对象。

        收集过程分为四个阶段:

        1)初始标记:标记GC Roots直接关联到的对象,并修改TAMS指针的值,让下一阶段并发运行时,能正确地在可用的Region中分配对象。这个阶段要STW,但耗时短。

        2)并发标记:从GC Roots开始对堆中的对象进行可达性分析,找到可回收对象。这个阶段耗时较长、但不需要STW。当对象图扫描完成后,还要重新处理SATB记录下的在并发时有引用变动的对象。

        3)最终标记:处理并发标记阶段遗留下来的少量STAB记录,需要STW。

        4)筛选回收:负责更新Region的 统计数据,对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间来制订回收计划,可以自由选择任意多个Region构成回收集合,然后把要回收的那部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。需要STW,由多个收集器线程并行完成。

四、JVM执行子系统

1.Class文件结构

class文件是一组以8字节为基础的二进制流。

         魔数

        每个class文件的前4个字节称为魔数,作用是确定这个文件是否为能被虚拟机接受的class文件。

        版本号

        紧接着魔数的4个字节是class文件的版本号:第5、第6个字节是次版本号,第7、第8个字节是主版本号。 高版本的JDK能向下兼容以前版本的class文件,但不能运行以后版本的class文件。         常量池

        常量池可比喻为class文件的资源仓库,它是class文件结构中与其它项目关联最多的数据,也是占用class文件空间最大的数据之一。 常量池的入口放置了一个u2类型的数据,代表常量池容量计数值。容量计数从1开始。如常量池容量为0x0016,即十进制22,代表常量池中有21个常量,索引范围为1-21。

        常量池中主要存放两类常量:字面量和符号引用。字面量比较接近JAVA语言层面的常量概念,如文本字符串、final常量值等。而符号引用属于编译原理方面的概念,主要包括:

        1)被模块导出或开放的包

        2)类和接口的全限定名

        3)字段的名称和描述符

        4)方法的名称和描述符

        5)方法句柄和方法类型

        6)动态调用和动态常量

        访问标志

        常量池之后的2个字节,用于识别一些类或接口层次的访问信息,包括:是类还是接口、是否为public类型、是否定义为abstract、是否申明为fianl等。

        类索引、父类索引与接口索引集合

        类索引用来确定类的全限定名、父类索引用来确定父类的全限定名、接口索引集合用来描述类实现了哪些接口。

        字段表集合

        用来描述接口或类中申明的变量。

        方法表集合

        描述方法的定义。

        属性表集合

        存储 Class 文件、字段表、方法表都自己的属性表集合,以用于描述某些场景专有的信息。如方法的代码就存储在 Code 属性表中。

2.类加载机制

        类的生命周期

加载、验证、准备、解析、初始化、使用和卸载。

        类加载的时机

        类初始化的6种情况:

        1)遇到new、getstatic、putstatic、invokestatic字节码指令时。

        2)使用java.lang.reflect包的方法对类进行反射调用的时候。

        3)当初始化一个类的时候,其父类还没有初始化时,触发其父类初始化。

        4)当虚拟机启动时,用户需要指定一个执行的主类(包含main方法),虚拟机会先初始化这个类。

        5)当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial这四种类型的方法句柄,并且这个方法句柄对应的类没初始化,则需要先触发其初始化。

        类加载的过程

        加载

        加载阶段,虚拟机需要完成三件事: 通过类的全限定名获取类的二进制字节流。 将字节流所代表的静态存储结构转化为方法区的运行时数据结构。 在内存中生产一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。

        验证

        是连接的第一步,目的是确保Class文件的字节流中包含的信息符合《JAVA 虚拟机规范》的全部约束,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。 大致需要完成四个阶段的检验动作: 文件格式验证 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。验证包括:魔数、主次版本号、常量池等等。 元数据验证 对字节码描述的信息进行语义分析,以保证其描述的信息符合《JAVA 语言规范》的要求。验证包括:类是否有父类、类是否继承了不允许被继承的类、类的字段和方法是否与其父类产生矛盾等。 字节码验证 通过数据流分析和控制流分析,确定程序的语义是合法的、符合逻辑的。 符号引用验证 验证符号引用全限定名代表的类是否能够找到,对应的域和方法是否能找到,访问权限是否合法。

        准备

        为类中定义的类变量(被static修饰的变量)分配内存并设置初始值。通常情况下初始化是零值,只有变量被final修饰时,才会初始化为指定的值。

         解析

        将常量池中的符号引用替换为直接引用的过程。

        初始化

        根据程序员编码制订的主观计划区初始化类变量和其他资源。

3.类加载器

       对于任意一个类,都必须由加载它的类加载器和这个类本身来确定其在JAVA虚拟机中的唯一性。每一个类加载器都都拥有一个独立的类名称空间。

        主要有四种类加载器:

        启动类加载器(Bootstrap ClassLoader):用来加载JAVA 核心库(JAVA_HOME\lib目录下)。

        扩展类加载器(Extension ClassLoader):用来加载JAVA扩展库(JAVA_HOME\lib\ext目录下)。

        应用类加载器(Application ClassLoader,也称系统类加载器):负责加载用户类路径(ClassPath)上的所有类库。可通过ClassLoader.getSystemClassLoader()来获取。如果应用中没有自定义的类加载器,一般情况下为应用程序中的默认类加载器。

        自定义类加载器(User ClassLoader):通过继承java.lang.ClassLoader类实现。

4.双亲委派模式

        如果一个类加载器接收到了加载类的请求,它首先不会去自己加载这个类,而是委托给父类加载器加载,依次递归。只有当父类加载器不能完成加载任务时,自己才去加载。 双亲委派模式的好处:JAVA中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如,java.lang.Object类,无论哪个类加载器加载,最终都会委托给最顶层的启动类加载器加载,因此Object类在程序的各种类加载器环境中都能保证是同一个类。

        Tomcat打破双亲委派模型。

五、JVM性能优化

1.内存溢出

        程序在申请内存时,没有足够的空间。

        栈溢出

        方法循环调用(StackOverflowError)、线程太多(OutOfMemoryError)。

        堆溢出

        不断创建对象,分配对象大堆内存。

        方法区溢出

        在经常动态生产大量 Class 的应用中,CGLIb 字节码增强,动态语言,大量 JSP(JSP 第一次运行需要编译成 Java 类),基于 OSGi 的应用(同一个类,被不同的加载器加载也会设为不同的类)。

        直接内存溢出

        可以通过-XX:MaxDirectMemorySize来设置。

2.内存泄露

        程序在申请内存后,无法释放已申请的内存。 可能存在内存泄露的情况:

        长生命周期的对象持有短生命周期的对象

        如将ArrayList设置为静态变量,则容器中的对象在程序结束之前将不能被释放。

        连接未关闭

        如数据库连接、网络连接、IO连接等。 变量作用作用域不合理 一个变量定义的作用范围大于其使用范围,并没有及时设置为null。

        内部类持有外部类

        Java 的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此,如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏。

        Hash值改变

        在集合中,如果修改了对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄露。

3.JDK提供的优化工具

        命令行工具:

        jps

        列出当前机器上正在运行的虚拟机进程。

        jps 显示进程和启动类名称

        jps –q 只显示进程

        jps –m 输出主函数传入的参数

        jps –l 输入主函数完整包名

        jps –v 输入启动程序制定的jvm参数

        jstat

        用于监视虚拟机各种运行信息的命令行工具。显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT(即时编译)等运行数据。

        用法:jstat –xx 进程号

        jstat –class 类加

        jstat –compiler JIT

        jstat –gc  GC堆状态

        jstat –gccapacity 各区大小

        jstat –gccause 最近一次gc统计和原因

        jstat –gcnew 新生代统 

        jstat –gcnewcapacity 新生代大小

        jstat –gcold 老年代统计

        jstat –gcoldcapacity 老年代大小

        jstat –gcpermcapacity 永久代大小

        jstat –gcutil gc统计汇总

        jstat –printcompilation 虚拟机编译统计

        jinfo

        查看和修改虚拟机参数

        用法:jinfo [option]

        jinfo –sysprops 获取虚拟机参数,等价于System.getProperties()

         jinfo –flag 输出对应名称的参数

        jinfo –flag [+|-] 开启或关闭对应名称的参数

        jinfo –flags 输出所有的参数

        jmap

        用于查看堆和永久代的详细信息、生成堆转储快照(dump文件)

        用法:jmap [option]

        jmap –heap 查看堆

        jmap –finalizerinfo查询 finalize 执行队列

        jmap –dump 导出堆dump文件 例如:jmap –dump:live –format=b,file=D:\heap.bin 1740

        jhat

        生成dump文件分析,可在浏览器上访问:http://localhost:7000

        jhat

        jstack

        查看虚拟机线程信息。

        可视化工具:

        jconsole

        JAVA_HOME\bin\jconsole.exe

        jvisualvm

        JAVA_HOME\bin\jvisualvm.exe

你可能感兴趣的:(java进阶知识总结,java)