JVM的总结

第二章:java内存区域和内存溢出异常:

2.1内存分布图:

内存分布图
方法区和堆是线程共享的;
虚拟机栈,本地方法栈,程序计数器是线程私有的;
1. 线程计数器:用来指定当前线程执行字节码的行数。JVM的多线程是通过线程轮流切换分配执行时间来实现的,在任何时刻,每个处理器都只会执行一个线程中的指令,当线程进行切换的时,为了线程能恢复当正确的位置,所以每个线程必须有个独立的线程计数器,这样才能保证线程之间不互相影响。
2. 虚拟机栈:生命周期与线程是同步的,每个方法在执行的同时,都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出入口等信息,每个方法的调用到执行完成的过程就是一个栈帧入栈到出栈的过程;局部变量表存储方法相关的局部变量,包括基本数据,对象引用和返回地址等。
3. 本地方法栈:与虚拟机栈执行的基本相同,一般混为一谈,统称栈,唯一的区别就是虚拟机栈是执行Java方法的,本地方法栈是执行native方法的;本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,
4. Java堆:线程共享。所有对象都在这里分配内存,是垃圾收集的主要区域(“GC 堆”)。主要存储对象的实例。可拓展。 逻辑连续,物理不一定连续
5. 方法区(永久区):存储被虚拟机加载的类信息(字段,方法,接口)、常量、静态变量、即时编译的代码等数据等;物理上不需要连续,运行时常量池是方法区的一部分,Class 文件中的常量池(编译器生成的字面量和符号引用,直接引用)会在类加载后被放入这个区域。除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。jdk6.0之前,字符串常量池一直在方法区。Jdk7.0后,在堆区。
6. 直接内存:,不是虚拟机运行时的一部分,可以直接访问堆外的内存;所以当内存空间无法动态扩展的时候就会出现OutOfMemoryError异常;在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。

2.2 对象创建与异常

1.对象的创建:
几种方式:克隆(clone 不调用构造函数),new 关键字,反序列化(不调用构造函数),反射。
先new ->检查常量池中是否有符号引用,并且检查符号引用表示的类是否被加载过,没有则先执行类加载过程,再开始分配内存。
分配内存:1. 指针碰撞(java内存规整,带有压缩整理的垃圾收集器Serial,ParNew),空闲列表(空闲内存相互交错,没有压缩整理CMS)
解决对象创建的同步问题:1.分配内存空间的过程同步处理(CAS配上失败重试)2.把内存分配放在不同的空间进行(分配本地线程缓冲TLAB)

2.对象的内存布局:
1, 对象头:对象自身运行时的数据(哈希码,GC分代年龄,锁状态标志,)(类型指针:指向元数据的指针)
2, 实例数据:对象真正储存的有效信息(各种字段),
3, 对齐填充:占位符,补全字节。

3.对象的访问定位:
通过栈上的引用数据类型来操作堆上的具体对象
两种方式:

  1. 句柄:在对象被移动时(垃圾收集时会移动对象即内存整理),句柄只需修改句柄中的对象实例数据的指针,栈中的reference(引用)不需要修改。而直接指针需要修改。
  2. 直接指针:速度更快,因为少了一次指针定位的时间开销。

4.OutOfMemoryError异常:
1.java堆(heap)溢出:对象太多。注意概念:内存泄露:程序申请了内存空间,没有用,也无法释放。和内存溢出:没有足够的空间供其使用,
2.虚拟机栈和本地方法栈溢出:
1.请求的栈深度大于了允许的最大深度。 :StackOverflowError
2.扩展栈时无法申请到足够的内存空间。:OutOfMemoryError
3.方法和运行时常量池溢出:
4.本机直接内存溢出:

第三章:垃圾回收器与内存分配策略

