目录
JVM作用
jvm是将字节码文件加载到虚拟机中,再将字节码文件编译/解释成机器码。
管理运行时的数据存储和垃圾回收,现在的jvm还可以执行其他语言的字节码。
JVM构成
1.类加载器
将硬盘上的字节码文件加载到jvm上
类加载子系统:
类加载过程:
✔️加载
以二进制的形式加载字节码;
在内存中为类生成一个class对象,将静态存储转为动态存储。
✔️链接
1.验证:
- 验证class文件的格式是否正确,class文件在文件开头有特定的文件标识(字节码文件都以CA FE****BA BE 标识开头)。
- 元数据验证:验证语法是否正确。
2.准备
- 为类的静态属性分配内存,并设置默认初始值,例如:
public static int a = 10; 准备阶段后的值是0,而不是10,初始化阶段才为10;
注意:final修饰的static变量在编译时进行初始化。
3.解析
将静态文件中的指令符号引用 替换成 内存直接引用
✔️初始化
为类变量(静态变量)赋予正确的值
类加载器的分类:
类加载器:真正实现类加载的具体实现者
✔️JVM角度(宏观)
- 引导类加载器(启动类加载器):不是用java语言实现的,c/c++ JVM底层实现
- 其他所有类加载器:用java语言写的实现类,都继承java.lang.ClassLoader
✔️开发者角度(微观)
引导类加载器 :
java中系统提供的类,都是由启动类加载器加载,例如String
扩展类加载器 :
java语言编写的,由sun.misc.LauncherAppClassLoader实现,
派生于ClassLoader类
jre/lib/ext子目录(扩展目录)下加载类库
应用程序类加载器 :
java语言编写的,由sun.misc.LauncherAppClassLoader实现。
派生于ClassLoader类
加载我们自己定义的类,用于加载用户类路径(classpath)上所有的类
自定义类加载器 :
例如我们自己写一个集成ClassLoader
再例如Tomcat这种容器,都会有自己加载类的加载器
双亲委派机制:
当加载一个类的时候,先让上一级的类加载去加载,直到找到引用类加载器;
如果上级类加载器找到了,就是要上级类加载器加载我的类,
如果找不到,就逐级向下委托,使用子级类加载器加载我的类,
如果都找不到就报异常。
java
public class ClassLoaderDemo {
public static void main(String[] args) throws ClassNotFoundException {
//类加载器为null,说明是引导类加载器加载的
System.out.println(String.class.getClassLoader());
//我们的类是由sun.misc.Launcher$AppClassLoader@18b4aac2,应用程序类加载的
System.out.println(ClassLoaderDemo.class.getClassLoader());
//sun.misc.Launcher$ExtClassLoader@1b6d3586 应用程序类是由扩展类加载的
System.out.println(ClassLoaderDemo.class.getClassLoader().getParent());
//扩展类加载器是由引导类加载器加载的
System.out.println(ClassLoaderDemo.class.getClassLoader().getParent().getParent());
}
}
输出结果:
双亲委派机制测试:当创建一个自己的String类,调用其中的类变量,由于双亲委派机制,java核心类库会创建String对象,而不会使用我们自己创建的String的类,因此报错。
双亲委派机制的优点:
安全,避免改变java核心类。
如何打破双亲委派机制?
在 ClassLoader 类中涉及类加载的方法有两个,loadClass(String name), findClass(String name),这两个方法并没有被 final 修饰,也就表示其他子类可以,重写 findClass 方法。
我们可以通过自定义类加载重写方法打破双亲委派机制, 再例如 tomcat 等都有自己定义的类加载器。
2.运行时数据区
运行时数据区组成:
程序计数器
作用:用来存储下一条指令的地址,由执行引擎读取下一条指令。
特点:
是一个很小的内存空间,但是运行速度最快的存储区域;
-
是线程私有(每个线程都会有自己的计数器);
-
生命周期与线程一致;
-
不会出现内存溢出(唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域),不会有垃圾回收。
虚拟机栈
栈是运行单位,管理方法(java自己写的方法)运行;
调用方法入栈,运行出栈;
栈是线程私有的,存在内存溢出可能,不存在垃圾回收;
一个线程就是一个栈;
一个方法入栈后,可以看做是一个栈帧。
访问速度仅次于程序计数器。
栈帧:
局部变量表:存储方法中定义的变量
操作数栈:表达式计算
方法返回地址:与返回值无关
本地方法栈
作用:管理本地方法的调用
本地方法是线程私有的,不会有垃圾回收,是用c语言写的,用native关键字修饰;
如果线程请求分配的栈容量超过本地方法栈允许的最大容量抛出StackOverflowError(内存溢出)。
常见的本地方法:
Object中:
hashCode() 内存地址
getClass()
clone()
notiy(),唤醒,wait(),otiyAll()
FileInputStrem中:
native int read()
Thread
native void start0()
堆
存放程序中产生的对象
JVM管理的最大一块内存空间
大小可以调节
线程共享
会出现内存溢出,会进行垃圾回收
堆空间有区域划分,为什么要进行划分?
新生代:
伊甸园区:刚刚创建的对象存放在伊甸园区
幸存者0:伊甸园剩余的对象和幸存者1中的对象
幸存者1:幸存者1中的对象
老年代:
存放生命周期长的/非常大的对象
经历过15次回收后依然存活的对象,将放在老年代
为什么要分区:
可以根据不同对象的存活时间进行划分
生命较长的对象放在老年区,减少垃圾回收的频率和扫描次数
对象创建以及在内存分布过程:
1.新创建的对象放在伊甸园区
2.当垃圾回收时,将伊甸园存活的对象移入到幸存者0区
3.继续运行,再次创建的对象还是保存到伊甸园区
4.下一次垃圾回收到来时,将伊甸园区存活的对象和幸存者0区移入到幸存者1区,幸存者0区和幸存者1区保证有一个为空。
5.当一个对象经历过15次垃圾回收后仍然存活,那么就将此对象移入到老年代,在对象头中4个bit位用来记录回收次数,可以设置回收次数,但是最大值是15。
老年代:新生代比例=2:1
伊甸园和两个幸存者比例=8:1:1
堆空间的参数设置:官网:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-XX:+PrintFlagsInitial 查看所有参数的默认初始值
-Xms:初始堆空间内存
-Xmx:最大堆空间内存
-Xmn:设置新生代的大小
-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails 输出详细的 GC 处理日志
垃圾回收名词:
Minor GC:主要回收新生代
Major GC:回收老年代
FULL GC: 整堆回收;老年代不足时,方法区空间不足时也会触发堆回收
方法区
主要存储加到jvm中的类的信息
是线程共享的,会出现内存溢出;
方法区包含了一个特殊的区域"运行时常量池"。
方法区垃圾回收,必须同时满足三个条件
1.该类的所有对象以及子类对象都不存在
2.加载该类的加载器不存在了
3.该类的class对象没有被其他地方引用
一般情况下可以认为类是不会被卸载的。
3.执行引擎
作用:将高级语言翻译为机器码,即负责将装在到虚拟机中的字节码 解释/编译为机器码。
**解释器:**当 Java 虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容"翻译"为对应平台的本地机器指令执行。
**JIT(Just In Time Compiler)编译器:**就是虚拟机将源代码一次性直接编译成和本地机器平台相关的机器语言,但并不是马上执行。
前端编译:java-javac----->.class
后端编译:执行引擎----->机器码
解释执行:sql,css,html,js,python,解释器逐行进行解释执行
缺点:效率低,优点:省去编译时间
编译执行:将某段代码进行整体编译,然后执行编译后的结果优点:效率更高,缺点:耗时
java是解释执行+编译执行
4.本地库接口
1.本地方法:被native关键字修饰的,不是java语言实现,而是操作系统实现。
2.为什么使用:java环境与外界交互,因为上层的高级语言没有对底层硬件直接操作的权限,而是需要调用操作系统的接口进行调用。