JVM学习笔记内存与垃圾回收篇
Posted 九死九歌
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM学习笔记内存与垃圾回收篇相关的知识,希望对你有一定的参考价值。
一、JVM与java体系结构
1 虚拟机的结构
2 java的编译
有句话说java是半编译半解释型语言,因为从*.java到*.class文件的过程是编译。而*.class文件的运行是解释。
但其实*.class文件的运行过程是半编译半解释的。有些需要反复用到的字节码是直接编译成机器指令来执行的。就好比域名的解析,常用访问的域名解析是直接通过本地域名服务器,不常用的才会自顶向下层层解析。例如for循环中的代码是要重复利用的,就把这段字节码编译成机器指令缓存起来,每次循环都调用缓存区中的机器指令,而不需要进行解释。如图:
3 jvm的指令集
指令集架构有基于栈的指令集架构和基于寄存器的指令集架构。
jvm中使用的就是基于栈的指令集架构。它的好处是不需要硬件支持,可移植性高,零地址指令多。但性能不及寄存器式架构(如x86汇编)
栈式架构必然是大量使用到栈。例如我们将如下代码进行反汇编:
package com.spd.jvm;
public class Test
public static void main(String[] args)
int i = 2;
int j = 3;
int k = 2 + 3;
得到的结果是:
iconst_2 // 将int型数2压栈
istore_1 // 将栈顶int型存入本地变量1
iconst_3 // 将int型数3压栈
istore_2 // 将栈顶int型存入本地变量2
iconst_5 // 将int型数5压栈
istore_3 // 将栈顶int型存入本地变量3
return // 从当前方法返回void
而对x86汇编而言是这么写的:
mov ds : [0], 2 ; 设ds : [0]为i
mov ds : [4], 3 ; 设ds : [4]为j
mov eax, ds : [0]
add eax, ds : [4]
mov ds : [12], ax ; 设ds : [12]为k,k = ax = i + j
ret
由此可见,确实比起x86汇编更多使用到了栈。另外也能看出来同一功能栈式架构比寄存器式架构需要用更多指令,但是他的指令集更小。
二、类加载子系统
1 类加载子系统的结构
2 类加载子系统的作用
类加载子系统只负责类的加载,而类能否运行由执行引擎决定。
3 类加载的加载过程
① 通过一个类的全限定名获取定义此类的二进制字节流
② 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
③ 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
4 类加载的链接过程
① 验证:验证魔数及版本号。
② 准备:为类变量分配内存并设置为他们的初始值。初始化阶段才赋值。(不会分配final修饰的static,因为他们在编译过程就已经分配了。准备阶段只会进行显式初始化。也不会为实例变量分配初始化。)
package com.spd.jvm;
public class Test
private static int age = 18; // prepare: age = 0 -> initial: age = 18
private static char sex = '男'; // prepare: sex = '\\0' -> initial: sex = '男'
public static String name = "spd"; // prepare: name = null -> initial: name = "spd"
public static void main(String[] args)
System.out.println("Hello world!");
③ 解析:将常量池中的符号引用转换成直接引用。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是他们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能简介定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
5 类加载的初始化过程
① <clinit>()方法:
初始化过程实际上就是执行类构造器方法<clinit>()的过程。
该方法不等同于类的构造器(类的构造器是<init>()函数),他不需要定义,是javac编译器自动收集类中的所有类变量类中的所有变量的赋值动作和静态代码块中的语句合并而来。因而如果类中没有静态代码块也没有静态变量,那就没有<clinit>()方法。
有父类,则先执行父类<clinit>()
虚拟机必须保证一个类的<clinit>()方法在多线程中被同步加锁。
package com.spd.jvm;
public class Test
private static int a = 1;
static
a = 2;
b = 20;
public static int b = 10;
public static void main(String[] args)
System.out.println("Hello world!");
对这段代码反汇编查看字节码中的<clinit>()方法:
iconst_1 // 将int型1压入栈顶
putstatic #5 // 为指定类的静态域赋值,此处#5即为a(不知道需不需要弹栈)
iconst_2 // 将int型2压入栈顶
putstatic #5 // 为指定类的静态域赋值
bipush 20 // 将单字节常量值(-128 ~ 127)压入栈顶,此处为20
putstatic #6 // 为指定类的静态域赋值,此处#6即为b
bipush 10 // 将单字节常量值(-128 ~ 127)压入栈顶,此处为10
putstatic #6 // 为指定类的静态域赋值
return // 从当前方法返回空
如上便是<clinit>()方法的作用。那为什么静态代码块中b的赋值语句明明在b的声明语句之前,静态代码块还能给b赋值呢?因为准备阶段就给静态变量分配好了内存空间,赋值的过程是在其后的初始化过程中完成的。
prepare :
a = 0, b = 0;
initial:
a = 1;
static
a = 2;
b = 3;
b = 4;
但是可以在静态代码块中前向赋值,却不可以前向调用。例如如下代码就会报错:
package com.spd.jvm;
public class Test
private static int a = 1;
static
a = 2;
b = 20;
System.out.println(a);
System.out.println(b); // <- 此处报错
public static int b = 10;
public static void main(String[] args)
System.out.println("Hello world!");
6 类加载器的分类
jvm支持两种类型的类加载器,引导类加载器和自定义类加载器。凡是派生于抽象类ClassLoader的类加载器都被划分为自定义类加载器。(扩展类加载器和系统类加载器等)
引导类加载器、扩展类加载器、系统类加载器。三者并不存在上层下层关系,更不存在继承关系。而是呈现包含关系的。就像文件的层级目录一样。
另外引导类加载器是用c/c++语言编写的,其他类加载器都是用java写的。
我们可以用代码演示他们之间的异同。
package com.spd.jvm;
public class Test
public static void main(String[] args)
// 获取系统类加载器
ClassLoader sys = ClassLoader.getSystemClassLoader();
System.out.println(sys);
// 获取其上层:扩展类加载器
ClassLoader ext = sys.getParent();
System.out.println(ext);
// 试图获取其上层:引导类加载器
ClassLoader bootstrap = ext.getParent();
System.out.println(bootstrap); // 获取不到,打印空
// 自定义类默认使用系统类加载器
ClassLoader loader1 = Test.class.getClassLoader();
System.out.println(loader1);
System.out.println(loader1 == sys);
// java核心api中的类是用引导类加载器加载的。
ClassLoader loader2 = String.class.getClassLoader();
System.out.println(loader2);
以上是关于JVM学习笔记内存与垃圾回收篇的主要内容,如果未能解决你的问题,请参考以下文章