JAVA知识点全总结——(一)JVM

1. JVM

1.1 运行时数据区内存模型

运行时数据区是JVM把自己管理的内存部分抽象出来的模型,抽象出来的不同的数据区域,以便于管理,具体有如下几个区域。

1.1.1 分类

程序计数器,堆,栈,本地方法栈,方法区

1.1.2 程序计数器

在线程中,记录每个线程当前执行的语句行数,不会发生OOM,每个线程都有。

1.1.3 栈

栈是描述方法,描述线程的一块区域,每个线程都拥有一个栈,栈中的栈帧表示方法,用FILO的方法表示方法之间的调用。栈帧中有局部变量,局部变量包括,当前方法所在的对象的this指针,定义的局部变量,方法参数,方法内catch的异常中的参数。操作数栈,操作数栈是用来做计算的,因为JVM是一种面向栈的计算语言,普通的计算机有寄存器的概念,用0至15个寄存器做加减,但是JVM用的是栈的方法做计算,比如a+b,在JVM中是将a和b入栈,再使用一个iadd命令,从栈顶弹出a和b做加法,这种就类似于后缀表达式计算方法。栈中还有返回地址和一些其他信息等。

1.1.4 本地方法栈

本地方法栈是在JVM调用本地的native方法的时候会使用的栈,在JDK源代码中有很多方法都是本地的native修饰的方法,这种方法jdk是不实现的,直接调用操作系统的一些源语,JVM只是获取返回值。比如arraycopy方法等。这里也有程序计数器,但是没行号。

1.1.5 堆

堆中用来分配new创建的对象,堆分为新生代和老年代,长时间存在的对象或者较大的对象会进入老年代,刚生成的对象会进入新生代,新生代分为两类区域,一部分为Eden此部分较大占据百分之80的空间,另一部分为Survivor空间,一般会有两个Survivor空间各占百分之10左右。用来存放新生代的对象。

1.1.6 方法区

理论上方法区的位置在堆上,但是方法区和堆存放的数据却天差地别。方法区中存放类的信息,当类加载进来的时候会把字节码的信息存在方法区中,而且类文件里的类变量也存放在方法区中。

从1.8开始,方法区不再作为JVM内存中的一个部分而是将方法区的数据放在本地内存上,作为元空间,这样的好处在于降低了方法区OOM的可能性,因为方法区主要加载的就是类的信息,如果类的信息过大可能导致方法区OOM,而这种OOM通常不是我们操作带来的问题,不是很好处理。

1.1.7 直接内存

在JVM中有一块直接存储数据的区域,在JDK1.4之后,这块区域用给NIO做数据的缓冲,通过一个native的方法来分配出一块区域,通过一个在堆中的对象直接操作者部分内存。避免了在堆和系统内存中不断的复制空间的问题。

1.2 OOM异常

1.2.1 OutOfMemoryError堆溢出

如果我们在堆上创建了一些不是很大的对象,但是创建数量很多就有可能造成堆溢出。这个时候我们通常可以设置参数HeapDumpOnOutOfMemory来打印溢出瞬间的堆空间快照。一般的情况下堆溢出分为两类,一类是堆空间溢出,空间不够大,我们可以设置参数增大的大小和自动扩展范围。还有一种是内存泄漏导致空间比实际的空间小,比如ThreadLocal配合线程池的情况,这种情况下我们通过工具进行可达性分析,分析出有问题的部分想办法进行回收即可。

1.2.2 StackOverflowError栈溢出

栈溢出是我们在一个线程之中调用的方法过多导致的,这种情况一般是递归的逻辑有问题造成的,我们在测试的时候就能测试出这种错误。除此之外一般不会发生StackOverflow的问题。

1.2.3 OutOfMemoryError栈溢出

栈还可能因为扩展的时候内存不够而溢出,这种情况比较特殊,是线程过多,而且每个线程中的调用链特别长的时候可能出现。此时为了解决这个问题,我们可以采用降低栈深度的方法,因为每个栈的深度降低了,总体可用的栈个数就能增加了。

1.2.4 方法区溢出

方法区在1.8中已经不存在了,因为随着CGLib,groovy,JSP热部署等技术的出现,动态修改字节码生成类和类加载器替换生成类的操作越来越多,如果我们将方法区放在堆中已经难以满足现状的类数量,把方法区放本地内存中是一个不错的选择。新特性也不能神奇地消除类和类加载器导致的内存泄漏。

1.2.5 直接内存溢出

直接内存溢出是我们在使用NIO的时候声明了一块区域,但这块区域的大小使得整个直接内存部分放不下,从而OOM。一般Thread Dump文件不大,但是其中有NIO操作的话可能是直接内存溢出导致的。直接内存溢出是NIO的底层通过Unsafe类来分配空间,分配的大小超过限额导致的。

