1、JVM 的位置:
JVM 运行在操作系统之上,JVM 也是一个软件,Java 文件就在这层软件上执行。
2、JVM 的体系结构(简图):
JVM 调优:就是解决垃圾回收,主要在堆中,方法区是特殊的堆,因此 JVM 调优就是堆内存调优。(下面讲堆的时候会讲到)
本地方法栈:Native Method Stack,登记 native 方法,在最终执行的时候,通过本地方法接口(JNI)加载本地方法库中的方法
3、类加载器:
作用:加载 Class 文件进内存
类加载器又分为几种:
①虚拟机自带的加载器
②启动类(根)加载器 (AppClassLoader)
③扩展类加载器 (ExtClassLoader)
④应用程序加载器 (BootstrapClassLoader)
4、双亲委派机制
(1)类加载器收到类加载的请求,然后进行初始化
(2)将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器。
(3)启动类加载器检查是否能够加载当前这个类,能加载就结束,就使用当前的加载器,否则抛出异常,通知子加载器进行加载。依次检查。
5、native
比如在代码中 new 一个 Thread(new Thread().start(); ),在 start() 方法中,有一个方法,叫做 private native void start0();,在一些 Java 底层代码中都会有用 native 关键字修饰的方法:private native void start0();
native:凡是带了 native 关键字的,说明 Java 的作用范围已经达不到了,就会去调用底层 C语言的库。native 关键字修饰的方法定义在本地方法栈中,它会自动调用本地方法接口(JNI),然后再调用本地方法库中的方法。
JNI 的作用:扩展 Java 的使用,融合不同的编程语言为 Java 所用。
最初是想调用 C 和 C++,因为在 Java 诞生的时候,C 和 C++ 非常火,因此在内存中专门开辟了一块区域,叫做本地方法栈,用来登记 native 方法,在最终执行的时候通过 JNI 去调用本地方法库中的方法,也就是调用 C语言库中的方法。现在要想调用其他语言的东西,可以通过 http 进行调用。
6、程序计数器(Program Counter Register)
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也就是即将要执行的指令代码),在执行时读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
7、方法区(Method Area)
方法区被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在方法区中定义。简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区域。
静态变量(static修饰)、常量(final修饰)、类的信息(Class、构造方法、接口定义)、运行时的常量池存在方法区中,但是,实例变量存在堆内存中,和方法区无关。
8、栈(Stack)
栈是一种数据结构(先进后出,后进先出。类似 桶)
队列(先进先出,后进后出。类似 管道)
喝多了吐就是栈,吃多了拉就是队列
栈:栈内存,主管程序的运行,其生命周期和线程同步。main 方法也是一个线程,main 方法结束后,也就是一个线程结束,栈内存也就清空了。所以栈内存的生命周期和线程同步。栈内存结束了,程序就结束了。因此对于栈来说,不存在垃圾回收问题。
问题:为什么 main() 方法会先执行,最后结束?
答:因为程序的入口是 main 方法,当运行程序时,会把 main 方法入栈,在 main 方法中又会调用其他方法,那么其他的方法也会入栈,并且压在 main 方法的上面,当其他方法执行完之后依次出栈,最后 main 方法才能出栈,main 方法出栈代表程序运行结束,因此 main 方法会先执行最后结束。
程序正在执行的方法一定在栈的顶部。如果栈满了,就会抛出栈溢出的错误。
栈中主要有哪些东西:8大基本数据类型+对象引用(引用的地址)+实例的方法
栈+堆+方法区 的交互关系:(一个对象在内存中实例化的过程)
9、堆(Heap)
一个 JVM 只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件之后,一般会把什么东西放到堆中?
堆中存放:类的实例、类里面的方法、常量、变量。保存所有引用类型的真实对象。
堆内存中还要细分出三个区域:
①新生区(伊甸园区)
一个类诞生和成长的地方,甚至死亡
新生区分为伊甸园区和幸存区(幸存0区、幸存1区,或者from区、to区。叫法不一样,实质是一样的)
所有的对象都是在伊甸园区 new 出来的
②养老区
经过重GC之后再次幸存的对象都在养老区
③永久区(JDK1.8之后叫元空间)
永久区常驻内存,用来存放JDK自身携带的 Class 对象、Interface 元数据。存储的是 Java 运行时的一些环境或类信息。永久区不存在垃圾回收。关闭虚拟机就会释放这个区域的内存。
Jdk1.6之前:永久代,常量池在方法区中
Jdk1.7:永久代,但是慢慢退化了,叫做去永久代,常量池在堆中
Jdk1.8之后:无永久代,常量池在元空间中
在 JDK8 以后,永久存储区改了个名字,叫做元空间。
元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
堆内存调优:
默认情况下,给 JVM 分配的总内存(JVM 使用的最大内存)是电脑内存的 1/4,JVM 的初始化内存为电脑内存的 1/64
在 VM options 中加上参数:-Xms8m -Xmx8m -XX:+PrintGCDetails (将初始内存和总内存都调节为8MB),在打印信息中将新生区和养老区的内存加起来除以 1024,就等于我们设置的总内存数,这也就解释了元空间逻辑上存在,物理上不存在。还可以打印垃圾回收机制信息,看到轻GC 和重GC 的过程。
问题:在一个项目中如果遇到了 OOM 故障(java.lang.OutOfMemoryError:Java heap space),什么原因?怎么解决?该如何排除?
遇到 OOM 问题的原因就是堆内存满了
解决方法:
(1)尝试扩大堆内存,看结果是否还报 OOM 错误
(2)如果还报这个错,就分析内存,使用下面的方法排除问题
使用内存快照分析工具 MAT(Eclipse 最早集成的,现在用的很少)、Jprofiler 能够清楚的看到代码第几行出错。
MAT、Jprofile 的作用:
①分析 Dump 内存文件,快速定位内存泄漏问题
②获得堆中的数据
③获得大的对象
要想分析 Dump 文件,需要在 VM options 中使用命令:-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemeryError(设置该 JVM 的初始化内存为 1MB,最大内存为 8MB(不需要太大),假如程序报了 OutOfMemery 错误,就把文件 Dump 下来)
10、垃圾回收机制(GC)
垃圾回收机制的原理:
垃圾回收机制分为两种,一种是轻量级的一种是重量级的,轻量级的垃圾回收机制叫做轻GC,重量级的叫做重GC。当我们 new 一个对象时,这个对象被放在伊甸园区中。当伊甸园区中 new 了很多个对象之后,伊甸园区就满了,就会触发一次轻GC。在新生区的垃圾回收机制都称为轻GC,准确来说在伊甸园区的垃圾回收机制称为轻GC。一次轻GC会清理掉很多对象,没有被清理掉的对象就会进入到幸存区。幸存区又分为 from 区和 to 去(或者叫做幸存0区和幸存1区)。from 区和 to 区会动态的交换位置,第一个放到了 to 区,该 to 区就变成了 from 区,另一个就变成了 to 区。第二个幸存下来的也会放到 to 区,并且上次幸存的对象也会从 from 区复制到 to 区,然后该区再变成 from 区。在经历一次或多次轻GC之后幸存下来的对象都会被放在 from 区(伊甸园区和 to 区永远都是空的)。可以把幸存区的上限设置为15次(默认)或者20次,或者其他次数,当超过这个次数就会触发一次重GC,就会把伊甸园区和幸存区都清一次,在重GC中没有被回收的对象就会被放在养老区,养老区的对象一般都不会被回收了。经过研究,99%的对象都是临时对象,就是 new 出来之后使用一次就不用了,因此很少有对象能够进入到养老区。
假设在伊甸园区中只能存10个对象,当我们 new 了10个对象,那么伊甸园区就满了,就会触发一次轻GC,轻GC主要清理伊甸园区的对象,如果轻GC之后只有一个对象幸存下来了,就将该幸存下来的对象放到幸存区(准确来说是放在 from 区)。在多次轻GC过后,from 区满了,就会触发一次重GC,重GC主要清理伊甸园区和 from 区的对象,并将再次幸存下来的对象放到养老区。此外在永久存储区中存储的是基本类型和 JVM 内置的一些东西。
垃圾回收机制作用范围:
垃圾回收机制主要发生在新生区和养老区,准确的说是在伊甸园区和养老区。from 区和 to 区(幸存0区和幸存1区)在伊甸园区和养老区之间作为一个过渡期,垃圾回收机制很难清掉幸存区的对象。
11、GC算法
①引用计数法(这个几乎不用)
首先给每个对象分配一个计数器,初始值都为0。假设对象A使用了一次,就加上1,对象B使用了两次,就加上2,对象C没有被使用,就为0,对象Z使用了一次,加上1。最后使用GC把计数器为0的对象清除掉。这种方法为每个对象都分配一个计数器,这就需要消耗成本,而计数器中存放着对象使用的次数,所以计数器本身也会有一个消耗。当两个对象出现循环引用时,这两个对象的计数器永远不会为0,导致GC永远不会回收这两个对象,因此 Java 虚拟机不使用引用计数法
②复制算法(新生区主要用复制算法)
最开始时,新生区和养老区都是空的,当一个类加载进来之后,新 new 出来的对象就放在伊甸园区中,经过一次轻GC之后一些对象被清理掉,没有被清理掉的对象就从伊甸园区进入到幸存区中的 to 区,其实就是对象的复制,将没有被清理掉的对象从伊甸园区复制到幸存区的 to 区,然后将伊甸园区清空。幸存区包括两部分,一个叫做 to 区,一个叫做 from 区,这两个是动态的进行转换。对象进入 to 区,该区就变为 from 区,另一个就变成 to 区了。当再次进行轻GC之后,没有被清理掉的对象就会从伊甸园区复制到 to 区,并且将伊甸园区清空,上次GC之后在 from 区中存储的对象也会被复制到 to 区,然后将该 from 区清空,此时 to 区就会变成 from 区,而另一个就变成 to 区。也就是说在每次GC之后伊甸园区和 to 区都是空的。当一个对象经历15次(默认值)轻GC之后依然在 from 区中没有被清理掉,那么该对象就会进入养老区。
复制算法的好处:就是没有内存的碎片
复制算法的坏处:浪费了幸存区一半的空间(to 区永远是空的)。在极端情况下(假设对象100%存活),会一次性的将很多对象从伊甸园区复制到 to 区,或者一次性的将很多对象从 from 区复制到 to 区,所需要的成本非常高。因此复制算法的最佳使用场景就是对象存活度较低的时候。
③标记清除算法
简单来说,就是对存活的对象进行标记
扫描这些对象,对存活的对象进行标记,然后在执行GC时再次扫描整个内存,将没有标记的对象清除掉。
标记清除算法的好处:不需要额外的空间
标记清除算法的坏处:首先对整个内存进行两次扫描,严重浪费时间,其次标记这些对象需要消耗一些资源,然后被清除掉的对象的位置会留下空位,就会产生内存碎片
④标记压缩算法
再次扫描整个内存,向一端移动存活的对象,防止碎片的产生,但是又多了一次扫描和移动的成本。
改进:可以连续标记清除几次之后执行一次压缩,可以节省一些成本。
因此,没有最好的算法,只有最合适的算法
⑤分代收集算法
分代收集算法顾名思义就是不同的区使用不同的算法。
新生区使用复制算法最好,因为在新生区对象存活率低,复制算法需要的成本就低
养老区使用标记清除算法+标记压缩算法