多线程并发 (一) 了解 Java 虚拟机 - JVM

章节:
多线程并发 (一) 了解 Java 虚拟机 - JVM
多线程并发 (二) 了解 Thread
多线程并发 (三) 锁 synchronized、volatile
多线程并发 (四) 了解原子类 AtomicXX 属性地址偏移量,CAS机制
多线程并发 (五) ReentrantLock 使用和源码

           目录

1.Java 虚拟机执行流程

2.Java虚拟机结构

3.运行时数据区域

4.对象的创建过程

5.对象在堆中的内存布局

6.Java对象在虚拟机中的生命周期

7.Java中的引用

8.垃圾标记算法

9.垃圾收集算法思想


Java虚拟机引入并发编程

从java虚拟机一环一环的去引入多线程并发编程,有时候学的知识联系不到一起。我们了解java虚拟机,了解了多线程并发,了解了锁。可曾想过为什么会出现锁这个概念?为什么会有多线程?是由java虚拟机那部分结构引入的呢。希望你能从中找到答案!

1.Java 虚拟机执行流程

当我们运行一个java程序时,他的运行流程如下:

java流程

一段程序的运行分为两部分:1.编译时期将.java通过 javac 命令编译成.class文件。2.java虚拟机运行时期 classloader 加载.class文件。
注:Java 虚拟机与 Java 语言没有什么必然的联系,它只与特定的二进制文件: Class 文件有关 。 因此无论任何语言 只要能编译成 Class 文件,就可以被 Java 虚拟机识别并执行。

2.Java虚拟机结构

java虚拟机内存模型大致分为5个区域:java虚拟机栈,堆,本地方法区,方法区,程序计数器。

方法区和 Java 堆就是所有线程共享的数据区域

3.运行时数据区域

程序计数器、 Java 虚拟机栈、本地方法栈、 Java 堆和方法区。
1.程序计数器:为了保证程序能不间断的运行,程序计数器起到记录当前运行的指令的地址。Java 虚拟机的多线程是通过轮流切换并分配处理器执行时间的方式来实现的,在一个确定的时刻只有一个处理器执行一 条线程中的指令,为了在线程切换后能恢复到正确的执行位置 ,每个线程都会 有一个独立的程序计数器,因此程序计数器是线程私有的。(每个线程在创建的时候,都会分配一个程序计数器,用来记录当前线程运行的位置。比如当有两个线程A,B。同一时间处理器只会处理一个线程的任务,当从A线程切换到B,再从B切换到A,这时候就需要从A程序计数器中获取之前程序运行的位置。所以是线程私有的。)

2.Java 虚拟机栈(涉及到java内存模型):每一条线程都有一个线程私有的 Java 虚拟机栈(Java Virtual MachineStacks )。它的生命周期与线程相同,与线程是同时创建的 。Java 虚拟机栈存储线程中 Java方法调用的状态,包括局部变量、参数、返回值以及运算的中间结果等。(每个线程在创建的时候,都会分配一个对应的虚拟机栈空间,运行方法时方法入栈,会储存方法內局部变量等,所以是线程私有的。)

3.本地方法栈:Java 虚拟机实现可能要用到 C Stacks 来支持 Native 语言 ,这个 C Stacks 就是本地方怯枝( Native Method Stack )。它与 Java 虚拟机枝类似,只不过本地方怯枝是用来支持 Native方峙的。(和java虚拟机栈一样,本地方法栈是native方法引用的内存栈空间,也是线程私有的。)

4. Java 堆:Java 堆 (Java Heap )是被所有线程共享 的运行时内存区域。 Java 堆用来存放对象实例,Java 堆存储的对象被垃圾收集器管理,这些受管理的对象无法显式地销毁。从内存回收的角度来分, Java 堆可以粗略地分为新生代和老年代。Java 堆所使用的内存在物理上不需要连续,逻辑上连续即可。(java虚拟机堆保存了几乎所有的java对象,因此堆是GC垃圾回收的主要战场。线程共享的。)

5. 方法区(Hotspot中的永久代):方法区( Method Area )是被所有线程共享 的运行时内存区域,用来存储已经被 Java虚拟机加载的类的结构信息,包括运行时常量地、字段和方法信息、静态变量等数据。Java 堆所使用的内存在物理上不需要连续,逻辑上连续即可。(方法区主要存放类结构数据,常量池,静态属性是线程共享的,且一般GC满意度比较低。)

