JVM 知识点
A. JVM内存管理
JVM主要把内存划分如下几个区域
- 方法区
- 堆区
- 本地方法栈
- 虚拟机栈
- 程序计数器
1. 方法区
方法区存放要加载的类的信息,静态变量,final常量,field和方法信息。
方法区也包含运行时常量池,用于存储编译器生成的字面常量,符号引用(用字符表示某个变量接口的位置)和翻译出来的直接引用(符号引用翻译出来的地址)。
这块区域是持久代,垃圾回收很少。
通过 --XX:PermSize 定义最小值 --XX:MaxPermSize 定义最大值
2. 堆区
堆区大小通过 -Xms 和 -Xmx控制
-Xms : JVM启动时申请的heap内存,默认物理内存的1/64
-Xmx : JVM可申请的最大heap内存, 默认物理内存的1/4
年轻代
对象在被创建时,内存首先在年轻代分配,当年轻代回收时出发Minor GC操作。
年轻代由Eden Space 和两块相同大小Survivor Space组成, 年轻代区域连续的,分配很快,回收也很快。
老年代
老年代用于存放年轻代中多次垃圾回收还存活的对象,如缓存对象。大对象也可直接在老年代分配内存,当老年代满了进行垃圾回收成为Major GC
3. 本地方法栈
本地方法栈用于支持native方法的执行,存储了每个native方法调用状态。和虚拟机栈运行机制一致,只不过虚拟机栈执行java方法,本地方法栈执行native方法。
4. 虚拟机栈
每个线程对应一个虚拟机栈,是线程私有的,分配十分高效。栈帧中存储了局部变量表,操作栈,动态链接和方法出口。
当线程调用的栈深度大于虚拟机允许的最大深度,抛出栈溢出异常。
5. 程序计数器
程序计数器用于指示当前线程所执行的字节码执行位置,如果执行的是java方法,计数器记录的是虚拟机字节码指令的地址,如果执行native方法,计数器的值为undefined。
6. java对象访问方式
Object objRef = new Object()
- Object objRef 表示本地引用, 存储在JVM本地变量表中。
- new Object() 存储在堆中。
- 堆中还记录查询Object的类型数据(接口,方法,field,对象类型),实际数据保存中方法区中。
有两种对象访问方式,一种是通过句柄访问。
另一种通过直接指针访问
7. JVM 内存分配
JVM对象占用内存主要在堆上实现,因为堆是共享的需要加锁,导致对象开销比较大。
为了提升分配效率,在年轻代的Eden区,hotspot采用两种技术来加快内存分配,分别是bump the pointer和TLAB(Thread local allocation buffers)。
bump the pointer : 跟踪最后创建的对象,在对象创建时,只需检查最后一个对象后面是否有足够的内存。
TLAB : 为每个新创建的线程在新生代的Eden Space上分配一块独立的空间,成为TLAB。通过-XX:TLABWasteTargetPercent指定占用EDen space百分比,默认1%。一般优先在TLAB上分配,如果对象过大或TLAB用完,则在堆上分配。
8. 内存回收方式
JVM通过GC来回收堆和方法区中的内存,要执行内存回收,我们需要确定哪些内存需要回收;确定什么时候需要执行GC以及如何执行GC。
引用计数器收集器
通过计数器记录对象是否被引用,当计数器为9,说明对象不再被使用,可进行回收。
跟踪收集器
跟踪收集器会全局记录引用的状态,根据一定条件触发,执行时需要扫描对象的引用关系,可能会造成应用程序暂停,主要有,复制,标记-清除,标记-压缩三种算法。
为什么要分代回收
一开始,GC采用标记清除压缩方式进行的,当对象分配较多,扫描和移动越来越耗时,造成内存回收越来越慢。
9. 垃圾收集器
垃圾回收器主要有串行,并行,和CMS(Concurrent Mark Sweep)收集器。
串行收集器,minor和major GC都是用一个线程进行垃圾回收。
并行收集器采用多线程方式回收,通过-XX:ParallelGCThreads=来控制并行的线程数量。
CMS收集器用于对暂停时间要求很高的场景,采用多线程并发来减少垃圾收集过程中的暂停,CMS收集器不会对存活的对象进行复制或移动。
B. JAVA 字节码
1. 魔数
魔数是用来区分文件类型的一种标志。
2. 版本号
版本号分为主版本号和次版本号。
3. 常量池
常量池是Class文件中的资源仓库,在Class Name和Interfaces中都有涉及。
主要存储两大类常量:字面量和符号引用。
字面量如文本字符串,java中声明为final的常量池。
符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符。
4. Access_Flag 访问标志
5. 类索引,父类索引
类索引用于确定类的全限定名
父类索引用于确定类的父类的全限定名
6. 接口索引
接口有2 + n个字节,前两个字节表示的是接口数量,后面跟着n就是接口的表。
7. 字段表示集合
字段表用于描述类和接口中声明的变量,包含类级别变量以及实例变量,不包含方法局部变量。
8. 方法
方法表描述类中定义的方法。
方法中有方法表,Code表,变量表。
9。 Attribute
SourceFile 表示生成class文件的源码文件名称
https://github.com/zxh0/classpy
C. 类加载机制
JAVA 类加载过程主要为5个部分
加载 --> 验证 --> 准备 --> 解析 --> 初始化
加载:通过类的完全限定查找该类的字节码文件,并利用字节码创建一个class对象
验证:确保class字节流合法,包含4种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
准备:为类变量(static变量)分配内存并初始化,类变量会被分配到方法区中。
解析:将常量池符号引用替换为直接饮用过程,符号引用是一组符号描述的目标,直接引用是指向目标的指针。
初始化:类加载最后阶段,若该类有超类,对其进行初始化,
类加载器根据类的全限定名,读取该类的二进制字节流到JVM中
Bootstrap类加载器
bootstrap加载器加载JVM自身需要的类,将<JAVA_HOME>/lib下的核心库或-Xbootclasspath参数指定的jar包加载到内存,虚拟机是按照文件名来加载jar包的
Extension类加载器
Extension负责加载<JAVA_HOME>/lib/ext目录下或-Djava.ext.dir制定位路径中的类库
System类加载器
负责加载系统类路径java -classpath 或-Djavva.class.path
1. 双亲委派模式
原理
除了顶层的启动类加载器,其余的类加载器都应当有自己的父类加载器
当需要加载类的时候,先把请求委托给父类加载器去执行,依次递归,如果父类可以完成加载,则成功返回,然后自加载器才会尝试自己去加载。
好处:避免类的重复加载,安全性高。
loadClass(String)
当类加载请求到来时,先从缓存中查找该类对象,没有则交给父类加载器加载,仍没有就用findClass方法加载
findClass(String)
findClass()在loadClass()中被调用,当loadClass()方法中父加载器加载失败后,会调用自己findClass()来进行类的加载。
defineClass(byte[] b, int off, int len)
defineClass()将byte字节流解析成JVM能够识别的Class对象,通过这个方法可以通过class文件实例化Class对象,defineClass通常与findClass一起使用
resolvClass(Class<?> c)
使类的Class对象创建同时也解析,
2. 类与类加载器
判断两个class对象是否为同一个类:类的完整类名必须一致,包括包名。加载这个类的ClassLoader必须相同。
在JVM中,如果两个类对象来源于同一个Class文件,被同个虚拟机加载,只要ClassLoader实例对象不同,这俩类也是不相等的。
因为不同的ClassLoader实例对象有不同的独立的类名称空间,所以加载的class会存在不同的类名称空间。
显示加载与隐试加载
显示加载是通过ClassLoader加载对象,隐式加载是不直接在代码中调用ClassLoader方法加载class对象。
3. 类加载器编写
实现自定义类加载器需要继承ClassLoader或者URLClassLoader,继承ClassLoader需要自己编写findClass方法。
当class文件不再ClassPath下,默认系统类加载器无法找到class,需要自定义额ClassLoader来加载;或者当class通过网络传输时;或需要实现热部署功能时。
热部署类加载器
热部署就是利用同一个class文件不同的类加载器中内存中创建出两个不同的class对象。我们可以直接调用findClass()方法,而不能调用loadClass方法,因为loadClass方法中调用了findLoadedClass检车了类是否已经被加载。
线程上下文类加载器
JAVA应该用中有很多服务提供者接口SPI,如JDNC,JNDI。这些SPI接口属于JAVA核心库,在rt.jar包,由Bootstrap类加载器加载,而第三方代码则被放在classpath路径下,因为SPI接口代码需要加载第三方实现类并调用其方法,但SPI核心接口类由引导类加载器加载,Bootstrap类加载器无法加载SPI实现类,这就需要contextClassLoader。
通过getContextClassLoader()和setContextClassLoader(ClassLoader cl)来获取和设置线程的上下文类加载器。初始的contextClassLoader时AppclassLoader,在线程中运行的代码可以通过此类加载器来加载类的资源。
使用线程类加载器破坏了双亲委派加载链模式,使程序可以逆向使用类加载器。外部实现类不能通过Bootstrap类加载器加载,就委托线程上下文加载器把jdbc.jar中实现类加载到内存中方便SPi 相关类调用。
D. JAVA解释执行过程
An interpreter is a software program that converts code from high level language to machine format.
JAVA编译器首先验证代码的正确性,然后把JAVA源代码转化成byte-code文件,与此同时计算常量的值以及缓存字符串。
JVM在bytecode层面上操作,JVM 把bytecode最终转变成machine code,然后最终在OS上执行。
bytecode和优化后的用户代码,以及java库和OS调用一起合作来执行JAVA程序。
现代JAVA会用JIT来直接产生native code,从而执行效率更高。
JVM在load java代码的时候会很慢,因为JVM不仅load jars和classes,也在做cc -O3
操作。