什么是JVM
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
JDK 、JER、JVM三者之间的关系
JDK(Java Development Kit)是针对Java开发员的产品,是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。Java Runtime Environment(JRE)是运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。JVM是Java Virtual Machine(Java虚拟机)的缩写,是整个java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。
JVM 位置
jvm是运行在操作系统上的,而操作系统的下一层是硬件体系
Jvm的体系结构
- 方法区:是被所有线程共享的,属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、运行时的常量池存在方法区中
- 线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
- 本地方法栈(native method stack)主要作用是登记native方法,然后在execution engine执行的时候加载本地方法库。
- 对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
- 每一个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区的方法字节码(用来存储指向一条指令的地址,即将要执行的指令代码)在执行引擎读取下一个指令,是一个非常小的内存空间。
- 来一张图帮助大家理解每个区域存储的内容
类加载器
作用:加载class文件
这里我们首先要明白,类是一个模版,对象是具体的,写一段代码帮助大家理解
package jvm;
public class Catr {
//类是模版,,对象是具体的
public static void main(String[] args) {
Catr catr1 = new Catr();
Catr catr2= new Catr();
Catr catr3 = new Catr();
System.out.println(catr1.hashCode());
System.out.println(catr2.hashCode());
System.out.println(catr3.hashCode());
System.out.println(catr1.getClass().hashCode());
System.out.println(catr2.getClass().hashCode());
System.out.println(catr3.getClass().hashCode());
}
}
从hashcode的值我们可以清楚的知道,类的模版是同一个,而对象却不相同,这就是类是模版,对象是实列的解释。
- 类加载器的分类
1.Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
2.ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
3.AppClassLoader:主要负责加载应用程序的主函数类
package jvm;
public class Catr {
//类是模版,,对象是具体的
public static void main(String[] args) {
Catr catr1 = new Catr();
Class<? extends Catr> aClass1 = catr1.getClass();
ClassLoader classLoader = aClass1.getClassLoader();
System.out.println(classLoader);//AppClassLoader
System.out.println(classLoader.getParent());//ExtClassLoader
System.out.println(classLoader.getParent().getParent());
//null 的原因可能是 不存在 或者 java程序获取不到 rt.jar
}
}
双亲委派机制
在java包下新建一个lang包,创建一个String类
package java lang;
//双亲委派机制 保证安全
//1.先在AppClassLoader找->ExtClassLoader-.boot(最终执行)
public class String {
public java.lang.String toString(){
return "hello String";
}
public static void main(String[] args) {
String string = new String();
string.toString();
}
}
这里是因为它根本就不是走的我们写的man方法,因为双亲委派机制会从AppClassLoader>ExtClassLoader->bootClassLoad一层一层的去找最终便会在bootClassLoad中找到所以便找到rt.jar包下的java.lang包下的Sring类。
如果我们定义一个没有被定义过得类呢?
public class Student {
public String toString(){
return "hello";
}
public static void main(String[] args) {
Student student = new Student();
System.out.println(student.toString());
}
}
为了让大家更好的理解双亲委派机制,我们用一张图来解释
从上图中我们就更容易理解了,当一个Student.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
- 双亲委派机制的作用
1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。如果有人想替换系统级别的类:String.java,篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
4、沙箱安全机制
Java安全模型的核心就是java沙箱,沙箱是一个限制程序运行的环境,沙箱机制就是将 java代码限定在java虚拟机(JVM)特定运行范围当中,并且严格限制代码对本地系统的访问,通过这样的措施来保证代码的有效隔离,防止对本地系统造成破坏,沙箱主要限制系统访问资源。
系统资源包括什么?
CPU、内存、文件系统、网络,不同级别的沙箱对这些资源的访问限制也可以不一样
native
public class Demo {
public static void main(String[] args) {
new Thread(()->{
},"my thread name").start();
}
//凡是带了native关键词的,说明java的作用范围达不到了,回去调用底层C语言的库
// 会进入本地方法栈,调用本地方法接口JNI
//JNI 扩展java的使用,融合不同的编程语言,为java所用,最初是为了调用C和C++的程序
//java在内存区域专门开辟了一块标记区域,native method stack,登记native方法
//在最终执行的时候加载本地方法库中的方法通过JNI
//java程序驱动打印机会用到native
private native void start0();
}
栈
- 栈是一种数据结构
- 栈:先进后出
- 栈内存主管程序的运行,生命周期和线程同步(通俗的将 程序执行栈里面的东西,栈里面是空的程序就结束了)
- 线程结束栈内存也就释放了,对于栈来说不存垃回收的问题(线程结束 栈就空了)
为什么main方法先执行最后结束?
因为程序一开始首先加载main方法,再由main方法调用其他的方法
eg:我们写段代+图来分析一下
public class Test{
public static void main(String args[]){
Test t = new Test();
t.test
}
public void test(){
System.out.println("test")
}
}
栈会先加载main方法
然后加载test方法
执行完后先把test拿出去最后才能把main拿出去
再来深入理解一下栈的运行原理,这便要涉及到栈帧
下面再来看看 栈+堆 + 方法区:交互关系
三种JVM
- sun公司的HotSpot Java HotSpot™ 64-Bit Server VM (build 25.131-b11, mixed mode)
- BEA JRockit
- IBM J9Vm
我们一般用的都是HotSpot
堆
-
一个JVM只有一个堆内存,堆内存的大小是可以调节的
-
类加载器读取到类文件后,一般会把什么东西放到堆中呢?(类、方法、常量、变量、保存我们所有引用类型的真实对象)
-
堆内存还要细分为三个区
-
gc垃圾回收主要是在伊甸园区 和养老区
堆内存调优
经过研究,有99%的对象都是临时对象
永久区:
这个区域这区域常驻内存的,用来存放JDK自身携带的Class对象,interface元数据
,存储的是java运行时的一些环境或类信息,这个区域不存在垃圾回收,关闭vm虚拟机就会释放这个区域的内存
一个启动类加载了大量的三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类不断地被加载,就会出现OOM
Jdk1.6之前:永久代 常量池是在方法区
Jdk1.7: 永久代 ,但是慢慢的淡化了,去永久代 常量池是在堆中
Jdk1.8:无永久代,常量池在元空间
- 元空间:逻辑上存在,物理上不存在
- 扩大堆内存
发生OOM的原因:我们来写一个程序并将内存改小得到以下错误
package jvm;
import java.util.Random;
public class Demo01 {
public static void main(String[] args) {
String str = "xiaotianw";
while(true){
str += str + new Random().nextInt(888888888) + new Random().nextInt(888888888);
}
}
}
这个错误就是OOM堆内存不足的错误
在一个项目中,突然出现OOM,那么改如何排除 研究为什么出错
我们一般会采用两种方式
- 1 能够看到代码第几行出错:内存快照分析工具,AMT、Jprofiler
- 2 Dubug 一行行分析代码
AMT、Jprofiler作用
1分析Domp内存文件。快速定位内存泄漏
2获得堆中的数据
3获得大的对象 - 用JProfiler分析内存问题
1.在IDEA 下载内置jprofiler 插件
2.下载jprofiler 在IDEA的jprofiler插件配置下载的jprofiler路径
我们来写一个程序利用JProfiler来分析到底是那一行出问题了
package jvm;
import java.util.ArrayList;
//-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
//-Xms设置初始化内存分配大小 默认1/64
// -XX:+PrintGCDetails 打印GC垃圾回收
// -Xmx 设置最大分配内存 默认1/4
//-XX:+HeapDumpOnOutOfMemoryError oom dump
public class Demo03 {
byte[] arry = new byte[1*1024*1024];//1m
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<>();
int count =0 ;
try{
while(true){
list.add(new Demo03());
count ++;
}
}catch (Exception e){
System.out.println("count:"+count);
e.printStackTrace();
}
}
}
修改堆内存,造成OOM,参数-XX:+HeapDumpOnOutOfMemoryError是导出OOM错误
运行程序,在程序路径找到dump出的文件
点击在文件夹打开
按打开文件路径一层一层向上找,找到java_pid15828.hprof,双击打开
GC常用算法
JVM在进行GC时,并不是对这三个区域统一回收,大部分回收都是新生代
新生代
幸存区(from to)
老年代
GC两种:轻GC(普通GC) 重GC(全局GC)
GC的算法
标记清除法
标记压缩法
复制算法
引用计数器
- 引用计数法:
- 复制算法
这里要明白幸存to(幸存1区) 和(幸存from幸存0区),总有一个要保持为空,比如第一次幸存下来的对象存入了幸存from区,第二次会将第二次幸存下来的对象连同一次幸存的对象一起存入幸存to区,这时候幸存from区便成为幸存to区,幸存to区变成了from区这是因为幸存to区和幸存from区会来回交换,且谁空谁是to
好处:没有内存的碎片
坏处: 浪费空间(浪费了一个幸存区的空间 因为幸存区总有一个是空的)
复制算法最佳使用时间:对象存活度较低的时候 - 标记清除算法:
缺点:两次扫描严重浪费时间,会产生内存碎片
优点:不需要额外空间- 标记清除压缩:
先标记清除几次然后在进行压缩
总结
内存效率 复制算法> 标记清除算法> 标记压缩算法 (时间复杂度)
内存整齐度 复制算法 =标记压缩法 >标记清除法
内存利用率:标记压缩=标记清除>复制算法
年轻代:存活率低 复制算法
老年代: 存活率高 区域大 (标记清除(前提内存碎片不是太多)+标记压缩混合实现)