JVM系列(一)--- JVM的作用与表现形式

JVM系列(一)— JVM的作用与表现形式

1 引论

  JVM是Java Virtual Machine(Java虚拟机)的缩写。作为一个JAVA工程师,对JVM的了解和学习往往最为中高级工程师的分界线,要想学习JVM,就要明白JVM到底做了什么,为什么要对JVM进行优化。
JAVA作为一门高级语言,在开发过程中简化了很多工作,使得开发者更容易学习和开发。但是事情往往是两面的,这既是优点也是缺点。
优点:

  • 1、入门简单
  • 2、开发容易

缺点:

  • 对内存了解不深,容易产生瓶颈,往往随着对于底层了解深入,才能成为高级开发工程师和架构师。

  所以本篇文章从底层向上层一步步展开,深入论证一下JVM的工作,希望每一位读者都能更容易的学会JVM。

2 JVM的职责

  所有的高级语言最底层都是C语言实现的,如果想了解JVM做了什么,可以通过观察C语言和JAVA语言的区别来做到。在实现相同功能的情况下,如果有些内容C语言需要做但是JAVA不需要做,那么这部分一定是JVM帮你做了。
  本次我们重点说一下内存的使用管理。在JAVA语言中对于内存的使用管理对于开发者来说是完全无感知的,而在C语言里是完全相反,C语言中内存的管理对于开发者来说是很重要的一部分,尤其是堆内存的管理。
其实操作系统对于内存的分配是非常复杂的,本篇文章旨在论证JVM,并不对操作系统内存管理进行详述,我们只关注内存中的两块:堆和栈。本片文章会通过简单的例子讲解堆栈的区别,以及C语言中堆栈的用法,和JAVA进行对比,来梳理JVM的工作内容。
  我们首先先看一下C语言中堆栈是如何使用的。

struct Person{
	char name[10];
	int age;
};
void main(){
	int i;                   //以下是将变量存在栈中
	double d;
	char c;
	char *name = "xiaobai";
	struct Person p;
	strcpy(p.name, name); //字符串赋值
	p.age = 20;

	struct Person *p2;                   //以下是将变量存在堆中
	p2 = malloc(sizeof(struct Person)); //申请堆内存
	strcpy(p2->name, name); //字符串赋值
	p2->age = 20;
	
	
	free(p2); //释放P2指向的堆内存
}

  通过上面的例子可以看出,在C语言里,堆和栈的申请和释放是完全不同的,栈的使用完全由操作系统控制,一般栈空间在申请变量时自动分配,到作用域结束时自动释放,完全不需要开发者考虑。而想将数据存放在堆中,一般需要在栈中申请一个指针类型变量,然后使用malloc函数申请堆内存,并将堆的首地址赋值给指针类型,这样我们就将数据存放在堆中了,堆的释放需要使用free函数,也就是说只要不遇到free函数,堆内存永远都不会释放。

  以下是上面代码中变量的存放方式:
在这里插入图片描述
  上面例子讲述了C语言中堆栈的使用上的区别,但是变量使用栈就可以了,为什么还要使用堆呢?接下来我们再举一个例子。
  假设我们想实现一个初始化函数,函数负责初始化Person结构体,并进行初始化赋值,有点类似于JAVA中的构造函数的功能,于是我们写了如下代码。

struct Person{
	char name[10];
	int age;
};

struct Person init_Person(char *name, int age){
	struct Person p;
	strcpy(p.name, name); //字符串赋值
	p.age = age;
	return p;
}

void main(){
	char *name = "xiaobai";
	struct Person p = init_Person(name, 30);
}

  首先通过前面的讲解已知init_person中的p变量和主函数中的p变量都存在栈中但是并不是同一个变量,在进行函数返回时,会将init_Person中的p变量整体复制传给主函数中的p变量,init_Person函数结束后,其内部p变量的栈空间就会回收等待下一次使用。
  但是上面这种实现方式缺点很明显,那就是函数返回时要进行大量内存复制,于是很多初学者做了以下修改,注意:以下是一种错误的实现方式。

struct Person{
	char name[10];
	int age;
};

struct Person * init_Person(char *name, int age){
	struct Person p;
	strcpy(p.name, name); //字符串赋值
	p.age = age;
	return &p;
}

void main(){
	char *name = "xiaobai";
	struct Person *p = init_Person(name, 30);
}

  初学者往往会犯以下错误,错误原因是应为init_Person中的p的数据存放在栈中,当函数结束时就会进行回收,如果代码量较少,有时候看结果好像并没有发生错误,原因是因为虽然地址空间已经回收,但是由于还没有再次进行分配使用,里面的值还没有变化,当主函数用指针去取时,还可以取到预想的数值。但是,很明显这种方式是不对的,发生这种错误往往后期出现诡异的错误并且很难排查。
  那么我们如何才能既保证返回值时不发生大量内存拷贝,同时也不希望所有的变量都在主函数中进行初始化呢?
  很简单,我们可以将数据存放在堆中,堆内存只要不执行free函数,永远不会释放,于是有了如下的代码:

struct Person{
	char name[10];
	int age;
};

struct Person * init_Person(char *name, int age){
	struct Person *p;
	p = malloc(sizeof(struct Person));
	strcpy(p->name, name); //字符串赋值
	p->age = age;
	return p;
}

