07-JVM初探学习记录
JVM
常见面试题
- 谈谈对JVM的理解?java8虚拟机和之前的变化更新?
- 什么是OOM,什么是栈溢出StackOverFlowError?怎么分析
- JVM常用调优参数有哪些?
- 内存快照如何抓取,怎么分析Dump文件?
- JVM中,类加载器的认识?
JVM位置
JVM的体系结构
java栈、本地方法栈、程序计数器不可能存在垃圾,所以JVM调优99%是对堆和方法区(方法区属于特殊的堆)的优化
类加载器
作用:加载class文件 new xx();
负责加载class文件(classs文件在文件开头有特定的文件标识),将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构;ClassLoader只负责加载class文件的加载,至于它是否可以运行,则由Execution Engine决定。
1、BootStrapLoader(引导类加载器)
2、Extension ClassLoader(扩展类加载器)
3、Application ClassLoader(应用程序类加载器)
4、虚拟机自带的加载器
Car c1 = new Car();
Car c2 = new Car();
Car c3 = new Car();
System.out.println(c1.hashCode());
System.out.println(c2.hashCode());
System.out.println(c3.hashCode());
Class<? extends Car> c1Class = c1.getClass();
System.out.println(c1Class);
System.out.println(c1Class.hashCode());
System.out.println(c1Class.getClassLoader()); //AppClassLoader
System.out.println(c1Class.getClassLoader().getParent()); //ExtClassLoader jre/lib/ext/*.jar
System.out.println(c1Class.getClassLoader().getParent().getParent()); //null, Java程序获取不到,用C写的 rt.jar
String s = "kaka";
System.out.println(s.getClass().getClassLoader());
//===========================================460141958
1163157884
1956725890
class com.kaka.jvm.pojo.Car
356573597
sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$ExtClassLoader@677327b6
null
460141958
1163157884
1956725890
class com.kaka.jvm.pojo.Car
356573597
sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$ExtClassLoader@677327b6
null
null //根加载器
双亲委派机制
package java.lang;
public class String {
/**
* 双亲委派机制:安全考虑
* APP===》EXT ===》 BOOT(最终执行根加载器中的)
* 只有当EXT和BOOT中没有这个类的时候,才会使用APP
*
*
* 1、类加载器收到类加载的请求
* 2、将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
* 3、启动类加载器检查是否能加载当前这个类,能加载就结束,否则抛出异常,通知子类加载器进行加载
* 4、直到ApplicationClassLoad,如果也不能加载,抛异常 ClassNotFoundException
* @return
*/
public String toString(){
return "kaka";
}
public static void main(String[] args) {
String s = new String();
s.toString();
}
}
//=====================================
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
沙箱安全机制
组成沙箱的基本组件:
- 字节码校验器(bytecode verifier):确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类
- 类加载器(ClassLoader):类加载器在三个方面对沙箱起作用
- 防止恶意代码干涉善意代码;
- 守护了被信任的类库边界;
- 将代码归入保护域,确定代码可以进行哪些操作
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被加载的类将有一个名字,这个命名空间是由java虚拟机为每一个类加载器维护的,他们之间也是不可见的。
类加载器采用的机制是双亲委派机制:
- 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
- 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内部类,破坏代码就自然无法生效。
- 存取控制器(AccessController):可以控制核心API对操作系统的存取权限,而控制的策略设定可以由用户指定;
- 安全管理器(SecurityManager):是核心API和操作系统之间的主要接口,实现权限控制,比存取控制器优先级高;
- 安全软件包(SecurityPackage):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
- 安全提供者
- 消息摘要
- 数字签名 keytools
- 加密
- 鉴别
native
new Thread(()->{
System.out.println("猎杀时刻开始了");
},"my thread").start();
//java.lang.Thread#start0
private native void start0();
凡是带了native关键字的,说明java的作用范围达不到了,会去调底层c语言的库!
会进入本地方法栈 ----》调用本地方法接口(JNI java native interface)
JNI作用:扩展java的使用,融合不同的编程语言为java所用,现在如果想要使用其他语言的功能,可以使用socket、webservice、http
java专门为native开辟了一块标记区域-本地方法栈(native method stack),负责登记native方法,在执行引擎(Execution Engine)最终执行的时候,通过JNI加载本地方法库中的方法
PC寄存器
程序计数器:program counter register
每一个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也就是即将执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
方法区
method area:
- 方法区是被所有线程共享,所有字段和方法字节码以及一些特殊的方法,如构造函数、接口代码,简单的说所有定义的方法的信息都保存在该区域,此区域属于共享区间
- ==静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池也在方法区中,但是实例变量存在于堆内存中,与方法区无关==
- static 、final、Class类模板、常量池
栈
栈内存,主观程序的运行,生命周期和线程同步,线程结束,则栈内存释放,所以对于栈来说,不存在垃圾回收问题。
内容:
- 八大基本类型
- 对象引用
- 实例方法
运行原理:栈帧(一个方法可以不认为就是一个栈帧)
栈如果满了,就会抛出错误:StackOverflowError
三种JVM
① Sun公司的HotSpot ② BEA公司的JRockit ③ IBM公司的J9 JVM
在JDK1.7及其以前我们所使用的都是Sun公司的HotSpot,但由于Sun公司和BEA公司都被oracle收购,
jdk1.8将采用Sun公司的HotSpot和BEA公司的JRockit两个JVM中精华形成jdk1.8的JVM
堆 heap
一个JVM只有一个堆内存,堆内存的大小是可以不调节的。
类加载器读取了类文件后,会将 类、方法、常量、变量等所有引用类型的真实对象保存到堆内存中!
堆内存分类:
- 新生区(伊甸园区)young/new youngGC
- 养老区old FullGC
- 永久存储区(JDK8以后,改成元空间,归并到方法区)
堆内存满了,也会抛出错误OOM(OutOfMemoryError)
新生区
- 类:诞生和成长的地方,甚至死亡
- 分为伊甸园区(Eden space)、幸存0区、幸存1区,比例默认是8:1:1
老年区
youngGC没有清理掉的
永久区
- jdk6之前:永久代,常量池在方法区中
- jdk7:永久代,慢慢退化,提出
去永久代
,常量池在堆中 - jdk8:无永久代,常量池在元空间
这个区域是常驻内存的,用来存放jdk自带的Class对象、Interface元数据,存储的是java运行时的一些环境或类信息,这个区域不存在垃圾回收,关闭JVM虚拟机就会释放这个区域的内存。
崩溃情况
- 一个启动类加载了大量的三方jar包
- Tomcat部署了太多的应用
- 大量动态生成的反射类,不断的被加载,知道内存满就会出现oom
//
long maxMemory = Runtime.getRuntime().maxMemory();
//返回jvm的初始化总内存
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("max:"+maxMemory/1024.0/1024);
System.out.println("total:"+totalMemory/1024.0/1024);
//=====================
max:1792.0
total:121.0
16:1
//调整JVM参数
//-Xms1024m -Xmx1024m -XX:+PrintGCDetails;
max:981.5
total:981.5
Heap
PSYoungGen total 305664K, used 26214K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
eden space 262144K, 10% used [0x00000000eab00000,0x00000000ec499be8,0x00000000fab00000)
from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
Metaspace used 3290K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 358K, capacity 388K, committed 512K, reserved 1048576K
//PSYoungGen + ParOldGen = 305664K + 699392K = 981.5m = total;
//Metaspace : 不在堆中
在一个项目中,突然出现oom故障,name如何排查?
- Debug,一行行分析代码(线上问题不好排查)
- 内存快照工具:MAT 、Jprofiler
MAT 、Jprofiler作用:
- 分析Dump内存文件,快速定位内存泄漏;
- 获取堆中的数据
- 获得大的对象
下载安装打开客户端:
打开idea
配置jvm参数:-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid124.hprof ...
Heap dump file created [7719145 bytes in 0.023 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.kaka.jvm.Demo03.<init>(Demo03.java:7)
at com.kaka.jvm.Demo03.main(Demo03.java:15)
用Jprofiler打开该文件:java_pid124.hprof
public class Demo03 {
byte[] arr = new byte[1*1024*1024]; //1m
public static void main(String[] args) {
ArrayList<Demo03> list = new ArrayList<>();
int count = 0;
try{
while(true){
list.add(new Demo03());
count++;
}
}catch (Error e){
System.out.println("失败次数:"+count);
e.printStackTrace();
}
}
}
//=================================
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid11272.hprof ...
Heap dump file created [7722385 bytes in 0.017 secs]
失败次数:6
java.lang.OutOfMemoryError: Java heap space
at com.kaka.jvm.Demo03.<init>(Demo03.java:7)
at com.kaka.jvm.Demo03.main(Demo03.java:15)
GC
JVM在进行GC时,并不是对三个区域统一回收,大部分时候都是回收新生代
- 新生代
- 幸存区(from、to)
- 老年代
GC两种类:轻GC、重GC,也可以认为是youngGC和fullGC
面试题:
- JVM的内存模型和分区,详细到每个分区放什么?
- 堆里面的分区有哪些?Eden、from、to、old,说说各区特点?
- GC的算法有哪些?标记清除法、标记压缩、复制算法、引用计数器,怎么用的?
- 轻GC和重GC分别在什么时候发生?
引用计数(Java没有采用)
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,但无法解决对象相互循环引用的问题,如下图所示。
①当出现左边绿色的情况时,假设外部对A的引用消除,此时因为A引用计数从1减为0,A将被清除。从而对B的引用也消除,B的计数减为0,GC将正确回收对象{A, B}
②然而若出现右边橙色状况,假设外部对E的引用消除,外部对于对象集{C,D,E}不再有引用,但他们之间出现循环引用现象,计数始终保持为1,导致{C,D,E}无法被回收。
复制算法(jvm新生代回收)
-
每次GC都会将Eden活的对象移到幸存区中,一旦Eden被GC,就会变为空的!
-
当一个对象经历了15次GC,都还没有被清理,就会到老年代。这个次数由下面这个参数控制
-
适用场景:对象存活度较低的时候
-XX:MaxTenuringThreshold=15
MaxTenuringThreshold设置垃圾的最大年龄. 默认为15 . 最大也是15,在jdk8中. 范围为 0到15.
标记清除法(jvm老年代回收)
标记压缩(jvm老年代回收)
优化标记清除算法,防止内存碎片产生
总结
内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
内存整齐度:复制算法=标记压缩算法>标记清除算法
内存利用率:标记压缩算法=标记清除算法>复制算法
疑问:没有最优算法么?
答:没有最好的算法,只有最合适的算法------》GC:分代回收算法
新生代:
- 存活率低
- 复制算法
老年代:
- 区域大:存活率
- 标记清除(内存碎片不是太多)+标记压缩混合实现
JMM
Java Memory Model(JMM)java内存模型,区别与java内存结构。JMM定义了一套在多线程读写共享数据(变量、数组)时,对数据的可见性、有序性和原子性的规则和保障。
参考:https://www.jianshu.com/p/51768fcdf6cb
synchronize和volatile对比
1、volatile是线程同步的轻量级实现,性能比synchronize好 2、volatile只能修饰变量,而synchronize可以修饰方法、代码块和变量 3、volatile多线程时不会发生阻塞,而synchronize会阻塞线程 4、volatile可以保证可见性和有序性(禁止指令重排),无法保证原子性,而synchronize都可以保证 总结:volatile就是保证变量对其他线程的可见性和防止指令重排序 而synchronize解决多个线程访问资源的同步性