JVM总结

java虚拟机在运行java程序时 会将其管理的内存划分成不同的区域:其中所有线程共享的区域有:方法区和堆内存,线程私有的程序计数器,虚拟机栈和本地方法栈。

程序计数器占用内存的很小的一块区域,它是当前线程执行字节码时的行号指示器。

java虚拟机栈是线程私有的,虚拟机栈描述了JAVA方法执行的内存模型,每个方法的执行都会创建一个栈帧用于存放局部变量、动态链接、操作数栈、方法出口等信息。每一个方法从调用到执行完成,就意味着栈帧在操作数栈中入栈到出栈。局部变量表存放了编译器可知的各种基本数据类型、对象引用和returnAdress类型。

java堆是内存中最大的一块,这一部分的目的是存放对象实例,几乎所有的对象实例都存放在这个区域。它是垃圾收集器管理的主要区域。从内存回收角度,收集器主要采用的分代收集,可以将堆内存分为新生代和老生代,如果更细致的分可以分为Eden空间、From Survivor空间和To Survivor空间。

方法区和java堆类似都是线程公用的区域,这一区域主要存放虚拟机加载的类信息、常量、静态变量以及编译器编译后的代码等。

运行时常量池是方法区的一部分,主要存放编译器生成的各种字面量、符号引用等,这些内容将在类加载后进入方法区运行时常量池中。

 

对象的创建步骤:

1、当创建对象时,虚拟机要先看方法区常量池中是否存在这个类的符号引用,并且是否已经被加载、解析和初始化过。如果没有的话,就要执行类加载的步骤。

2、为新建对象分配内存。其中需要解决如果划分内存空间以及线程安全问题

3、将分配到的内存空间都初始化为零值(对象头除外)

4、将类的元数据信息、类的哈希码以及类的GC分代信息等储存到对象头中

5、执行init方法完成对象的初始化

 

对象的内存布局:

对象在内存中所储存的内存布局有三部分:对象头、实例数据和对齐填充。

 

对象的访问定位:

通过操作虚拟机栈的reference数据来操作堆上的对象实例。主流的访问方式有两种:通过句柄访问以及通过指针直接访问

使用句柄访问的好处是reference中存放的是稳定的句柄地址,在对象被移动时只是改变句柄中实例对象数据的地址。

而使用直接指针访问的好处是速度快,节省了一次指针定位的时间开销。

 

垃圾回收:

垃圾回收所关注的内存区域是java堆和方法区。因为虚拟机栈、程序计数器和本地方法栈三个区域,这三个区域随着线程而生线程而灭。,栈中的栈帧随着方法进入退出而不断入栈出栈,每个栈帧所分配的内存在类结构确定下来后就一直了,所以这部分区域内存的分配和回收就确定了,所以不需要过多考虑垃圾回收。当线程结束时内存自然就得到了回收。而java堆和方法区中一个接口多个实现类所需要的内存不一样,一个方法中的多个分支(例如有if else语句)所需要的内存也不一样,需要的方法运行时才知道分配内存的多少,这部分内存时动态分配和回收的,垃圾收集器最关注的是这一部分。

 

对象已死?

判断对象是否已经可以回收的方法主要有引用计数算法和可达性分析算法。

引用计数算法是为每个对象添加一个引用计数器,每当有一个地方引用它时引用计数器加1,当引用失效时,引用计数器减1,当对象的引用计数为0时表示,已经没有办法访问到这个对象,垃圾回收器便将其回收。但是这种方法没有办法解决对象间互相引用的问题。

 

可达性分析算法:通过将GC ROOTS作为起始点,从这个点开始向下搜索,搜索所走过的路径称之为引用链。当一个对象到GC ROOTS没有任何引用链相连的话,这个对象就是不可达到的,可以将其回收。

可以作为GC ROOTS的对象可以是:

1、虚拟机栈中引用的对象

2、方法区中类静态类型属性所引用的对象

3、方法区中常量引用的对象

4、本地方法栈中Native方法所引用的对象

 

引用的4种方式

强引用:这种引用,只要引用的对象还存在就不会被回收。

软引用:这种引用比强引用弱,对象有用但不必需,当内存不够用的时候,就会回收这部分内存,如果还是不够用的话,就会抛内存溢出异常。

弱引用:这部分对象是不必需的,被弱引用关联的对象只能生存到下次垃圾收集器回收之前。无论内存是否足够,都会回收。