1.判断对象回收条件:
1,引用计数算法:添加一个计数器来记录。有一个引用就+1.引用失效就-1.任何时候为0就死了。但是不能解决相互循环引用的问题。
2.可达性分析算法(主流):选择一个对象作为起点。一个对象到GC roots链不可达就是不可用。起点的选择。
GC Roots的选择:1.虚拟机栈(栈帧中的本地向量表)中引用的对象 2.方法区中类静态属性引用的对象 3.方法区中常量引用的对象 4.本地方法栈中native方法引用的对象。
四种引用:
1.强引用:被强引用关联的对象不会被回收。使用 new 一个新对象的方式来创建强引用。
Object obj = new Object();
2. 软引用:被软引用关联的对象只有在内存不够的情况下才会被回收。使用 SoftReference 类来创建软引用。
Object obj = new Object();
SoftReference sf = new SoftReference(obj);
obj = null; // 使对象只被软引用关联
3. 弱引用:被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。使用 WeakReference 类来创建弱引用。
Object obj = new Object();
WeakReference wf = new WeakReference(obj);
obj = null;
4. 虚引用:又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。使用 PhantomReference 来创建虚引用。
Object obj = new Object();
PhantomReference pf = new PhantomReference(obj, null);
obj = null;
方法区(永久代)的回收:废弃常量(没有任何对象指向该池中的常量)和无用的类(1。该类的所有实例都被回收了 2。加载该类的ClassLoader被回收了 3。该类的java.lang.Class对象没有在任何地方被引用,无法反射访问)。

2.垃圾回收算法(重点):
1.标记-清除算法:效率不高,产生大量不连续的内存碎片
2.复制算法(新生代):分成一块大的Eden区和survivor区 。先使用Eden区和一块survivor区 回收时:存活的对象复制到另一块survivor区。然后清除,比例:8:1 不够就找老年代分配担保。
3.标记清理算法:标记-整理-清除。 所有存活的对象向一边移动,清掉边界外的。
4.分代收集算法:新生代:复制算法 老年代 :标记-清除或标记-整理
3.几种垃圾回收器:
单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程;
串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
在这里插入图片描述
1.Serial: 最基本的,client模式下默认的新生代收集器
2.ParNew:Serial的多线程版,server模式首选的新生代收集器。只有前面这两个可以和CMS合用
3.Parallel Scavenge:新生代;复制算法。关注吞吐量。
4.Serial Old: Serial老年代版本:标记-整理,一般用于client,1,与Parallel Scavenge搭配 作为CMS的后备预案。
5.Parallel Old :Parallel Scavenge的老年代版本。 标记-整理
6.CMS:标记-清除。获取最短回收时间为目标。比较可以的。特点:对cpu资源敏感,
无法处理浮动垃圾,大量的空间碎片产生,
7.G1:科技前沿。特点:并行和并发,分代收集,空间整合,可预测的停顿,

