从实践中学习-JVM
1.从Hello World开始
最近在复习java知识,但是效果不是很理想,单纯的去记忆一些东西感觉没有那种特别的感觉,所以想通过写博客记录的方式帮助自己加深印象。今天我们就从一个Demo开始开启我们的java之旅,请各位朋友多多指教。
1.1使用IDE写我们的第一个程序
代码如下所示:
/**
* @version 1.0.0
* @description:
* @author: author
* @time: 2020/4/19 9:16
*/
public class HelloWorld {
private int AA = 5;
private static Integer MIN_VALUE = 0;
private static Integer MAX_VALUE = 9;
private static final Integer BB = 9;
public static void main(String[] args) {
HelloWorld helloWorld = new HelloWorld();
System.out.println("HelloWorld.main");
helloWorld.add(MIN_VALUE, MAX_VALUE);
}
public void add(Integer min, Integer max){
System.out.println(min + max);
}
}
运行结果:
HelloWorld.main
9
Process finished with exit code 0
现在我们一起来看看程序是如何将页面上的代码变成结果的
1.1.1运行过程
既然是一种语言那我们当然需要了解对应的开发运行环境
Java和JDK的关系
JDK(Java Development Kit) Java开发工具包,它包括:编译器,Java运行环境,监控和诊断工具等,而Java则表示一种开发语言.
JRE(Java Runtime Environment) Java运行环境,
JVM(Java Virtual Machine) Java虚拟机 JVM是可运行Java代码的假想计算机,包括一套字节码指令集、一组寄存器、一个栈、 一个垃圾回收,堆 和 一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接的交互。
运行过程:
- 先把Java代码编译成字节码,也就是把 .java类型的文件编译成 .class类型的文件.这个过程的大致执行流程: Java源代码 -> 词法分析器 -> 语法分析器 -> 语义分析器 -> 字节码生成器 ->最终生成JVM字节码文件*.class,其中任何一个节点执行失败就会造成编译失败;
- 把class文件放置到Java虚拟机,这个虚拟机通常指的是Oracle官方自带的Hotspot JVM;
- Java虚拟机使用类加载器(Class Loader)装载class文件;
- 类加载完成之后,会进行字节码校验,字节码校验通过JVM解释器会把字节码翻译成机器码交由操作系统执行.但不是所有代码都是解释执行的,JVM对此做了优化, 比如, 以Hotspot虚拟机来说, 它本身提供了JIT (Just In Time)也就是我们通常所说的动态编译器,它能够在运行时将热点代码编译成机器码,这个时候字节码就变成了编译执行.
1.1.1.1类加载机制
什么是类的加载?
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这 五个过程。
1.1.1.1.1加载
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个Class文件获取,这里既 可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理), 也可以由其它文件生成(比如将JSP文件转换成对应的Class类)
代码:
HelloWorld helloWorld = new HelloWorld();
当new的时候,会用到HelloWorld.class,所以第一时间找到HelloWorld.class文件,并加载到内存中
执行该类的static代码块,给HelloWorld.class进行一个初始化
heap(堆)中开辟空间,分配地址
heap(堆)中建立对象的特有属性,并进行默认初始化
对属性进行显示初始化
对对象进行构造代码块初始化
对对象进行的与之对应的构造函数进行初始化
把内存地址赋给stack(栈)中的helloWorld变量
1.1.1.1.2连接
1.1.1.1.2.1验证
这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
1.1.1.1.2.2准备
为类的静态变量分配内存,初始化为系统的初始值(不初始化静态代码块),对static final,直接定义为用户赋的值
public static int AA = 8080;
private static final int BB = 9090;
如上代码,在准备阶段过后AA的初始值是0而不是8080,将AA赋值为8080的putstatic指令是程序被编译后,存放于类构造器方法之中。而在编译阶段会为BB生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将BB赋值为9090。
1.1.1.1.2.3解析
该阶段将常量池中的符号引用转化为直接引用。
符号引用:就是class文件中的CONSTANT_Class_info、CONSTANT_Field_info、CONSTANT_Method_info等类型的常量。符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。
在编译时,Java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如我们有一个com.test类引用了阿里巴巴的com.aliyun类,编译时Test类并不知道阿里巴巴类的实际内存地址,因此只能使用符号com.aliyun。
直接引用:通过对符号引用进行解析,找到引用的实际内存地址,直接引用可以是目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
1.1.1.1.3初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其他操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。该阶段可实现应用程序自身类加载顺序。
初始化阶段是执行类构造器方法的过程,即真正执行类中定义的Java程序代码。
比如:
String str = new String("need a Girlfriend");
使用new实例化一个String对象,此时就会调用String类的构造方法对str进行实例化。
说完类加载过程,就不得不说类加载器,什么是类加载器?
1.1.1.1.3.1类加载器
还记得使用JdbcTemplate连接数据库过程吗?我们把数据库连接封装为工具类DruidUtils方法,该方法就用到了类加载器:
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
/**
* 提供连接
*/
public class DruidUtils {
private static DataSource dataSource = null;
static { // 必须优先执行 只执行一次
try {
// 需要一个文件流
InputStream is = DruidUtils.class.getClassLoader().getResourceAsStream("db.properties");
// 创建配置文件对象
Properties props = new Properties();
props.load(is);
// 核心类
dataSource = DruidDataSourceFactory.createDataSource(props);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 返回数据源方法
*
* @return
*/
public static DataSource getDataSource() {
return dataSource;
}
/**
* 提供连接的方法
*
* @return
*/
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}
其中,class.getClassLoader()就是类加载器的标志了。Jvm设计之初就把加载动作放到外部实现,以便让应用程序决定如何获取所需的类,所以提供了3种类加载器:
1)启动类加载器(Bootstrap ClassLoader):加载jre/lib包下面的jar文件,比如说常见的rt.jar。 (java.time.、java.util.、java.nio.、java.lang.、java.text.、java.sql.、java.math.*等等都在rt.jar包下)
2)扩展类加载器(Extension or ExtClassLoader):加载jre/lib/ext包下面的jar文件。由Java语言实现,父类加载器为null。
3)应用类加载器(Application or AppClassLoader):根据程序的类路径(classpath)来加载Java类。由Java语言实现,父类加载器为ExtClassLoader。JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader 实现自定义的类加载器
待补充
1.1.1.1.3.2双亲委派
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父 类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中, 只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的 Class),子类加载器才会尝试自己去加载。
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载 器加载这个类,终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载 器终得到的都是同样一个Object对象
protected synchronized Class<?> loadClass(String name,boolean resolve)throws ClassNotFoundException{
//check the class has been loaded or not
Class c = findLoadedClass(name);
if(c == null){
try{
if(parent != null){
c = parent.loadClass(name,false);
}else{
c = findBootstrapClassOrNull(name);
}
}catch(ClassNotFoundException e){
//if throws the exception ,the father can not complete the load
}
if(c == null){
c = findClass(name);
}
}
if(resolve){
resolveClass(c);
}
return c;
}
1.1.1.2虚拟机
JVM: 堆、方法区、虚拟机栈、本地方法栈、程序计数器
堆(类实例区)
用两张图来简单描述虚拟机
public class Parent {
int P_MIN = 0;
static int P_MAX = 9;
static final int P_MID = 99;
{
System.out.println("父类的初始化代码块");
}
static {
System.out.println("父类的静态方法");
}
Parent(){
System.out.println("父类的构造方法");
}
void parentMethod(){
System.out.println("父类的普通方法");
System.out.println("P_MIN\t" + P_MIN);
System.out.println("P_MID\t" + P_MID);
System.out.println("P_MAX\t" + P_MAX);
}
}
public class Child extends Parent {
int C_MIN = 0;
static int C_MAX = 9;
static final int C_MID = 99;
static int value = value();
{
System.out.println("子类的初始化代码快");
}
static {
System.out.println("子类的静态方法");
}
Child(){
System.out.println("子类的构造方法");
}
@Override
void parentMethod() {
super.parentMethod();
}
void childMethod(){
System.out.println("子类的普通方法");
System.out.println("C_MIN\t" + C_MIN);
System.out.println("C_MID\t" + C_MID);
System.out.println("C_MAX\t" + C_MAX);
}
public static void main(String[] args) {
Child child = new Child();
child.childMethod();
}
static int value(){
System.out.println("静态代码块");
return 3;
}
}