虚引用:

 

方法区垃圾回收

堆被称为虚拟机的新生代,而方法区被称为永久代,对永久代进行垃圾回收的效率比较低。主要回收废弃常量和无用的类两种。

对无用类的回收要满足三个条件:

1、该类在堆中没有任何的实例对象

2、加载该类的ClassLoader已经被回收

3、该类所对应的java.lang.Class对象没有在任何地方被引用。无法在任何地方通过反射访问该类的方法。

 

虚拟机类加载机制

虚拟机将描述类的Class文件加载到内存,经过虚拟机的校验、转换解析以及初始化,将其转化为可以被虚拟机直接使用的java文件。

 

类的加载时机

类的生命周期:加载--连接(验证、准备、解析)--初始化--使用--卸载

 

类初始化的五种情况

1、当使用new去生成一个类的实例的时候,以及当引用或设置一个类的静态字段,引用静态方法的时候会使得类进行初始化

2、使用反射时,如果一个类没有初始化会先初始化类

3、初始化一个类时,如果其父类没有初始化,要先初始化其父类

4、当虚拟机启动时,要指定一个要执行的主类(含有main方法的类),首先初始化主类。

5、java.lang.invoke.MethodHandle实例最后的结果是REF_getStatic、REF_putStatic的方法句柄,并且这个句柄所对应的方法没有初始化,那么要先将其初始化。

有且只有这五种方式会引发对类的初始化,称为主动引用。其他对类的引用都不会引发对类的初始化。

 

当通过子类去引用父类的静态字段时,只有父类会初始化,而子类不会初始化,这正好符合第一种情况。

当引用一个类的常量池时,不会触发类的初始化,因为常量在类的编译阶段储存到常量池,而初始化类发生在运行期,这时ConstClass类和未初始化的类编译成class文件后便已经没有了联系。

 

接口的初始化和类的初始化有些不同,当接口初始化时不要求其父类全部都初始化完成。当使用到父类方法时才会对其初始化。

 

类加载过程

加载是类加载过程的一个阶段,在加载阶段虚拟机主要完成3个步骤:

1、根据类的全限定名加载定义类的二进制字节码文件。

2、将字节码中的静态储存结构转化到方法区运行时数据区。

3、在内存中生成类对应的java.lang.Class对象,作为访问方法区中该类数据的入口。

验证阶段是检查字节码文件是否符合虚拟机的规范,并且能够保证虚拟机的运行安全。

主要的验证动作有四个阶段:文件格式验证、元数据验证、字节码验证和符号引用验证。

准备阶段正式为类变量分配内存和将变量初始化,这部分将类变量存入到方法区中,这部分的变量是类变量(被static修饰的变量),不包括实例变量。而实例变量将会在对象实例化后随对象一起进入Java堆中。此时所说的初始化是将变量初始化为零值。而这些类变量的真实值需要在初始化阶段才能被赋值。

解析阶段是虚拟机将常量池中的符号引用解析为直接引用。

初始化阶段是类加载的最后一步,这一阶段才开始执行类中定义的JAVA程序。这一阶段只要是根据程序制定的主观计划去初始化变量或其他资源,从另一个角度表达:初始化阶段是类构造器执行<clinit>()方法的过程。

<clinit>()方法是有编译器自动收集类中所有类变量的赋值动作和 静态代码块中语句合并产生的。编译器手机的顺序是由语句在原文件中出现的顺序决定的,静态代码块只能访问定义在之前的变量,定义在之后的变量可以被赋值,但是不能有被访问。

类加载器

在加载阶段通过类的全限定名来获取描述类的二进制字节码文件的这一动作代码模块称之为类加载器。这一部分放在虚拟机外部运行,这样我们可以让程序自己决定如何获得所需要的类。

判断两个类是否相等,不止要看是不是来自于同一个class文件,还要看是否有同一个类加载器加载。

虚拟机提供的类加载器有三种:启动类加载器、扩展类加载器和应用程序加载器。

启动加载器主要是加载java_home/lib中的类库加载到虚拟机内存中。启动类加载器无法被java程序直接使用。

扩展类加载器主要是加载java_home/lib/ext中的类库

应用程序加载器加载用户指定类路径下载类库,如果用户没有自己定义加载器的话,那么这个加载器就是程序中默认的加载器。

 