void main(){
	char *name = "xiaobai";
	struct Person *p = init_Person(name, 30);

	free(p);
}

  以上的代码已经可以很好的体现C语言中堆栈的使用方式了,在C语言中一般只要不是一个函数内部临时变量,一般结构体都会使用堆内存进行存储,方便在函数间进行传递使用。
  C语言的堆栈使用几经很清楚了,接下来我们分析以下JAVA语言的内存使用。有JAVA基础的应该都知道JAVA内部变量分为两种:值类型和引用类型。值类型只有8个:

  • 整型 byte short int long
  • 浮点型 float double
  • 字符串类型 char
  • 布尔类型 Boolean

  其他所有的变量都是引用类型,包括我们常用的字符串和数组,以及所有的对象变量。
  我们经常说值类型是存放在栈中的,引用类型是存放在堆中的,其实并不准确,根据C语言可知,其实引用类型变量包含两个部分:存放在栈中的指针和存放在堆中的数据。。当然,如果是对象里的引用类型属性,那么指针也存在堆中。我们举一些例子来说明:

public class Person{
	public String name;
	public int age;
};

public class Test{
	public static void main(String[] args){
		int i;
		double d;
		char c;
		Person p = new Person();
		p.name = "xiaobai";
		p.age =  20;
	}
}

在这里插入图片描述
  以上是JAVA语言变量的代码及内存示意图,代码中有两个类,为了方便对比,我们将两个类放到一起,通过和C语言对比就可以发现,我们在JAVA中使用了堆内存,但是从没有写过malloc函数和free函数,那么是谁做了这件事呢?毫无疑问,JVM帮我们做了,JVM负责帮我们在申请应用类型变量时调用malloc函数,在合适的时候使用free函数,具体说到底什么时间合适?怎样才能更好的利用对内存?怎样能够让代码执行效率更高?我们后面会一一给大家讲解。
  毫无疑问,我们已经梳理出来了JVM的一个重要作用:内存管理。尤其是堆内存的管理。
  当然,除了内存管理以外,JVM作为操作系统和JAVA的中间耦合的工具,JVM也一定会负责JAVA类的加载和解析。 所以,一般讲解JVM的书籍和文章一般都从类加载机制、内存管理、垃圾回收(GC,堆内存的回收机制)几个方面进行讲解。当然,本系列文章这些内容都会一一覆盖到。
  上面我们已经理解了JVM的工作内容,接下来,我们会讲解一下为什么需要JVM优化。

3 为什么需要JVM优化

  我们在实现JAVA代码的过程中,不同的人有不同的实现方式,例如,我们实现99乘法表,可以算出十以内乘法,第一种方式就是直接用CPU算出结果,第二种是将99乘法结果存储在二维数组中,直接根据脚标就可以得出结果。我们暂且不论是否有人这样实现,毕竟第二种用空间(内存)换取时间(运算)的方式是一种常见的优化方法。毋庸置疑,第一种方法只用了很少的栈空间,而第二种用了相对较大的堆空间。同样的结果,由于实现方式不同,对内存的需求是完全不一样的。 当然,这种需求不仅仅是大小,还包括需要的垃圾回收速度、内存管理机制都可能是不一样的。
  而JVM负责了这一部分,为了保证自己写的程序能够更好、更快的运行,我们自然需要对相应的指标进行调整和优化,这就是我们进行JVM优化的原因。

4 JVM用什么样的表现方式进行调优

  我们已经了解了为什么要进行JVM优化,那么我们是以什么样的表现方式进行优化的呢?在刚学习JAVA的时候,一定学习过手动编译执行JAVA代码,即使用javac命令编译出class文件,java命令执行。毫无疑问,java命令执行的时候就会调用JVM虚拟机,运行后得出结果,我们就是通过在执行java命令的时候加参数,来修改对应指标,进行优化的。运行时,我们可以通过一些方式查看运行时内存状态等,确认优化结果好坏。例如:

javac Test.java
java Test -Xms128m -Xmx512m

  我们先不讨论后面加的参数是什么意思,但是可以很清楚看到优化JAVA的方式就是在java命令执行时在后面加参数调节就可以了。我们会在后面的系列文章里讲解常用的参数。
  这时候很多读者可能会感觉迷茫,我们常用IDE来进行开发,或者我们开发一个JAVA WEB程序,我们一般打包放到Tomcat的webapps目录就自动执行了,没有执行java命令,那我们如何添加参数呢?其实IDE执行底层都是执行的java命令,IDE都可以设置执行时加哪些参数,因为每个人用的IDE不一样,我们就不一一介绍了,读者知道有这个功能就可以了,需要时搜一下就可以了。至于Tomcat,还是原来的道理,既然你自己没有执行java命令,那么一定是tomcat帮你干了,Tomcat目录的bin文件夹下catalina.bat(如果是Linux,就是catalina.sh文件)里有个JAVA_OPTS变量,通过修改这个变量就可以向java命令后面添加JVM调优参数。
  本篇文章就到这里结束了,我们讲解了JVM的工作内容以及JVM调优表现方式,是为了让读者先有一个直观感受,JVM调优并不是一个难度很高,只有少数人才能会的技能,最终的表现方式也只是添加参数而已,当然参数的意义我们还要具体去了解,希望每个人都能克服心里的障碍,更加清楚的学习JVM内容。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值