Java核心基础(1)——JVM
Java为何能跨平台
.java(代码) ——> .class(字节码) ——> 运行代码
javac java
javap -c反编译.class字节码至指令码(可查找指令码手册看指令含义)
由于JVM,不同平台的JVM生成不同系统的字节码与机器码
代码运行在不同的JVM上,生成不同系统机器码,实现了跨平台,JVM从软件层面屏蔽了不同操作系统在底层硬件和指令上的区别。
JVM内部结构
结构图

线程私有:栈、程序计数器、本地方法栈
线程共享:堆、方法区
栈
栈(stack)
可叫线程栈,每一个线程运行时,JVM会分配一块独立的内存空间,其中包括栈空间,栈中存放线程运行中的局部变量等。
栈帧
JVM为每一个方法分配一块独立内存空间,这个空间即是这个方法对应的栈帧,一个方法对应一个栈帧。
JVM中的栈和数据结构栈的区别
JVM使用类似数据结构中的栈存储组成栈空间,达到先进后出。
先运行的方法,会先创建栈帧,后运行的方法后创建栈帧;但是后运行的方法会先pop,先运行结束,栈帧出栈便销毁,后进先出。
栈帧内部组成
局部变量表,动态链接,操作数栈,方法出口
局部变量表
存储线程中的局部变量。
操作数栈
存放线程运行中的操作数,使用字节码操作,并与局部表量表交互,是一个临时内存空间。
方法出口
存储方法的回调位置,JVM在程序进入方法之前,就将此方法的出口位置存储在此方法栈帧的方法出口内存空间中。
动态链接
每个栈帧都保存了一个可以指向当前方法所在类的运行时常量池, 目的是: 当前方法中如果需要调用其他方法的时候, 能够从运行时常量池中找到对应的符号引用, 然后将符号引用转换为直接引用,然后就能直接调用对应方法, 这就是动态链接。
局部表量表与操作数栈的交互
如何查看与阅读字节码?
首先javac XXX.java —> XXX.class
然后javap -c XXX.class ——> 字节码(指令码)可打开阅读
进行举例:

程序计数器
为线程每一步程序计数,方便线程被抢占后,回来继续执行程序,每一个线程有一个程序计数器。
栈和堆的联系
栈中局部变量如果是对象类型,其值为堆中对应对象的内存地址,对象创建在堆上,栈中局部变量仅存储其内存地址。
举例:
Math math = new Math();

方法区(元空间)
jdk8后元空间不在放在JVM运行时数据区中,而是存放在物理内存里。
用于存储常量(常量池)、静态变量、类信息(由类装载子系统将类信息加载到方法区java a.class)
static User user = new User();
静态变量存储在方法区,指向存储在堆里的对象,方法区存储对象的地址信息。

本地方法栈
native修饰的方法叫本地方法,由c语言构成,当线程运行到本地方法时,JVM为线程分配本地方法栈。
存放本地方法执行中的内存数据。
字节码执行引擎
方法的实现、代码的运行都是字节码执行引擎负责执行字节码。
JVM线程管理内存图

堆构造
堆由年轻代、老年代组成。
年轻代由Eden、Survivor组成,Survivor由S0、S1组成。

年轻代占总堆内存的1/3,老年代占总堆内存的2/3。
年轻代中,Eden占年轻代内存的8/10,S0、S1分别占年轻代内存的1/10。
内存管理
如何找到垃圾对象
引用计数法
为每一个对象添加一个引用计数,记录被引用次数,每减少一次引用,则计数减1,当计数为0时,表示无引用,为垃圾对象。
无法解决循环引用,导致内存泄漏
可达性分析算法
使用GC Roots进行搜索,GC Roots可以是栈的本地局部变量、常量池中常量、方法区的静态变量、本地方法栈中JNI指针。
GC可达性分析
GC Roots根节点:可以是栈的本地局部变量、常量池中常量、方法区的静态变量、本地方法栈的变量等。
定义:将GC Roots作为起点,以这些节点开始搜索引用的对象,直到找不到下一个引用,将之前这一条链路上的对象都标记为非垃圾变量,其余未标记对象都是垃圾对象。

