虚拟机定义: 软件模拟的具有完整硬件功能的,运行在独立的隔离环境中的计算机系统
JVM虚拟机 java虚拟机,通过软件模拟java字节码的指令集,是java程序的运行环境
类加载过程概述
编译(javac.exe)----》加载-----》解释器(java.exe)------》内存分配,字节码执行,垃圾回收,高效并发处理
编译,装载过程和平台环境无关,编译过后的.class文件被不同的虚拟机解释执行
class文件结构
依次是魔数,此版本号,主版本号,常量池,访问标志(类还是接口,是否public 是否final 是否abstract等)
常量池包括两大类,字面量和符号引用,字面量类似于java中的常量,如文本字符串,被声明为final的常量值,符号引用包括 被模块导入或开放的包,类的接口和全限定名,字段的名称和描述符,方法的名称和描述符,方法句柄和方法类型,动态调用点和动态常量。
class文件不会保存方法,字段的最终在内存中的布局信息,这些字段,方法的符号引用不经过虚拟机在运行期的转换是无法得到最终的内存地址入口的
分析字节码文件的工具 javap -verbose *.class //verbose 冗长的
public class TestClass {
private int m=5;
public int test(){
return m+1;
}
}
$ javap -verbose TestClass.class
Classfile /D:/background-start/background-starter/src/main/java/com/mxx/starter/test/TestClass.class
Last modified 2020-12-30; size 306 bytes
MD5 checksum df860a3649a5a9be02a83941d8a63169
Compiled from "TestClass.java"
public class com.mxx.starter.test.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/mxx/starter/test/TestClass.m:I
#3 = Class #17 // com/mxx/starter/test/TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 test
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 com/mxx/starter/test/TestClass
#18 = Utf8 java/lang/Object
{
public com.mxx.starter.test.TestClass(); //构造函数
descriptor: ()V
flags: ACC_PUBLIC
Code: //方法内的操作
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_5
6: putfield #2 // Field m:I
9: return
LineNumberTable:
line 10: 0
line 12: 4
public int test();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 14: 0
}
SourceFile: "TestClass.java"
字面量
类加载,连接,初始化
类从加载到卸载出内存,生命周期如图
加载
- 通过类的全限定名查找并加载类的二进制数据,
- 转化为方法区的运行时数据结构,
- 在堆上创建java.lang.Class对象,用来封装类在方法区内的数据结构,并向外提供访问类内数据结构的接口
jvm自带的类加载器 (按组合方式顺序加载)
-
启动类加载器(bootstrapClassLoader) 打印输出为null 负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的固定类名的jar包加载到内存中,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类,jdk自身提供得类
-
jdk8 扩展类加载器(extensionClassLoader)/ jdk9以后 平台类加载器(platformClassLoader)
扩展类加载器加载jre/lib/ext/下文件
- 应用程序类加载器(appClassLoader) 负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,classpath目录是指打包生成jar包中 lib和classes的同级目录或者是target/classes目录 在idea中 查看
URLClassLoader systemClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
for (URL url : systemClassLoader.getURLs()) {
System.out.println("app 路径----"+url);
}
- 自定义类加载器
方便对加载的类做一些增强 因为符合双亲委派机制,被加载的类.class不能存在classpath目录下
public class MyClassLoader extends ClassLoader{
//覆盖findClass 则遵循双亲委派模型
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = new byte[0];
try {
//文件转为二进制数组
bytes = loadClassDate(name);
} catch (IOException e) {
e.printStackTrace();
}
return this.defineClass(name,bytes,0,bytes.length);
}
public byte[] loadClassDate(String name) throws IOException {
byte[] bytes=null;
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
name = name.replace(".", "\\");
//项目根目录下classes文件夹放置编译好的class文件
InputStream inputStream=new FileInputStream(new File("classes/"+name+".class"));
int a=0;
while ((a=inputStream.read())!=-1){
byteArrayOutputStream.write(a);
}
bytes = byteArrayOutputStream.toByteArray();
return bytes;
}
}
MyClassLoader myClassLoader=new MyClassLoader();
try {
//剪贴编译生成target目录下Test.class到项目根目录下classes目录下
System.out.println(myClassLoader.loadClass("com.example.demo.Test").getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
双亲委派机制
一个类如果需要被加载,那么加载这个类的类加载器会委托给父类去加载,每一层都是如此,递归到最顶层,每一层返回C如果C为null 则去调findClass方法返回C,当父类加载器在其路径下找到此类全路径名则直接返回,如果没有找到,子类加载类才会尝试去加载,这里的双亲只是指的父类加载器,并且父子关系之间不是继承,而是组合(调用)
// First, check if the class has already been loaded 先判断class是否已经被加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false); //找他爸爸去加载
} else {
c = findBootstrapClassOrNull(name); //没爸爸说明是顶层了就用Bootstrap ClassLoader去加载
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name); //最后如果没找到,那就自己找,找到了则直接返回这一层的C
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
这样做的目的是
- 从类的内部来说保证类能够安全加载,因为这样做类有了层次结构,这样如果有不法分子自己造了个java.lang.Object,里面嵌了不好的代码,如果我们是按照双亲委派模型来实现的话,最终加载到JVM中的只会是我们rt.jar里面的东西,也就是这些核心的基础类代码得到了保护。
- 从类的外部来说能保证引用的类具有公共性,一致性,各个不同模块中引入的公共的类只加载一次并是同一个。
破坏双亲委派
因为某些情况下父类无法在自己的范围内加载到需要的类,只能委托给子类去加载需要的class文件,如各种spi,jdk定义接口,不同厂商实现,DriverManager需要加载不同数据库的驱动,DriverManager是启动类加载器,如mysql驱动属于应用程序类加载器,这个时候就需要启动类加载器来委托子类来加载Driver实现。通话Thread的setContextClassLoader, getContextClassLoader来设置获取子类加载器,底层实现还是应用程序类加载器。在如热部署中也没有严格按照双亲委派模型
连接
将已经读入内存的类二进制数据合并到jvm运行时环境
验证
Java语言的安全性是通过编译器来保证的,不符合规范的类不能被正确编译,但虚拟机只认二进制字节流,它不会管所获得的二进制字节流是哪来的。当然,如果是编译器给它的,那就相对安全。但如果是从其他途径获得的,那么无法确保该二进制字节流是安全的。
保证class文件中的信息符合jvm规范中的全部约束,包括
-
文件结构 验证是否符合jvm的class文件结构规范 我们知道加载的过程是二进制字节流从外部到内存区的过程,二进制字节流只有通过此阶段的验证才被允许进入方法区,在进行接下来的验证,说明加载和验证是交叉进行的
-
元数据验证
对描述的信息进行语义分析,保证符合jvm规范要求,比如数据类型是否正确,是否继承父类,如果有父类是不是final类,如果是的话则不允许继承,如父类是不是抽象类,如果是的话有没有重载其抽象方法等
-
字节码验证
通过对数据流和控制流的分析,确保语义是合法和符合逻辑的,对方法体进行验证,如return 后是否还有语句
-
符号引用验证
对类自身以外的各类信息进行匹配性校验,比如引用的类是否能访问到,常量池中的常量是否有效等
并且保证这些信息不会损害到虚拟机的安全
准备
为类的静态变量分配内存并初始化默认值,在JDK7及以前,类变量是存储在方法区当中的,而在JDK8及之后,类变量已经随着Class对象一起存放在Java堆当中了
解析
把class文件定义的常量池中的符号引用转化为直接引用,解新动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行
符号引用(Symlxiuc References):符号引用以一组符号来描述所引用的日标,符号可以是任何形式的字面量, 只要使用时能无歧义地不冲突的定位到目标即可
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄
初始化
为类的静态变量赋初始值执行静态代码块或者说执行类构造器< client >方法
1.这个方法是由编译器自动收集类中所有类变量的赋值操作和静态代码块中的语句合并产生的,收集的顺序是由语句在源文件中出现的顺序决定的,静态代码块只能访问到定义在它之前的类变量,但是可以为定义在它之后的类变量赋值。因为在准备阶段已经加载了变量并赋了初始值,可以赋值,但初始化没有完成,不能访问。如下:
public class Test{
static {
i = 0; //编译通过
System.out.print(i); //编译失败
}
static int i = 1;
}
static {
i = 0; //编译通过
}
static int i = 1;
public static void main(String[] args) {
System.out.println(Test2.i); // 1
}
- 如果类存在父类且没有初始化,则先初始化父类
3.如果一个类实现一个接口或者一个接口继承父接口,则在初始化阶段不会初始化父接口,只会在首次使用接口中的变量或者调用接口中的方法时才会导致接口初始化。如:
public interface Parent {
public static final String s="parent 变量";
public void t1();
}
public class Test4 implements Parent{
public static String ss="子类的静态变量";
static {
System.out.println(ss);
}
@Override
public void t1() {
System.out.println("接口中的方法");
}
public static void main(String[] args) {
System.out.println(Test4.s);//子类的静态变量 parent 变量
}
}
- 使用类加载器去加载某个类时,不会触发类的初始化,因为不是对类的主动使用(如果有default方法时会初始化)
什么时候会初始化
- 实例化某个类
- 访问某个类或接口的静态变量或静态方法
- 反射某个类
Class<?> aClass = Class.forName("com.mxx.starter.test.Test3");
4.初始化未初始化类的子类
例题1
public class Test4 {
public static Test4 test4=new Test4();
public static int a=0;
public static int b;
public Test4() {
a++;
b++;
}
public static Test4 getInstance(){
return test4;
}
public int getA() {
return a;
}
public int getB() {
return b;
}
}
Test4 instance = Test4.getInstance();
System.out.println(instance.getA());//0
System.out.println(instance.getB());//1
分析:准备阶段静态变量赋默认值 int a=0;int b=0 初始化阶段按顺序赋值 执行构造器 a=1,b=1 ,a在赋值0,b没有重写赋值 ,输出 a=0,b=1 (想起了js中也有同样的变量初始化套路 )
案例2
public class Test4 {
public static int a=0;
public static int b;
public static Test4 test4=new Test4();
public Test4() {
a++;
b++;
}
public static Test4 getInstance(){
return test4;
}
public int getA() {
return a;
}
public int getB() {
return b;
}
}
Test4 instance = Test4.getInstance();
System.out.println(instance.getA());//1
System.out.println(instance.getB());//1
分析:参考上
不会导致初始化
子类引用父类的静态变量,子类不会初始化
访问子类的常量,不会初始化
实例化类的数组
卸载
jvm 自带的类加载器加载的类是不会被卸载得到(所以类加载只会进行一次),自定义类加载器加载的类是可以卸载的
内存管理
概览
程序计数器(pc寄存器)
线程私有的,用于存储程序执行下一条指令的地址,执行本地方法时值是undefined,唯一不会oom的区域
虚拟机栈
描述方法执行的每个线程的内存模型,每个方法执行会同步创建一个栈帧,多个栈帧组成一个栈,每个帧存储一个方法的局部变量表,操作数栈(方法进行一系列操作的进站出站行为),方法返回地址,动态连接(每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用)等信息,调用一个方法时会创建一个帧并压栈,退出方法时,修改栈顶指针并销毁帧。局部变量表存放编译期可知的各种基本类型和引用类型包括局部变量和参数变量,以slot形式存放,每个slot存放32位数据,优点存取快,缺点存放的数据大小,生存期在编译期就决定了
本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。又称非堆。从jdk8开始,使用本地内存的元空间来替代。class文件中字面量和符号引用将在类加载后存放在方法区内运行时常量池。一般来说,符号引用翻译过来的直接引用也存放到常量池中。运行期间也可以将新的常量放入池中,如String.intern()方法
注 静态变量和常量存在于方法区即静态区 成员变量存在于堆内存,局部变量存在于栈内存。
堆
线程共享存放创建的对象和数组,在运行期动态分配内存大小并进行垃圾回收
堆内存结构 分代思想
新生代存放新分配的对象,新生代经过垃圾回收没有回收掉的对象复制到老年代,老年代存放的对象比新生代年龄要大的多,老年代也会存储大对象
整个堆大小=新生代+老年代
新生代=Eden区+存活区
从前的持久代,用于存放Class Method 的原信息的区域从jdk8开始存储在元空间,元空间不在虚拟机内,而是使用的本地内存
对象在内存布局包括
对象头 实例数据 和对齐填充
对象头 包括
mark word 存储对象自身的运行状态,如锁状态标识,gc分代年龄,hashCode等
类型指针 对象指向它的类元数据的指针,通过这个指针确定该对象是哪个类的实例
对象的访问
通过句柄访问(间接引用)
通过指针 (直接引用)
hotspot虚拟机主要使用第二种方式
trace参数跟踪
-version
-showversion
jvm启动时打印版本信息在继续执行
-server
以客户端启动(影响堆栈默认内存大小,提供性能)
-client
以服务端启动
以下为打印GC日志和OOM时dump的参数(jdk9以后引入了统一的-Xlog)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=xxxx/heapdump.hprof
-XX:+PrintGC
-XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGc
-Xms 初始化堆的大小
-Xmx 堆的最大值
-Xmn 设置年轻代大小(初始化和最大),默认整个堆的3/8
-XshowSettings:范围
打印配置项信息,可选项有all / locale / properties / vm
设置元空间初始内存和最大内存为512m
-XX:MaxMetaspaceSize=512m -XX:MetaspaceSize=512m
xms xsx默认值:
如果下载的是32位jdk,默认启动时以客户端启动,client模式下,JVM初始和最大堆大小为:
在物理内存达到192MB之前,JVM最大堆大小为物理内存的一半,否则,在物理内存大于192MB,在到达1GB之前,JVM最大堆大小为物理内存的1/4,大于1GB的物理内存也按1GB计算
举个例子,如果你的电脑内存是128MB,那么最大堆大小就是64MB,如果你的物理内存大于或等于1GB,那么最大堆大小为256MB。 Java初始堆大小是物理内存的1/64,但最小是8MB。
如果下载的是64位jdk,默认启动时以客户端启动server模式下: 与client模式类似,区别就是默认值可以更大,比如在32位JVM下,如果物理内存在4G或更高,最大堆大小可以提升至1GB,,如果是在64位JVM下,如果物理内存在128GB或更高,最大堆大小可以提升至32GB。
-XX:HeapDumpOnOutOfMemoryError
-XX:newRatio 老年代和新生代所占内存的比值 默认是2 即老年代:新生代=2:1
-XX:SurvivorRatio eden与survivor比值
-Xss 方法的调用深度