JVM
- JVM是什么?
- JVM 的主要组成部分
- JVM工作流程
- JVM内存模型
- 直接内存与堆内存的区别:
- 堆栈的区别
- Java会存在内存泄漏吗?
- 简述Java垃圾回收机制
- 垃圾收集算法
- 轻GC(Minor GC)和重GC(Full GC)
- 新生代gc流程
- JVM优化与JVM调优
JVM是什么?
JVM是Java Virtual Machine(Java虚拟机)的缩写。
虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
简单来说JVM是用来解析和运行Java程序的。
JVM 的主要组成部分
JVM包含两个子系统和两个组件,两个子系统为Class loader(类加载子系统)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
- Class loader(类加载子系统):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
- Execution engine(执行引擎):执行classes中的指令。
- Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
- Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
JVM工作流程
- 通过编译器把 Java 代码转换成字节码
- 类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内
- 字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令
4.底层系统指令由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
JVM内存模型
- 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
- Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表(基本类型是值、引用类型是句柄或者指针)、操作数栈(保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间)、动态链接、方法出口等信息(8大基本类型 + 对象引用 + 实例方法);
- 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
- Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;从内存回收角度来看java堆可分为:新生代(Young)和老生代(Old)。新生代 ( Young ) 又被划分为三个区域:Eden(伊甸园区)、From Survivor(幸存区2)、To Survivor(幸存区1)。
- 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量池、静态变量(static)、方法信息(修饰符、方法名、返回值、参数等)、即时编译后的代码等数据。
直接内存与堆内存的区别:
直接内存申请空间耗费很高的性能,堆内存申请空间耗费比较低
直接内存的IO读写的性能要优于堆内存,在多次读写操作的情况相差非常明显
堆栈的区别
堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。
存放的内容不同
堆存放:对象的实例和数组。因此该区更关注的是数据的存储
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
Java会存在内存泄漏吗?
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,Java是有GC垃圾回收机制的,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。
但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。泄漏积少成多,内存泄漏 会导致 内存溢出。
内存溢出(OOM) 是指 程序在申请内存时,没有足够的内存空间供其使用,出现内存溢出。
解决:①设置JVM的堆参数( -Xmx:JVM最大内存 -Xms:启动初始内存 -Xmn:新生代大小 -Xss:每个线程虚拟机栈及堆栈的大小 ) 例如:-Xms1024m -Xmx1024m -Xmn512m -Xss5m
。
②分析内存,看一下那个地方出现了问题(专业工具:Jprofiler,MAT)分析Dump内存文件, 快速定位内存泄漏,怎么查找dump文件,直接找到文件的文件夹打开获得大的对象。
简述Java垃圾回收机制
垃圾回收机制简称GC。在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
Java中的垃圾回收是根据可达性分析算法和 引用计数算法来判断对象是否存活的。
System.gc(); // 手动回收垃圾
垃圾收集算法
-
标记清除算法:首先标记所有需要回收的对象,在标记完成后回收所有被标记的对象。
优点:算法比较简单
缺点:会产生大量不连续的内存碎片,而且效率不高 -
复制算法
这种算法会将内存划分为两个相等的块,每次只使用其中一块。当这块内存不够使用时,就将还存活的对象复制到另一块内存中,然后把这块内存一次清理掉。年轻区主要用复制算法,幸存区复制,一般都是from复制到to,谁空谁是to,适用与对象存活度较低。
优点:效率比较高,也避免了内存碎片。
缺点:因为另一半内存一直是空的,比较浪费空间。 -
标记-整理算法
标记-清除算法的升级版,也叫标记-压缩算法。在完成标记阶段后,不是直接对可回收对象进行清理,而是让存活对象向着一端移动,然后清理掉边界以外的内存。
优点:避免了内存碎片和内存利用效率低。
缺点:增加了一个移动的成本。 -
分代收集算法
年轻代:存活率低-复制算法
老年代:区域大存活率高-标记清除+标记整理算法混合实现
轻GC(Minor GC)和重GC(Full GC)
Minor GC:当新对象去伊甸园区(Eden)申请内存失败的时候,就会进行Minor GC,对伊甸园区(Eden)回收非存活对象,而没有被回收的对象,会进入幸存区(Survivor),这种GC只发生在伊甸园区(Eden),不会影响到老年区。因为新对象分配内存大部分都在伊甸园区(Eden),所以伊甸园区(Eden)GC比较频繁。
注意:在GC之后,还存活的对象,进入幸存区(Survivor),谁空谁是to,可以交换位置,当一个对象经历了15次GC(可以配置次数:-XX:+MaxTenuringThreshold=15),还存活,就进入老年区。
Full GC
清理整个堆,因为Full GC需要对整个堆进行回收,所以比Minor GC慢,因为我们要尽可能的减少Full GC的次数。我们所说的JVM调优,很大一部分就是对Full GC的优化。
会造成 Full GC:
- 老年区满了:年轻区的对象转入或创建大对象才会满。
- 方法区满了(jdk8及之后版本):系统中要加载的类过多。
- System.gc() 被显示调用
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存:第一次Minor GC之后,有2MB的对象转入老年区,然后在下一次Minor GC的时候就会判断老年区的空间是否有2MB,如果没有就进行Full GC。
新生代gc流程
-
刚刚新建的对象在Eden中,经历一次Minor GC,Eden中存活对象就会被移动到幸存区1,Eden被清空。
-
等Eden区再满了,触发Minor GC,Eden和幸存区1中存活的对象又会被复制到幸存区2中(这个过程非常重要,因为这种复制算法保证了幸存区2中来自幸存区1和Eden两部分的存活对象占用了连续的空间,避免碎片化)
-
幸存区1和Eden被清空,然后下一轮幸存区1与幸存区2互换角色,如此循环,经历15次GC,还存活对象,放进入老年区
JVM优化与JVM调优
JVM优化:JVM本身自带的编译时、运行时的优化机制,各个jdk版本会有些不同,不需要程序员干涉,程序员学习了解即可;
JVM调优(JVM性能调优)是程序员设置虚拟机参数(不直接使用默认参数)满足自己的需求,是程序员的一项工作技能。
JVM性能调优的目标:使用较小的内存占用来获得较高的吞吐量(计算型)或者较低的延迟(交互型)。
规则:
我们要对Java堆分配策略进行优化,合理规划Java堆容量、年轻代、老年代比例,使自动内存分配和回收高效进行,这里关注内存回收,即GC操作。
第一条要求GC整个垃圾收集过程,消耗的时间尽量小就必须要一个更小的堆,
第二条要求GC整个垃圾收集过程,次数尽量少,必须保证一个更大的堆,
注意,第一条要求和第二条要求是互斥的,不能同时满足,我们要把握一个适度适中的原则,一个相对大小的堆。
第三条要求老年代的空间比例尽量大一些,这样Full GC的次数就会比较少,周期比较长,要平均相隔比较长的一段时间才有一个Full GC,即Full GC一定不要太频繁。
即第一条和第二条要求是一个大小适中的堆,第三条要求这个堆中老年代的空间容量比例尽量高一些。
技巧:
新生代中Eden:Survivor默认是8:1,一般不改动,JVM调优集中在新生代和老年代大小比例,新生代和老年代默认比例是1:2。
JVM性能调优原则(优先代码调优,参数调优作为补充)
-
在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。
-
初始堆值和最大堆内存内存越大,吞吐量就越高,但是也要根据自己电脑(服务器)的实际内存来比较。
-
最好使用并行收集器,因为并行收集器速度比串行吞吐量高,速度快。当然,服务器一定要是多线程的
-
减少GC对老年代的回收。设置生代带垃圾对象最大年龄,进量不要有大量连续内存空间的java对象,因为会直接到老年代,内存不够就会执行GC
代码调优经验
- 避免创建过大的对象及数组:过大的对象或数组在新生代没有足够空间容纳时会直接进入老年代,如果是短命的大对象,会提前出发Full GC。
- 避免同时加载大量数据,如一次从数据库中取出大量数据,或者一次从Excel中读取大量记录,可以分批读取,用完尽快清空引用。
- 对象引用及时置null,当集合中有对象的引用,这些对象使用完之后要尽快把集合中的引用清空,这些无用对象尽快回收避免进入老年代。
- 尽量避免长时间等待外部资源(数据库、网络、设备资源等),缩小对象的生命周期,避免进入老年代,如果不能及时返回结果可以适当采用异步处理的方式等