1、JVM
1.1 基本概念
JVM是可运行Java代码的假象计算机,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收、堆和一个存储方法域。JVM是运行在操作系统之上的它与硬件没有直接的交互。 image.png1.2 原理
image.png运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,他就会被解释执行或者是被即时代码发生器有选择的转换成机器码执行。
JVM在它的生命周期中的任务就是运行Java程序,因此当Java程序启动的时候就产生JVM的一个实例,当程序运行结束的时候该实例也就消失了。JVM是程序与底层操作系统和硬件无关的关键。
1.3 结构
image.png-
Class Loader类加载器
负责加载.class文件,class文件在文件开头有特定的文件标识,并且ClassLoader负责class文件的加载等,至于它是否可以运行则由Execution Engine(执行引擎)决定。作用有定位和导入二进制class文件;验证导入类的正确性;为类分配初始化内存;帮助解析符号引用。 -
Native Interface本地接口
本地接口的作用是融合不同的编程语言为Java所用。 -
Execution Engine执行引擎
执行包在装载类的方法中的指令,也就是方法。 -
Runtime data area运行数据区
虚拟机内存或者Jvm内存在整个计算机内存中开辟一块内存存储Jvm需要用到的对象变量等,运行区又分很多小区,分别为:方法区,虚拟机栈,本地方法栈,堆,程序计数器。
2、JVM数据运行区详解
image.png2.1 PC Register程序计数器
每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码(下一个要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2.2 Native Method Stack本地方法栈
本地方法栈则为Native方法服务, 如果一个VM实现使用C-linkage模型来支持Native调用, 那么该栈将会是一个C栈,但HotSpot VM直接就把本地方法栈和虚拟机栈合二为一。
2.3 VM Stack虚拟机栈
是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
image.png2.4 Heap堆 运行时数据区
是被线程共享的一块内存区域,创建的对象和数组都保存在Java堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代VM采用分代收集算法, 因此Java堆从GC的角度还可以细分为: 新生代(Eden区、From Survivor区和To Survivor区)和老年代。
image.png2.5 方法区/永久代
即我们常说的永久代(Permanent Generation), 用于存储被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区, 这样HotSpot的垃圾收集器就可以像管理Java堆一样管理这部分内存, 而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)。 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。 Java虚拟机对Class文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。
3、Heap堆
image.png3.1 新生代
是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。新生代又分为 Eden区、ServivorFrom、ServivorTo三个区。
Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
3.1.1 MinorGC的过程(复制->清空->互换)采用复制算法
-
eden、servicorFrom 复制到ServicorTo,年龄+1
首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果ServicorTo不够位置了就放到老年区); -
清空eden、servicorFrom
-ServicorTo和ServicorFrom互换
ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。
3.2 老年代
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
MajorGC采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。
3.3 永久存储区
指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域,它和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。
4、 GC常用算法
判断对象是否存活一般有两种方式:
- 引用计数算法:此对象有一个引用则+1,删除一个引用则-1,只用收集计数为0的对象。缺点是a.无法处理循环引用的问题。B.引用计数的方法需要编译器的配合,编译器需要为此对象生成额外的代码。
-
根搜索算法:建立若干中根对象,当任何一个根对象到某一个对象均不可达时则认为这个对象是可以被回收的。根对象一般为虚拟机栈中的引用的对象,方法区中的类静态属性引用的对象,方法区中的常量引用的对象,本地方法栈中的引用对象。
在根搜索算法的基础上,现代虚拟机的实现当中垃圾搜集算法主要有三种,分别是标记-清除算法、复制算法、标记-整理算法。 - 标记-清除算法:当堆中的有效内存空间被耗尽的时候,就会停止整个程序,然后进行两项工作,第一项是标记,第二项是清除。标记的过程就是遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。缺点是效率低(递归与全堆对象遍历),清理出来的空间不连续,在进行GC的时候要停止应用程序。
- 复制算法:将内存分为两个区间,在任意时间点所有动态分配的对象都只能分配在其中一个区间。当有效内存空间耗尽时,JVM将暂停程序运行,开启复制算法GC线程,将活动区间的存活对象全部复制到空闲空间,且严格按照内存地址一次排列,与此同时,GC线程将更新存货对象的内存引用地址指向新的内存地址。此时空闲空间与活动区间交换。缺点是浪费了一半内存,如果对象的存活率很高,将很浪费。
- 标记-整理算法:当堆中的有效内存空间被耗尽的时候,就会停止整个程序,然后进行两项工作,第一项是标记,第二项是整理。标记的过程就是遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。整理的过程就是移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。
- 分代搜集算法:新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
5、GC垃圾收集器 image.png
image.png-
Serial收集器(串行)
是一个单线程的收集器,他在工作时,必须暂停其他所有的工作线程(Stop-The-World),直到他收集结束。这是难以忍受的,但是他的效率较高。 -
ParNew收集器(并行)
是Serial收集器的多线程版本。 -
Parallel Scavenge收集器(并行)
是一个新生代收集器,他也是使用复制算法。他的目标规则是达到一个可控制的吞吐量。
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那么吞吐量就是99%。 -
Serial Old收集器
Serial Old是Serial收集器的老年代版本,他同样是一个单线程收集器,使用标记-整理算法。 -
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。 -
CMS垃圾收集器(优点: 并发收集、低停顿 缺点: 产生大量空间碎片、并发阶段会降低吞吐量)的四个步骤:
a.初始标记(需要暂停)仅仅标记GCroot能直接关联的对象
b.并发标记(不需要暂停)GC Roots Tracing的过程
c.重新标记(需要暂停)修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
d.并发清除(不需要暂停) -
G1垃圾收集器:
特点是空间整合,采用标记-整理算法不会产生空间碎片;可预测停顿。
将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。
jdk8环境下,默认使用 Parallel Scavenge(新生代)+ Serial Old(老年代)
6、JVM配置
堆设置
- -Xms:初始堆大小
- -Xmx:最大堆大小
- -XX:NewSize=n:设置年轻代大小
- -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
- -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
- -XX:MaxPermSize=n:设置持久代大小
- -Xss:设置每个线程的栈大小
收集器设置 - -XX:+UseSerialGC:设置串行收集器
- -XX:+UseParallelGC:设置并行收集器
- -XX:+UseParalledlOldGC:设置并行年老代收集器
- -XX:+UseConcMarkSweepGC:设置并发收集器
7、Minor GC ,Full GC 触发条件
Minor GC触发条件:
当Eden区满时,触发Minor GC。
Full GC触发条件:
- 调用System.gc时,系统建议执行Full GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
8、Java四种引用类型
基本类型包括:byte,short,int,long,char,float,double,Boolean,returnAddress
引用类型包括:类类型,接口类型和数组。
-
强引用
把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。 -
软引用
软引用需要用SoftReference类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。 -
弱引用
弱引用需要用WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,总会回收该对象占用的内存。 -
虚引用
虚引用需要PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
9、堆内存溢出
- java.lang.OutOfMemoryError:Java heap space:Java堆内存不够,一个原因是真不够,一个原因是程序中有死循环。 image.png
- java.lang.OutOfMemoryError:GC overhead limit exceeded:当GC为释放很小空间占用大量时间时抛出;一般是因为堆太小,没有足够的内存。 image.png
- java.lang.OutOfMemoryError:PermGen:space:这种是P区内存不够,可通过调整JVM的配置 image.png
- java.lang.StackOverflowError:线程栈的溢出,要么是方法调用层次过多,要么是线程栈太小。通过-Xss参数增加线程栈的大小。比如说太深的递归。递归函数会不断的占用栈空间。 image.png
10、JVM的调优
- 将新对象预留在年轻代
- 让大对象进入年老代
- 设置对象进入年老代的年龄
- 尝试使用大的内存分页
- 增大吞吐量提升系统性能
- 年老代年轻代大小划分
- 内存泄漏
- 垃圾回收算法设置合理
11、Java对象的大小
基本数据的类型的大小是固定的,非基本类型的Java对象,其大小就值得商榷。
在Java中,一个空Object对象的大小是8byte,这个大小只是保存堆中一个没有任何属性的对象的大小。
看下面语句:
Object ob = new Object();
它所占的空间为:4byte+8byte。4byte是上面部分所说的Java栈中保存引用的所需要的空间。而那8byte则是Java堆中对象的信息。因为所有的Java非基本类型的对象都需要默认继承Object对象,因此不论什么样的Java对象,其大小都必须是大于8byte。
Class NewObject {
int count;
boolean flag;
Object ob;
}
其大小为:空对象大小(8byte)+int大小(4byte)+Boolean大小(1byte)+空Object引用的大小(4byte)=17byte。
但是因为Java在对对象内存分配时都是以8的整数倍来分,因此大于17byte的最接近8的整数倍的是24,因此此对象的大小为24byte。
12、Java异常
image.png-
Throwable:
Throwable是Java语言中所有错误或异常的超类。
Throwable包含两个子类:Error和Exception。它们通常用于指示发生了异常情况。
Throwable包含了其线程创建时线程执行堆栈的快照,它提供了printStackTrace()等接口用于获取堆栈跟踪数据等信息。 -
Exception:(被检查的异常)
Exception及其子类是Throwable的一种形式,它指出了合理的应用程序想要捕获的条件。Java编译器会检查它,此类异常要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。 -
RuntimeException:(运行时异常)
那些可能在Java虚拟机正常运行期间抛出的异常的超类。如果代码会发生RuntimeException异常,则需要通过修改代码进行避免。例如除数为零。Java编译器不会检查它,也就是出现这类异常时,倘若没有通过throws声明抛出它也没有try-catch捕获它还是会编译通过。虽然java编译器不会检查运行时异常但是我们也可以通过throws进行声明抛出。 -
Error
用于指示合理的程序不应该视图捕获的严重问题。编译器不会对错误进行检查。程序本身无法修复这些错误。
throw和throws的区别:
- throws用在函数上,后面跟的是异常类,可以跟多个;而throw用在函数内,后面跟的是异常对象。
- throws用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式;throw抛出具体的问题对象,执行到throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。也就是说throw语句独立存在时,下面不要定义其他语句,因为执行不到。
- throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常对象。
- 两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
网友评论