4.对象的创建过程

一、类的加载过程
首先,Jvm在执行时,遇到一个新的类时,会到内存中的方法区去找class的信息,如果找到就直接拿来用,如果没有找到,就会去将类文件加载到方法区。在类加载时,静态成员变量加载到方法区的静态区域,非静态成员变量加载到方法区的非静态区域。静态代码块是在类加载时自动执行的代码,非静态代码块是在创建对象时自动执行的代码,不创建对象不执行该类的非静态代码块。
加载过程:
1、JVM会先去方法区中找有没有相应类的.class存在。如果有,就直接使用;如果没有,则把相关类的.clss加载到方法区。
2、在.class加载到方法区时,先加载父类再加载子类;先加载静态内容,再加载非静态内容
3、加载静态内容:把.class中的所有静态内容加载到方法区下的静态区域内静态内容加载完成之后,对所有的静态变量进行默认初始化所有的静态变量默认初始化完成之后,再进行显式初始化当静态区域下的所有静态变量显式初始化完后,执行静态代码块
4、加载非静态内容:把.class中的所有非静态变量及非静态代码块加载到方法区下的非静态区域内。5、执行完之后,整个类的加载就完成了。
对于静态方法和非静态方法都是被动调用,即系统不会自动调用执行,所以用户没有调用时都不执行,主要区别在于静态方法可以直接用类名直接调用(实例化对象也可以),而非静态方法只能先实例化对象后才能调用。
二、对象的创建过程
1、new一个对象时,在堆内存中开辟一块空间。
2、给开辟的空间分配一个地址。
3、把对象的所有非静态成员加载到所开辟的空间下。
4、所有的非静态成员加载完成之后,对所有非静态成员变量进行默认初始化。
5、所有非静态成员变量默认初始化完成之后,调用构造函数。
6、在构造函数入栈执行时,分为两部分:先执行构造函数中的隐式三步,
====①执行super()语句   ②对开辟空间下的所有非静态成员变量进行显示初始化  ③执行构造代码块====
再执行构造函数中书写的代码。
7、在整个构造函数执行完并弹栈后,把空间分配的地址赋给引用对象。
注:  super语句,可能出现以下三种情况:
1)构造方法体的第一行是this()语句,则不会执行隐式三步,而是调用this()语句所对应的的构造方法,最终肯定会有第一行不是this语句的构造方法。
2)构造方法体的第一行是super()语句,则调用相应的父类的构造方法, 
3)构造方法体的第一行既不是this()语句也不是super()语句,则隐式调用super(),即其父类的默认构造方法,这也是为什么一个父类通常要提供默认构造方法的原因。
引用:https://www.cnblogs.com/ttty/p/10431676.html

总结三步:开辟空间创建属性对象赋默认值,调用构造方法赋初始值,给栈里的对象赋引用。(cpu指令重排提高效率)

5.对象在堆中的内存布局

                                                                                         对象分布

                                                                                       hotspot

过程描述:一个对象创建之后包括四个部分如上图,其中markword对象头中保存了对象的hashCode,分代年龄,锁标志。
new出来的对象还是个无锁的状态,当有一个线程访问时候,该锁就升级成了偏向锁/这时的hashCode就被转移到了该对象的stack栈空间中,对象的markword中保存当前线程id等,此时偏向锁标志被标记为1,。如果这时出现了另外一个线程在等待此对象的锁,那么这个锁就升级成了轻量及锁(自旋锁:一直在for循环等待试探是否可以获取锁,消耗cpu)如果自旋超过10次,该锁就被升级为重量级锁,重量级锁是操作系统OS处理的,重量级锁会把当前等待的线程丢到等待队列中去,等锁释放了再从队列中拿出来。GC每回收一次分代年龄就会加1 当gc年龄到达6岁对象就升到老年代区域。

6.Java对象在虚拟机中的生命周期

1. 创建阶段
 总结三步:开辟空间创建属性对象赋默认值,调用构造方法赋初始值,给栈里的对象赋引用。(cpu指令重排提高效率)
