java内存区域划分
1. 程序计数器
- 当前线程所执行的字节码行号指示器
- 每个线程都有独立的程序计数器
- 如果执行的是java方法,记录字节码指令地址。如果执行Native方法,则为空(Undefined)
2. 虚拟机栈
- 线程私有,生命周期与线程相同
- 描述java方法执行的内存模型:每个方法都会创建一个栈帧用于存储局部变量,方法出口,操作数栈等信息。每个方法调用对应一个栈帧在虚拟机栈中入栈到出栈的过程
- 存放基本数据类型(8种)和对象引用类型(地址的指针或者对象的句柄)
- 请求栈深度大于虚拟机允许深度时,抛出StackOverflowError异常
- 如果动态扩展仍无法申请足够的内存,抛出OutOfMemoryError异常
3.本地方法栈
- 作用和虚拟机栈一样
- 区别为:本地方法栈服务虚拟机使用到的Native方法
4. 堆
- 虚拟机管理的内存最大的一块
- 被所有线程共享的区域
- 所有对象的实例在此分片内存
- 可细分为多个代
5.方法区
- 所有线程共享的区域
- 存储类信息,常量,静态变量
- 在HotSpot虚拟机上也称永久代
- 垃圾收集行为很少出现在这个区域,因为可回收的内存很少
6.运行时常量池
- 是方法区的一部分
- 用于存放编译器生成的各种字面量和符号引用
7.直接内存
- 不包括在JVM内存区域中,不受JVM参数影响
- JVM使用缓冲区时,会在该区区域分配内存
- 配置时注意给该区域预留空间,而不是把所有内存都分给JVM
- -XX:MaxDirectMemorySize指定,不指定默认与堆最大值一样(-Xmx)
如何计算对象已死
1.1 引用计数器算法
引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为0的时候,JVM就认为对象不再被使用,是“垃圾”了。
引用计数器实现简单,效率高;但是不能解决循环引用问问题(A对象引用B对象,B对象又引用A对象,但是A,B对象已不被任何其他对象引用),同时每次计数器的增加和减少都带来了很多额外的开销,所以在JDK1.1之后,这个算法已经不再使用了。
1.2 可达性分析算法
可达性分析算法是通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径称为引用链(Reference Chain),当一个对象没有被GC Roots的引用链连接的时候,说明这个对象是不可用的。
GC Roots对象包括:
虚拟机栈(栈帧中的本地变量表)中的引用的对象。
方法区域中类的静态属性引用的对象。
方法区域中常量引用的对象。
本地方法栈中JNI(Native方法)的引用的对象。
上面只是标记了对象是否可以被回收,实际上在java中首先会标记下对象,会调用对象里面的protected void finalize()这个方法,这个时候对象还有救,只要在这个方法把该对象和引用链对接上,其实可以逃脱被回收
回收的区域
新生代
永久代
新生代,这个很容易理解,一般来说永久带是最不好回收的。永久代主要回收以下部分内容:
废弃常量
无用的类
垃圾回收算法
标记—清除算法
标记—清除算法包括两个阶段:“标记”和“清除”。在标记阶段,确定所有要回收的对象,并做标记。清除阶段紧随标记阶段,将标记阶段确定不可用的对象清除。
标记—清除算法是基础的收集算法,标记和清除阶段的效率不高,而且清除后回产生大量的不连续空间,这样当程序需要分配大内存对象时,可能无法找到足够的连续空间。
复制算法
复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上,然后把这块内存整个清理掉。这种方式听上去确实是非常不错的方案,但是总的来说对内存的消耗十分高。
复制算法实现简单,运行效率高,但是由于每次只能使用其中的一半,造成内存的利用率不高。现在的JVM用复制方法收集新生代,由于新生代中大部分对象(98%)都是朝生夕死的,所以两块内存的比例不是1:1(大概是8:1),也就是常提到的一块Eden(80%)和两块Survivor(20%)。当然也会存在10%不够用的情况,这个后面在进行梳理,会有一个补偿机制,也就是分配担保。
标记—整理算法
复制收集算法会存在一种极端情况,就是对象都没死。这种情况会在老年代有几率的出现,所以根据老年代的特点提出了标记—整理算法。 标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存,如下图所示:
分代收集
分代收集是根据对象的存活时间把内存分为新生代和老年代,根据个代对象的存活特点,每个代采用不同的垃圾回收算法。新生代采用标记—复制算法,老年代采用标记—整理算法。
垃圾算法的实现涉及大量的程序细节,而且不同的虚拟机平台实现的方法也各不相同。上面介绍的只不过是基本思想。
Jdk和Jre和JVM的区别
-
Jdk包括了Jre和Jvm,Jre包括了Jvm
-
Jdk是我们编写代码使用的开发工具包
-
Jre 是Java的运行时环境,他大部分都是C和C++语言编写的,他是我们在编译java时所需要的基础的类库
-
Jvm俗称Java虚拟机,他是java运行环境的一部分,它虚构出来的一台计算机,在通过在实际的计算机上仿真模拟各种计算机功能来实现Java应用程序
JVM问题排查步骤
1、通过top命令找到消耗cpu高的进程id号pid
2、根据pid找到消耗cpu资源比较高的线程id
3、对当前的线程做jstack,输出前进程的所有堆栈信息
4、将第2步中得到的线程id转换成16进制进得到结果
5、根据相应的线程id在堆栈信息中找到相关的内容
6、解读对应的堆栈信息,定位代码位置并排查问题原因
-
通过jps命令查看Java进程「基础」信息(进程号、主类)。这个命令很常用的就是用来看当前服务器有多少Java进程在运行,它们的进程号和加载主类是啥
-
通过jstat命令查看Java进程「统计类」相关的信息(类加载、编译相关信息统计,各个内存区域GC概况和统计)。这个命令很常用于看GC的情况
-
通过jinfo命令来查看和调整Java进程的「运行参数」。
-
通过jmap命令来查看Java进程的「内存信息」。这个命令很常用于把JVM内存信息dump到文件,然后再用MAT( Memory Analyzer tool 内存解析工具)把文件进行分析
-
通过jstack命令来查看JVM「线程信息」。这个命令用常用语排查死锁相关的问题
-
还有近期比较热门的Arthas(阿里开源的诊断工具),涵盖了上面很多命令的功能且自带图形化界面。这也是我这边常用的排查和分析工具
JVM调优
一般调优JVM我们认为会有几种指标可以参考:『吞吐量』、『停顿时间』和『垃圾回收频率』
基于这些指标,我们就有可能需要调整:
- 内存区域大小以及相关策略(比如整块堆内存占多少、新生代占多少、老年代占多少、Survivor占多少、晋升老年代的条件等等)
比如(-Xmx:设置堆的最大值、-Xms:设置堆的初始值、-Xmn:表示年轻代的大小、-XX:SurvivorRatio:伊甸区和幸存区的比例等等)
(按经验来说:IO密集型的可以稍微把「年轻代」空间加大些,因为大多数对象都是在年轻代就会灭亡。内存计算密集型的可以稍微把「老年代」空间加大些,对象存活时间会更长些)
- 垃圾回收器(选择合适的垃圾回收器,以及各个垃圾回收器的各种调优参数)
比如(-XX:+UseG1GC:指定 JVM 使用的垃圾回收器为 G1、-XX:MaxGCPauseMillis:设置目标停顿时间、-XX:InitiatingHeapOccupancyPercent:当整个堆内存使用达到一定比例,全局并发标记阶段 就会被启动等等)
类加载的执行过程
-
加载:根据查找路径找到相应的class文件然后导入;
-
验证:检查加载的class文件的正确性;
-
准备:给类中的静态变量分配内存空间;
-
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
-
初始化:对静态变量和静态代码块执行初始化工作。
四种类加载器
1、启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
2、扩展类加载器(extensions class loader):它用来加载Java的扩展库。Java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
3、系统类加载器(system class loader):它根据Java应用的类路径(CLASSPATH)来加载Java类。一般来说,Java应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
4、用户自定义类加载器,通过继承java.lang.ClassLoader类的方式实现。
双亲委派模型:
如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。