JVM的生命周期
(1)虚拟机的启动
Java虚拟机的启动时通过引导类加载器(Bootstrap Class Loader)创建一个初始类(Initial Class)来完成的,这个类是由虚拟机的具体实现指定的。
(2)虚拟机的执行
一个运行中的Java虚拟机有一个清晰的任务:执行Java程序
程序开始执行时才运行,程序结束时就停止
执行一个所谓的Java程序时,真正执行的是一个叫做JVM的进程
(3)虚拟机的退出
有如下几种情况:
程序正常执行结束
程序在执行过程中遇到了异常或错误而终止
由于操作系统出现错误而导致JVM进程终止
某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且Java安全管理器也允许这次exit或halt操作
JNI规范描述了用JNI Invocation API来加载或卸载JVM时
JVM的位置
JVM的体系结构
- 方法区:是被所有线程共享的,属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、运行时的常量池存在方法区中
- 线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(StackFrame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
- 本地方法栈(native method stack)主要作用是登记native方法,然后在executionengine执行的时候加载本地方法库。
- 对于绝大多数应用来说,这块区域是 JVM所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread LocalAllocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
- 每一个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区的方法字节码(用来存储指向一条指令的地址,即将要执行的指令代码)在执行引擎读取下一个指令,是一个非常小的内存空间。
类加载器:
作用:加载Class文件
我们要知道,类是一个模板,对象是具体的,每一个对象都是不一样的
public class Demo {
public static void main(String[] args) {
Car car1=new Car();
Car car2=new Car();
System.out.println(car1==car2);
System.out.println(car1.getClass());
System.out.println(car2.getClass());
System.out.println("=====================================");
System.out.println(car1.hashCode());
System.out.println(car2.hashCode());
System.out.println("=====================================");
Class class1=car1.getClass();
Class class2=car2.getClass();
System.out.println(class1.hashCode());
System.out.println(class2.hashCode());
System.out.println("=====================================");
ClassLoader classLoader=class1.getClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
System.out.println(classLoader.getParent().getParent());
}
}
类加载器的分类:
1.BootstrapclassLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
2.ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
3.AppClassLoader:主要负责加载应用程序的主函数类
双亲委派机制:
首先我们创建一个java->lang包,写一个String类
package java.lang;
public class String {
public static void main(String[] args) {
new String().toString();
}
@Override
public String toString() {
return "Hello World";
}
}
是不是很奇怪,我们明明定义了main,但是,程序偏偏说我们没有定义main方法,这里是因为在双亲委派机制下,他走的不是我们的main,这个类会先走AppClassLoader->BootstrapclassLoader一层一层的找,最后从rt.jar包下找到了原来就有的String
如果我们写一个不存在的类呢?
public class Student {
@Override
public String toString() {
return "Hello World";
}
public static void main(String[] args) {
System.out.println(new Student().toString());
}
}
这里搞一张图方便理解双亲委派机制
从上图中我们就更容易理解了,当一个Student.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
双亲委派机制的作用
1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。如果有人想替换系统级别的类:String.java,篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
-
沙箱安全机制
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();
}
PC寄存器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
方法区
Method Area方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
static final,Class,常量池
栈
栈是运行时的单位,Java 虚拟机栈,线程私有,生命周期和线程一致。描述的是 Java
方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack
Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
1.栈是一种数据结构
2.栈:先进后出
3.栈内存主管程序的运行,生命周期和线程同步(通俗的将 程序执行栈里面的东西,栈里面是空的程序就结束了)
4.线程结束栈内存也就释放了,对于栈来说不存垃回收的问题(线程结束 栈就空了)
为什么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 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组
- 一个JVM只有一个堆内存,堆内存的大小是可以调节的
- 类加载器读取到类文件后,一般会把什么东西放到堆中呢?(类、方法、常量、变量、保存我们所有引用类型的真实对象)
- 堆内存还要细分为三个区
新生区
- 包括伊甸园区幸存区
- 类:诞生和成长的地方
- 伊甸园:所有对象都是在这里new出来的
- 幸存区(0,1),当伊甸园区满的时候会触发轻gc,幸存下来的对象会进入幸存区
老年区
永久区
这个区域常驻内存,用来存放JDK自身携带的Class对象,interface元数据,存储的是java的一些环境
一个启动类,加载了大量的第三方jar包,Tomcat部署了太多的应用,大量动态生成的反射类,不断的被加载,便会出现OOM
- JDK1.6之前:永久代,常量池在方法区中
- JDK1.7:永久代,但是慢慢退化了,去永久代,常量池在堆中
- JDK1.8之后:没有永久代,常量池在元空间中
堆和栈的对比
一、栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
二、栈因为是运行单位,因此里面存储的信息都是跟当前线程相关的信息。包括:局部变量(含形参)、程序运行状态、方法返回值等等;而堆只负责存储对象信息。
三、在方法中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配(这一句里面的方法和函数感觉有点不对,方法和函数区别)。堆内存用于存放由new创建的对象和数组。
四、在Java中一个线程就会相应有一个线程栈与之对应,这点保证了程序的并发运行。
而堆则是所有线程共享的,也可以理解为多个线程访问同一个对象,比如多线程去读写同一个对象的值
五、栈内存溢出包括StackOverflowError和OutOfMemoryError。StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存;
堆内存溢出是OutOfMemoryError。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出OutOfMemoryError异常。
三种JVM
1.sun公司的HotSpot Java HotSpot™ 64-Bit Server VM (build 25.131-b11, mixed mode)
2.BEA JRockit
3.IBM J9Vm
我们一般用的都是HotSpot
堆内存调优
java查看核数以及内存使用量
/**
* 我的机器是8核心,16G内存
*/
public class JvmDemo {
public static void main(String[] args) {
// 查看机器的核数
System.out.println(Runtime.getRuntime().availableProcessors());
// 返回java虚拟机试图使用最大的内存量
long maxMemory = Runtime.getRuntime().maxMemory();
// 返回java虚拟机中的内存总量
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("-Xmx:maxMemory = " + maxMemory + " byte\t" + maxMemory / (double) 1024 / 1024 + " MB");
System.out.println("-Xms:totalMemory = " + totalMemory + " byte\t" + totalMemory / (double) 1024 / 1024 + " MB");
}
}
调优
Xms:设置初始堆内存分配大小,默认为物理内存的 1 / 64
-Xmx:设置最大内存分配大小,默认为物理内存的 1 / 4
-XX:+PrintGCDetails:输出相信的GC处理日志
实际生产环境中, 我们通常将初始化堆(-Xms) 和 最大堆(-Xmx) 设置为一样大。以避免GC和应用程序争抢内存,应用程序频繁的申请堆空间使其产生停顿,内存忽高忽低,最终出现
JVM在进行GC时,并不是对这三个区域统一回收,大部分时候,回收都是新生代
- 新生区
- 幸存区
- 老年区
gc垃圾回收主要是在伊甸园区 和养老区
复制算法
- 好处:没有内存的碎片
- 坏处:浪费了内存空间,多了一半永远是to
标记清除
- 优点:不需要额外空间
- 缺点:两次扫描,浪费时间,会产生内存碎片
标记压缩
总结
内存效率:复制算法>标记清除算法>标记压缩算法
内存整齐度:复制算法=标记压缩算法>标记清除算法
内存利用率:标记压缩算法=标记清除算法>法制算法
JMM
-
什么是JMM
JMM:java Memory Model的缩写 -
这是干什么的
作用:缓存一致性协议,用于定义数据读写的规则(遵守)。
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)