双亲委派模型能够保证java程序稳定运行。双亲委派模型的工作过程:当一个类加载器收到一个类的加载请求后,首先它不会先自己加载这个类,而是委派给父类加载器加载,每个层次的类加载器都是如此。因此所有的加载请求都应该传送到顶层的启动类加载器。只有当父类加载器反馈无法完成这个加载请求后 才会由子类加载。

例如:类java.lang.Object,该类存放于rt.jar中,无论哪个类药加载这个类,最终都是由启动类加载器加载,因此Object类在程序中都是加载的同一个Object类,如果不使用双亲委派模型的话,由各个加载器自行加载Object的话,在程序中就可能出现很多Object类,这样变回导致程序混乱。

 

虚拟机字节码执行引擎

字节码执行引擎是java虚拟机的核心组成部分。它输入的是字节码文件、处理过程是字节码解析的等效过程以及输出的是执行结果。

 

运行时栈帧结构:

虚拟机栈帧是虚拟机栈的栈元素,它主要是用来支持虚拟机方法的调用和运行。栈帧存储的是方法的局部变量、操作数栈、动态链接和返回值等信息。每一个方法的执行与运行结束对应着栈帧在虚拟机中入栈和出栈的过程。对于执行引擎来说,在活动线程中只有位于栈顶的栈帧才是有效的,成为当前栈帧。与之对应的方法为当前方法。

 

操作数栈也成为操作栈,他是后入先出的栈,和局部变量一样,他在编译阶段就已经确定了操作数栈的最大深度。当一个方法开始执行时,操作数栈是空的,随着方法的执行,会不断有字节码指令入栈和出栈。

例如:整数加法指令iadd在运行的时候操作数栈最顶端的两个元素在执行iadd指令时会出栈,然后进行相加操作,最后的结果入栈。

 

方法调用

方法调用并不等同于方法执行,方法调用的唯一目的是确定调用方法的版本,而与方法内部的具体运行过程无关。一切方法的调用在Class文件里面储存的都只是符号引用,而不是方法在运行过程中的内存布局的入口地址。这为java的动态扩展提供了强大的动力。

在类加载的解析阶段,会将一部分的符号引用转化为直接引用,这个过程成立的前提是方法的运行之前就已经确定了方法运行的版本,并且的运行期间是不可改变的。符合这个前提的主要有四大类:静态方法、私有方法、实例构造器和父类方法

 

静态分派和动态分派

依赖静态类型来确定方法执行版本的行为成为静态分派,典型的静态分派是方法重载(overload),静态分派发生于编译阶段,所以这个动作实际不是有虚拟机完成的。

在运行期根据实际类型确实执行版本的分派过程称之为动态分派。典型的动态分派是方法重写(override)。

 

JAVA内存模型与线程

并发处理的广泛应用原因:1、充分利用计算机的处理能力。多任务处理一方面的原因是计算机的运算能力不断强大,另一方面的原因是计算机的运算能力与他的存储与通信子系统速度的差距太大,如果大部分的时间花费在磁盘IO,网络通信或数据库读取上,会造成资源很大的浪费,所以采用多任务处理。

2、一个服务端对多个客户端提供服务。衡量一个服务器的好坏高低,每秒钟事物的处理量是重要的指标之一。对于计算量相同的任务,程序线程协调的越有条不紊,那么服务器的效率就会越来越高。反之,如果线程之间频繁阻塞死锁,就会很大程度上影响服务器效率。

 

在硬件层面上提升并发处理效率:

为了解决处理器与内存交互效率低得问题,提出了高速缓存作为处理器与内存间的缓冲,将运算中用到的数据复制到缓存区,实现处理器的高速读取提高效率,运算结束后,再将结果数据存储到内存中。

除了增加高速缓存来增加效率外,处理器通过指令重排序使得内部运算单元更加充分的利用。

 

在软件层面上(JAVA内存模型):

java内存模型的目标是定义程序中各个变量的访问规则,这里的变量与JAVA编程的概念不同,这里不包括局部变量和方法参数,因为他们是线程私有的不存在数据共享的问题。将JAVA内存模型分为了主内存和工作内存。所有的变量存储在主内存中,每个线程都有自己的工作内存,里面存储的运算需要的主内存中变量的副本。线程对变量的操作都是在工作内存中的,他不能之间访问主内存中的变量。

 