4.内存分配和回收策略:
Minor GC :年轻代空间(包括 Eden 和 Survivor 区域)回收内存
Major GC:清理永久代。
Full GC :清理整个堆空间—包括年轻代和永久代。条件:
1.老年代空间不足。
2.永生代(jkd7)或者元数据空间(jkd8)不足。
3.System.gc()方法调用。
4.CMS GC时出现promotion failed和concurrent mode failure
5.YoungGC时晋升老年代的内存平均值大于老年代剩余空间
6.有连续的大对象需要分配
Stop-The-World:Java中一种全局暂停的现象。全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互
在垃圾回收过程中经常涉及到对对象的挪动(比如上文提到的对象在Survivor 0和Survivor 1之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。
内存分配策略:
1.对象优先在Eden分配
2.大对象(很长的字符串和数组)直接进入老年代
3.长期存活的对象(对象年龄计数器)将进入老年代
4.动态对象年龄判断
5.空间担保分配

第七章:虚拟机类加载机制
1.类加载的过程。
类在内存中:加载验证准备,解析,初始化,使用,卸载。 加粗的这几个顺序确定。解析穿插其中。
没有明确加载开始的时间,但是初始化的时间是确定的,有且仅仅有必须

  1. new 实例化,读取或设置类的静态字段,调用静态方法。
  2. java.lang.reflect。反射调用时。
  3. 初始化一个类时,先触发父类的初始化。
  4. Jvm启动时,包含main的主类,先初始化。
  5. 动态语言那个啥句柄对应的类没有初始化,
    下面三种不会:
  6. 通过子类引用父类的静态字段,不会导致子类初始化。
    System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
    2.通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
    SuperClass[] sca = new SuperClass[10];
    3.常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
    System.out.println(ConstClass.HELLOWORLD);

1.加载:1>通过类的完全限定名称获取定义该类的二进制字节流。2>将该字节流表示的静态存储结构转化为方法区运行时的数据结构,3>生成class对象,作为方法区这个类的各种数据的访问入口。
二进制流获取方式:
1. 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
2. 从网络中获取,最典型的应用是 Applet。
3. 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
4. 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。

  1. 验证:文件格式验证,元数据验证(对字节码描述的信息进行语义分析),字节码验证(通过数据流和控制流分析,确保程序语义是合法、符合逻辑的,将对类的方法体进行校验分析),符号引用验证
  2. 准备:类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在 Java 堆中。初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。public static int value = 123;如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。public static final int value = 123;
  3. 解析:将常量池的符号引用替换为直接引用的过程。解析动作主要针对:类和接口,字段,类方法,接口方法。
  4. 初始化:真正开始执行类中的java代码。虚拟机执行类构造器 () 方法的过程。
    () 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法。但接口与类不同的是,执行接口的 () 方法不需要先执行父接口的 () 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 () 方法。
    虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都会阻塞等待,直到活动线程执行 () 方法完毕。如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。

2.类加载器
1. 需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性。
分类:
1.启动类加载器:使用 C++ 实现,是虚拟机自身的一部分;\bin下jvm识别的
2.扩展类加载器:加载\bin\ext目录下,或者java.ext.dirs系统变量路径下的类库
3.应用程序类加载器:加载用户类路径(classpath)上指定的类库

双亲委派模型:双亲委派模型来组织三种类加载器之间的关系,使得 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,从而使得基础类得到统一。
工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
还有一个逃逸分析:分析对象的动态作用域,如果别的方法或线程无法通过任何任何途径访问到这个对象。可以对这个对象进行优化。
三个特点:栈上分配,同步消除,标量替换。

第十二章,java内存模型和线程。
1.Java内存模型:定义程序中各个变量的访问规则
1.主内存(对应堆中的对象实例数据部分):所有的变量都存储在主内存中
2.工作内存(虚拟机栈部分,可以与前面将的处理器的高速缓存类比),线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成
2.内存间的交互操作:
lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
操作满足规则:
read和load store和write 必须顺序执行。不一定连续执行
• 不允许read和load、store和write操作之一单独出现
• 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
• 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
• 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
• 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
• 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
• 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
• 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
Volatile的特殊性:稍弱的同步机制,可见性,禁止指令重排序。没有保证原子性。
Long和double的特殊规则:8字节。读写允许划分两个步骤。很少出现。
原子性(锁,原子类):一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。
可见性(volatile,synchronized,final):当一个线程修改了共享变量后,其他线程能够立即得知这个修改。
有序性(volatile,synchronized):如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的。
先行先发生原则;程序次序规则,管道锁定规则,volatile变量规则,线程启动规则,线程终止规则,线程中断规则,对象终结规则,传递性
状态转化:
在这里插入图片描述线程共包括以下5种状态。

  1. 新建状态(New) : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
  2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
  3. 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
  4. 阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    (01) 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。
    (02) 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
    (03) 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  5. 死亡状态(Dead) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

第十三章:线程安全和锁优化
线程安全的等级:
1. 不可变:final 修饰 string等不可变类。绝对线程安全。
2. 绝对线程安全:基本没有绝对的。
3. 相对线程安全:这就是我们一般说的线程安全。如:vector等。
4. 线程兼容;arraylist等,
5. 线程对立:无法在多线程中使用,
线程安全的实现方法:
1. 互斥同步(阻塞式同步)

  1. 同步指的是:多个线程并发访问共享数据时,保证共享数据在同一时刻只能被一个线程使用。
  2. 互斥指的是:同步的手段。如:临界区、互斥量和信号量。
  3. 最基本的同步互斥手段:synchronized关键字。
    a) 原理:synchronized关键字经过编译后,会在同步代码块前后分别形成monitorenter和monitorexit这两个字节码指令。
    b) 在执行monitorenter指令时,首先要尝试获取对象锁。若这个对象没被设定锁,或已经拥有了这个对象锁,把锁的计数器+1。
    c) 相应的执行monitorexit指令时,锁计数器-1,当为0时释放锁。
    d) 若获得对象失败则阻塞当前线程。
  4. 另一种手段:ReentrantLock来实现
    reentrantLock增加的新功能:
    a) 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待。
    b) 公平锁:可以讲多个线程在等待同一个锁时,必须按照申请锁的时间顺序依次获得锁。
    c) 锁绑定多个条件:一个ReentranLock对象可以同时绑定多个Condition对象。
  5. 两种方式的比较:
    a) 在JDK1.6之前在多线程环境中synchronized的吞吐量下降的严重,但ReentranLock性能几乎不变。
    b) 在JDK1.6之后二者性能基本上持平。
    c) 应该优先考虑使用synchronized来同步。
    2. 非阻塞同步
    1,互斥同步属于一种悲观的并发策略。即总认为不去做同步措施就一定会出现问题,故无论共享数据是否真的会出现竞争,都要加锁。
    2,非线程阻塞:是基于冲突检测的乐观并发控制策略。即先操作,如果没有其它线程征用共享数据,则成功。如果共享数据有征用,采取补偿措施。(不断的重试,直到成功)
    3. 无同步方案
    如果方法中不涉及共享数据,那它自然无须任何同步措施去保证正确性。
    1)可重入代码
    可重入的代码都是线程安全的,但并非所有的线程安全的代码都是可重入的。
    2)线程本地保存

