前言
希望自己每一次学习都有不同的理解
文章目录
- 前言
- 1. jvm的组成
- 取消永久代使用元空间原因
- 2. 运行时数据区
- 3. 堆栈区别
- 队列和栈,队列先进先出,栈先进后出从栈顶弹出
- 4. GC、内存溢出、垃圾回收
- 4.1 如何确定引用是否会被回收
- 4.1.1 Java中的引用类型
- 4.1.2 如何定位对象
- 4.2 垃圾回收算法以及收集器
- 4.3 分代垃圾回收器是怎么工作的
- 4.4 jvm的类加载器
- 什么是双亲委派机制
- 类的加载过程
- 4.5 jvm调优
- 流程
- 参数
- 如何调整参数
1. jvm的组成
-
Jdk1.6及之前:方法区(永久代), 常量池在方法区
-
Jdk1.7:有永久代,但已经逐步“去永久代”,常量池在堆
-
Jdk1.8及之后: 无永久代,元空间, HotSpot JVM
-
类加载子系统 Class loader 装载class文件到Runtime data area中的method area
-
执行引擎 Execution engine执行classes中的指令
-
本地方法接口库,与native libraries交互,是其它编程语 言交互的接口
-
运行时数据区
首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader) 再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方 法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作 系统去执行,因此需要特定的命令解析器执行引擎(ExecutionEngine),将 字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他 语言的本地库接口(Native Interface)来实现整个程序的功能。
取消永久代使用元空间原因
1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。
2. 运行时数据区
-
程序计数器:线程所执行的字节码的行号 指示器
-
栈:存储栈帧,栈帧包含局部变量表、操作 数栈、动态链接、方法出口等信息
-
本地方法栈:与线程栈一样,但是是为调用native方法服务
-
堆:内存大的一块,是被所有线程共享 的,几乎所有的对象实例都在这里分配内存,1.7之后常量、静态变量也在此区
-
方法区:存储已被虚拟机加载的类信息,、即时编译后的代码等数据,1.8后改为元空间
Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。
- 静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间
- 运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池
3. 堆栈区别
-
物理地址分配
堆的是不连续的,性能相对慢一些,GC的时候也要考虑到 不连续的分配,所以有各种算法,比如标记-消除,复制,标记-压缩,分代 (即新生代使用复制算法,老年代使用标记—压缩)
栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的
-
内存
堆内存远大于栈
-
内容以及范围
堆内主要存放数组、对象,关注的是数据的存储,对整个应用是可见的,静态变量是存在于方法区的,但是静态的对象是存在堆
栈主要存放局部变量、操作数栈、返回结果,关注的是方法的执行,仅对线程可见
队列和栈,队列先进先出,栈先进后出从栈顶弹出
4. GC、内存溢出、垃圾回收
内存溢出也就是jvm发生了Full GC,
永久代(java8之前)内存超出临界值后也会Full Gc,Java8之后取消永久代,增加元空间
原因:长生命周期的对象拥有短生命周期对象的引用,导致短生命周期对象未能被释放导致
4.1 如何确定引用是否会被回收
- 可达性分析法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。 当一个对象到 GCRoots 没有任何引用链相连时,则证明此对象是可以被回收的。
GC Root 对象可以是一些静态的对象,Java方法的local变量或参数, native 方法引用的对象,活着的线程,虚拟机栈中引用的对象
- 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用 被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用 的问题
4.1.1 Java中的引用类型
- 强引用:长期不会被释放
- 软引用:有用但不必须,在发生内存溢出之前会被回收
- 弱引用:有用但不必须,在下一次GC时会被回收
- 虚引用:无法通过虚引用获得对象,虚引用的用途是在 gc 时返回一个通知,用PhantomReference 实现虚引用
4.1.2 如何定位对象
主要通过jvm线程栈上的引用访问jvm堆,现在主流的方式有句柄式和直接指针;
-
直接指针: 直接指向对象,优势是速度更快,节省了一次指针定位的开销。
-
句柄: 可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是 指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的 真实内存地址。
Java堆中有一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中 包含了对象实例数据与对象类型数据各自的具体地址信息,具体构造如下图所 示
优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是 非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改
4.2 垃圾回收算法以及收集器
-
标记-清除算法:标记无用对象,然后进行清除回收。
- 优点:实现简单,不需要对象进行移动,后面算法以此为基础
- 缺点:效率不高,产生大量不连续的内存碎片,无法清除。
-
复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的 对象复制到另一块上,然后再把已使用的内存空间一次清理掉。
- 优点:解决了内存碎片的问题,
- 缺点:内存使用率不 高,只有原来的一半。
- 收集器
- Serial收集器: 新生代单线程收集器,标记和清理都是单线程,优点 是简单高效
- ParNew收集器:多线程版本
- Parallel Scavenge收集器:新生代并行收集器,追求高吞吐量,高效 利用 CPU。吞吐量= 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高 效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不 高的场景
-
标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清 除掉端边界以外的内存。
-
优点:解决了内存碎片的问题
-
缺点:局部对象移动降低效率
-
收集器
- Serial Old收集器: 老年代单线程收集器,Serial收集器的老年代版本;
- Parallel Old收集器 : 老年代并行收集器,吞吐量优先, Parallel Scavenge收集器的老年代版本;
-
-
分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代和永久代(元数据区)
-
新生代基本采用复制算法
-
老年代采用标记整理算法
- CMS收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最 短GC回收停顿时间,但是会出现大量的内存碎片,当内存不够使用的时候会采用 Serial Old 回收器进行垃圾清除
- G1收集器 (标记-整理算法): Java堆并行收集器,基于标记-整理算法,但是G1回收的范围是整个Java堆(包括新生代,老年代)
-
**在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。 **
4.3 分代垃圾回收器是怎么工作的
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
- 把 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)时,升级为老生代。大对象也会直接进入老生代
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的 执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程
对象分配:
java对象优先分配在eden区,大对象(需要连续的内存空间的对象,不经过eden和survivor)和长期存活的对象分配至老年代
4.4 jvm的类加载器
启动类加载器:负责加载JRE的核心类库,如jre目标下的rt.jar,charsets.jar等
扩展类加载器:负责加载JRE扩展目录ext中JAR类包
系统类加载器:负责加载ClassPath路径下的类包
用户自定义加载器:负责加载用户自定义路径下的类包 (继承ClassLoader,然后覆盖findClass()方法)
什么是双亲委派机制
要加载一个类MyClass.class,从低层级到高层级一级一级委派,先由应用层加载器委派给扩展类加载器,再由扩展类委派给启动类加载器;启动类加载器载入失败,再由扩展类加载器载入,扩展类加载器载入失败,最后由应用类加载器载入,如果应用类加载器也找不到那就报ClassNotFound异常了
双亲委派机制的优点:
1.保证安全性,层级关系代表优先级,也就是所有类的加载,优先给启动类加载器,这样就保证了核心类库类。
2.避免重复,如果父类加载器加载过了,子类加载器就没有必要再去加载了
类的加载过程
- 加载
- 通过过一个类的全限定名获取该类的二进制流
- 将该二进制流中的静态存储结构转化为方法去运行时数据结构
- 在内存中生成该类的 Class 对象,作为该类的数据访问入口
- 验证
- 文件格式验证:验证字节流是否符合 Class 文件的规范,常量池中的常量是否有不被支持的类型
- 元数据验证: 对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。
- 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
- 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。
- 准备 : 为类的变量以及静态变量分配内存并将其初始化为默认值
- 解析:要完成符号引用到直接引用的转换动作
- 初始化:到了初始化阶段,才真正开始执行类中定义的 Java 程序代码
4.5 jvm调优
性能调优指标:
- 吞吐量:重要指标之一,是指不考虑垃圾收集引起的停顿时间或内存消耗,垃圾收集器能支撑应用达到的最高性能指标。
- 延迟:其度量标准是缩短由于垃圾啊收集引起的停顿时间或者完全消除因垃圾收集所引起的停顿,避免应用运行时发生抖动。
- 内存占用:垃圾收集器流畅运行所需要 的内存数量。
这三个属性中,其中一个任何一个属性性能的提高,几乎都是以另外一个或者两个属性性能的损失作代价,不可兼得,具体某一个属性或者两个属性的性能对应用来说比较重要,要基于应用的业务需求来确定。
流程
- 工具:
- 于 JDK 的 bin 目录下的工具:jconsole、jvisualvm、jmap
- mat工具,可以分析到具体某个线程,某个对象占用了空间
- 检查范围
- Dump线程详细信息:查看线程内部运行情况,内存热点分析
- 死锁检查
- 线程监控
调优的目的是什么:
- GC的时间足够的小
- GC的次数足够的少
- 减少老年代的对象数量
- 发生Full GC的周期足够的长
但是很明显,第一条和第二条相悖,因为GC时间小必然需要一个较小的堆,而次数少则必然需要一个较大的堆
参数
为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值
- -Xms2g:初始化推大小为 2g;
- -Xmx2g:堆最大内存为 2g;
- -XX:NewSize :新生代空间大小初始值
- -XX:MaxNewSize :新生代空间大小最大值
- -Xmn :新生代空间大小,此处的大小是(eden+2 survivor space)
如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。可以通过监控堆状态老年代内存占有情况来决定比例
- -XX:NewRatio=2:设置年轻的和老年代的内存比例为 1:2;默认为1:2
- -XX:SurvivorRatio=8:设置新生代Eden 和 Survivor 比例为 8:2;
- –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
- -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
- -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组 合;
- -XX:+PrintGC:开启打印 gc 信息;
- -XX:+PrintGCDetails:打印 gc 详细信息。
- XX:MetaspaceSize=2M : 元空间初始大小,如果没有指定这个参数,元空间会在运行时根据需要动态调整,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:+UseParallelGC :选择垃圾收集器为并行收集器。 此配置仅对年轻代有效。即上述配置下,年轻代 使用并发收集,而年老代仍旧使用串行收集。
-XX:ParallelGCThreads=20 :配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值 最好配置与处理器数目相等。
-XX:+UseParallelOldGC :配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
-XX:MaxGCPauseMillis=100 : 设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动 调整年轻代大小,以满足此值。
-XX:+UseAdaptiveSizePolicy :设置此选项后,并行收集器会自动选择年轻代区大小和相应的 Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直 打开。
如何调整参数
系统完成之后不对jvm进行参数调整,进行压测,查看GC日志,GC日志指令: -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:
fullGC后查看GC数据
之后可以根据一下修改配置,修改之后再进行压测
User user = new User()做了什么操作,申请了哪些内存
-
new User(); 创建一个User对象,内存分配在堆上
-
User user; 创建一个引用,内存分配在栈上
-
= 将User对象地址赋值给引用