volatile变量的特殊规则

1、变量可见性,这里的可见性是指,当一个线程中volatile变量修改之后,其他线程是可以立即得知的。而普通变量线程间的传递只能通过主内存实现。

变量设置成volatile之后,其他线程是可以立即得知的,但这不是能说基于volatile变量在并发条件下是线程安全的。比如:

private static volatile int a = 0;

public void increase(){

a++;

}

因为a++不是原子操作,所以当把a读取到栈顶时a值是正确的,并且所有线程可知的,但当执行++操作时有可能其他线程已经将值++过了。所以对于非原子操作并不能保证线程安全。

 

2、禁止指令重排序

 

保证并发时线程安全内存的三个重要特性:原子性、可见性和有序性。

 

先行发生原则:它是内存模型中定义的两项操作间的偏序关系。如果操作A在操作B之前执行,那么A所产生的影响是可以被B察觉到的。衡量并发安全问题不要受时间顺序干扰,一切必须以先行发生原则为准。

 

JAVA线程调度

线程调度是指系统为线程分配处理器使用权的过程,主要的线程调度方式有两种:协同式调度和抢占式调度。

协同式调度是当一个线程任务执行完毕会主动通知系统切换到其他线程,但是这种方式的缺点是当一个线程比较耗时或者说一个线程的执行时间不可控时就会引起其他线程的线程阻塞。

抢占式调度的每一个线程的执行时间是由系统分配的,线程的切换是由系统来控制的。这样不会造成线程阻塞。JAVA采用的是抢占式线程调度方式。

 

我们可以通过优先级来设定线程,优先级越高的线程越容易被系统执行。但是这种设定优先级的方式并不是很靠谱,当系统发现某个线程执行的特别频繁的话,系统就会越过优先级为其分配执行时间。

 

java定义了线程五种状态,每个线程有且只有一种线程。这五种状态分别是新建、运行、等待(无限期等待、有限期等待)、阻塞、终止。

 

阻塞与等待的区别:等待是线程在等待唤醒的动作。而阻塞是线程已经被唤醒,但是线程没有获得排它锁。

 

线程安全严谨的定义

当多个线程访问一个对象时,不用考虑线程在运行时环境下的调度和交替执行,也不用进行额外的同步,或者调用方可以对对象进行任意的协调操作都能获得正确的结果,这样就说这个对象是线程安全的。

 

考虑线程安全的前提是线程间有共享数据。

按线程安全安全性的强弱来分,JAVA将共享操作的数据分为五种:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

不可变:比如final修饰的数据,本身就是不可变的,自然也是线程安全的。

绝对线程安全:能够带来线程安全的同时也付出很大的代价。

相对线程安全:这是通常意义上所讲的线程安全。他需要保证对这个对象的单独操作是线程安全的,但是如果对于一些有特定顺序的连续调用就必须使用额外的同步手段保证正确性。比如vector是线程安全的,但是在不同线程中对vector进行增删数据操作的话,就会引起错误的结果。

线程兼容:像ArrayList本身不是线程安全的,但是可以通过同步手段使得其在并发环境中可以安全的使用。

线程对立:很少见,就是无论在上面情况下都不能解决线程安全问题。比如Thread的suspend和resume

 

线程安全的实现方法

1、互斥同步

同步是指当多个线程同时访问共享数据时,确保同一时刻只能有一个线程(或者一些,使用信号量的时候)对共享数据进行使用。最基本的互斥手段是synchronize关键字。synchronize同步块对同一个线程来说是可重入的,自己不会把自己锁死。synchronize是重量级操作,当唤醒或阻塞线程时,需要系统的调度,这就需要从用户态转换为核心态,这种转化是需要消耗处理器时间的。

除了synchronize还有重入锁(ReentrantLock),它有一些高级功能,像线程中断、公平锁和多条件绑定。

 

2、非阻塞同步

互斥同步的问题是线程阻塞和唤醒转换影响性能。其有成为阻塞同步,它属于悲观策略。而非阻塞同步时乐观策略,其先让操作运行,如果没有其他线程争用共享数据,那么这个操作就成功了,而如果有其他线程争用数据,那么就采取相应的措施(比较常用的就是不断进行尝试,知道成功为止)。像CAS(compare and swap)。

 

3、无同步方案

线程本地存储ThreadLocal

 

锁优化

自旋锁、自适应自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值