系统与JVM与线程简述

JVM在OS中的体现

1.任务管理器中的JVM

  • 操作系统的内存条以下简称"内存" , jvm 所使用的空间是内存,其他的exe也都是运行在内存中

  • javaw.exe是java在windows中的启动装置之一,它和java.exe的区别是由javaw启动的java进程将在GUI中运行,而不是命令行。无论是在eclipse中运行的main方法,还是在eclipse中启动的tomcat,都是由eclipse使用javaw.exe来驱动的,所以说你看到的进程名是javaw。
    在这里插入图片描述

  • 但如果通过tomcat/bin/startup.bat来启动tomcat,那么启动就是由tomcat自己驱动的,没有经过eclipse,而tomcat自己用命令行启动,所以使用到的启动装置是java.exe, main函数就是一个java应用的入口,main函数被执行时,java虚拟机就启动了
    系统与JVM与线程简述_第1张图片

  • 你运行几个application就有几个java.exe/javaw.exe。或者更加具体的说,你运行了几个main函数就启动了几个java应用,同时也启动了几个java的虚拟机。

2. JVM在OS中占用的空间

  • 在java应用中,有时候会出现这样的错误:OutOfMemoryError: unable to create new native thread.这种怪事是因为JVM已经被系统分配了大量的内存(比如1.5G)(32位系统,4G内存条),并且它至少要占用可用内存的一半。有人发现,在线程个数很多的情况下,你分配给JVM的内存越多,那么,上述错误发生的可能性就越大。
  • 每一个32位的进程最多可以使用2G的可用内存,因为另外2G被操作系统保留。这里假设使用1.5G给JVM,那么还余下500M可用内存。这500M内存中的一部分必须用于系统dll的加载,那么真正剩下的也许只有400M,现在关键的地方出现了:当你使用Java创建一个线程,在JVM的内存里也会创建一个Thread对象,但是同时也会在操作系统里创建一个真正的物理线程(参考JVM规范),操作系统会在余下的400兆内存里创建这个物理线程,而不是在JVM的1500M的内存堆里创建。在jdk1.4里头,默认的栈大小是256KB,但是在jdk1.5里头,默认的栈大小为1M每线程,因此,在余下400M的可用内存里边我们最多也只能创建400个可用线程。这样结论就出来了,要想创建更多的线程,你必须减少分配给JVM的最大内存
  • JVM:所有的jar程序,其运行环境完全是由JVM来提供,包括运行时,各类资源的调度,而JVM的架构,其设计为一个JVM里面可以运行多个java程序,JVM就像一个真正的“机器”,可以跑着多个程序。如果去看看一些企业级的JVM(例如tom cat,WAS),从OS的进程管理中,一般你只能看见一个JVM的进程(当然,你也可以起多个JVM,但JVM架构就是OS-JVM-APP的3层运行时模式),而看不见JVM里面运行的程序,而一个JVM里,可以跑多个java app。简单得说,JVM完全屏蔽了应用程序和OS之间的联系,而改用JVM充当了中间层,这也是一个真正跨平台运行时VM必须要做到的。只要是相同的JDK,JVM为所有在其中运行的程序,提供了完全一致的运行环境,而不论你是什么样的底层OS和硬件条件。因此这也是我在其他一篇答案中提到,JVM的特点是取底层OS和硬件环境的交集,从而保障这种一致性。而所有应用程序和底层资源的互动,一定是依赖JVM的传递和转换来实现。JVM真正实现了一个OS对应用程序运行时管理的所有功能。从开发环境角度和运行时角度,都是完全一致的真正VM.在JVM的机制中,不同的程序,打包以后,他们都是在运行层级真正独立的程序(指程序应用他们相互之间的关系,而不是和JVM的关系),即便他们在包里使用了同样的类,运行时都是单独加载,单独运行的(及加载多遍)。应用跟线程没什么关系,线程是个计算执行的概念,而应用上下文占用的是内存。你如果打印线程名称,就能看到应用线程是动态分配的,线程1可以在应用1出现,也可以在应用2出现

