Java运行时数据区的内存区域简介:
一个Java类从编码到最终完成执行,包括两个过程:编译运行。
编译:通过javac命令将.java文件编程成.class文件。
运行:将.class文件通过类加载器加载到内存中,并运行。
类在JVM中的生命周期: 加载、链接(验证、准备、解析)、初始化、使用、卸载。
类的加载时机:
JVM运行的时候,并不是一次性加载所有类的,而是使用到哪个就加载哪个,并且只会加载一次。
1、new 一个对象实例的时候。
2、访问类或接口的静态变量,或者给静态变量赋值。
3、调用类的静态方法。
4、反射 Class.forName(“com.demo.ClassA”)。
5、初始化一个子类,首先会初始化父类。
类装载器 ClassLoader 是负责加载class
文件的,将class
文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构。ClassLoader
只负责文件的加载,至于它是否可运行,则由Execution Engine
决定。
这里需要区分一下class
与Class
。小写的class
,是指使用javac
命令编译 Java 代码后所生成的以.class
为后缀名的字节码文件。而大写的Class
,是 JDK 提供的java.lang.Class
,可以理解为封装类的模板。多用于反射场景,例如 JDBC 中的加载驱动,Class.forName("com.mysql.jdbc.Driver");
下图Car.class
字节码文件被ClassLoader
类装载器加载并初始化,在方法区中生成了一个Car Class
的类模板,而平时所用到的实例化,就是在这个类模板的基础上,形成了一个个实例,即car1
,car2
。反过来讲,可以对某个具体的实例进行getClass()
操作,就可以得到该实例的类模板,即Car Class
。再接着,对这个类模板进行getClassLoader()
操作,就可以得到这个类模板是由哪个类装载器进行加载的。
Tip: 扩展一下,JVM并不仅仅只是通过检查文件后缀名是否是.class
来判断是否加载,最主要的是通过class
文件中特定的文件标示,即下图test.class
文件中的cafe babe
。
1、虚拟机自带的类加载器
Bootstrap
),也叫根加载器,加载%JAVAHOME%/jre/lib/rt.jar
Extension
),加载%JAVAHOME%/jre/lib/ext/*.jar
,例如javax.swing
包AppClassLoader
),也叫系统类加载器,加载%CLASSPATH%
的所有类。2、 用户自定义的加载器 : 用户可以自定义类的加载方式,但必须是Java.lang.ClassLoader
的子类。
父类委托机制。
通过下面代码来观察这几个类加载器。首先,我们先看自定义的MyObject
,首先通过getClassLoader()
获取到的是AppClassLoader
,然后getParent()
得到ExtClassLoader
,再getParent()
竟然是null
?可能大家会有疑惑,不应该是Bootstrap
加载器么?这是因为,BootstrapClassLoader
是使用C++
语言编写的,Java
在加载的时候就成了null
。
我们再来看Java自带的Object
,通过getClassLoader()
获取到的加载器直接就是BootstrapClassLoader
,如果要想getParent()
的话,因为是null
值,所以就会报java.lang.NullPointerException
空指针异常。
输出中,sun.misc.Launcher
是JVM相关调用的入口程序。
自定义了一个java.lang.String
类,并且创建main
方法后运行,发现报错了,提示找不到main
方法,但是明明我们定义了main
方法啊,引出双亲委派和沙箱安全。
(1)双亲委派,当一个类收到了类加载请求,它首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,因此所有的加载请求都应该传送到启动类加载器中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
采用双亲委派的一个好处是,比如加载位于rt.jar
包中的类java.lang.Object
,不管是哪个加载器加载这个类,最终都是委派给顶层的启动类加载器进行加载,确保哪怕使用了不同的类加载器,最终得到的都是同样一个Object
对象。
(2)沙箱安全,是基于双亲委派机制上采取的一种JVM
的自我保护机制,假设你要写一个java.lang.String
的类,由于双亲委派机制的原理,此请求会先交给BootStrapClassLoader
试图进行加载,但是BootStrapClassLoader
在加载类时首先通过包和类名查找rt.jar
中有没有该类,有则优先加载rt.jar
包中的类,因此就保证了java
的运行机制不会被破坏,确保你的代码不会污染到Java
的源码。保证了大家使用的类是同一套体系的,统一的class
。保证java
源代码不受污染,保证源码干净一致,这叫沙箱安全机制。
所以,类加载器的加载顺序如下:
AppClassLoader
加载一个class
时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader
去完成。ExtClassLoader
加载一个class
时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader
去完成。BootStrapClassLoader
加载失败(例如在$JAVA_HOME/jre/lib
里未查找到该class
),会使用ExtClassLoader
来尝试加载。ExtClassLoader
也加载失败,则会使用AppClassLoader
来加载,如果AppClassLoader
也加载失败,则会报出异常ClassNotFoundException
。Tip: rt.jar
是什么?为什么可以在idea
这些开发工具中可以直接去使用String、ArrayList
、甚至一些JDK
提供的类和方法?因为这些都在rt.jar
中定义好了,且直接被启动类加载器进行加载了。
Java7之前,堆结构图如下,而Java8则只将永久区变成了元空间。
Minor GC
针对的是新生代的垃圾回收。
在新生代中经历了几次Minor GC
仍然存活的对象,就会被放到老年代。
Major GC
针对的是老年代的垃圾回收。
Full GC
是针对整堆(包括新生代和老年代)做垃圾回收的。
永久代(Perm
)主要存放已被虚拟机加载的类信息,常量,静态变量等数据。
1、首先,新生区是类的诞生、成长、消亡的区域。一个类在这里被创建并使用,最后被垃圾回收器收集,结束生命。
2、其次,所有的类都是在Eden Space
被new
出来的。而当Eden Space
的空间用完时,程序又需要创建对象,JVM
的垃圾回收器则会将Eden Space
中不再被其他对象所引用的对象进行销毁,也就是垃圾回收(Minor GC
)。此时的GC可以认为是轻量级GC。
3、然后将Eden Space
中剩余的未被回收的对象,移动到From Space
,以此往复,直到From Space
也满了的时候,再对From Space
进行垃圾回收,剩余的未被回收的对象,则再移动到To Space
。To Space
也满了的话,再移动至Old Space
。
4、最后,如果Old Space
也满了的话,那么这个时候就会被垃圾回收(Major GC or Full GC)并将该区的内存清理。此时的GC可以认为是重量级GC。如果Old Space
被GC垃圾回收之后,依旧处于占满状态的话,就会产生我们场景的OOM
异常,即OutOfMemoryError
。
Survivor 0 Space
,幸存者0区,也叫from
区;
Survivor 1 Space
,幸存者1区,也叫to
区。
其中,from
区和to
区的区分不是固定的,是互相交换的,意思是说,在每次GC之后,两者会进行交换,谁空谁就是to
区。
(1)Eden Space
、from
复制到to
,年龄+1。
首先,当Eden Space
满时,会触发第一次GC,把还活着的对象拷贝到from
区。而当Eden Space
再次触发GC时,会扫描Eden Space
和from
,对这两个区进行垃圾回收,经过此次回收后依旧存活的对象,则直接复制到to
区(如果对象的年龄已经达到老年(15)的标准,则移动至老年代区),同时把这些对象的年龄+1。
(2)清空Eden Space、from
然后,清空Eden Space
和from
中的对象,此时的from
是空的。
(3)from
和to
互换
最后,from
和to
进行互换,原from
成为下一次GC时的to
,原to
成为下一次GC时的from
。部分对象会在from
和to
中来回进行交换复制,如果交换15次(由JVM参数MaxTenuringThreshold决定,默认15),最终依旧存活的对象就会移动至老年代。
总结一句话,GC之后有交换,谁空谁是to
。
-Xms
:初始堆分配大小,默认为物理内存的 1/64
-Xmx
:最大分配内存,默认为物理内存的 1/4
-XX:+PrintGCDetails
:输出详细的GC处理日志
IDEA中配置JVM内存参数:
在【Run】->【Edit Configuration…】->【VM options】中,输入参数-Xms1024m -Xmx1024m -XX:+PrintGCDetails
,然后保存退出。
JVM的初始内存和最大内存一般怎么配:
初始内存和最大内存一定是一样大,理由是避免GC
和应用程序争抢内存,进而导致内存忽高忽低产生停顿。
出现java.lang.OutOfMemoryError: Java heap space
异常,说明Java虚拟机的堆内存不够,造成堆内存溢出。原因有两点:
-Xms
和-Xmx
来调整。引用计数法、标记清除法、标记压缩算法、复制算法、分代算法。
线程栈,每一个线程运行的时候,Java虚拟机都会给这个线程分配一块独立的栈内存空间,来放线程的局部变量。
栈的组成:
栈实际上就是栈帧组成的,而每一个栈帧又存储着与之对应的方法的局部变量表,操作数栈,动态链接,方法出口。
也就是一个方法对应一个栈帧,栈帧就是Java中每个方法的存放空间。
public class Math {
public static final int initDate = 666;
public static User user = new User();
public Math() {
}
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 3;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
System.out.println("end...);
}
}
1、将class文件加载进类加载子系统
2、开辟一个包含堆、方法区、栈、本地方法栈、程序计数器的空间
3、字节码引擎开始执行
反编译的结果:
分析compute()函数的指向流程理解帧栈这个空间。
Compiled from "Math.java"
public class com.shen.Main.jvm.Math {
public static final int initDate;
public com.shen.Main.jvm.User user;
public com.shen.Main.jvm.Math();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: new #2 // class com/shen/Main/jvm/User
8: dup
9: invokespecial #3 // Method com/shen/Main/jvm/User."":()V
12: putfield #4 // Field user:Lcom/shen/Main/jvm/User;
15: return
public int compute();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: iconst_3
8: imul
9: istore_3
10: iload_3
11: ireturn
public static void main(java.lang.String[]);
Code:
0: new #5 // class com/shen/Main/jvm/Math
3: dup
4: invokespecial #6 // Method "":()V
7: astore_1
8: aload_1
9: invokevirtual #7 // Method compute:()I
12: pop
13: return
}
栈帧的局部变量其实是用一个数组进行存储的
其中特殊的局部变量0就是this
public int compute();
Code:
0: iconst_1 将局部变量1 放入到操作数栈
1: istore_1 将int类型的值存入局部变量1(将int值赋给在局部变量表的局部变量)
2: iconst_2
3: istore_2
(这就明白了 这两个实际上指行的就是 现在局部变量表中开辟一个b的空间 然后在从操作数栈中弹栈赋给局部变量b)
4: iload_1 //局部变量1压入栈
5: iload_2 //局部变量2 压入栈
6: iadd //弹栈两次 执行int 类型的add
7: iconst_3 //将计算结果 压入栈
8:bipush //将 10压入栈
9: imul //计算乘法结果 在压入栈中
10: istore_3 //将结果存给局部变量3
11: iload_3 //取出局部变量3的值
12: ireturn //return int
属于线程私有,用来存放线程执行代码的位置(就是.class文件中的行号) 由字节码执行引擎来操作。
Java是多线程的,当一个线程执行的过程中,被挂起了,程序计数器就是记录了被挂起时运行到的位置,然后当重新唤醒后,就会从程序计数器记录的位置处开始执行。
常量 public static final int initDate = 666;
静态变量(指向堆空间) public static User user = new User();
类信息
JDK8方法区使用的是物理内存,叫元空间。
存放各种new出来的对象
局部变量表会指向
方法区中的静态变量也会指向
注: 只是Eden 区。对于S区,一个是 s0 存放着存活对象,一个是 s1 空的,等待 Minor GC 完成后,来转移存活的对象,并不用于分配给新生对象。
老年代存放的对象: 对象类型的静态变量,缓存对象,数据库连接池中的对象,Spring容器中的Bean(Controller,Service),这些对象都一直被 GC Root引用,所以最终都会放到老年代。还有就是大对象会直接放入到老年代,大对象就是一块连续的内存空间在 Eden 区放不下,会直接进入老年代。
stop the world 停止整个世界
STW 会停止所有的用户线程。
进行 Minor GC 和 Full GC 前都会触发STW。
Java虚拟机在进行垃圾回收的时候回触发STW:
如果在进行GC的时候,用户线程也在运行中,GC Root和对象是不断变化的,无法确定一个对象的状态,这时Java设计了STW,暂停所有的用户线程,让所有的对象都有一个确定的状态,然后快速的进行GC来回收垃圾对象,一般STW的时候会很短,对用户来说只是卡顿了下,但是频繁的STW也会使用户感觉网站很卡,用户体验不是很好。
JVM虚拟机调优的目的: 减少 full gc 次数,也就是减少STW次数,减少了STW次数也就是减少了暂停用户线程的次数,卡顿次数少,使用户体验更好。
1、评估系统每秒产生对象的大小
2、新产生的对象在一秒后如果变为垃圾对象,是否会被在minor gc清理
3、可以通过对象的年龄和在minor gc时是否占用survivor区50%,判断对象是否需要被清理还是放到老年代
4、如果在minor gc时频繁的通过占用survivor区50%将对象放到老年代,其实这些对象在一秒后也变成了垃圾对象,但是被放到了老年代,需要通过full gc才会被清理,表示年轻代配置不合理,这时需要加大年轻代的占用空间
优化前参数:
优化后参数:
JVM优化之 -Xss -Xms -Xmx -Xmn 参数设置
新生代占比: 堆的 1/3
老年代占比: 堆的 2/3
新生代内部划分占比: 分成10份,伊甸区:8/10,两个survivor区分别为1/10
-Xms 堆内存的最小大小,默认为物理内存的1/64
-Xmx 堆内存的最大大小,默认为物理内存的1/4
-Xmn 堆内新生代的大小。通过这个值也可以得到老生代的大小:-Xmx减去-Xmn
-Xss 设置每个线程可使用的内存大小,即栈的大小。在相同物理内存下,减小这个值能生成更多的线程,当然操作系统对一个进程内的线程数还是有限制的,不能无限生成。线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。
频繁的 full gc ,会导致网站很卡。
分析:
在促销的时候每秒产生1000多单,有3台8G内存的订单系统,分配到每台就是300单/秒。
假如每个订单对象是1KB,每秒就是300KB,下单的时候还会涉及到其他对象,比如库存,积分之类的,差不多放大20倍,每秒产生的对象300KB20对象。
可能同时还会有其他的操作,比如订单查询,库存查询,再放大10倍,每秒产生的对象就是 300KB20*10 = 60M,在一秒后都变为垃圾对象。
JVM设置给堆的大小是3G,默认老年代是2G,年轻代是1G,年轻代按照8:1:1划分,eden就是800M,每个s区分别就是100M。
按照每秒产生60M对象,在13秒的时候就会占满eden区,进行young gc,这时有一秒的对象是存活的,也就是60M,会进入s区,根据如果在一次young gc后,一批存活的对象总大小大于s区内存大小的50%,那么此时这批对象就可以直接进入老年代。那么我们每13秒就会有60M对象进入老年代,其实这批60M的对象在一秒后就变为垃圾对象了,老年代大小是2G,2048/60*13 差不多在7分钟左右就把老年代占满了,这时会进行full gc把所有的垃圾对象清除。
每7分钟左右进行full gc是不合理的,因为full gc是重量级的,stw会比较长,会导致系统很卡。
我们优化的方向因为是在young gc阶段就把垃圾对象清除掉,因为young gc是轻量级的,stw停顿几乎可以忽略,几乎不会对系统产生影响。
根据上面的案例我们就可以吧年轻代的大小调高到2G,这时eden就有1.6G。每个s区就是200M,这时候大概运行25秒的时候回占满eden,然后进行young gc,存活的对象就是60M,这时没有超过s区的一半,所以不会进入老年代,直接放在s区了,在进行下一次young gc的时候,还是只有60M对象存活,保证了垃圾对象在young gc的时候就被清除了,只有少量一直存活的对象会进入老年代,不会频繁的进行full gc了。
通过可达性分析来判定对象是否存活。
通过一系列被 GC Roots 引用的对象,作为起点,从这些起点向下搜索,当一个对象没有被任何一个 GC Roots 引用时,该对象可以回收。
什么对象是"GC Roots"的对象?
复制算法:
将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当一块内存用完了,将存活的对象复制到另一块上面,然后把已使用的内存空间一次清理掉。
这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等情况,对象按顺序分配内存即可,实现简单,运行高效。
只是这种算法将内存缩小为原来的一半,代价较高。
标准-清除:
分为两个阶段:首先标记出需要回收的对象,在标记完成后统一回收被标记的对象。
标记清除后会产生大量不连续内存碎片。
标记-整理:
与 标准-清除 算法一样,但后续不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。解决了内存碎片问题。
年轻代垃圾回收算法: 使用了复制算法,划分了两个 s 区,总有一个 s 区是空的,用来存放存活的对象。因为每次 young gc 存活的对象很少,不需要划分很大的内存空间,减少了代价。
老年代垃圾回收算法: 使用了 标记-整理。
-XX:PretenureSizeThreshold=
参数(默认是0,表示任何对象首先在 Eden 区分配),大于这个值的参数直接在老年代分配,避免新生代中的 Eden 区及两个 Survivor 区发生大量内存复制。-XX:MaxTenuringThreshold
(默认是15)参数设定的值时,将会移动到老年代。参考:
JVM的体系结构及底层原理
jvm诸葛老师笔记
JVM Minor GC 与 FullGC 的触发时机