堆、栈、线程、进程、java虚拟机
Published by Shangyu Liu,
线程进程这个老问题重新整理一下,这次从程序在内存是如何运行的角度来说。
进程之间不共享内存,线程之间可以共享相当多的内存资源。内存有不同的分区,主要是堆和栈,堆比栈大得多,函数的调用好比括号的配对嵌套,而程序的执行就是函数的调用,栈区就负责维护函数的调用现场,它是由操作系统维护的,不能动态地申请内存空间,每一个线程独享一个栈,栈中存放着局部变量和程序执行的上下文环境,注意如果局部变量是非基础类型,则存放的是变量的引用,指向堆。堆中存放的是类、类的实例、方法、全局变量等,堆空间是可以被线程共享的。
下面进入比喻阶段:
整个操作系统是一个餐厅,餐厅做饭有n个厨房,n个厨房就是n个进程,不同的厨房做饭不共享任何食材、炊具。各个厨房内部做的饭是一样的,比如厨房1是宫保鸡丁,厨房2是地三鲜。每个厨房有若干厨师,每个厨师炒菜就是一个线程,在这个线程中,大家共用某些食材,刀具,切菜板等等,这些都放在“堆"里,但每个人的炒锅是独享的,这就是“栈",“栈"记录着炒菜进行到了第几分钟这个执行上下文,不同的厨师进展不同,锅里的鸡丁就是局部变量,不同的厨师炒出来的菜量、闲淡、甜辣都不尽相同,这就是各个线程执行结果不同,但归根结底都是宫保鸡丁,他们炒菜的控制流程以及分支是一样的,是同一份代码的产物,不同的进程则是不同的控制流程,也就是不同的代码。
有了这些理解来看java虚拟机(jvm),简单介绍为啥会有jvm,c/c++编译过后形成与具体机器相关的机器码,一旦机器变化需要重新编译,比如在a机器上编译后形成了一个exe文件,顺利运行,放到b机器上挂了,就是因为机器码不具有跨平台的特性。java不把源码编译成机器码而是翻译成中间码,在不同的机器上装jvm,jvm在运行时把中间码翻译成机器码。这样java代码编译之后就可以去所有装了jvm的机器上运行了,但代价是,不同的机器要装不同的jvm,jvm不是跨平台的。比如支持1.7版本的jvm,有不同版本,但只要源码是符合java1.7就可以翻译成适合当前机器的机器码。
因此从java程序员的角度就可以把jvm当成真正的操作系统环境,jvm中同样有虚拟的堆、栈概念,但同时还推出了一个方法区的概念,非常类似堆,当一个java程序开始运行时,jvm会首先启动,加载Main类到方法区,加载main方法到栈区初始化调用栈,发现需要Test类的实例test1,先去内存(jvm内存)方法区找类信息,没找到,就先把Test类加载到方法区,然后实例化test1到堆区(这个过程中栈区有了出栈入栈的活动),又发现需要test2实例,用方法区的Test实例化一个test2,放到堆区(同样这个过程中栈区有了出栈入栈的活动),Test类具有的成员变量,分别保存在test1,test2中,但Test类具有的成员函数仍然在方法区的Test中,test1,test2的成员方法保存的是指向方法区的引用。当调用test1的成员函数testFunction时,在栈区的局部变量test1指向堆区的实例test1,通过实例存储的testFunction的引用,到方法区找到testFunction的字节码开始执行。
单线程语言:
比如js,就是厨房只放得下一个厨师,异步是这个厨房的厨师把任务交给餐厅(比如浏览器或者node环境),餐厅到别的厨房给他加工他想要的东西,与这个运行js厨房相关的厨房里并非在运行js,可能是c++或者其他的程序了。
根据进程线程的特点,如果选取程序是多进程还是多线程的呢,从资源的角度出发,对于操作系统而言,windows多进程的开销远远大于linux,因此鼓励在windows上使用多线程,linux上使用多进程。抛开性能不提,是否所有的多线程都可以用多进程替代呢,理论上是的,但是由于多进程无法共享内存,当多个进程必须要一个同步的变量时,进程通信就会对于多进程编程造成困难。反之,我们要求各自运行的程序不许篡改其他运行程序的变量时,我们就使用多进程。