3. JVM在OS中的大概结构

  • jvm内存模型
    系统与JVM与线程简述_第2张图片

  • 操作系统内存布局
    系统与JVM与线程简述_第3张图片

  • 操作系统中的jvm
    系统与JVM与线程简述_第4张图片

  • 为什么jvm的内存是分布在操作系统的堆中呢??因为操作系统的栈是操作系统管理的,它随时会被回收,所以如果jvm放在栈中,那java的一个null对象就很难确定会被谁回收了,那gc的存在就一点意义都莫有了,而要对栈做到自动释放也是jvm需要考虑的,所以放在堆中就最合适不过了。

  • 操作系统+jvm的内存简单布局
    系统与JVM与线程简述_第5张图片

  • 从上图中,你有没有发现什么规律,jvm的内存结构居然和操作系统的结构惊人的一致,你能不能给他们对号入座?还不能,没关系,再来看一个图,我帮你对号入座。看我下面红色的标注系统与JVM与线程简述_第6张图片

  • 从这个图,你应该不难发现,原来jvm的设计的模型其实就是操作系统的模型,基于操作系统的角度,jvm就是个该死的java.exe/javaw.exe,也就是一个应用,而基于class文件来说,jvm就是个操作系统,而jvm的方法区,也就相当于操作系统的硬盘区,所以你知道我为什么喜欢叫他permanent区吗,因为这个单词是永久的意思,也就是永久区,我们的磁盘就是不断电的永久区嘛,是一样的意思。而java栈和操作系统栈是一致的,无论是生长方向还是管理的方式,至于堆嘛,虽然概念上一致目标也一致,分配内存的方式也一直(new,或者malloc等等),但是由于他们的管理方式不同,jvm是gc回收,而操作系统是程序员手动释放,所以在算法上有很多的差异
    系统与JVM与线程简述_第7张图片

  • 将这个图和上面的图对比多了什么?没错,多了一个pc寄存器,我为什么要画出来,主要是要告诉你,所谓pc寄存器,无论是在虚拟机中还是在我们虚拟机所寄宿的操作系统中功能目的是一致的,计算机上的pc寄存器是计算机上的硬件,本来就是属于计算机,(这一点对于学过汇编的同学应该很容易理解,有很多的寄存器eax,esp之类的32位寄存器,jvm里的寄存器就相当于汇编里的esp寄存器),计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,pc寄存器它表现为一块内存(一个字长,虚拟机要求字长最小为32位),虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址,它甚至可以是操作系统指令的本地地址,当虚拟机正在执行的方法是一个本地方法的时候,jvm的pc寄存器存储的值是undefined,所以你现在应该很明确的知道,虚拟机的pc寄存器是用于存放下一条将要执行的指令的地址(字节码流)。

  • 再对上面的图扩展
    系统与JVM与线程简述_第8张图片

  • 多了什么?没错多了一个classLoader,其实这个图是要告诉你,当一个classLoder启动的时候,classLoader的生存地点在jvm中的堆,然后它会去主机硬盘上将A.class装载到jvm的方法区,方法区中的这个字节文件会被虚拟机拿来new A字节码(),然后在堆内存生成了一个A字节码的对象,然后A字节码这个内存文件有两个引用一个指向A的class对象,一个指向加载自己的classLoader,如下图。
    系统与JVM与线程简述_第9张图片

  • 记录一个class自己的class对象引用和一个加载自己的ClassLoader引用之外,还有一些类信息这里就不具体描述了… (因为懒… 有好奇的可以自己百度一下)

4. 插一个JVM方法区的概念

  • 当jvm使用类装载器装在某个类时,它首先要定位到对应的class文件,然后读入这个class文件,最后提取该文件的内容信息,并将这些信息存储到方法去,最后返回一个class实例。方法区是系统分配的一个内存逻辑区域,是一块所有线程共享的内存区域,用来存储类型信息(类型信息可以理解为类的描述信息(类的全限定名,访问修饰符,字段,方法等)),方法区的大小决定了系统可以包含多少个类,如果系统类太多,方法区内存不够会导致方法区溢出,虚拟机同样会抛出内存溢出信息。

  • 方法区特点:
    1.方法区是线程安全的,由于所有的线程都共享方法区,所以方法区里的数据访问必须被设计成线程安全的。例如,假如同时有两个线程都企图访问方法区中的同一个类,而这个类还没有被装入jvm,那么只允许一个线程去装在它,而其他线程必须等待。
    2.方法区的大小不必是固定的,jvm可根据应用需要动态调整,同时,方法区也不一定是连续的,方法区可以在一个堆(甚至是jvm自己的堆)中自由分配。
    3.方法区也可被垃圾收集,当某个类不在被使用时,jvm将卸载这个类,进行垃圾收集。

  • 方法区存放内容:
    1.类的全限定名(类的全路径名).
    2.类的直接超类的权全限定名(如果这个类是Object,则它没有超类)。
    3.类的类型(类或接口)。
    4.类的访问修饰符,public,abstract,final等。
    5.类的直接接口全限定名的有序列表。
    6.常量池(字段,方法信息,静态变量,类型引用(class))等
    7.类变量:类变量为静态变量,在方法去中有个静态区,静态区专门用来存放静态变量以及静态块。