1.3 对象回收判断

1.3.1 引用计数法

引用计数法非常简单,如果有引用指向了这个对象则把这个对象计数加一,如果一个对象的计数为0说明这个对象没有引用可以回收。但是这种方法存在明显的问题,1.无法判断四种不同类型的引用情况;2.如果发生对象之间的循环引用无法清理,导致内存泄漏。

1.3.2 可达性分析

可达性分析就是从一系列的ROOT对象出发,能够达到的对象我们就认为是不应该被回收的,其他对象无论是成环还是其他形状的对象我们都会进行回收操作。ROOT包括所有的栈上的对象以及在方法区中的静态变量能够联系到的堆中的对象。

1.4 垃圾回收算法

1.4.1 标记-清除

标记清除指现现内存堆区域标记一遍,发现所有需要回收的数据,将其打上标记,然后从头开始清理垃圾。这样的问题在于,有可能清理之后空间中有很多不连续的可用空间,碎片化的情况比较严重。这种算法的优势是简单。

1.4.2 复制算法

将堆空间分成两边,每次只用其中的一部分,需要进行清理的时候,将所有存活下来的数据放到另外一块区域上,之后统一把这半边的数据全部刷掉。这种方法的好处在于清理简单,劣势在于会造成空间的浪费,每次只用一半。

1.4.3 标记-整理

标记整理在标记清除的基础上进行了优化,先把标记后存活的对象都放在堆空间的一侧,之后从结尾刷掉其他所有数据。

1.4.4 商业虚拟机回收算法

在商业中,新生代的虚拟机运用复制算法,把空间分成8比1比1的三个部分,每次用其中的9块,把剩余的存货对象放入1块中,因为新生代的对象普遍寿命短。在老年代中因为寿命比较长久,使用标记整理算法。

1.4.5 如何触发回收

新生代和老年代的回收相比,新生代比老年代快10倍,两者触发回收都是在空间(新生代是Eden)满了的情况下。

1.5 垃圾收集器

1.5.1 基础垃圾收集器

垃圾收集器有很多种,比较基本的垃圾收集器会在某个时刻进行“Stop The World”的方式进行回收,这样的GC堆程序的响应非常差。

1.5.2 常用垃圾收集器——CMS

CMS采用了更加科学的方式进行垃圾回收,先会使用一个线程短暂的“Stop The World”进行标记所有ROOT节点,之后采用一个同步线程和其他线程一起进行可达性分析。但是在这其中可能会有一些对象也在更新,导致清理的不完整,所以再采用一个多线程“Stop The World”进行并发标记,确保完全被标记,之后用并发线程进行回收。CMS的好处在于缩短了大部分可达性分析的时间。

1.5.3 常用垃圾收集器——G1

G1和CMS差不多,流程都一样,就是G1多了一个分区域回收的功能,能够将内存区域划分成多个块,判断是否需要回收等,比CMS更加高效。

1.6 JVM命令行工具

1.6.1 jstack

查看栈内存空间,可以查看多线程死锁的问题,查看各个进程的状态快照。

1.6.2 jmap

查看堆内存空间,可以查看堆空间的数据分布情况,新生代和老年代。

1.6.3 jinfo

查看虚拟机的配置信息

1.6.4 java -option

对JVM的属性进行配置

1.6.5 javac

编译java文件

1.6.6 javap

对class文件进行反汇编

1.6.7 jps

查看jvm进程

1.7 JVM调优

1.7.1 查看内存——新生代空间

可能因为程序会产生非常大量的小对象,比如hashmap百万个,这些数据都不能回收,这样会出发很多次GC,但是效果都不好,而且默认的情况下我们使用复制算法进行操作,这样就会导致大量的复制操作,既然对象不符合“朝生夕死”的特点,我们也没必要使用复制算法了,这里推荐去掉两个小空间,然后直接将新生代的放到老年代里面去。

1.7.2 查看内存——老年代空间

如果老年代空间满了可能是有很多大对象,比如DOM分析出来的大文件全部加载到内存的时候就会放在老年代,这个时候我们可以预先对老年代扩容,免得老年代会不断的自动扩展。

1.7.3 查看内存——直接内存空间

如果我们发成了溢出OOM但是Dump下来的文件基本没啥东西,而且有用到NIO,基本就有可能是直接内存溢出了,这种情况一般是JVM占用的内存空间太大了,因为NIO用的空间是另外算的,所以我们减小JVM的内存空间,给NIO的直接内存多分配一些就可以。

1.7.4 查看GC时间——调整空间结构