常见的垃圾回收算法
标记——清除算法
为每一个对象做标记,标记需要回收的对象,之后回收对象所占用空间。
容易产生垃圾碎片,导致后续空间不够,无法存入大对象以使用连续空间。
复制算法
将内存分为大小两块,每次使用一块,当这一块用完了,将上面的存活的对象赋值到空着的另一半,再将前一块全部清空,不会出现碎片。
会浪费空间!
标记——压缩算法
与标记——清除算法相同,先标记,再将存活对象向一段移动,然后清理边界外内存,即标记后整理再清理。
但是效率低!
分代算法
利用对空间,按照分代年龄进行清理。
minor GC与full GC工作原理(分代算法)

JVM调优最终目的
减少full GC次数与full GC耗时。
进入老年代条件
对象达到阈值分代年龄(默认15)
为何15?对象头中用4bit表示分代年龄,最大1111=15。
可使用-XX:MaxTenuringThreshold = x调整阈值。
动态年龄判断
当Survior中年龄1 + 年龄2 + ... + 年龄n的多个对象内存相加超过Survivor总内存的50%,则大于年龄n的对象放入老年代,相当于提前将年龄大的对象放入老年代。

大对象直接入老年代
内存大于阈值的大对象,如数组、字符串直接入老年代,可通过-XX:PretenureSizeThreshold = x设置阈值大小。
这样做避免大对象在Survivor中来回复制,提高回收效率。
STW(Stop the world)
在GC过程中,会先收集线程1 GC Root中的垃圾对象,之后再去收集线程2,若线程1突然结束,则GC之前收集的非垃圾对象也会变为垃圾对象,导致收集错误。
定义:将用户线程停止,直到GC工作完成,再继续线程。
会导致用户体验不佳、卡顿,但能保证GC正常工作。
常见垃圾分类器
Serial(年轻代)
利用STW,单线程穿行回收,会造成卡顿。
使用复制算法。
Parallel Scavenge(PS)(年轻代)
多线程并行收集。
复制算法清理。
ParNew(年轻代)
PS升级版,可以和CMS合作并行回收。
Serial Old(老年代)
Serial老年代版。
使用标记——压缩算法。
Parallel Old(老年代)
PS老年代版。
使用复制算法。
CMS(ConcurrentMarkSweep)(老年代)
并发收集,效率高,算法复杂。
使用标记——清除算法,降低STW时间。
G1分类器
不再区分老年代和年轻代。
基于标记——压缩算法,空间连续。
JDK8默认垃圾回收器:PS + Parallel Old
永久代(jdk1.7)与元空间(jdk1.8)的区别
①永久代必须指定大小限制;元空间可以设置,也可以不限,受限于物理内存大小。
②字符串常量:1.7存放在永久代,1.8存在堆。
③类文件常量池存放在元空间。
④方法区是一个逻辑概念,1.7实际对应永久代,1.8对应元空间。
四种引用
强引用
必须有,即使抛出异常,GC也不会回收。
软引用
可有可无,如果内存不足,GC收集软引用对象,加速收集,避免溢出。
弱引用
可有可无,不管内存是否充足,GC扫描到弱引用都会收集。
虚引用
用来跟踪对象被回收的活动,GC找到虚引用,会被对象放入引用队列,对象再引用队列中被回收前可进行操作。
类加载机制
通过反射实现类的加载。
类加载过程
加载 ————> 连接 ————> 初始化
(验证—>准备—>解析)
其中,加载:①通过全限定类名获取此类二进制字节流
②将字节流的静态存储结构转换为方法区的运行时数据结构
③内存中生成该类的class对象,作为访问入口
连接:包括验证、准备、解析三个过程
JVM内置ClassLoader:①BootstrapClassLoader,最顶层,C++实现,加载 /lib 下jar包
②ExtensionClassLoader,java实现,加载 java_home/lib/ext 目录下包和类
③AppClassLoader,加载当前应用 classpath下类和包
双亲委派机制
在类加载中,会首先判断当前类是否被加载,加载时先委派父类加载器处理,若父类加载器无法出理再自己加载,可以避免类的重复加载。
如何避免双亲加载?可以创建自定义加载器,继承java.lang.ClassLoader重写其中方法loadClass()。

浙公网安备 33010602011771号