什么是JVM
- JVM 是 java虚拟机,是用来执行java字节码(二进制的形式)的虚拟计算机。
- jvm是运行在操作系统之上的,与硬件没有任何关系。
JVM跨平台的原理
- 跨平台:由Java编写的程序可以在不同的操作系统上运行:一次编写,多处运行。
- 原理:编译之后的字节码文件和平台无关,需要在不同的操作系统上安装一个对应版本的虚拟机(JVM)
JVM版本
- HotSpot :常用,Sun公司出品
- BEA Jrockit
- IBM J9VM
JVM架构图
JVM类加载图
类加载器Class loader
作用:加载class文件
public class Car {
public static void main(String[] args) {
//类是一个模板,是抽象的,而对象是一个具体的
Class<Car> carClass = Car.class;
Car car1 = new Car();
//获取对象car1的类(模板)
Class<? extends Car> aClass = car1.getClass();
//car2,car3的getClass()是一样的,但是是三个不同的对象
Car car2 = new Car();
Car car3 = new Car();
}
}
类加载器的分类:
-
根类加载器: BootStrap ClassLoader,也叫启动类加载器。主要负责加载Java基础类,对应加载的文件是%JRE_HOME/lib/ 目录下的rt.jar、resources.jar、charsets.jar和class等,构造ExtClassLoader和APPClassLoader。
该类加载器没有父加载器,是用C#实现的(Java调不到,返回null),它负责加载虚拟机的核心库,如 java.lang.* 等。java.lang.Object就是由根加载器加载的。根类加载器从系统属性 sun.boot.class.path 所指定的目录中加载类库。
根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,他并没有继承java.lang.ClassLoader类。 -
扩展类加载器: Extension ClassLoader,它的父类是根加载器.主要负责加载jre/lib/ext目录下的一些扩展的jar和Class。一般可以在ext目录下做扩展
它的父类是根类加载器。它从java.ext.dirs系统属性中所指定的目录中加载类库,或者从JDK的安装目录的 jre\lib\ext 子目录(扩展目录)下加载类库。
如果用户创建的JAR文件放在这个目录下,也会被自动由扩展类加载器加载。扩展类加载器是纯java 类,是java.lang.CLassLoader类的子类。 -
应用程序加载器: APP ClassLoader,它的父类是扩展类加载器。主要负责加载应用程序的主函数类。
它的父加载器为扩展类加载器,它从环境变量classpath 或者系统属性java.class.path 所指定的目录中加载类。
他是用户自定义的类加载器的默认父加载器。系统类加载器是存java 类,是java.lang.ClassLoader类的子类。 -
用户自定义加载器:由Java实现。我们可以自定义类加载器,并可以加载指定路径下的class文件。
public class Car {
public static void main(String[] args) {
//类是一个模板,是抽象的,而对象是一个具体的
Class<Car> carClass = Car.class;
System.out.println(carClass.getClassLoader()); //AppClassLoader
System.out.println(carClass.getClassLoader().getParent()); //ExtClassLoader
System.out.println(carClass.getClassLoader().getParent().getParent()); //null
}
}
双亲委派机制:向上检索,向下加载
详细流程:
从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
为什么要使用双亲委派机制
又两个好处:防止重复加载和防止核心.class被篡改
如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。正如下面演示的:自己重新洗了一个String类,结果运行的时候并没有使用自己重写的类
沙箱安全机制
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
作用:
保证JVM的安全: 将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并规范运行。
比如:用文本编辑器写一个Hello.java,少一些分号,然后进行编译,发现会报错,这就依靠于class文件校验器
https://blog.csdn.net/qq_30336433/article/details/83268945
Native
native:凡是用native修饰的,说明java的作用范围已经做不到了,会去调用底层的C语言库或者其他编程语言库
会进入本地方法栈,会调用本地方法接口(JNI)
JNI的作用扩展java的使用,融合不同的编程语言为java所用(如C、C#等),因为java刚诞生的时候C和C#横行,想要立足,必须要调用C和C#的方法。
java在内存区域专门在运行时数据区专门开辟了一个区域(本地方法栈),登记native方法。最终运行的时候,通过本地方法接口加载本地方法库。现在:一般通过Http亲求、socket、WebService等
本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而**本地方法栈则是为虚拟机使用到的Native方法服务。**虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
程序计数器
也叫PC寄存器,程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。
java虚拟机的多线程是通过线程轮流切换并分配CPU的时间片的方式实现的,因此在任何时刻一个处理器(如果是多核处理器,则只是一个核)都只会处理一个线程,为了线程切换后能恢复到正确的执行位置,**每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,因此这类内存区域为“线程私有”的内存。**如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。
程序计数器主要有两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(Class)、常量(final)、静态变量(static)、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。
相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了
栈
后进先出,或者叫栈内存,主管程序的运行,生命周期和线程同步。线程结束,栈内存也就释放了。不存在垃圾回收问题。一般存放:8大基本类型+对象的引用(0X00FF对象的地址)+ 实例的方法 。
每执行完一个方法都会弹出栈。
栈帧:每一个执行的方法都会产生一个栈帧
https://blog.csdn.net/qian520ao/article/details/79118474
变量作为方法的参数或者返回值时是线程不安全的。
Math
javap -c MainTest.class反汇编字节码文件,生成math.txt文件
堆
堆是Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:其中新生代又分为:Eden(伊甸)空间、Survivor From 、Survivor To 空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。“分代回收”是基于这样一个事实:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。
在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。
元空间只在逻辑上存在,并不是实际存在的。
打印JVM的GC日志:-Xmx10m -Xms10m -XX:+PrintGCDetails
直到最后OOM,堆内存溢出。
分析OOM工具 eclipse:eclipseMAT idea:jprofile
当发生OOM时生成dump文件:-Xmx10m -Xms10m -XX:+HeapDumpOnOutOfMemoryError
GC:垃圾回收机制
1.引用计数法,给每一个对象加一个技术器(引用了多少次),用完一次-1,直到为0,就是没有再引用了,就可GC掉了。
这种方式用的很少,因为计数器损耗空间和性能,会产生内存碎片
2.复制算法
每一次GC,都会将Eden Space存活的对象移动到幸存区中,一旦Eden被GC都会变为空的,
幸存区To和From始终有一个空的,如果一次GC之后发现两个幸存区都又对象,就把其中一个复制到另一个,保证有一个是空的(幸存区To)。这个算法主要用于年轻代
https://blog.csdn.net/luzhensmart/article/details/81369091
什么时候会进入养老区?
当一个对象经历15次(默认值)GC就会进入养老区。修改方法:-XX:MaxTenuringThreshold=15,通过这个参数设置进入老年的GC次数条件
缺点:始终都会有一个空白区(幸存区To)
3.标记清楚法
GC回收时有两步操作:第一步对活着的对象做标记,第二步对没有标记的对象做清除
优点:不要额外的存储空间(相对于复制清楚法)
缺点:两次扫描,严重浪费时间,同时耶会产生内存碎片(不连续的内存空间)
4.标记压缩清楚法
对标记清楚的优化,扫描两次无法优化,所以只能优化内存碎片
再加一个扫面,移动存活的对象,让他们的存活位置连续
缺点:又加了一次扫描