锁优化:
锁优化技术:适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁。
1.适应性自旋
1.自旋锁:当两个或两个以上的线程同时并行执行,让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有所的线程是否能很快释放锁。
2.缺点:自旋的线程只会白白消耗处理器资源,带来性能的浪费。故需要使等待时间有一定的限度
3.改进:等待时间要自适应。等待时间随着程序运行和性能监控信息的不断完善。

2.锁消除
1.锁消除:虚拟机及时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
2.判断依据:源于逃逸分析的数据支持。若堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以吧它们当作栈上的数据看待。
3.粗粒化
1.上面出现的问题:如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
2.措施:如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,【将会把加锁同步的范围扩展到整个操作序列外部】。
4.轻量级锁
1.加锁过程:
1)代码进入到同步代码块的时候,如果此同步对象没有被锁定,将会在当前线程的栈帧中建立一个名为锁记录(Lock Record)空间,用来存储对象目前的头数据。
2)将对象的头记录更新为指向Lock Record的指针。
a)更新成功:那么这个线程就拥有了该对象的锁
b)失败:检查对象的对象头是否指向当前线程的栈帧。
a)是:说明当前线程已经拥有这个对象的锁,直接进入同步块中执行。
b)否:说明这个锁对象已经被其他线程抢占。
3)如果两条以上线程征用同一个锁,那轻量级锁失效。
2.解锁过程:
1)若对象的对象头仍然指向现成的锁记录,则把对象当前的Mark Word和线程中副本替换回来。
A)成功:整个同步过程完成
b)失败:说明其他线程尝试获取该锁,那就要释放锁的同时唤醒被挂起的线程。
5.偏向锁
1.与轻量级锁的区别:
轻量级锁:是在无竞争情况下,消除同步使用的互斥量。
偏向锁:在无竞争情况下把整个同步都消除掉。
2.加锁过程:
1)当锁对象第一次被线程获取的时候,虚拟机包对象头中的标记为设为“01”。
2)获取到这个锁的线程的ID记录在对象的Mark Word之中。
a)成功:持有偏向锁的线程以后每次进入这个锁相关的同步块时,可不再进行任何同步操作。
3.解锁过程:
1)当有另外一个线程去尝试获取这个锁时,偏向模式宣告结束。
2)根据锁对象目前是否处于被锁定的状态,撤销偏向后恢复到“01”或“00”。
4.优点:提高带有同步但竞争的程序性能。

猜你喜欢

转载自blog.csdn.net/qq_41703795/article/details/89280905
今日推荐