JVM
JVM模型
两个子系统+两个组件
Class loader(类加载): 根据给定的全限定名类名(java.lang.Object)来装载class文件到Runtime data area(运行时数据区)中的method area。
Execution engine(执行引擎): 执行classes中的指令;
Native Interface(本地接口): 与native libraries交互,是其它编程语 言交互的接口;
Runtime data area(运行时数据区域): 这就是我们常说的JVM的内存;
JVM工作流程
首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader) 再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(ExecutionEngine),将字节码翻译成底层系统指令,再交由CPU去执行,而这个过程中需要调用其他 语言的本地库接口(Native Interface)来实现整个程序的功能。
运行时数据区
- 程序计数器(Program Counter Register): 当前线程所执行的字节码的行号 指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的 字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个 计数器来完成;
- Java 虚拟机栈(Java Virtual Machine Stacks): ** 用于存储局部变量表、操作数栈、动态链接、方法出口**等信息;
- 本地方法栈(Native Method Stack): 与虚拟机栈的作用是一样的 ,只不过虚 拟机栈是服务 Java方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
- Java 堆(Java Heap):Java 虚拟机中内存大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
- 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变 量、即时编译后的代码等数据;
各部分对线程来说的共享/隔离:
说一下堆栈的区别
-
物理地址
堆的物理地址分配对对象是不连续的,栈的物理地址是连续的;
-
内存分别
堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的; -
存放的内容
堆存放的是对象的实例和数组。因此该区更关注的是数据的存储;
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。 -
程序的可见度
堆对于整个应用程序都是共享、可见的。
栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。
Java内存模型
深拷贝和浅拷贝
- 浅拷贝(shallowCopy): 只是增加了一个指针指向已存在的内存地址 ;
- 深拷贝(deepCopy): 增加了一个指针并且申请了一个新的内存,使这个增加 的指针指向这个新的内存,
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来 的对象也会相应的改变。深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。
内存溢出异常
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说, Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收 掉,自动从内存中清除。但是, 即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露, 尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。
对象的创建
5种创建对象的方法
- 使用new关键字
- 使用Class的newInstance方法
- 使用Constructor类的newInstance方法
- 使用clone方法
- 使用反序列化
其中,前面3种方法都会使用构造函数创建新的对象,后面两种不是使用构造函数;
对象的创建流程
-
检查类加载
对需要创建的对象的类进行检查,查看是否被加载到内存中,如果没有加载则去执行相应的类加载;
-
内存分配
若Java堆中内存是 绝对规整的,使用“指针碰撞“方式分配内存;如果不是规整的,就从空闲列表 中分配,叫做”空闲列表“方式。
-
并发问题处理
一般有两种方法:CAS同步处理,或者本地线程分配缓冲(Thread Local Allocation Buffer, TLAB);
-
对象设置
内存空间初始化操作,接着是做一些必要的对象设置(元信 息、哈希码…),后执行方法;
内存分配方法
-
指针碰撞:
如果Java堆的内存是规整的,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
-
空闲列表:
如果Java堆的内存是不规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录;
选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所 采用的垃圾收集器是否带
有压缩整理功能决定。
处理并发安全问题
对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。有两种解决方法:
-
同步处理
采用 CAS + 失败重试来保障更新操作的 原子性
-
本地线程分配缓冲
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆 中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB) ,哪个线程要分配内存,就在哪个线程的TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁。通过-XX:+/-UserTLAB参数来设定虚拟机是否使 用TLAB。
GC
java的引用类型
-
强引用: 发生 gc 的时候不会被回收。
-
软引用: 有用但不是必须的对象,在发生内存溢出之前会被回收。
-
弱引用: 有用但不是必须的对象,在下一次GC时会被回收。
-
虚引用: 无法通过虚引用获得对象,用PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。
简述Java垃圾回收机制
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一
个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不
足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中进行回收;
GC回收算法
-
标志-清除算法
标记需要回收的对象,然后进行清除回收,但是会产生内存碎片,效率不高;
-
复制算法
将内存空间平均划分为2块,当一块的空间使用完后,将其中存活的对象(不要被回收的对象)转移到另一块内存空间中,然后GC清除这块内存空间的对象实例;这个方法解决了内存碎片的问题,但是存在内存空间利用效率低的问题;
-
标记-整理算法
标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存;
-
分代算法
根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法;
GC收集器
新生代收集器
- Serial:基于复制算法,新生代单线程 收集器,标记和清理都是单线程,优点 是简单高效;
- PraNew:基于复制算法,新生代收并行 集器,实际上是Serial收集器的多线程 版本,在多核CPU环境下有着比Serial更好的表现;
- Parallel Scavenge:基于复制算法,新生代并行收集器,追求高吞吐量,高效利用 CPU;
老年代收集器
- Serial Old:基于标记-整理算法,老年代单线程收集器,Serial收集器的老年 代版本;
- Parallel Old:基于标记-整理算法): 老年代并行收集器,吞吐量优先, Parallel Scavenge收集器的老年代版本;
- CMS:(Concurrent Mark Sweep)基于标记-清除算法,老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最 短GC回收停顿时间;
全局收集器
- G1:(Garbage First)基于标记-整理算法,Java堆并行收集器,G1收集器是 JDK1.7提供的一个新收集器;此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代);
可以发现,新生代都是通过复制算法实现垃圾回收的,而老年代通过标记-整理算法进行垃圾回收,在启动 JVM 的参数加上 "-XX:+UseConcMarkSweepGC"
来指定使用CMS垃圾回收器;
分代垃圾回收算法流程
分代回收器将内存分为新生代和老年代,新生代:老年代内存空间 = 1 : 2 ;新生代使用复制算法,新生代将空间分为3个分区:Eden : To Survivor : From Survivor = 8 : 1 : 1,可以通过-XX:SurvivorRatio=30
调整三者之间的额关系,执行流程如下:
- 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
- 清空 Eden 和 From Survivor 分区;
- From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor;
- 每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是15)时,升级为老生代。大对象也会直接进入老生代;
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记-整理的执行算法,以上这些循环往复就构成了整个分代垃圾回收的整体执行流程;
JVM内存分配
对象主要分配在新生代的 Eden 区, 如果启动了本地线程缓冲,将按照线程优先在TLAB 上分配。少数情况下也会直 接在老年代上分配。总的来说分配规则不是百分百固定的,其细节取决于哪一种 垃圾收集器组合以及虚拟机相关参数有关,内存分配的几种规则:
-
对象优先在 Eden 区分配
多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行 分配时,虚拟机将会发起一次 Minor GC。如果本次 GC后还是没有足够的空 间,则将启用分配担保机制在老年代中分配内存。这里我们提到 Minor GC,如果你仔细观察过 GC 日常;
Minor GC: 指发生在新生代的GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;
Major GC: 指发生在老年代的GC,出现了 Major GC 通常会伴随至少一次 Minor GC,Major GC 的速度通常会比 Minor GC 慢 10 倍以上;
Full GC: Full GC 是清理整个堆空间—包括年轻代和永久代
-
大对象直接进入老年代
所谓大对象是指需要大量连续内存空间的对象,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。
-
长期存活对象将进入老年代
如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被 移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬 过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升 到老年代;
类加载器
什么是类加载
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初 始化,最终形成可以被虚拟机直接使用的java类型;
类加载器
启动类加载器-Bootstrap ClassLoader: 用来加载java核心类库,无法被 java程序直接引用;
扩展类加载器-extensions class loader: 用来加载 Java 的扩展库。 Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类;
系统类加载器-system/App classLoader: 根据 Java 应用的类路径(CLASSPATH)来加载 Java类。一般来说,Java 应用的类都是由它来 完成加载的。可以通过ClassLoader.getSystemClassLoader()
来获取它;
用户自定义类加载器: 通过继承 java.lang.ClassLoader类的方式实现,重写loadClass()方法或findClass方法,但是建议重写findClass()方法,loadClass方法实现了双亲委派机制;实例代码:
public class MyClassLoader extends ClassLoader{
private String path;
public MyClassLoader(ClassLoader parent, String path) {
super(parent);
this.path = path;
}
public MyClassLoader(String path) {
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
BufferedInputStream bis = null;
ByteArrayOutputStream bais = null;
try {
//输入class文件,输出字节码文件
bis = new BufferedInputStream(new FileInputStream(path + name + ".class"));
bais = new ByteArrayOutputStream();
int len;
byte[] buf = new byte[1024];
while ((len = bis.read(buf))!=-1){
bais.write(buf,0,len);
}
//转化为字节数组
byte[] byteArray = bais.toByteArray();
return defineClass(null,byteArray,0,byteArray.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if (bis!=null) bis.close();
if (bais!=null) bais.close();
}catch (Exception e){
e.toString();
}
}
return null;
}
}
//Dome-classLoader
MyClassLoader myClassLoader = new MyClassLoader("D:\\JavaCode\\MyJVM\\src\\xyz\\pojo\\");
Class<?> aClass = myClassLoader.loadClass("Person");
System.out.println(aClass.getClassLoader());
System.out.println(aClass.getClassLoader().getParent().getClass().getName());
运行结果:
xyz.utils.MyClassLoader@4554617c
sun.misc.Launcher$AppClassLoader
类加载的执行过程
- 加载: 根据查找路径找到相应的 class 文件然后导入;
- 验证: 检查加载的 class 文件的正确性;
- 准备: 给类中的静态变量分配内存空间;
- 解析: 虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为 一个标示,而在直接引用直接指向内存中的地址;
- 初始化: 对静态变量和静态代码块执行初始化工作。
双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去加载 这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类;
在ClassLoader类中使用了loadClass()方法,实现了双亲委派机制,核心代码如下:
if (parent != null) {
c = parent.loadClass(name, false); //委派父类去加载
} else {
c = findBootstrapClassOrNull(name); //启动类加载
}
双亲委派机制的作用
双亲委派机制有效的保护了java类库中类的加载的安全性,有效避免了用户自定义类对java类库中类的破坏;
JVM调优
JVM常用参数
-Xms2g 初始化堆大小为2g,还有单位为m等;
-Xmx2g 堆的最大内存为2g;
-XX:NewRatio=4 设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8 设置新生代Eden 和 Survivor 比例为 8:2,即8 : 1 : 1;
–XX:+UseParNewGC 指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC 指定使用 ParNew + Parallel Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC 指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC 开启打印 gc 信息;
-XX:+PrintGCDetails 打印 gc 详细信息;
-XX:+HeapDumpOnOutOfMemoryError 通过下面参数可以控制OutOfMemoryError时打印堆的信息;
-XX:HeapDumpPath=指定文件路径 可以通过下面的参数打Heap Dump信息;
-XX:+PrintGCTimeStamps 打印GC执行时间戳;
JVM 调优的工具
jps命令
jdk提供的一个查看当前java进程的小工具。
命令格式:jps [options ] [ hostid ]
- jps -l 输出主类或者jar的完全路径名;
- jps –v 输出jvm参数;
jstat命令
命令可以查看堆内存各部分的使用情况,以及加载类的数量,但是不能查看使用哪种垃圾回收器;
命令格式: jstat [-options] [vmid] [间隔时间/毫秒] [查询次数]
常用-options:
option | 解析 |
---|---|
-class | 类加载器 |
-compiler | JIT |
-gc | GC堆状态 |
-gcutil | GC统计汇总 |
-printcompilation | HotSpot编译统计 |
-gccapacity | 各区大小 |
- gcnew/-gcold | 新区/老区统计 |
- gcnewcapacity/-gcoldcapacity | 新区/老区大小 |
试例:
PS D:\JavaCode\MyJVM> JSTAT -gcutil 14244
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 100.00 8.33 88.30 95.80 88.72 174 0.781 0 0.000 0.781
- S0:幸存1区当前使用比例
- S1:幸存2区当前使用比例
- E:eden区使用比例
- O:老年代使用比例
- M:元数据区使用比例
- CCS:压缩使用比例
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
jinfo命令
可以用来查看 Java 进程运行的 JVM 参数,系统属性等;
命令格式:jinfo [ option ] pid
-flags 打印命令行参数
-sysprops 打印系统属性
jmap命令
可以获得运行中的jvm的堆的快照,从而可以离线分析堆,以检查内存泄漏,检查一些严重影响性能的大对象的创建,检查系统中什么对象最多,各种对象所占内存的大小等等;
jstack命令
java虚拟机自带的一种堆栈跟踪工具,jstack用于生成java虚拟机当前时刻的线程快照,线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源;
可视化工具
- jconsole命令:可以查看堆内存,线程,类,CPU,JVM等信息;
- jvisualvm命令: 对 Java 应用程序做性能分析和调优,包括生成和分析海量数据、跟踪内存泄漏、监控垃圾回收器、执行内存和 CPU 分析;这个较jconsole命令功能更加强大;