JVM的组成及其相关知识
NIO
JDK1.4引进NIO,可以操作堆外内存(系统内存),提高了运行效率。
线程安全的两个处理方式
1.线程同步(加锁)
2.使用threadLocal给每一个线程分配一个独立的内存缓冲区域
对象访问的定位
如图:
对象在内存中的存储形态
Mark Word具体信息
详解地址:https://blog.csdn.net/lkforce/article/details/81128115
对象获取锁的流程
注:GC次数达到晋升阈值时候会晋升入老年代,晋升阈值默认为15。
对象定位方式有两种
句柄访问方式
句柄访问又称为间接访问,他通过访问堆里面的句柄池获得实例池中的对象实例数据和方法区中的对象类型数据。
直接指针访问方式
直接引用则是不经过句柄池,直接访问实例池里面的实例数据对象,同时通过实例数据对象里面存放的实例类型对象地址去访问实例类型对象。
jvm的垃圾回收器
对象活性判断
程序计数法:当一个对象被引用时计数器的数值+1,这个对象失去引用时计数器数值-1,当计数器的数值为0的时候认为该对象失去活性,将被回收。由于两个对象之间可以互相依赖会造成闭环所以这种方式不被jvm所使用。
可达性分析算法:通过判定对象是否和GC ROOTS之间有直接关联或者间接关联来区分对象是否存活。
常见的GC ROOTS:
虚拟机栈
方法区中的常量池引用的对象
方法区类型对象实例引用的对象(类的对象属性)
本地方法栈中引用的对象
垃圾回收算法
标记清除法:
将存活的对象标记起来(根节点可达性算法计算),标记完成后清除未标记的对象。标记请吃饭的缺点是多次标记清除之后会出现大量的不连续内存空间。当在对一个大对象分配空间时,寻址会花费很多时间,搞不好还会导致再次垃圾回收。
复制算法:
复制算法的本质是将新生区域划分为两块区域,始终保持其中一块空白,另一块填充满了时进行GC,将依然存活的对象复制到空白区域同时清除原区域。这种算法的好处是效率高,不会存在不连续空间,但是Young GC发生的概率是GC中最高的,新生代区域中有98%的对象都会在GC时候被回收,所以复制算法会导致大量的内存空间浪费。
但是现在大量的youngGC回收算法都是采用的复制算法,因为现在的新生代被分为两个区域:Eden区 + Survivor区,他们的大小比例为 8:1:1 。survivor区域又分为from区和to区。发生垃圾回收时候回将Eden去和from区里面存活的对象复制到to区中,然后清除eden和from区。下次GC时,上次的to区就会被当做from区域。 只有10%的区域会被浪费掉,利用率90%。
标记整理算法:
对于老年代,回收的垃圾较少时,如果采用复制算法,则效率较低。标记整理算法的标记操作和“标记-清除”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一端移动,并更新引用其对象的指针。
分代收集算法
针对不同的年代进行不同算法的垃圾回收,针对新生代选择复制算法,对老年代选择标记整理算法。
垃圾回收器
Serial收集器
单线程垃圾收集器、最基本、发展最悠久。它的单线程的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。偶尔用在桌面应用中。
ParNew收集器
可多线程收集垃圾,收集新生代,使用复制回收算法
Parallel Scavenge收集器
注:Serial、ParNew、Parallel收集器都是作用于新生代同时在工作的时候都是STOP THE WORLD,进行垃圾回收时只有垃圾回收线程在运行。新生代的垃圾回收算法除Parallel Scavenge:复制算法!
Serial Old收集器
Serial Old垃圾收集器作用于老年代,采用的是‘标记-整理’算法,和Serial一样,单线程操作老年代。操作时依然保持STOP THE WORLD。
Parallel Old收集器
Parallel Old收集器采用的收集算法为:标记-整理算法。但是Parallel Old的作用区域是老年代。同时他也非常关注系统的吞吐量,可以通过参数来打开自适应调节策略。
CMS垃圾回收器
Concurrent Mark Sweep,采用标记-清除算法,用于老年代,常与ParNew协同工作。优点在于并发收集与低停顿。
注:并行是指同一时刻同时做多件事情,而并发是指同一时间间隔内做多件事情
工作过程
- 初始标记
标记老年代中所有的GC Roots对象和年轻代中活着的对象引用到的老年代的对象,时间短;
- 并发标记
从“初始标记”阶段标记的对象开始找出所有存活的对象;
- 重新标记
用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象,时间短;
- 并发清理
清除那些没有标记的对象并且回收空间。
缺点:占用大量的cpu资源、无法处理浮点垃圾、出现Concurrent MarkFailure、空间碎片。
G1收集器
G1(Garbage First)垃圾收集器是当今垃圾回收技术最前沿的成果之一,早在JDK7就已加入JVM的收集器大家庭中,成为HotSpot重点发展的垃圾回收技术。
优势:
并行(多核CPU)与并发;
分代收集(新生代和老年代区分不明显);
空间整合;
限制收集范围,可预测的停顿。
步骤:
初始标记、并发标记、最终标记和筛选回收。
内存分配
内存优先分配在Eden区
大对象直接分配到老年代
多次(16次GC)存活的对象分配到老年代
动态地对对象年龄进行判断
空间分配比例:Eden : survivor1(From) : survivor2(To) = 8 : 1 : 1
常见操作
- 打印日志: -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log
- 查看本地JDK的jvm相关信息: java -XX:+PrintCommandLineFlags -version
- 修改JVM参数: -Xms 最小 -Xmx 最大 -Xmn 新生代 -XX:survivorRatio 设置Eden比例 --XX:MetaSpaceSize 元空间大小
- 修改JVM的垃圾回收器:默认为Parallel -XX:+UseG1GC
- G1收集器 -XX:+UseConcMarkSweepGC
- -XX:MaxTenuringThreshold最大年龄,默认为15
JVM命令大全:https://zhuanlan.zhihu.com/p/141669715
元空间和老年代差不多,他们最大的区别是元空间不使用JVM内存,而是使用本地内存,仅受本地内存大小影响。
逃逸分析
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。
简而言之就是局部变量变成了全局变量。
NIO(网络IO)使用的是堆外内存,堆外内存不会进行垃圾回收
class文件详解
- JAVA代码具有平台无关性:一次编译,永久运行!
- class文件结构图解
类型 | 名称 | 数量 | 说明 |
u4 | magic | 1 | 魔数,用于区分java class文件 |
u2 | minor_version | 1 | java class文件次版本 |
u2 | major_version | 1 | java class文件主版本 |
u2 | constant_poot_count | 1 | 线程池数量值 |
cp_info | constant_pool | pool_count -1 | 线程池 |
u2 | access_flags | 1 | class文件信息,类或接口,是否私有、是否静态等特性 |
u2 | this.class | 1 | 本类的常量池的索引 |
u2 | super.class | 1 | 父类的常量池索引 |
u2 | interface_count | 1 | 接口数量值 |
u2 | interfaces | interface_count | 接口 |
u2 | fields_count | 1 | 属性数量值 |
field_info | fields | field_count | 类属性 |
u2 | method_count | 1 | 方法数量值 |
method_info | methods | method_count | 方法 |
class文件就是通过jvm编译之后的可运行的文件,java class文件是一个八位字节的二进制流,数据项按照顺序存放在class文件中,java class文件之间没有间隔,这也是二进制文件的一个特性。
class文件一共有两种数据结构:无符号和表。类比为xml 和 json。
- 字节码指令
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。操作码的长度为1个字节,因此最大只有256条,是基于栈的指令集架构。
注:java虚拟机在加载对象进入内存并操作时,有可能会发生指令从排序。
字节码指令也分为多种:
- 加载与存储指令
将数据在栈帧中的局部变量表和操作数栈之间来回传输。iload,istore,ipush,wide
- 运算指令
运算指令或者算数指令是将数据从操作栈中加载到寄存器中进行运算后再写入操作数栈
- 类型转换指令
向上转型(宽向转换)时不需要做类型转换指令,因为子类对象继承了父类的所有属性。向下类型转换需要调用类型转换指令,也就是我们代码中常用的强转。
Father f = new Son(); // 向上转型 不需要做类型转换指令
Son s = Son(f); // 向下转型 需要做类型转换指令
- 对象创建与访问指令
创建的对象分为对象和对象数组,他们之间的创建指令不同。这就要牵扯到签名说的对象在内存里面的结构。单例对象的对象头中只有mark word和klass word没有arrays length。
类的加载机制
类的加载过程指的是:jvm将class文件加载到内存中的一系列过程:加载、验证、准备、解析、初始化。通过这一系列过程将class文件加载到内存中成为一个可运行的java class对象。java类的加载是懒加载。
- 加载
- 通过全限命名获取到class文件(二进制字节码)
- 将这个静态的class文件转换可运行的java.lang.class文件
- 在jvm内存中为每一个java.lang.class文件分配内存地址
- 验证:
- 验证class文件的魔数以及主次版本信息,确保是一个合格的java文件
- interfaces、fields、methods等信息验证
- 引用数据验证,并且将间接引用转换为直接引用
- 准备:
- 为类变量分配内存并且设置初始值,内存的分配都在堆中的方法区。
- 准备阶段的赋值都是赋予其默认值而非实际值。如:int x = 0 而非 x = 223
- x = 233 这个动作是在初始化后的操作!
- 解析:
- 常量池中的符号引用修改为直接引用
- 初始化:
- 容器执行init方法,加载java代码
注意:部分情况是先初始化后解析
- new
- reflect
- 启动类
- 初始化子类时其父类未初始化时
类的加载器
- Bootstrap Class loader:普通类的加载器,一般加载/lib下的jar
- Extension Class loader:拓展类加载器,一般夹杂/lib/ext下的jar
- Application Class loader:应用类加载器,一般加载application
双亲委派模型
双亲委派模型定义:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
java.lang.classloader 本身实现了双亲委派模式。
总结
上述就是对jvm的基础版本认识,在后续会陆陆续续的更新部分高级知识。知识来源于对《深入理解java虚拟机第三版》的总结与学习。