文章目录
JVM结构图
程序计数器:寄存器。存储着下一条指令的执行地址。用于在上下文切换时进行记录。 由此可见是线程私有的。不存在内存溢出。
Java Virtual Machine Stacks(Java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
- 共享的变量需要考虑线程安全,私有的不用考虑。如果方法内局部变量没有逃离方法的作用访问,它是线程安全的。如果是局部变量引用了对象,并逃离方法的作用方法,需要考虑线程安全
- 栈内存溢出:栈帧过多导致栈内存溢出(常见)StackOverflowError。栈帧过大导致栈内存溢出(不常见)。
案例1:cpu占用过多
用top定位哪个进程对cpu的占用过高
ps H -eo pid,tid,%cpu|grep 进程id(用ps命令进一步定位是哪个线程引起的cpu占用过高)
·jstack进程id
·可以根据线程d找到有问题的线程,进一步定位到问题代码的源码行号
本地方法栈
为本地方法的运行提供空间。
堆
·通过new关键字,创建对象都会使用堆内存
特点
·它是线程共享的,堆中对象都需要考虑线程安全的问题
·有垃圾回收机制
垃圾回收
引用计数法:有缺陷,如果有循环引用则无法被回收。
可达性分析算法。
使用MAT查看堆内存步骤:
1.使用jps命令列出当前的所有进程id。例如查找到进程id21384.
jps
2.配合jmp命令抓取内存快照。
E:\git\jvm\out\production\jvm\cn\itcast\jvm\t4>jmap -dump:format=b,live,file=1.bin 21384
3.使用MAT查看哪些可以作为GC Root对象
1.Object,HashMap,String等虚拟机运行时一些核心对象。
2.Java虚拟机在执行时必须调用一些系统方法,系统方法所引用的一些java对象。
3.活动线程中局部变量所使用的一些对象。即栈帧内所使用的一些对象。例如下面被list1所引用的ArrayList对象。
List<Object>list1=new ArrayList<>();
4.正在加锁的对象。
五种引用:
软引用:没有强引用且进行垃圾回收内存不够的情况下对象会被回收。之后引用进入引用队列。例如一些图片的加载,在内存不够的时候就可以先回收。
弱引用:没有强引用且进行了垃圾回收对象就可能会被回收。之后引用进入引用队列。
虚引用:必须配合引用队列使用,主要配合ByteBuffer使用引,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用Unsafe.freeMemory方法释放直接内存。
终结器引用:无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次Gc时才能回收被引用对象。
垃圾回收
1.标记清除算法。
2.标记整理算法。
3.复制算法。
分代垃圾回收
对象首先分配在伊甸园区域。如果大对象放不下则会直接 放到老年代。
新生代空间不足时,触发minor gc,伊甸园和rom存活的对象使用copy复制到to中,存活的对象年龄加1并且交换from to。
minor ge会引发stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)。
当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发flgc,STW的时间更长。一个线程的OOM并不会导致主线程的结束。
相关JVM参数
堆初始大小 | -Xms |
---|---|
堆最大大小 | -Xmx-或XX:MaxHeapSize=size |
新生代大小 | -Xmn或(-XX:NewSize=size ±XX:MaxNewSize=size |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio和-XX:+UseAdaptiveSizePolicy |
幸存区比例 | XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC前MinorGC | -XX:+ScavengeBeforeFullGC |
垃圾回收器
串行
-XX:+UseSerialGC=Serial + Serialold
吞吐量优先
-XX:+UseParallelGC ~-XX:+UseParallelO ldGC
-XX:+UseAdaptiveSizePolicy #动态调整Eden,跟Survivor的比例
-XX:GCTimeRatio=ratio
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n
响应时间优先
-XX:+UseConcMarkSweepGC ~-XX:+UseParNewGC Serial0ld
-XX:ParallelGCThreads=n ~-XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark
G1垃圾回收器
适用场景
·同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是2O0ms
·超大堆内存,会将堆划分为多个大小相等的Region
·整体上是标记+整理算法,两个区域之间是复制算法
相关JVM参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
JDK8u20字符串去重
优点:节省大量内存
缺点:略微多占用了cpu时间,新生代回收时间略微增加
-XX:+UsestringDeduplication
·将所有新分配的字符串放入一个队列
·当新生代回收时,G1并发检查是否有字符串重复
·如果它们值一样,让它们引用同一个char0
注意,与String.intern()不一样
·String…intern()关注的是字符串对象
·而字符串去重关注的是char[]
·在JVM内部,使用了不同的字符串表。
JDK8u40并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类。
-XX:+ClassUnloadingWithConcurrentMark #默认启用
JDK8u60回收巨型对象
·一个对象大于region的一半时,称之为巨型对象
·G1不会对巨型对象进行拷贝
·回收时被优先考虑
·G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉。
垃圾回收调优
低延迟:CMS,G1,ZGC
高吞吐量:ParallelGC
内存占用几个不合理的方面:
数据是不是太多,数据是否太臃肿,对象大小Integer 24 int 4。是否存在内存泄漏。
新生代调优
·新生代的特点
1.所有的new燥作的内存分配非常廉价
TLAB thread-local allocation buffer
2.死亡对象的回收代价是零
3.大部分对象用过即死
4.Minor GC的时间远远低于Full GC
新生代能容纳所有【并发量*(请求-响应)】的数据。
幸存区大到能保留【当前活跃对象+需要晋升对象】。
晋升阈值配置得当,让长时间存活对象尽快晋升
-XX:MaxTenuringThreshold=threshold
老年代调优
以CMS为例
·CMS的老年代内存越大越好
·先尝试不做调优,如果没有FullGC那么已经,否则先尝试调优新生代
·观察发生FullGC时老年代内存占用,将老年代内存预设调大1/4~1/3
-XX:CMSInitiatingOccupancyFraction=percent#老年代比例达到%多少进行垃圾回收
方法区
方法区在虚拟机启动时被创建 。
常量池
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
常量池中的字符串仅是符号,第一次用到时才变为对象。
利用串池的机制,来避免重复创建字符串对象。
字符串变量拼接的原理是StringBuilder(l.8)。
字符串常量拼接的原理是编译期优化。
可以使用intern方法,主动将串池中还没有的字符串对象放入串池。
public static void main(String[]args){
String s1="a";//懒惰的
String s2 ="b";
String s3 "ab";
String s4 = s1 + s2;//new StringBuilder().append("a").append("b").tostring()new String("ab")
Str1ngs5 ="a" + "b";//javac在编泽期间的忧化,结果已经在编泽期确定为ab
System.out.println(s3 = s4);//false s3在字符串常量池,s4在堆中
System.out.println(s3 = s5);//true
}
StringTable调优:
系统内字符串常量比较多可以调大StringTableSize
XX:StringTableSize-=桶个数
考虑字符串对象入池。
addredd.add(line.intern());
直接内存
常见于NIO操作时,用于数据缓冲区。读取直接内存避免了从系统内存到Java堆内存复制的这步流程。
分配回收成本较高但读写性能高
不受JVM内存回收管理。
使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
public void run(){
if(address == 0)(
return;
}
unsafe.freeMemory(address);
address 0;
Bits.unreserveMemory(size,capacity);
}
直接内存溢出报错提示:
Exception in thread "main"java.lang.OutofMemoryError:Direct buffer memory
堆内存诊断
l.jps工具
·查看当前系统中有哪些java进程
2.jmap工具
·查看堆内存占用情况
jamp -heap 线程id
3.jconsole具
·图形界面的,多功能的监测工具,可以连续监测
类加载运行流程
4.1加载
·将类的字节码载入方法区中,内部采用C+的instanceKlass描述java类,它的重要field有:
·_java_mirror即java的类镜像,例如对String来说,就是String.class,作用是把kass暴露给java使用
·_super即父类
_fields即成员变量
_methods即方法
,_constants即常量池
·_class_loader即类加载器
·_vtable虚方法表
_itable接口方法表
·如果这个类还有父类没有加载,先加载父类
4.2链接
验证:验证类是否符合JVM规范,安全性检查
准备:为static变量分配空间,设置默认值
static变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于_java_mirror末尾
static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
如果static变量是fnal的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
如果static变量是final的,但属于引用类型(new 对象),那么赋值也会在初始化阶段完成
解析:将常量池中的符号引用解析为直接引用
package cn.itcast.jvm.t3.load;
/*
解析的含义
*/
public class Load2{
public static void main(String[]args)throws ClassNotFoundException,IOException{
ClassLoader classloader Load2.class.getclassLoader();
//1oadc1ass方法不会导致类的解析和初始化
Class<?>c classloader.loadclass("cn.itcast.jvm.t3.load.C");
//new c();
System.in.read();
}
}
class c{
D d new D();
]
class D{}
loadclass:
new c():
4.3初始化
(cinit>()V方法
初始化即调用()V,虚拟机会保证这个类的『构造方法』的线程安全。
调用静态代码块即可验证类是否初始化。
发生的时机
概括得说,类初始化是【懒惰的】
main方法所在的类,总会被首先初始化
·首次访问这个类的静态变量或静态方法时
·子类初始化,如果父类还没初始化,会引发
·子类访问父类的静态变量,只会触发父类的初始化
Class.forName
·new会导致初始化
不会导致类初始化的情况
访问类的static final静态常量(基本类型和字符串)不会触发初始化
·类对象.class不会触发初始化
·创建该类的数组不会触发初始化
·类加载器的1 oadClass方法
Class…forName的参数2为false时
public class Load1 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
// 1. 静态常量(基本类型和字符串)不会触发初始化
// System.out.println(B.b);
// 2. 类对象.class 不会触发初始化
// System.out.println(B.class);
// 3. 创建该类的数组不会触发初始化
// System.out.println(new B[0]);
// 4. 不会初始化类 B,但会加载 B、A
// ClassLoader cl = Thread.currentThread().getContextClassLoader();
// cl.loadClass("cn.ali.jvm.test.classload.B");
// 5. 不会初始化类 B,但会加载 B、A
// ClassLoader c2 = Thread.currentThread().getContextClassLoader();
// Class.forName("cn.ali.jvm.test.classload.B", false, c2);
// 1. 首次访问这个类的静态变量或静态方法时
// System.out.println(A.a);
// 2. 子类初始化,如果父类还没初始化,会引发
// System.out.println(B.c);
// 3. 子类访问父类静态变量,只触发父类初始化
// System.out.println(B.a);
// 4. 会初始化类 B,并先初始化类 A
// Class.forName("cn.ali.jvm.test.classload.B");
}
}
class A {
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}
public class Math {
public static final int initData = 666;
public static User user = new User();
public int computer(){//一个方法对应一块栈帧区域
int a= 1;
int b = 2;
int c = (a + b) *10;
return c
}
public static void main(String[] args) {
Math math = new Math();
math.computer();
System.out.println();
}
}
当main方法主线程开始运行的时候,给主线程分配一块内存空间,线程调用到computer方法的时候分配方法的内存区域 栈帧,为什么采用栈的结构?computer方法后调用后分配内存空间却最先结束释放。
栈帧分析
使用javap 命令对class文件进行反编译
a= 1,b = 2 的赋值操作对应0-3首先会把int类型常量1压入操作数栈,然后把值赋值给局部变量表里的a.45读取a,b的值到操作数栈,6把两个数出栈相加,把结果10重新压入操作数栈,执行乘法得到结果30把结果压回操作数栈,之后把30出栈赋值给局部变量表中的c,此时c=30。字节码执行引擎会不断修改程序计数器的值来告诉线程要执行哪一行代码。
操作数栈即运算过程中产生的结果所分配一块临时空间。
动态链接:将符号引用转换直接引用。computer方法分配在方法区,根据“computer”这个符号引用转换成直接引用,找到该方法在方法区取得入口。
方法出口:记录了computer方法执行完毕之后该回到main方法何处去执行。
main方法中的局部变量表保存了一个math对象,对象分配在堆中,所以局部变量表中村的是math对象的一个引用地址,
首先需要明确,静态变量在内存空间中只有一份,被类的多个对象共享。并且不同的JDK版本,静态变量的存放位置是有所差异的,需要分类讨论。
JDK6:静态变量是存储在方法区(永久代)中的。
JDK7及其以后:静态变量存储在堆中。为什么把移到了堆中?主要就是因为堆的GC比较频繁,而方法区是很少GC的,这也就会导致内存空间的回收不及时。
值得一提的是,字符串常量池StringTable的存放位置和静态变量存储的位置相同,也是JDK6方法区,JDK7及以后的堆。
堆内存分配:其中字节码执行引擎负责对垃圾进行回收
字节码图解运行流程
流程可参考 黑马JVM P110-111
bipush 10
·将一个byt压入操作数栈(其长度会补齐4个字节),类似的指令还有
·sipush将一个short压入操作数栈(其长度会补齐4个字节)
·ldc将一个int压入操作数栈
·ldc2_w将一个long压入操作数栈(分两次压入。因为long是8个字节)
·这里小的数字都是和字节码指令存在一起,超过sot范围的数字存入了常量池
public class Demo3_9{
public Demo3_9(){}
private void test1(){}
private final void test2(){}
public void test3(){}
public static void test4(){}
public static void main(String[]args){
Demo3_9 d new Demo3_9();
d.test1();
d.test2();
d.test3();
d.test4();
Demo3_9.test4();
}
}
只有这个public的 test3是调用需要再运行时确定,即多态。其余是静态绑定。如果通过对象调用静态方法,则会产生20-21的无用字节码,可以直接通过类名调用。
当执行invokevirtual指令时
1.先通过栈帧中的对象引用找到对象
2.分析对象头,找到对象的实际Class
3.Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
4.查表得到方法的具体地址
5.执行方法的字节码
finally语句块出现return会吞掉异常。
类加载器
名称 | 加载的类 | 说明 |
---|---|---|
Bootstrap ClassLoader(启动类加载器) | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader(拓展类加载器) | JAVA_HOME/jre/lib/ext | 上级为Bootstrap,显示为null |
Application ClassLoader(应用程序类加载器) | classpath | 上级为Extension |
自定义类加载器 | 自定义 | 上级为Application |
双亲委派模式
线程上下文类加载器
自定义类加载器
使用Arthas查看JVM内存情况
java -jar arthas-boot.jar
使用Arthas可以替换class文件
第一种是直接在服务器上将对应class文件反编译后使用vim修改java文件然后再编译为class文件;
使用redefine
命令:redefine
【修改后的class
文件】
jad --source-only com.Jvm.CpuTest > D:/compiler/CpuTest.java
[arthas@10000]$ mc D:/compiler/CpuTest.java -d D:/compiler/
Memory compiler output:
D:\compiler\com\Jvm\CpuTest.class
Affect(row-cnt:1) cost in 424 ms.
[arthas@10000]$ redefine D:/compiler/com/Jvm/CpuTest.class
redefine success, size: 1, classes:
com.Jvm.CpuTest
由于垃圾回收的时候对象是否是垃圾的判断可能随时会变化,所以Java虚拟机在垃圾回收的时候采用了STW的方式。
JVM调优主要就是减少full gc以及STW的时间。
如何快速定位线上OOM问题
OOM原因:
1.一次性申请的太多。
2.内存资源耗尽未释放。
3.本身资源不够。
排查可以事先配置dump文件导出路径
使用jvisualvm来载入dump文件
这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是final的:因为在创建Candy11$1对象时,将x的值赋值给了Candy11$1对象的valx属性,所以x不应该再发生变化了,如果变化,那么valx属性没有机会再跟着一起变化