《深入理解Java虚拟机》笔记

    xiaoxiao2021-03-25  144

    一、内存管理

    1、运行时的内存区域

    线程私有:虚拟机栈、本地方法栈、程序计数器 线程共享:堆、方法区

    2、各个内存区域可能抛出的异常

    栈 1、当单线程时,栈的深度太大,会发生StackOverflowError,比如无穷的递归调用。 2、当多线程时,若不停地创建线程,则会导致OutOfMemoryError,因为除去堆和方法区之外,剩下的栈总空间是有限的,不停创建线程则会不停申请栈空间,最终会导致内存溢出。堆 当不停地创建(new)对象时,会导致OutOfMemoryError方法区 运行时产生大量的类,去填满方法区,比如用CGLib去无穷生成类。直接内存 使用Unsafe分配本机内存时,可能导致OutOfMemoryError。

    3、各个内存区域容量设置的参数

    -Xss2M:设置栈的容量为2M-Xms10M:设置堆的初始容量为10M-Xmx10M:设置对的最大容量为10M-XX:PermSize=10M:设置方法区的初始容量为10M-XX:MaxPermSize=10M:设置方法区的最大容量为10M-XX:MaxDirectMemorySize=10M:设置直接内存的最大容量为10M

    4、对象的创建

    如何在堆中分配内存 根据内存是否规整,即GC收集器是否带有压缩整理功能,分为指针碰撞和空闲列表两种。如何处理内存分配冲突 1、CAS+失败重试;2、TLAB,即本地线程分配缓冲。对象在内存中的布局 对象头(哈希码、GC分代年龄、所状态标志等)、实例数据、对象填充如何访问对象 1、通过句柄(栈上的指针指向句柄,句柄中分别有指向对象的指针,和指向类信息的指针) 2、直接指针(栈上的指针直接指向堆中的对象,对象中头部有一个类型指针,指向类型信息)

    5、对象存活判定

    即如何判断一个对象所占用的内存是否该回收? 有两种方法:1、引用计数法;2、可达性分析法。 - #####引用计数法 该方法容易出现循环引用的问题,JVM并未采用。 - #####可达性分析法 判断是否能从GCRoots中找到一条到达该对象的路径。 GCRoots包括:栈中变量引用的对象、方法区中静态属性(static)引用的对象、方法区中常量(final)引用的对象。

    6、垃圾回收

    引用的种类 1、强引用:通常new出来的对象的引用都是强引用。 2、软引用(SoftReference):如果某次回收完之后,还是可能发生内存溢出,则进行第二次回收,在第二次回收时会回收软引用,若这次回收后仍是内存不够,这时候才发生内存溢出。 3、弱引用(WeakReference):其指向的对象只能生存到下一次垃圾收集之前,无论当前内存是否足够,都会回收弱引用指向的对象。 4、虚引用(PhantomReference):设置虚引用的目的可能是为了,在该对象被回收前能够得到一个系统通知。finalize方法 若某类覆盖了该方法,则其对象再被回收前会调用此方法(仅限于第一次回收该对象时)。方法区的回收 废弃的常量:当前系统中没有一个对象引用此常量。 无用的类:堆中不存在该类的任何实例,该类的类加载器已被回收,该类的Class对象没有在任何位置被引用。垃圾收集算法 1、JVM整体上是采用“分代收集算法”。 根据对象的存活周期的不同将Java堆分为新生代和老年代。 新生代又分为Eden区,Survivor From区,Survivor To区,其大小比例默认为8:1:1。 Java的方法区被定义为永久代 2、对于新生代,一般采用“复制算法”。因为新生代的存活时间相对较短,复制的时候不会复制太多对象,所以整体效率不至于太低。 当从Eden和Survivor From区向Survivor To区复制时,若Suvivor To区的空间不够,则需要依赖老年代进行“分配担保”。 3、对于老年代,一般采用“标记-清除”(容易产生内存碎片)或“标记-整理”算法。因为老年代的对象存活率较高,若仍是采用复制操作,则需要复制的对象太多,效率会很低。 4、Stop The World 在判断对象是否应该被回收时,是通过GCRoots来判断的。 当枚举GCRoots时,不可以出现在分析过程中,对象的引用关系还在不断发生变化,所以这时候必须停顿所有线程,这种现象称为“Stop The World”。 5、安全点和安全区域 安全区域是指:在这段代码片段内,引用关系不会发生变化。 6、内存分配和回收策略 对象优先在Eden区分配,若内存不够则进行一次Minor GC,将Eden区的活跃对象复制到Survivor To区。 若Survivor To区大小足够,则将其中的存活对象的GC年龄加1,并判断是否应该晋升到老年代区。 若Survivor To区大小不够,则进行分配担保,将对象复制到老年代。 若此时老年代内存大小不够,则进行一次Full GC。垃圾收集器 Serial:新生代收集器,单线程收集器,采用复制算法。ParNew:新生代收集器,多线程收集器,采用复制算法。Parallel Scavenge:新生代收集器,多线程收集器,侧重于提高程序运行的吞吐量。Serial Old:老年代收集器,单线程收集器,采用标记-整理算法。Parallel Old:老年代收集器,多线程收集器,采用标记-整理算法。 由于多线程的老年代收集器可以充分利用服务器多CPU的处理能力,所以常用Parallel Scavenge/Parallel Old组合,亦提高了吞吐量。CMS(Concurrent Mark Sweep) 老年代收集器 初始标记–>并发标记–>重新标记–>并发清除G1收集器 初始标记–>并发标记–>最终标记–>筛选回收

    二、执行子系统

    (一)、类文件结构

    1、平台无关性和语言无关性

    平台无关性:通过Java虚拟机,Java代码可以运行在不同的操作系统上。 语言无关性:不同的语言通过编译成字节码,均可以运行在JVM上。

    2、Class文件结构

    (1)、 两种数据类型:无符号数,表。 (2)、魔数:“CAFEBABE” (3)、次版本号,主版本号 (4)、常量池:常量个数,常量项(类型tag+内容)。包括字面量和符号引用。 (5)、访问标志:是否 public,final,super,interface,abstract,synthetic,annotation,enum (6)、类索引,父类索引,接口索引 (7)、字段表 (8)、方法表 (9)、属性表

    3、字节码指令

    (1)、字节码中的数据类型:byte、short、int、long、float、double、char、reference (2)、加载和存储指令:将数据加载到操作数栈,将数据存储到局部变量表 (3)、运算指令:加减乘除、取余、取反、位移、位运算(与、或、异或)、局部变量自增、比较 (4)、类型转换 (5)、对象创建和访问 (6)、操作数栈相关指令 (7)、控制转移指令:条件分支等 (8)、方法调用和返回: invokevirtual(实例方法) invokeinterface(接口方法) invokespecial(构造方法,私有方法,父类方法) invokestatic(静态方法) (9)、同步指令:monitorenter、monitorexit。通过管程实现,用于支持synchronized关键字。

    (二)、类加载机制

    1、类在什么时候会加载

    (1)、new(使用new实例化对象的时候),getstatic(读取一个类的静态字段,不包括final的),putstatic(设置一个类的静态字段,不包括final的),invokestatic(调用一个类的静态方法时)。 (2)、通过java.lang.reflect包的方法,对类进行反射调用的时候,若类未初始化,则会对其进行初始化。 (3)、当初始化一个类时,若其父类还未初始化,则先初始化其父类。 (4)、虚拟机启动时,会先初始化包含main方法的那个类,即主类。 (5)、被动引用不会导致类加载,比如:通过子类引用父类的静态字段,不会导致子类初始化;定义某类的数组,则该类不会初始化;引用某类的静态常量,则该类不会初始化。

    2、类加载的过程

    (1)、加载 通过类的全限定类名获取二进制流;将字节流转换为方法区内的运行时数据结构;在内存中生成一个代表该类的Class对象,作为方法区里这个类的各种访问数据的入口。 (2)、验证 校验Class文件中的信息是否复合JVM的要求。 文件格式验证(基于二进制字节流,校验主次版本号是否支持等); 元数据验证(是否继承的final类,是否实现了接口的所有方法等); 字节码验证(通过数据流和控制流分析,验证程序语义,比如只能父类引用指向子类对象,子类引用不能指向父类对象); 符号引用验证(当JVM将符号引用转换为直接引用时,会检查是否能根据名称找到相应的类,方法,字段等); 验证阶段其实不是必须的,如果该字节码被反复验证过,其实可以关闭验证。 (3)、准备 为静态变量设置初始值(int为0,reference为null等); 为常量设置初始值。 (4)、解析 将常量池里的符号引用解析为直接引用(即指向内存中某个区域的指针)。 解析的符号引用有:类或接口、字段、方法、接口方法等。 (5)、初始化 若父类没有加载,则先加载父类; 然后为静态变量设置初始值,执行静态代码块等;

    3、类加载器

    (1)、每个类,都要由“加载它的类加载器”和“这个类本身”一块确定该类在虚拟机里的唯一性。 (2)、类加载器种类 启动类加载器:加载/lib目录中的类; 扩展类加载器:加载/lib/ext目录中的类; 应用程序类加载器:加载CLASSPATH中的类,如果应用程序没有自定义过自己的类加载器,这个便是程序中默认的类加载器。 (3)、双亲委派模型

    当一个类加载器加载类的时候,首先不会亲自加载这个类,而是会把这个请求委派给父加载器去加载,如此递归下去,所有的请求最终会传到顶层的引启动类加载器。只有当父加载器无法加载该类时,才会让子类去尝试加载。 这样做可以保证,同一个类在虚拟机中不会被不同的类加载器加载很多次。

    (三)、字节码执行引擎

    1、运行时栈帧结构

    程序执行时,内存中的栈,里面是一个一个的栈帧。每一个方法的调用及其执行,都对应着一个栈帧。

    2、方法调用

    (1)、方法调用指令 invokestatic:调用静态方法 invokespecial:调用实例构造器、私有方法、父类方法 invokesvirtual:调用所有的虚方法(非static非final方法) invokeinterface:调用接口方法 (2)、分派 静态分派:对应于方法参数上的重载。编译器在重载时,是通过参数的静态类型,而不是实际类型作为判断依据的。 动态分派:对应于多态。当invokespecial指令执行时,第一步就是在运行期确定接受者的实际类型。

    三、代码优化

    编译期优化

    (1)、编译过程 词法分析,语法分析–> 填充符号表–> 处理注解–> 语义分析(标注检查、数据及控制流分析、解语法糖)–> 字节码生成。 (2)、Java的语法糖 泛型与类型擦除 自动装箱、拆箱 foreach循环 变长参数 注意:1、JVM在字节码里,用Signature属性存储了方法在字节码层面的方法签名,通过这项元数据,可以通过反射获取类的泛型信息。2、包装类的“==”运算,在不遇到算数运算时不会自动拆箱,这时候比较的是引用是否相等。

    运行期优化

    (1)JIT编译器(Just In Time) Java程序最初是通过解释执行的,当虚拟机发现“某个方法或某段代码块 ”运行特别频繁时,会把这些代码认定为“热点代码(Hot Spot Code)”。为了提高热点代码的执行效率,在运行时,会把这些代码编译为与本地平台相关的机器码,并进行各种优化。完成这个任务的编译器叫做即时编译器。 (2)、解释器与编译器并存 1、编译对象 被多次调用的方法:将整个方法作为编译对象。 被多次执行的循环体:依然将整个方法作为编译对象,进行栈上替换(On Stack Replacement),即替换栈帧。 2、热点探测的方式 基于采样:周期性的检查栈顶,若某方法经常出现在栈顶,则其是热点方法。 基于计数器:统计方法执行次数,到了一定的阈值,则其是热点方法。 3、两种计数器 JVM的热点探测是基于计数器的,有两种计数器:方法调用计数器和回边计数器。 方法调用计数器:即统计方法执行次数。 回边计数器:循环体中代码执行的次数。“回边”,即在字节码中遇到控制流向后跳转的指令。 4、编译优化技术 公共子表达式消除、数组边界检查消除、方法内联、逃逸分析等。

    四、并发

    1、JVM内存模型和线程

    (1)、JVM内存模型

    1、JVM内存模型主要是定义程序中变量的访问规则,即在虚拟机中将变量存储到内存,和从内存中取出变量这样的细节。这些变量指的是实例字段、静态字段等,不包括局部变量(因为局部变量是线程私有的,不被共享,不存在竞争问题)。 2、工作内存中保存了该线程使用到的变量的在主存中的拷贝。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行,不能直接读写主存中的变量。 3、Java内存模型中的工作内存只是个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其它的硬件和编译器优化。 4、内存间的交互操作 lock、unlock read、load、use、assign、store、write 5、volatile变量 6、原子性、可见性、有序性 7、happens-before原则 (2)、线程 1、线程的实现方式 三种方式:1:1、1:N、N:M。 Java是通过将线程映射到操作系统的线程上去实现的。 2、线程的调度方式 两种方式:协同式线程调度、抢占式线程调度。 Java中使用的是抢占式线程调度。 3、线程的状态转换

    1、线程安全和锁优化

    线程安全 (1)、共享数据的种类: 1、不可变:不可变对象是值“对象中带有状态的变量都声明为final”。 2、绝对线程安全 3、相对线程安全:Vector、Hashtable等 4、线程兼容:ArrayList、HashMap 5、线程对立 (2)、线程安全的实现方法 1、互斥同步(Mutual Exclution & Synchronization) 同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个(一些)线程使用。 互斥是实现同步的一种手段,临界区、互斥量、信号量都是互斥的实现方式。 互斥是因,同步是果;互斥是方法,同步是目的。 互斥同步又称为“阻塞同步”,属于一种悲观的并发策略。 2、非阻塞同步 非阻塞同步是一种基于“冲突检测”的乐观并发策略; 即先进行操作,如果“没有其它线程争用共享数据”,那操作就成功了。如果共享数据有争用,产生了冲突,那就再采取其它的补偿措施(比如不断重试,直到成功为止)。 因为这种策略不需要将线程挂起,所以成为非阻塞同步。 “操作和冲突检测”这两个步骤,需要具备原子性,这可以通过硬件的CAS来实现。 CAS指令需要三个操作数:内存位置,旧的预期值,新值。当且仅当“内存位置的值”符合“旧的预期值”时,处理器用“新值”更新“内存位置的值”,否则就不更新。最终无论是否更新,均会返回“旧值”。 3、不须同步 可重入代码:不依赖堆上的数据和共用的系统资源,用到的状态量都由参数中传入,不调用非可重入的方法。如果一个方法,它的返回结果可预测,相同的输入,均能返回相同的输出,它便是可重入的。 线程本地存储:即ThreadLocal,以当前线程哈希码为键,某变量位置的一个键值对。锁优化 1、自旋锁 因为当线程阻塞,或从阻塞中恢复时,挂起线程和恢复线程的操作都需要转入到内核态去完成,这给系统的性能带来了很大压力。 如果某个锁被占用的时间很短,这时候可以让“后面那个请求锁的线程”稍微等一下,不放弃CPU的执行时间,执行一个忙循环,直到获得锁。 2、锁消除 将一些代码上进行了同步,但实际不会存在共享数据竞争的锁进行消除。 3、锁粗化 如果对一个对象反复加锁和解锁,甚至这个对象在循环体内,则可以把锁加到循环体外,这样可以消除反复加解锁所带来的损耗。 4、轻量级锁 5、偏向锁
    转载请注明原文地址: https://ju.6miu.com/read-3094.html

    最新回复(0)