推荐一本书:《深入理解JVM》
再来一本:《自己动手写Java虚拟机》
这篇文章写得是挺乱的,但还是按照理解来写了一些比较重要的东西,有很多还不能够完全理解,等到时候去二刷吧,立一个Flag【2021年6月6日】
JVM不关心各种语言(不管是Java还是其他语言,都可以在JVM上面运行),JVM只关心字节码文件,需要符合JVM字节码规范。
11年出来了G1:包含了自动的内存管理 and 自动的垃圾回收功能
stack:适配小设备,跨平台(各种平台都可以重复使用),可移植,指令更多,指令集更小,性能更差。
register:指令更少,性能比stack高。
classic VM(sun) :只有解释器,只会运行,没有热点探测,没有优化
EXACT VM :热点探测,混合工作(编译器,解释器)
HotSpot VM :热点探测,融合了JRockit 的优势
JRockit VM :最快的JVM,直接面向服务端
J9 VM :
Graal VM
JIT即时编译器:
探测热点代码,暂停时间过长(因为JVM需要给他的内部进行优化)~等公交
时间与性能(运行效率)的博弈
很重要:
类加载子类系统:
引导类加载器
拓展类加载器
应用类加载器
自定义类加载器
运行时数据区域:
堆区、栈区、方法区、寄存器区
执行引擎:
垃圾回收、编译器、即时编译器
包含了引导类、拓展类、应用类引导器
引导类是由c/c++编写的,在程序内不可获取;Java的核心类库都是在这里加载的(只加载java、javax、sun开头的类)
拓展类是由Java语言编写的,都是在lib\ext包下的。
自定义类:可以对源码进行加密,可以防止源码泄露
继承URLClassLoader可以更加方便的测试(不用像继承ClassLoader一样需要编写findClass方法和字节流……)
加载:生成Class对象,加载各种类(引导、拓展、应用/系统类)
连接:
验证:确保Class文件满足JVM字节码的要求
准备:先准备好变量容器(变量就是容器,先赋零值),static final修饰的在编译的时候已经分配好了。
解析:将符号转变为直接引用
初始化:
【0】init执行默认构造器,子类加载init之前,父类完成加载(一个类只会被加载一次)
【1】clinit == 执行方法:== class init
Tips:执行静态代码块 static {}/静态的变量会在编译的时候就给予赋值
publicstaticvoidmain(String[]args)throwsException{// 【1】当前类的类加载器ClassLoaderclassLoader1=Class.forName("java.lang.String").getClassLoader();System.out.println(classLoader1);// 【2】当前线程上下文加载器ClassLoadercontextClassLoader2=Thread.currentThread().getContextClassLoader();System.out.println(contextClassLoader2);// 【3】引导类加载器(当前类的父级)ClassLoadersystemClassLoader=ClassLoader.getSystemClassLoader().getParent();System.out.println(systemClassLoader);}
如果一个类加载器接收到了请求,不会自己先去加载,而是会把请求交给父类加载器,直至顶层的类加载器;
顶层的类加载器如果能够处理,那么就返回成功,不能处理则交给子类加载,层层下传直到最底层的自定义类加载器。
好处:
防止类的重复加载,保证Java核心包的类不被替换
按需请求:只有用某个类的时候才会去请求加载某个类,并且将这个类加载到内存中。
区分类:同时双亲委派机制也可以帮助我们去区分两个类,两个类的包名不一样是不同的类,两个类的加载器不一样也不是同一个类。
进入方法我们就需要执行main方法,但是此时main方法所在的类需要被加载,而String没有main方法,所以报错。
开始使用的时候,我们自定义的String类会使用引导类加载器加载;而引导类加载器会使用rt.jar包下的String类,因为rt.jar包下的String类里面没有对应的方法,所以报错,这也体现了对源码的保护功能 == 沙箱安全机制。
包含了【本地方法栈、虚拟机栈、堆、方法区、PC寄存器(计数器)】
我们最常用的就是运行时数据区,“厨子”就是执行引擎
共享:方法区、堆区【主要的GC对象】
私有的:程序计数器,本地方法栈(本地方法接口、本地方法接口),虚拟机栈
【内存】就是CPU和网络/硬盘的 桥梁
Program Counter Register 程序计数器
Def:存储了下一条指令的地址(就像一个游标一样,告诉程序下一步应该怎么走。)
被Thread私有,每个线程独有一个程序计数器
不存在【GC、OOM】
栈中包含了栈帧,一个栈帧对应着一个方法。
就像Stack一样,来一个就放一个,只处理栈顶元素,管理【调用&返回】,【不存在GC】,但是有【栈溢出】
使用的指令架构:【栈】
内存溢出:栈溢出,但存在动态扩展机制,直到内存占满了才会出现OOM
线程:独有栈,之间的栈帧不共用。
局部变量表(Local Variables)、操作数栈(operand Stack)、动态链接(指向常量池)、方法返回地址(Return Address)、符加信息
Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference )保存在class文件的常量池里,此时我们就需要一个动态连接将符号引用转变为方法的直接引用。
Constant Pool : #1 = ……
方法调用的原理:
静态绑定:早期绑定——编译时——构造方法
动态绑定:晚期绑定——运行时——上转型对象(多态)
前提:1.父子关系;2.重写(三方API)
静态方法不可被重写,不加super/this == 虚方法(编译的时候不确定。)
同时有俩方法,接口的优先级比较低,父类重写就用自己的。
变量表需要显式赋值,否则无法使用。
Slot 变量槽
byte、short、boolean、char == > int,都是占用一个slot
long、double、float 占用两个slot
栈= 【数组】{链表实现}
拿到数据到执行引擎之后,执行引擎读到iadd指令,交给CPU进行运算,返回操作数给栈,再返回局变表。
对值进行push、pop、add操作
TOS技术:Top-of-Stack Cashing技术 == 栈顶缓存技术
将操作数缓存在【cpu寄存器中】,执行效率up(因为降低了内存的读写操作。)
正常就是一个返回地址,里面包含了PC程序计数器的值。
很多是底层由C语言写的,执行效率较高。
Native Method就是一个java调用非java代码的接口,调用的是非Java代码,没有具体的实现。
**native **是有方法体的,只是我们看不到,和abstract互斥。【非Java代码实现】
优点:执行效率高;与外界交互(底层);解释器也用到C库
所有的实例对象+数组都存放在堆中,进程对应着实例,一个进程中的线程共享堆,共享实例对象。
GC流程:对于一个用户线程,不够用再GC(给大内存,减少频繁GC),减少STW,可以去优化用户线程的体验
逻辑连续:新生区+养老区+永久代/元空间
-Xmx 最大堆内存大小
-Xms 初始堆内存大小
-Xmn 年轻代大小
-XXSurivivorRatio=3 年轻代被划分为 3:1:1
-Xms10m ; -Xmx10m设置相同的值可以去除扩展和减少的过程,
<==> memory start 使初始堆内存大小 = 最大内存大小
默认参数:物理电脑内存/64;最大:电脑内存/4
jps
jstat -gc 进程id
- 设置-XX:PrintGCDetail 读取详细参数
old区满了,装不下新创建的实例对象——Full GC
Default:
-XX:NewRatio=2,新生代和老年代比例为1:2
-XX:+UseAdaptiveSizePolicy,内存分配策略
-XX:SurvivorRatio=8,设置Eden和Survivor的比例
过程:
Eden区满了找垃圾,将Eden和S0一起回收,总有一个s区是空的(为to区)
阈值默认为15,满了之后会promotion到永久代/元空间中
未达到阈值的对象,把幸存的对象放入s区
GC:我们可以频繁的回收新生代,但是尽量不要对养老区进行垃圾回收(比较浪费资源)
Full GC:整堆回收
Major GC:老年区垃圾回收FGC
Minor GC:新生代垃圾回收YGC(占用的时间最短)
优先Eden区
大对象直接老年代(但是朝生夕死就很难受)
晋升规则:
长期存活达到阈值就晋升为老年代
动态对象年龄判断
空间分配担保
TLAB:Thread Local Allocation Buffer
堆共享,多个线程同时操作是不安全的,-XX:UseTLAB 内存分配机制
我们给每个线程都留下一些私有的TLAB空间
空间分配担保:
老年代连续空间大于新生代对象的总大小 or 大于之前晋升的平均大小 == YGC,否则Full GC
JIT:
若新建的对象没有逃逸出方法区,优化的方式是【栈上分配】
可用局部变量就不在外面定义。
基于逃逸分析:
栈上分配
线程同步省略
标量替换(默认使用)
待写……