一般如果我们在很短的时间之内进行了很多次GC,说明我们的空间分配发生了问题,一般也是调整新生代和老年代空间的方法,尽量让他们不要动态扩展。

1.7.5 选择垃圾回收器

如果默认的垃圾回收器不是CMS或者G1的话可以考虑使用这两个收集器,效率很高。

1.8 类文件结构

类文件结构开始会有魔数表示这是一个class文件,之后有使用的java的版本号,之后是一些类的信息,变量,方法,已经方法变量的名称参数等。具体的名字会存放在运行时常量池中。

1.9 类加载机制

1.9.1 加载

类加载可以从任何地方加载class文件进来,所以这也是jvm具有平台无关性的说法,只要是class文件就都能在jvm中运行。此处需要类加载器帮助加载。

1.9.2 链接——验证

加载后进入链接步骤,在java中链接是运行时会进行的步骤,链接的第一步是验证,验证class文件的内容是否合法,java虽然编译通过的文件一定合法,但是也有可能class文件是从别处获取存在风险,进行验证确保数据安全。

1.9.3 链接——准备

在class文件中会有一些static修饰的类的变量,这些变量的空间分配和清零是在这一步进行的。

1.9.4 链接——解析

在一些方法的参数部分会有一些引用变量,引用了其他的类,在解析这一步会将引用变量全部替换成直接变量,java的多态特性也是建立在这个的基础上,值得一提的是解析这一步并不一定在此处进行,可能在初始化之后进行也可。

1.9.5 初始化

初始化为类创建一饿class的对象,存放在堆中,并且此时有new或者反射的语句时,进行初始化过程,先查看父类的初始化情况,如果父类尚未初始化则重复进行上述结构,然后在堆上为new的对象分配空间清并空,然后执行static语句块、静态成员赋值、初始化语句块、对象成员赋值、构造方法等。

1.10 类加载器

类加载器是用来对class文件进行加载的工具,

1.10.1 双亲委派模型

其中含有双亲委派模型的概念,双亲委派模型是指在java中有三个类加载器分别是启动类加载器,扩展类加载器,应用类加载器。这三个加载器分别加载不同的包下面的文件,用来区分jdk的层级结构,当一个类需要加载的时候,首先我们的类会找到应用类加载器尝试加载,但是应用类加载器会找到上级加载器进行尝试加载,先让等级高的加载器加载如果不行的话再自己进行加载。这样保证了如果有两个相同的类,jdk能够认识具体应该使用哪些类。

1.10.1 双亲委派模型的破坏——SPI

双亲委派模型虽然规定了java的层级,但是还是有一些局限性,比如如果有rt中的类需要配合应用类加载器能够加载的类,这样的话根据类加载器模型,他因为还是会调用启动类加载器,所以无法加载用户编写的类,比如反射就是一个典型。在这样的情况下,我们添加了一个线程上下文加载器的概念,通过上下文加载器可以获取到当前使用的加载器,从而得到应用类加载器。

1.10.2 双亲委派模型的破坏——热部署

热部署是指动态的部署一个类文件,比如tomcat中的jsp文件,实际上也是一个class文件,如果我们运行tomcat,实际上是可以做到在项目运行的时候动态修改一个jsp文件的。那么这种操作是如何做到的呢?就是tomcat可以在运行的时候动态更换类加载器,用新的类加载器加载jsp文件,这样加载出来的类不在一个类加载器的命名空间下,所以不是同一个类,可以复用。这个时候的类加载器也不是层级结构,不会去启动类加载器里查询的过程,直接根据代码动态加载了。

1.11 多态分派

java是一种多态的语言,体现在解析这一步操作是在运行时完成的,每个方法中使用的变量具体是否符合类型的限制,我们在编译的时候不关心具体变量,这样使得在运行的时候我们可以用一个类的子类去替换父类。

1.11.1 重载——静态分派

多态也不全是和分派有关,比如重载可以说是一种静态的分派,在重载中,具体在运行时调用哪些类是在编译的时候根据类型决定的,我们把这种在编译时就能确定的方法叫做静态分派。

1.11.2 重写——动态分派

重写是一种动态分派,在重写中具体使用哪个方法是在运行时根据变量的实际类型决定的。

1.12 JMM内存模型

JMM内存模型的存在是为了屏蔽不同的操作系统和机器对一些内存,缓存的划分。JVM用一种主内存和工作内存关联的样子去刻画自己的这个内存模型。实际上,可以把主内存和堆内存做类比,工作内存和栈做类比。每个线程在工作内存中工作,通过读写和主内存交换数据。

下一篇:JAVA知识点全总结——(二)JAVA基础知识

猜你喜欢

转载自blog.csdn.net/quinnnorris/article/details/81180249