栈和堆的区别
栈是运行时单位,代表着逻辑,内含基本数据类型和堆中对象引用,所在区域连续,没有碎片;
堆是存储单位,代表着数据,可以多个栈共享(包括成员中基本数据类型,引用和引用对象),所在区域不连续,会有碎片。
区别:
- 功能不同
栈内存用来存储局部变量的基本数据类型和对象的引用,而堆内存用来存储Java中的对象,无论是成员变量,局部变量还是类变量,它们指向的对象都存储在堆内存中。
- 共享性不同
栈内存是线程私有的
堆内存是所有线程共享的
- 异常错误不同
如果栈内存或者堆内存不足都会抛出异常
栈空间不足:java.lang.StackOverFlowError.
堆空间不足:Java,lang.OutOfMemoryError.
- 空间大小
栈的空间大小远远小于堆的。
Jvm内存模型
方法区和堆是所有线程共享的内存区域。java虚拟机栈,本地方法栈和程序计数器是线程私有的内存区域。其中方法区和堆是垃圾回收的主要区域,java虚拟机栈,本地方法和程序计数器不会发生垃圾回收。
堆:是Java虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
方法区:方法区和堆一样都是线程共享的区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即是编译器编译后的代码等数据。
java虚拟机栈:是栈线程私有的,它的生命周期与线程相同,虚拟机栈描述的是Java方法执行的内存模型。每一个方法被执行的时候都会同时创建一个栈帧,用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈:本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行的Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
程序计数器:是一块是较小的内存空间,它可以看作当前线程所执行的字节码的行号指示器。字节码解析器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,每一个线程都需要一个独立的程序计算器,各个线程之间计数器互不影响,独立存储。
Jvm的垃圾回收算法
哪些要回收?如何判断对象是否是死忙?
引用计算算法
对象中添加一个引用计数器,每当一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
可达性分析算法
可达性分析算法的基本思路是通过一系列为"GC Roots" 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,通过搜索过程所走过的路径称为“引用链”,如果这个对象到GC Roots间没有任何引用链相连,或者用图论的话就是从GC Roots到这个对象不可达时,则证明此对象不可能再被使用的。
可以做为GC Roots的对象
- 在虚拟机栈中引用的对象,譬如各个线程被调用的方法堆,栈中使用带的参数、局部变量、临时变量等。
- 在方法区中类型静态属性引用的对象,譬如Java类的引用类型静态变量。
- 在方法区中引用的对象,譬如字符串常量池里的引用。
- 在本地方法栈中JNI引用的对象。
- Java虚拟机内部的引用,如基本数据可惜对应的Class对象,一些常驻的异常对象等。
- 所有被同步锁持有的对象。
垃圾回收算法
标记-清除算法
首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。这个算法有两个主要的缺点:第一个是执行效率不稳定,如果Java堆中包含有大量的对象,其中大部分是需要被回收的,这是必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;第二个是内存空间的碎化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-复制算法
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,这一块的内存用完了,就将还存活的着的对象复制到另一块上面,然后再把自己使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法将会产生大量的内存复制的开销,但是对于大多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不需要考虑空间碎片的复杂情况。但是这种回收算法的代价是将可用内存缩小为了原本的一半,造成空间的浪费。适合用于新生代。
标记-整理算法
首先标记处所有需要回收的对象,在标记完成后,让所有存活的对象都向内存空间的一端移动,然后直接清理掉边界以外的内存。适合于老年代对象。
类加载过程
类加载的定义:
Java虚拟机把描述类的数据从Class文件
image.png
加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作为虚拟机的类加载机制。
类加载的过程:
- 加载
在加载阶段,虚拟机主要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为这个类的各种数据的访问入口。
- 验证
验证是连接阶段的第一步,目的是确保Class文件的字节流中包含的信息符合虚拟机的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机的安全。验证字节码是Java虚拟机保护自身的一项必要措施。验证阶段非常必要,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击。验证阶段主要是对文件格式验证,元数据验证,字节码验证和符号引用验证。
- 准备
准备阶段是正式类中定义的变量,即静态变量分配内存并设置变量初始化值的阶段。需要注意的是初始值有以下两种情况:
public static int value=123;
public static final inr value=123;
第一行代码在准备阶段过后的赋值是0而不是123,因为这时尚未开始执行任何Java方法,如果被final修饰之后,在准备阶段就会根据ConstantValue属性所指定的初始值。
- 解析
解析阶段是Java虚拟机将常量池的符号引用替换为直接引用的过程。
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时无歧义地定位到目标即可。符号引用与虚拟机的内存布局无关,引用的目标不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一直的。因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。
- 直接引用是可以直接指向目标的指针,相对偏移量或者一个能简接定位到目标的句柄。直接引用是和虚拟机实现内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的 直接引用一般不会相同。
- 初始化
类的初始化阶段是类加载过程的最后一步骤,直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。在准备阶段时,变量已经赋过一次系统要求的初始值,而初始化阶段,则会根据程序通过程序编码定制的主观计划去初始化类变量和其他资源。直观表达就是初始化阶段就是执行类构造器<clinit>()方法的过程,主要完成静态代码块执行以及静态变量的赋值。
类加载器
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。 可以参考https://blog.csdn.net/m0_38075425/article/details/81627349
类加载器的介绍
站在Java虚拟机的角度看,只存在两种不同的类加载器:一种是启动类加载器,这个类加载器使用C++语言实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。在JDK8,之前一直都是使用三层类加载器。
- 启动类加载器:这个两类加载器负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所在指定的路径中存放的,而且是Java虚拟机能够识别的类加载到虚拟机的内存中。
- 扩展类加载器:这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路经中所有的类库。
- 应用程序类加载器:这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,所以有些场合中也被它称为“系统类加载器”。
双亲委派模型
类加载器之间的父子关系一般不是以继承的关系来实现,而是通常使用组合关系来复用父类加载器的代码。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都是应该传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。
双亲委派加载器的优势就是:Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类Java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器需要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。
image.png
Java内存模型
- 主要目的
Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出来变量值这样的底层细节。此处的变量与Java编程中所说的变量有所区别,它包括了实例字段,静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为后者是线程私有的,不会被共享,自然就不存在竞争的问题。
Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存保存了被线程使用的变量的主存的副本,线程对变量的所有操作都必须都是在工作内存中进行的,而不能直接读写主内存中数据。不同线程之间无法直接访问对方工作内存的对象,线程间变量值的传递均需要通过主存来完成。
- 原子性
原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,在多线程中一个操作一旦开始,就不会被其他线程所干扰。但是volatile不能保证原子性。
- 有序性:
可以总结为一句话:如果在本地线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有线程都是无序的。前半句是指“线程内是表现为串行的意义”后半句是指“指令重排序”的现象和“工作内存与主内存同步延迟”现象。Volatile关键字本身包含了禁止指令重排序的语义。而sychronied则是由“一个变量在同一时刻只能允许一条线程对其进行lock操作。”
禁止指令重排序是指:能够保证变量的赋值操作的顺序与程序代码中的执行顺序一致。而普通变量仅保证在改方法执行过程中所有依赖赋值结果的地方都能获取到正确的结果。
- 可见性
可见性是指当前一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。Java内存模型是通过在遍历修改后将新值同步到主内存中,在变量读取前从主内存中刷新变量值。Volatile保证了新值能够立即同步到主内存中,以及每次使用前立即从主内存中刷新。sychronied的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中。final的可见性是指:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把"this"的引用传递出去,那么其他线程中就能看见final字段的值。
可重入性是指当线程请求自己持有的对象锁时,如果请求成功,则说明是可重入锁。sychronied是一个可重入锁,因此在一个线程使用sychronized方法时调用该对象另一个sychronied方法,即一个线程得到一个对象锁后再次请求该对象锁是永远可以获得锁的。
synchronized可重入锁的实现
每个锁关联一个线程持有者和一个计数器,当计数器为0表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法。当一个线程获得锁后,Jvm会记下持有锁的线程,并将计数器为1。此时其他线程请求该锁,则必须等待,而持有该锁的线程如果再次请求这个锁,可以再次拿到,同时计算器会递增。当线程退出一个sychronied方法/块时,计数器就会递减,如果计数器为0则释放该锁。
网友评论