摘自 https://blog.csdn.net/Jbinbin/article/details/85004909

5. JVM线程模型

我们可以发现,Jvm其实就是操作系统的一种镜像。是软件层次的虚拟机。其中我们队Jvm内存模型分析可知:堆,方法区是线程共有的;栈是每个线程私有的。
讨论Java内存模型和线程之前,先简单介绍一下硬件的效率与一致性

由于计算机的存储设备与处理器的运算能力之间有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中没这样处理器就无需等待缓慢的内存读写了。
  
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主存,如下图所示:多个处理器运算任务都涉及同一块主存,需要一种协议可以保障数据的一致性,这类协议有MSI、MESI、MOSI及Dragon Protocol等。Java虚拟机内存模型中定义的内存访问操作与硬件的缓存访问操作是具有可比性的
系统与JVM与线程简述_第10张图片

主内存与工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。
  Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存(可以与前面将的处理器的高速缓存类比),线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成,线程、主内存和工作内存的交互关系如下图所示,和上图很类似
系统与JVM与线程简述_第11张图片

java内存变量的交互操作

主内存变量和工作内存变量之间的交互过程如下:
系统与JVM与线程简述_第12张图片

线程安全

通过上面分析,可以发现,每个线程都拥有自己的工作内存,工作内存是线程私有的。所以每个线程对堆中的共享变量进行修改对其他的线程而言是不可见的。
  Java内存模型中,程序(进程)拥有一块内存空间,可以被所有的线程共享,即MainMemory(主内存:堆);而每个线程又有一块独立的内存空间,即WorkingMemory(工作内存:栈)。普通情况下,当线程需要对某一共享变量进行修改时,通常会进行如下的过程:
1.从主内存中拷贝变量的一份副本,并装载到工作内存中;
2.在工作内存中执行代码,修改副本的值;
3.用工作内存中的副本值更新主存中的相关变量值。
  所谓“线程安全”,即多个线程同时执行同一段代码时,不会出现不确定的或者与单线程条件下不一致的结果。通常,下列三种条件居其一的并发访问被JVM认为是线程安全的:

有final关键字修饰且已被赋值;
有volatile关键字修饰;

volatile 解析: https://www.cnblogs.com/dolphin0520/p/3920373.html

有锁保护(synchronized、ReentrantLock等)。

synchronized、ReentranLock 简述: https://blog.csdn.net/Zheng548/article/details/54426947

摘自 https://blog.csdn.net/u014730165/article/details/81981154

6. 从JVM的角度来看java的多线程

多线程的目的
为什么要使用多线程?可以简单的分两个方面来说:

  1. 在多个cpu核心下,多线程的好处是显而易见的,不然多个cpu核心只跑一个线程其他的核心就都浪费了;
  2. 即便不考虑多核心,在单核下,多线程也是有意义的,因为在一些操作,比如IO操作阻塞的时候,是不需要cpu参与的,这时候cpu就可以另开一个线程去做别的事情,等待IO操作完成再回到之前的线程继续执行即可。

多线程带来的问题

  • 其实多线程根本的问题只有一个:线程间变量的共享
    java里的变量可以分3类:
    类变量(类里面static修饰的变量)
    实例变量(类里面的普通变量)
    局部变量(方法里声明的变量)
    下图是jvm的内存区域划分图:
    系统与JVM与线程简述_第13张图片
    根据各个区域的定义,我们可以知道:
    类变量 保存在“方法区”
    实例变量 保存在“堆”
    局部变量 保存在 “虚拟机栈”

“方法区”和“堆”都属于线程共享数据区,“虚拟机栈”属于线程私有数据区。

因此,局部变量是不能多个线程共享的,而类变量和实例变量是可以多个线程共享的。事实上,在java中,多线程间进行通信的唯一途径就是通过类变量和实例变量。

也就是说,如果一段多线程程序中如果没有类变量和实例变量,那么这段多线程程序就一定是线程安全的。

以Web开发的Servlet为例,一般我们开发的时候,自己的类继承HttpServlet之后,重写doPost()、doGet()处理请求,不管我们在这两个方法里写什么代码,只要没有操作类变量或实例变量,最后写出来的代码就是线程安全的。如果在Servlet类里面加了实例变量,就很可能出现线程安全性问题,解决方法就是把实例变量改为ThreadLocal变量,而ThreadLocal实现的含义就是让实例变量变成了“线程私有”的,即给每一个线程分配一个自己的值。

现在我们知道:其实多线程根本的问题只有一个:线程间变量的共享,这里的变量,指的就是类变量和实例变量,后续的一切,都是为了解决类变量和实例变量共享的安全问题。

如何安全的共享变量

