1.JVM位置
JVM在操作系统(Window,Linux,Mac)之上。JVM说白了就是个软件,跟个虚拟机软件是一样的。Java程序都是跑在JVM之上的。JVM是用C写的(C++ --)。操作系统之下是硬件。
Java程序原本是.java文件,通过javac命令编译成Class.File,之后经过类加载器Class Loader,之后加载到JVM中。JVM包含堆(堆Heap还包含方法区),方法区(Method Area特殊的堆),Java栈(Stack (线程私有)),本地方法栈(Native Method Area),程序计数器(线程私有)。JVM之下还要执行引擎,本地方法接口(连接本地方法库)。
Java栈(Stack),本地方法栈(Native Method Area),程序计数器是不会有垃圾的。垃圾只能是在堆中。GC只会存在在堆和方法区中。而JVM调优就是在调方法区和堆。99%都是在调堆。
2.类加载器及双亲委派机制
作用:加载Class文件。
类加载器有三层加载器。启动类(根)加载器Bootstrap Class Loader,扩展类加载器 Extension Class Loader ,应用程序加载器Application Class Loader. 用户自己定义的自定义类加载器也是Application Class Loader。
双亲委派机制:如果一个类加载器收到了类加载的请求,它首先会去委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有类加载请求最终都应该传送到最顶层的启动类加载器中。只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。
3.沙箱安全机制
就是将本地的代码和远程的代码根据安全来给出不同的操作权限。
组成沙箱的基本组件:
1.字节码校验器:确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护,但并不是所有的类文件都会经过字节码校验,比如核心类。
2.类装载器:类装载器会在三个方面对Java沙箱起作用。它防止恶意代码去干涉善意的代码(双亲委派),它守护了被信任的类库边界,它将代码归入保护域,确定了代码可以进行哪些操作。
4.Native、方法区
native关键字是调用本地C和C++库中的函数。调用native的方法会进入到本地方法栈,之后区调用底层的本地方法接口(JNI)。本地方法接口的作用是扩展Java 的使用,融合不同的编程语言为Java所用。本地方法栈就是再内存中专门开辟的一块标记区域,用来登记native方法,在最终执行的时候,加载本地方法库中的方法通过JNI
5.程序计数器
PC寄存器,程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码,在执行引擎读取下一条指令时+1,空间很小,可忽略不计。
6.方法区
线程共享的。保存所有字段和方法字节码,以及一些构造函数,接口代码也再次定义。简单来说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池(JDK8后存在于堆中),都是存在方法区的。但是实例变量存在堆内存中,和方法区无关。
7.栈
Java运行时候就是main先入栈(Java虚拟机栈),之后调用的方法入栈,结束的方法出栈。栈的生命周期和线程同步,因此栈是线程独有的。八大基本类型、对象的引用、实例的方法都是在栈中。
简单理解创建一个对象:从方法区的Class new出一个对象,对象本身存在堆中,堆的地址给栈中的引用。
8.堆
Heap,一个JVM只有一个堆内存(HotSpot虚拟机),堆内存的大小是可以调节的。类加载器读取了类文件后,一般会把什么放到堆中?类、方法、变量、引用类型的真实对象。
堆内存中可以分为三个区域:新生代、老年代、永久代(JDK8以后改为元空间,而且有一些改变)。(也有一些人认为JDK8开始的元空间不算堆因为元空间在本地内存中,堆只包含两部分)
其中新生代可以分为一个Eden区和两个Survivor区
轻(量级)GC负责新生代的GC,重(量级)GC负责老年代的GC。所以GC垃圾回收主要是在Eden区和老年代。
9.新生代、老年代、永久代
类诞生和成长的地方,甚至死亡。
分为Eden区,Survivor 1区,Survivor 2区.所有对象都是在Eden区中诞生。当Eden空间满了触发Minor GC,将Eden空间和from Survivor活下来的对象放入to Survivor区,每次清理过后空的Survivor区为下次的to区。此外Minor GC还会将新生代的对方放到老年代中
当整个新生代和老年代全满了,触发full GC,清理二者。当二者全满了且无法再垃圾回收时就发生OOM。
永久代一般用来存放JDK自身携带的Class对象、存储Java运行时的一些环境或类信息。这个区域不存在垃圾回收。在JVM关闭时会被释放。
jdk1.6前:永久代,常量池在方法区
jdk1.7:永久代,常量池在堆中
jdk1.8后:无永久代改为元空间,常量池和方法区在元空间中。有时候可以将方法区称为非堆。
Runtime.getRuntime().maxMemory();//返回虚拟机试图使用的最大内存 字节
Runtime.getRuntime().totalMemory();//返回jvm初始化总内存 字节
默认情况下试图使用的最大内存是电脑内存的1/4,初始化总内存是1/64
调参!!
VM options:-Xms1024m 调整初始化内存
-Xmx1024m 调整最大内存
-verbose:gc 输出GC的详细情况
-Xss128k 设置虚拟机栈的大小为128k
-XX:+PrintGCDetails 查看GC的一些详细消息
-XX:+PrintGC表示在控制台上打印出GC信息
-XX:PermSize=10M 初始分配永久代容量必须以M为单位
-XX:MaxPermSize=10M 允许分配的永久代最大容量必须以M为单位,大部分情况下默认为64M
-XX:+TraceClassLoading 查看类加载信息
-XX:+TraceClassUnLoading 查看类的加载信息
-XX:NewRatio=4 设置年轻代:老年代=1:4
-XX:SurvivorRatio=8 设置两个Survivor区共占Eden区的2:8.这个参数默认为8
-Xmn20M 设置年轻代大小为20M
-XX:+HeapDumpOnOutOfMemoryError出现内存溢出异常时Dump出当前的堆内存转储快照
-XX:+UseG1GC 表示让JVM使用G1垃圾收集器
-XX:PretenureSizeThreshold=3145728表示对象大于3145728(3M)时直接进入老年代分配,只能以字节作为单位
-XX:MaxTenuringThreshold=1 表示对象年龄大于1,自动进入老年代
-XX:CompileThreshold=1000 表示一个方法被调用1000次之后,会被认为是热点代码,并触发即时编译
-XX:+PrintHeapAtGC表示可以看到每次GC前后堆内存布局
-XX:+PrintTLAB表示可以看到TLAB的使用情况
-XX:+UseSpining 开启自旋锁
-XX:PreBlockSpin更改自旋锁的自旋次数,使用这个参数必须先开启自旋锁
OOM解决方法:1.调整扩大堆内存空间,看自己是否做错了代码 2.看代码是否有问题(测试工具–MAT, Jprofiler)
MAT, Jprofiler作用:1.分析Dump内存文件,快速定位内存泄漏;2.获得堆中数据 ; 3. 获得大的对象。
10.GC算法
GC作用区域:堆、方法区。
10.1引用计数法(用的比较少)
给每个对象一个计数器,标记这个对象被引用了多少次。当引用为0就将其淘汰。
缺点:计算器本身就占空间,会有损耗
10.2复制算法–主要用在年轻代
触发GC时,将Eden区的对象放到Survive To区,将Survive From区的东西根据其年龄复制到另一个Survive To区或老年代。
当一个对象经历15次GC还没死,就进入老年代,这个参数可以调,但是最多为15(因为对象头记录分代为4bits所以次数最大为15)。
好处:没有内存的碎片。
坏处:浪费了内存空间(多了一半Survive To区)(极端情况,From区满了且全存活,复制到To区,会OOM)所以复制算法的最佳使用场景:对象存活度较低时。
10.3标记清除法
回收时,先扫描这些对象,对活着的对象进行标记,之后在扫描一遍,对没有标记的对象进行清除。
缺点:两次扫描严重浪费时间,会产生内存碎片。
优点:不用额外空间。
10.4标记压缩法
在标记清除法的基础上进行优化。最后再次进行扫描,向一段移动存活的对象,将碎片的内存变为连续的内存。
缺点:需要多扫描一次并需要多移动一次。
总结:
时间效率:复制算法>标记清除法>标记压缩法
内存整齐度:复制算法=标记压缩法>标记清除法
内存利用率:标记压缩法=标记清除法>复制算法
没有最好的算法,只有最合适的算法。
GC:分代收集算法 年轻代的存活率低所以采用复制算法。老年代的区域大存活率大所以采用标记清除+标记压缩混合