jvm 又称java 虚拟机。一个庞大的体系,只有慢慢认识了它,才能玩的更好。
本文本着what,why,how 的原则展开,
1、JVM内存模型是啥?每个分区组成是啥?
2、运行时数据区有啥?堆内存结构 特点是啥?
3、gc 3种收集器的原理是啥?为啥这么搞?
4、如何使用gc 调优工具?how
首先从大局上给jvm一个定位哈
从图中可看出,jvm操作的是操作系统,与操作系统交互的是执行引擎,执行引擎负责解释命令,提交给操作系统。
接着认识下jvm内部组成。
中间区域即运行时数据区由5部分组成,程序计数器、堆、栈、方法区、本地方法区
注意粉红色区域是gc起作用的区域、说白了就是要进行垃圾回收的哈~
通常来说,我们部署的应用是jar,war 都是.class文件,那么一个.class文件如何加载到jvm中呢,请看下图
上图这个东东,让一个class文件—loader–Class对象模板构建到内存中。如下:
Java栈(线程私有-gc不优化)
线程私有 thread,copy 主内存,引用,
栈由一系列帧组成(因此Java栈也叫做帧栈)
帧保存一个方法的局部变量、操作数栈、常量池指针
每一次方法调用创建一个帧,并压栈
存啥-(method 表,局部变量表,操作数栈 指向运行时常量池引用 方法返回地址....入栈 8种类型+引用变量+方法 copy+入栈 出栈操作~)
pc寄存器、指针-指向方法区中字节指令
线程私有-
Java堆(线程共享-gc管辖 95%,红色)
和程序开发密切相关
应用系统对象都保存在Java堆中
所有线程共享Java堆
对分代GC来说,堆也是分代的
GC的主要工作区间 gc,new,
新生区,养老区,永久区(元空间-不变信息 堆外空间-同方法区)
新生区-细分3个
永久区-常量池 jdk 1.7,jdk 1.8,但jdk 1.6在方法区
方法区(共享-gc管辖 5%,红色)
方法区是啥?接口、规范、定义
保存装载的类信息
static,构造函数,class name,package name
静态变量+常量 final+类信息(构造方法+接口定义)+运行时常量池 string a="ccc"
字段,方法信息 filed name,method,inferce,
方法字节码
通常和永久区(Perm)关联在一起
栈、堆、方法区交互
栈存-堆对象的地址,堆的对象来自方法区模板Class(类信息)
内存模型
每一个线程有一个工作内存和主存独立
工作内存存放主存中变量的值的拷贝
每一个操作都是原子的,即执行期间不会被中断
对于普通变量,一个线程中更新的值,不能马上反应在其他变量中
如果需要在其他线程中立即可见,需要使用 volatile 关键字,强制从主内存取~
对于堆得认识我们需要进一步分析…
堆内部又分三块,新生,老年,元空间。对于新生代又分分三块,我们通过gc日志认识一下
首先我有个问题判断对象为垃圾对象,也就是此对象已经废了、jvm回收垃圾时,如何确定垃圾-gcRoots
初步猜测,每一个对象都通过引用来使用。那么没有引用不就废弃了。
其实猜的八九不离十,有两种策略
1、引用计数法:对象引用一次加1,为0时 回收 缺点:性能差,无法解决a,b互相引用
2、可达性分析:用到了gcroot、对象和gcroot之间是否有链路 无链路就回收 说白了-gcroot就是对象的引用区。
那么gcRoot里有哪些对象呢?---(局部变量表,方法区类属性引用,方法区常量引用,栈引用)
public class GCRoot {
private static GCRoot1 root2;
private static final GCRoo2 root3;
public static void mm() {
GCRoot root1=new GCRoot();
System.gc();
}
}
// 分析:方法区的static -final, 和方法mm中的局部变量都可作为gcroot对象
图为gcRoot和对象之间的关系
确定是垃圾了,采取何种算法回收垃圾 ?
1、引用计数 2、复制 3、标记清除 4、标记整理,其中2作用于新生代,3 4作用于老年代
tomcat需要在catalina.sh加参数
JAVA_OPTS="-XX:+PrintHeapAtGC -XX:+PrintGCDetails -server -Dfile.encoding=UTF-8 -Xms1024m -Xmx3096m -XX:PermSize=512m -XX:MaxNewSize=512m -XX:MaxPermSize=512m -Djava.awt.headless=true -Dfile.encoding=UTF8 -Djava.security.egd=file:/dev/./urandom -Djavax.net.debug=all"
了解了啥是垃圾,如何回收垃圾、那么给你一个正在运行的java程序、你如何查到某个jvm参数是否开启,值是啥 ?jvm参数配置-从大方向上分为3种。标配参数,x参数,xx参数。重点了解xx参数。其他2种都是不变的。
1、boolen类型 举例: -XX:+PrintGCDetails +开启,-关闭
2、key-value类型: -XX:ParallelGCThreads=20 key=value类型
如何查看系统默认jvm参数 ?工作常用jvm参数 ?
[root@vm10-0-1-157 ~] jps -l //java ps-可查看进程号对应java程序
3137 eureka-7003.jar
11874 sun.tools.jps.Jps
3114 eureka-7002.jar
细粒度查看,查看某个参数大小
jinfo -flag PrintGCDetails\MetaspaceSize 进程号
// java ps -查看机器上个的java进程
[root@vm10-0-1-157 ~]# jps -l
3137 eureka-7003.jar
3114 eureka-7002.jar
26381 sun.tools.jps.Jps
[root@vm10-0-1-157 ~]#
// 查看某一个java进程-jvm 默认值
[root@vm10-0-1-157 ~]# jinfo -flags 3137
Attaching to process ID 3137, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.181-b13
Non-default VM flags: -XX:CICompilerCount=2 -XX:InitialHeapSize=31457280 -XX:MaxHeapSize=482344960 -XX:MaxNewSize=160759808 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=10485760 -XX:OldSize=20971520 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
Command line:
[root@vm10-0-1-157 ~]#
[root@vm10-0-1-157 ~]# jinfo -flag InitialHeapSize 3137
-XX:InitialHeapSize=31457280
有个问题,参数都是 -XX: key=value.boolean这种值 ,那么xms xmx 如何看 ?
// 1、查看jvm 所有默认参数-
[root@vm10-0-1-157 ~]# java -XX:+PrintFlagsInitial
[Global flags]
uintx AdaptiveSizeDecrementScaleFactor = 4 {product}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product}
uintx AdaptiveSizePausePolicy = 0 {product}
uintx AdaptiveSizePolicyCollectionCostMargin = 50 {product}
uintx AdaptiveSizePolicyInitializingSteps = 20 {product}
uintx AdaptiveSizePolicyOutputInterval = 0 {product}
2、查看jvm修改的变量值:
java -XX:+PrintFlagsFinal -version
// := 修改的值
运行时实时修改
java -XX:+PrintFlagsFinal -XX:MetaspaceSize=512M Demo.java
3、java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=30141312 -XX:MaxHeapSize=482260992 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)
System.out.println(Runtime.getRuntime().maxMemory());
System.out.println(Runtime.getRuntime().totalMemory());
System.out.println(Runtime.getRuntime().availableProcessors());
工作常用参数:
-XX:+UseParallelGC 并行垃圾回收器
-XX:NewSize=41943040
-XX:OldSize=83886080
-XX:InitialHeapSize=125829120 ==-Xms
-XX:MaxHeapSize=2011168768 ==-Xmx
-XX:MaxNewSize=670040064 ==-Xmn
-XX:ThreadStackSize=1024k ==Xss
-XX:MetaspaceSize=512m
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:NewRatio=2
-XX:MaxTenuringThreshold=15
-Xms10m -Xmn10m -XX:+PrintGCDetails
栈内存
1、[root@vm10-0-1-157 ~]# jinfo -flag ThreadStackSize 3137
-XX:ThreadStackSize=1024
Xss == -XX:ThreadStackSize 单个线程栈的大小,默认512k-1024k
2、 -XX:MetaspaceSize=512m
3、-XX:SurvivorRatio=8 新生代3个区比例调整-
4、-XX:NewRatio=2 老年代占用大,避免频繁gc
5、-XX:MaxTenuringThreshold=15 设置垃圾的进入老年代最大年龄
谈谈强引用,软引用,弱引用,虚引用? 4种写法,加速对象 gc
(1)、强引用-
public static void test1(){
//强引用-
Object obj1=new Object();
Object obj2=obj1;
obj1=null;
System.gc();
System.out.println(obj2);
}
// 运行结果:
java.lang.Object@47d384ee
分析:手动触发gc,不回收-
(2)、软引用
设置jvm -Xms10m -Xmn10m -XX:+PrintGCDetails
public static void test2(){
//软,
Object obj1=new Object();
SoftReference<Object> obj=new SoftReference<>(obj1);
obj1=null;
System.gc();//内存够用,手动 gc
//内存不够用,自动 gc
Byte[] bytes=new Byte[30*1024*1024];
System.out.println(obj1);
System.out.println(obj.get());
}
(3)、弱引用-
public void test3(){
//弱引用-
Object obj1=new Object();
WeakReference<Object> obj=new WeakReference<>(obj1);
obj1=null;
System.gc();//内存够用,手动 gc
//内存不够用,自动 gc
System.out.println(obj1);
System.out.println(obj.get());
}
public void test4(){
WeakHashMap<Integer,String> map=new WeakHashMap<>();
Integer aa=new Integer(2);
map.put(aa,"2222");
System.out.println(map);
//弱引用-null、立即回收-
aa=null;
System.gc();
System.out.println(map);
}
应用场景:
(4)、虚引用
public static void test6(){
//虚引用-
Object obj1=new Object();
ReferenceQueue<Object> rq=new ReferenceQueue<>();
PhantomReference<Object> obj=new PhantomReference<>(obj1,rq);
System.out.println(obj1);
System.out.println(obj.get());
System.out.println(rq.poll());//引用队列-保存
System.out.println("======================");
obj1=null;
System.gc();//内存够用,手动 gc
//内存不够用,自动 gc
System.out.println(obj1);
System.out.println(obj.get());
System.out.println(rq.poll());//虚引用干掉之前-,保存-引用队列、做最后的挣扎...
}
5、oom有哪些?
6、垃圾收集器与垃圾算法,服务器默认是?
7、G1垃圾收集器
下面是一个案例抓取的gc日志,分析—总内存大小10m,b1-b2对象大小逐渐加大。
public static void main(String[] args) {
byte [] b1 = new byte[2 * 1024 * 1024];
byte [] b2 = new byte[2 * 1024 * 1024];
byte [] b3 = new byte[2 * 1024 * 1024];
byte [] b4 = new byte[4 * 1024 * 1024];
}
堆内存参数设置:-Xms10m -Xmn10m -XX:+PrintGCDetails
打印日志:gc 新生代,full gc 老年代
[GC (Allocation Failure) a,b,c可分配,d失败因为有6m [PSYoungGen: 5957K->1023K(8704K)] 5957K->3193K(11776K), 0.0079851 secs] [Times: user=0.01 sys=0.02, real=0.01 secs]
触发gc [Full GC (Ergonomics) [PSYoungGen: 1023K->443K(8704K)] [ParOldGen: 2170K->2664K(6144K)] 3193K->3107K(14848K), [Metaspace: 3222K->3222K(1056768K)], 0.0212271 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
Heap
PSYoungGen total 8704K, used 4737K [0x00000000ff600000, 0x00000000fff80000, 0x0000000100000000)
eden:from:to新生区为 8:1:1, 地址偏移量~
eden space 7680K,8m 55% used [0x00000000ff600000,0x00000000ffa314a0,0x00000000ffd80000)
from space 1024K,1m 43% used [0x00000000ffd80000,0x00000000ffdeefa0,0x00000000ffe80000)
to space 1024K,1m 0% used [0x00000000ffe80000,0x00000000ffe80000,0x00000000fff80000)
ParOldGen total 10752K, used 6760K [0x0000000088200000, 0x0000000088c80000, 0x00000000ff600000)
object space 10752K, 62% used
a,b,c复制到老年代 yong gc [0x0000000088200000,0x000000008889a018,0x0000000088c80000)
Metaspace used 3230K, capacity 4600K, committed 4864K, reserved 1056768K
class space used 346K, capacity 424K, committed 512K, reserved 1048576K
通过日志分析:
此处用到了复制算法:为何复制?
a,b,c 加起来超多6m、空间不够需要向其他区域转移。
这个算法有啥特点:eden挪到to 清理eden空间- gc时,效率高,无内存碎片、但浪费空间.
从上面可得出几个原则,
1、优先分配 eden 12%
2、大对象直接分配到老年代 -XX:PretenureSizeThreshold=6m
3、长期存活对象分配到老年代 -XX:MaxTenu ringThresho=15 达到15,进入老年代,如何算长期存活。新生代 from<->to 对象在这两个区来回挪动,每 gc一次 年龄加1,经历15次 直接 挪到老年代
对于gc 有专门的线程工作。我们看一道面试题
讲讲垃圾回收?
有关垃圾回收,我们首先要考虑两个问题。一是如何判断对象可回收,二是用什么样的方式来回收。
首先对于前者,有引用计数法和可达性分析两种方法,它们分别……(讲讲它们的含义,优缺点)
而对于后者,市面上主要有标记-清除,标记-整理,复制三种回收算法,它们分别……(讲讲含义,优缺点)
结合这些算法,市面上就出现了很多垃圾收集器,例如Serial,ParNew,CMS,G1……(顺便讲讲它们的回收逻辑,优缺点)
垃圾回收的话,自JDK1.8后,市面上就非常流行G1垃圾回收器了。它是不分新生代和老年代的,基本原理是……(讲讲含义,优点)
但是垃圾的频繁回收势必会导致用户体验的下降,虽然G1已经很优秀了,作为开发者我们还是需要关注JVM的优化(话锋一转,开始走向深、讲一些具体的优化策略)
gc对内存如何管理对象如何销毁GC(GarbageCollection)是垃圾回收机制(垃圾回收器),GC是JVM对内存(实际上就是对象)进行管理的方式
1、Java有了GC,就不需要程序员去人工释放内存空间。GC使得Java开发人员摆脱了繁琐的内存管理工作
2、当Java虚拟机发觉内存资源紧张的时候,就会自动地去清理无用变量所占用的内存空间
3、程序员可以在Java程序中显式地使用System.gc()或Runtime.getRuntime().gc()来通知垃圾回收程序
4、回收是垃圾收集器的一个动作,结果就是释放内存
5、实际上随着分配的对象增多, GC的时间与开销将会放大。所以,JVM的内存被分为了三个主要部分:新生代,老年代和永久代
所有新产生的对象一律都在新生代中, Eden区保存最新的对象,有两个 SurvivorSpace—— S1和 S0,三个区域的比例大致为 8:1:1
当新生代的 Eden区满了,将触发一次 GC,我们把新生代中的 GC称为 minor garbage collections
使用SerialGC的场景:
1、如果应用的堆大小在100MB以内。
2、如果应用在一个单核单线程的服务器上面,并且对应用暂停的时间无需求。
使用ParallelGC的场景:
如果需要应用在高峰期有较好的性能,但是对应用停顿时间无高要求(比如:停顿1s甚至更长)。
使用G1、CMS场景:
1、对应用的延迟有很高的要求。
2、如果内存大于6G请使用G1。