现在唯一的问题就是要让多个线程安全的共享变量(下文中的变量一般特指类变量和实例变量),上文提到了一种ThreadLocal的方式,其实这种方式并不是真正的共享,而是为每个线程分配一个自己的值。

比如现在有一个特别简单的需求,有一个类变量a=0,现在启动5个线程,每个线程执行a++;如果用ThreadLocal的方式,最后的结果就是5个线程都拥有一份自己的a值,最终结果都是1,这显然不符合我们的预期。

那么如果不使用ThreadLocal呢?直接声明一个类变量a=0,然后让5个线程分别去执行a++;这样结果依旧不对,而且结果是不确定的,可能是1,2,3,4,5中的任一个。这种情况叫做竞态条件(Race Condition),要理解竞态条件先要理解Java内存模型:

要理解java的内存模型,可以类比计算机硬件访问内存的模型。由于计算机的cpu运算速度和内存io速度有几个数量级的差距,因此现代计算机都不得不加入一层尽可能接近处理器运算速度的高速缓存来做缓冲:将内存中运算需要使用的数据先复制到缓存中,当运算结束后再同步回内存。如下图:
系统与JVM与线程简述_第14张图片
因为jvm要实现跨硬件平台,因此jvm定义了自己的内存模型,但是因为jvm的内存模型最终还是要映射到硬件上,因此jvm内存模型几乎与硬件的模型一样
系统与JVM与线程简述_第15张图片
每个java线程都有一份自己的工作内存,线程访问变量的时候,不能直接访问主内存中的变量,而是先把主内存的变量复制到自己的工作内存,然后操作自己工作内存里的变量,最后再同步给主内存。

现在就可以解释为什么5个线程执行a++最后结果不一定是5了,因为a++可以分解为3步操作:

  1. 把主内存里的a复制到线程的工作内存
  2. 线程对工作内存里的a执行a=a+1
  3. 把线程工作内存里的a同步回主内存

而5个线程并发执行的时候完全有可能5个线程都先执行了第一步,这样5个线程的工作内存里a的初始值都是0,然后执行a=a+1后在工作内存里的运算结果都是1,最后同步回主内存的值肯定也是1。

而避免这种情况的方法就是:在多个线程并发访问a的时候,保证a在同一个时刻只被一个线程使用。

同步(synchronized)就是:在多个线程并发访问共享数据的时候,保证共享数据在同一个时刻只被一个线程使用。

同步基本思想

为了保证共享数据在同一时刻只被一个线程使用,我们有一种很简单的实现思想,就是在共享数据里保存一个锁,当没有线程访问时,锁是空的,当有第一个线程访问时,就在锁里保存这个线程的标识并允许这个线程访问共享数据。在当前线程释放共享数据之前,如果再有其他线程想要访问共享数据,就要等待锁释放。

我们把这种思想的三个关键点抽出来:

  1. 在共享数据里保存一个锁
  2. 在锁里保存这个线程的标识
  3. 其他线程访问已加锁共享数据要等待锁释放

其实抛开实现的细节,java的多线程很简单:
java多线程主要面临的问题就是线程安全问题 --》
线程安全问题是由线程间的通信造成的,多个线程间不通信就没有线程安全问题–》
java中线程通信只能通过类变量和实例变量,因此解决线程安全问题就是解决对变量的安全访问问题–》
java中解决变量的安全访问采用的是同步的手段,同步是通过锁实现的–》
有三种锁能保证变量只有一个线程访问,偏向锁最快但是只能用于从始至终只有一个线程获得锁,轻量级锁较快但是只能用于线程串行获得锁,重量级锁最慢但是可以用于线程并发获得锁,先用最快的偏向锁,每次假设不成立就升级一个重量。

摘自 https://www.cnblogs.com/sheeva/p/6366782.html

可重入锁

重入进一步提升了加锁行为的封装性,因而简化了面向对象并发代码的开发。在以下程序中,子类改写了父类的 synchronized 方法,然后调用父类中的方法,此时如果内置锁不是可重入的,那么这段代码将产生死锁。由于 Widget 和 LoggingWidget 中 doSomething 方法都是 synchronized 方法,因此每个每个 doSomething 方法在执行前都会获取 Widget 上的锁。然而如果内置锁不是可重入的,那么调用 super.doSomething( )时无法获得 Widget 上的锁,因为这个锁已经被持有,从而线程将永远停顿下去,等待一个永远也无法获得的锁。重入则避免了这种死锁情况的发生。

public class Widget{
    public synchronized void doSomething(){
        ........
    }
}

public class LoggingWidget extends Widget{
    public synchronized void doSomething(){
        super.doSomething();
    }
}

你可能感兴趣的:(java,java)