2. 应用阶段
当对象被创建,并分配给变量赋值时,状态就切换到了应用阶段。
3. 不可见阶段
在程序中找不到对象的任何强引用,程序的执行已经超出了该对象的作用域。在不可见阶段,对象仍可能被特殊的强引用 GC Roots 持有着,比如被运行中的线程引用等。
4. 不可达阶段
在程序中找不到对象的任何强引用,并且垃圾收集器发现对象不可达。
5. 收集阶段
垃圾收集器已经发现对象不可达,并且垃圾收集器已经准备好要对该对象的内存空间重新进行分配,这个时候如果该对象重写了 finalize 方法则会调用该方法。
6. 终结阶段
在对象执行完 finalize 方法后仍然处于不可达状态时,或者对象没有重写 finalize 方法,
则该对象进入终结阶段,并等待垃圾收集器回收该对象空间。
7. 对象空间重新分配阶段
当垃圾收集器对对象的内存空间进行回收或者再分配时,这个对象就会彻底消失。

被标记为不可达的对象会立即被垃圾收集器回收吗?
很显然是不会的,被标记为不可达的对象会进入收集阶段,这时会执行该对象重写的 finalize 方法,如果没有重写 finalize 方法或者finalize 方法中没有重新与一个可达的对象进行关联才会进入终结阶段,并最终被回收。

7.Java中的引用

1.强引用
    默认创建一个对象就是强引用,具有强引用的对象垃圾收集器不会回收他,除非该对象对应的gc root 对象的引用被释放,否则就算抛出oom也不会回收该对象的内存。
2.软引用
    用SoftReference<O>标记的对象是软引用,当内存不足时就会被回收。
3.弱引用
    用WeakReference<O>标记的对象是弱引用,当发生GC时候就会被回收。
4.虚引用
    PhantomReference<O>标记虚引用,虚引用在任何时候都有可能会被回收。

8.垃圾标记算法

对于垃圾收集要知道三个问题
    1.那些对象需要回收?
    2.什么时候回收?
    3.怎么回收?

对于程序计数器,java虚拟机栈,本地方法栈这三部分内存随着方法结束和线程的结束内存会被回收,但对于方法区,堆则需要GC来回收。
(一)引用计数算法
    对于引用计数算法虚拟机并没使用这种算法,引用计数算法是给一个对象加一个计数器,每当有一个地方引用他时候,计数器就加一,失效就减一,当计数器为0时候就是没有被引用,这时候垃圾收集器就可以回收。但是这种算法有一个问题就是两个脱离GC root 的对象相互引用,导致计数器都不为0的现象。
(二)根搜索算法
    通过GC root 对象向下搜索,搜索走过的路径称为引用链,当一个对象没有在GC root引用链上,说明当前对象没有GC root 对象引用,此对象可以回收。
    一下可作为GC roots 对象:
    1.虚拟机栈中引用的对象
    2.方法区中的累静态属性引用的对象
    3.方法区中常量引用的对象
    4.本地方法栈中JNI引用的对象

9.垃圾收集算法思想

    (一)标记 - 请除算法
            标记清除算法包括两个阶段1.标记,2.清除。标记其实就是我们8中所说的标记算法。后续的垃圾收集算法都是基于这个思路对其进行改进。他的两个缺点:1.效率不高,2.空间间隔问题,标记清除后会有大量不连续的内存碎片,当程序再次给对象分配内存时没有足够的内存空间,就会再一次出发GC。

    (二)复制算法
            为了提高效率,在标记-清除算法基础上产生了复制算法。复制算法是将内存一份为大小相等的两份。每次使用其中的一份,当发生GC时候,就会把使用的那块内存中的存活的对象复制到另一半内存中,然后把之前那块内存清空。这样就解决了内存碎片的问题。但是这样有一半内存是空的对于资源是十分浪费。

    (三)标记-整理算法
            根据老年代的特点,提出了标记-整理算法。在标记-清除的算法基础上,让存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

    (四)分代收集算法
            当前商业虚拟机都采用此种算法,这种算法只是根据对象的存货周期的不同将内存分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最合适的收集算法。
    新生代中:GC 之后只有少量对象存活采用的是复制算法。
    老年代中:由于对象存活率高,空间少,采用的标记-清除 或 标记-整理算法。

由于本人记性不好学者忘着,所以才一字一句的记录下来,若有错误或其他问题请评论区教训我~。

摘学于:
深入理解Java虚拟机一书

发布了119 篇原创文章 · 获赞 140 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/WangRain1/article/details/103731546