- JDK体系结构
- Javac
- 类的加载机制
- 类加载器结构
- 双亲委派、父类委托
- 双亲委派的好处
- 如何打破双亲委派
- 运行时数据区
- 方法区
- 栈(线程私有)
- 程序计数器(程序计数器)
- 栈帧
- public static类变量 和 public static final全局变量的初始化时间
- 对象在内存中的布局
- 堆内存
- 老年代、元空间、永久代、方法区?
- 为什么要分young和old
- 为什么young要分eden和survivor?
- 为什么经过15次GC后,young的对象会被放入old?
- MetaSpace,为什么使用元空间代替永久代?
- 什么时候发生栈内存溢出?
- 什么时候发生堆内存溢出?
- 内存泄漏怎么发生的?
- 引用类型?
- ThreadLocal
- 说一说OOM发生时你的定位思路?
- 什么是垃圾对象
- 可达性分析算法(跟踪搜索算法)
- 什么时候GC
- 标记清除算法
- 复制算法
- 标记整理算法
- 垃圾回收器选择
- 并行和并发的概念
- 垃圾收集器
- STW
- CMS垃圾收集器
- 三色标记法(CMS垃圾收集器并发标记时的算法)
- JVM参数
- 获取对象在内存中大小分布情况
- Object obj = new Object()占多少内存空间?
- 如果Object有一个int 一个 String 类型的属性呢。
未经允许所有内容都可都可都可转载,不对的地方欢迎指正
JDK体系结构
JDK = jre (java运行时环境) + tool (编译器,打包工具等) JRE = java虚拟机 + java常用API
Javac
javac 将java文件编译成字节码文件.class,因为jvm识别的是字节码文件。 javac 做的就是.java文件和.class文件的对等信息转换。
类的加载机制
- 装载 -> 连接 -> 初始化 -> 使用 -> 卸载
装载
- 将class文件转化为——> 字节流 ——> 由类加载器加载
- 类加载器将自己流中所代表的静态数据接口 ——> 转化为方法区的运行时数据
- java堆中产生一个class文件对象 ——> 作为方法区中静态数据访问入口
连接 (验证—> 准备 —> 解析)
1、 验证: 验证字节码文件是否正确 2、 准备: 为类变量分配内存,赋值默认值,例如 private static int a = 1; 此时test分配的默认值为0 3、解析: 将符号引用变为直接引用,在类的编译期间,类中的引用在内存中的地址无法确定。因此都市符号引用。 例如: A.class调用了 B.class的test方法,A就包含了对B整个类的符号引用。通过解析,jvm会把整个引用编程一个指针指向B类test中方法在方法区中的位置。这个指针就是直接引用。
类加载器结构
启动类加载器 Bootstrap ClassLoader : C 语言编写,主要负责加载 %JAVA_HOME%/lib下的jar包和class文件。 扩展类加载器 ExtClass Loader: 负责加载 %JAVA_HOME%/lib/ext文件夹下的jar包和class文件。 AppClassLoader 负责加载classpath下的类文件。 如果我们需要自定义类加载器,需要继承AppClassLoader。
双亲委派、父类委托
1、当一个类需要被加载,先使用类名加锁。 2、查看该类是否被加载,如果没有加载继续。 3、在程序没有自定义类加载器的时候,程序写的类都是AppClassLoader加载的。他的双亲就是ExtensionClassLoader和BootStropClassLoader,双亲委派就是指我们的类被加载时,AppClassLoader不会直接加载,先交给父类ExtClassLoader加载,如果父类ExtClassLoader无法加载,ExtClassLoader也不会加载,继续往上交给BootStrapClassLoader。只有父类加载器无法加载的时候,参会由子类加载器加载。如果BootStapClassLoader 由上到下都无法加载,程序最终会抛出ClassNotFoundException (每个类加载器找不到都会抛出整个异常,源码中写的只有最后一个子类找不到才会抛出)
双亲委派的好处
如果没有双亲委派,每个类加载器都可以不管父类,自行加载类。 会导致同名实际上不同的类被同时加载。 例如,我么可以在src下建立ava.lang包,建立一个String类,在有双亲委派的机制下,我们自己编写的类可以正常编译,但是无法被正常加载。 如果没有双亲委派,类会被重复加载造成混乱。同时核心jar有被篡改的风险。
如何打破双亲委派
自定义类加载器,重新loadClass方法。
运行时数据区
- 包括 堆、虚拟机栈、本地方法栈、程序计数器、方法区
方法区
1、类型信息(类信息):
1) 类的完整有效名称,即包名加类名2) 父类完整有效名称3) 类的修饰符 (public abstract final)4) 实现的直接接口的有序列表
2、域名信息:(属性信息)
1) 属性字段的声明顺序 2) 字段的修饰信息(private protected public)3、方法信息:
1) 声明顺序 2) 返回值类型 3) 参数类型 4) 修饰符信息 5) 方法的字节码、操作数栈、局部变量表 6) 异常表, 每个异常处理的开始位置、结束位置、被捕获的 异常类的常量池索引4、运行时常量池:
栈(线程私有)
java虚拟机栈:每调用一个方法,就会进一次栈,线程私有 栈帧: invoke一个方法就会压一次栈 本地方法栈: native方法
程序计数器(程序计数器)
多线程执行时,抢占cpu资源,每个线程执行过程中,如果没有了执行权,程序计数器会记录该线程所执行到的位置即栈帧信息,到重新抢占到执行权后,从程序计数器记录位置继续执行。 每个线程有一个程序计数器,即线程私有。
栈帧
- 局部变量表: 局变变量、方法参数
- 操作数栈: 计算数据
- 动态连接: 符号引用变为直接引用,多态
返回地址: 正常 (return 或者 void),返回PC程序计数器返回地址值。
异常后如果外层无捕捉,需要异常处理器处理。 无论正常异常都需要返回到该方法指令的下一条指令的地址。Tread.currentThread.getStackTrace() 就是通过栈帧获取方法名称等信息。
public static类变量 和 public static final全局变量的初始化时间
non-final 类变量:
1) 静态变量和类关联在一起,随着类的加载而被加载。2) 所有实例共享,即使没有实例也可以访问。 初始化时间:在类加载的第二个阶段(装载、连接、初始化、使用、卸载)的连接阶段会被赋值默认值。然后在第三个阶段初始化阶段 会被显示初始化(赋值)
final类变量:
编译期就确定了final不可变对象,连接的第二个阶段又分为了(验证、准备、解析)三个阶段。 final类变量的初始化时间为准备阶段、而non-final的类变量在连接阶段的准备阶段只是赋值了默认中,在第三阶段初始化阶段才会被赋值。
对象在内存中的布局
- 对象头
1) markword (hash码,分代年龄,锁状态标志) 2) classPoint (指定对象对应的类元数据中的内存地址) 3) length (数组对象特有的长度)
- 实例数据
- 填充部门,保证对象大小为8字节的整数倍。
为什么是8的整数倍? 64位操作系统,每次寻址的单位为64bit,即8b, 8字节。
堆内存
- 年轻代 8(Eden):1(S0):1(s1)
1) 年轻代的特征,朝生夕灭,GC比较频繁,采用的是复制算法。 2) 由于采用的复制算法,因此 S0和S1的大小必须相等,这个是能复制的前提。 3)复制算法必须要保证S0 或者 S1 始终有一个是空着的。也就是说,Eden的空间最高达到90%就要进行垃圾回收。 4) 一般情况下,是内存满了,才gc,如果不是8:1:1,如果是 1:1:1 ,就意味着在内存到66% 的时候就要进行GC,这个显然是不合理的。 90% 与100% 比较接近。 而且 大多数对象都不会进入 幸存区。 综合 幸存区放10%的空间就足够 和 90%与100% 相差不大。默认采用了8:1:1
- 老年代:
1) 与eden的比例比例为2:1 2) 为什么是2:1,首先,当年轻代的大对象放不下,直接交给老年代管理,极端场景下,即年轻代为空老年代直接放的场景,也要保证老年代空间可以满足整个机制,所以,老年代至少要比年轻代大。 其次,经过15次 minorGC无法回收的对象也需要往来年代放,综合考虑设置为2:1
老年代、元空间、永久代、方法区?
- 方法区: 是JVM规范中定义的一个概念,用户存储类信息、常量池、静态变量、JIT编译后的代码数据等等。具体放在什么地方,不同的实现可以放在不同的地方!!!
- java7之前,永久代类似方法区的实现,没有把所有东西放永久代。
- java8,永久代被移除,取代的是一块与堆不相连接的一块本地内存- 元空间(Metaspace).
为什么要分young和old
内存中有两种对象: 一种朝生夕灭,声明周期非常短。 另一种声明周期比较长,经过很长时间依然被引用。 在内存中的大多数对象都属于前者。 如果不分young和old,会导致每次垃圾回收都要进行全局扫描,那些声明周期较长的对象可能会别多次垃圾回收重复扫描,全局扫描时间慢、浪费系统资源。
为什么young要分eden和survivor?
主要是解决内存不连续的。 年轻代采用的垃圾回收算法是复制算法。 如果不分,年轻代总大小为10,经过一次垃圾回收后,回收了9个,剩一个存活对象,把年轻代空间看做一个大小为10的数组,如果存活对象不再数组下标0,或者下标9的位置上,此时如果新产生一个大小为9的对象,理论上剩余空间为9能放下,实际上由于空间不连续,大小为9的对象年轻大放不下。 而分了eden和survivor后,经过一次minorGC后,剩余存活的对象会被放在survivor区的s0或者s1中,剩余的eden区域内存空间是连续的,就不会产生实际上空间够,而由于内存不连续放不下的问题。
为什么经过15次GC后,young的对象会被放入old?
对象头中的markdown信息存储了分代年龄,大小为4bit, 最小值是0000 最大值是1111 ,转化为十机制就是15.
MetaSpace,为什么使用元空间代替永久代?
永久代主要存储的是类相关的信息,所以对于冬天生成类的场景,比较容易出现内存溢出。 最典型的就是在JSP页面较多的时候,容易出现永久代内存溢出 PermGen Space. 元空间的本质和永久代类似。 都是JVM规范中方法区的实现,不同的是元空间使用的本地内存,而不是JVM虚拟机内存。 类和方法的信息是比较难确定大小的,太小容易出现永久代溢出。 元空间直接使用本地内存,默认情况下仅受本地内存限制。
什么时候发生栈内存溢出?
- 栈的声明周期,线程私有,和线程同周期
- 存放什么? 方法每压一次栈,存放一个栈帧,栈帧存放了局部变量表,操作数栈、动态连接、方法出口等信息。
- 放不下栈帧,即创建栈帧的请求超过了栈的最大深度。
- 怎么解决 先代码后参数 xss.
什么时候发生堆内存溢出?
存放的是什么? 对象,所有线程的对象都在堆中存放。 怎么放? new一个object就放一个。 怎么回收? 可达性分析算法回收,即一个对象直接或者间接关联Gcroot节点。就不是垃圾。 new出来的对象要往堆中存放,而堆中经过fullGc后,依然没有足够空间存放,发生内存溢出。
内存泄漏怎么发生的?
本该被回收的对象,却没有被回收,GCRoot跟节点间接可达。 1) 长生命周期的静态集合类对短生命周期的对象的引用。 2) 各种连接、数据库连接、网络连接、IO连接等。连接不关闭就不是垃圾。无法回收。 比如FileInputStream 这些类都重写了finalize方法。 3) 变量不合理的作用域, 本应该在方法中结束生命周期的对象,定义在类中就和类同生命周期。 4) 改变hash值,当一个对象被存储在hashset中后,改变hash值后,remove时就无法找到整个对象,造成内存泄漏。
引用类型?
- 强引用: 工CGRoot跟节点开始,有直接引用关系就是强引用。特点,如果内存空间不够,不会回收内存,直接抛出OOM异常。
- 软引用: 使用SoftReference
创建对象的软引用,适用于缓存。当内存空间不够时,会释放引用,回收对象。 - 弱引用: 通过WeakReference
创建对象的弱引用,只要经过垃圾回收,即只要垃圾回收器扫描到,就直接回收。 - 虚引用: 通过PhantomReferfence实现。用来跟踪对象被添加到队列的时刻,所以需要和队列一起使用,无法通过get获取虚引用的对象,因为他的get被重写了,返回的永远都是null . 构造方法需要通知指定对象和队列。 如果当执行玩对象的finalize方法会加入队列。
ThreadLocal
说一说OOM发生时你的定位思路?
- ps -ef | grep java 或者 top 找到对应的java进程的PID。
- 确认下内存分配大小和 使用情况 jmap -heap {PID}
- 将类名实例数大小排序 jmap -histo:live {PID} more
- 或者直接jamp -histo:file xxx 指定dump文件导出当前内存信息。
- jstack {PID} 打印栈信息。
- 使用 eclipse 的Mat memoryAnalyzer.exe分析dump文件。
- 也可以使用jvisulvm 直接连接查看。
什么是垃圾对象
java中操作对象都是通过引用操作的。 如果一个对象没有任何引用(引用计数法判断),就是垃圾。 主流的垃圾回收器都摒弃了这种算法,因为java中有循环引用
Object A = new A(); // A的引用数为1
Object B = new B();
A.setB(B);
B.setB(A); // A的引用数为2.
A = null; // 此时A的引用数为1
B = null; // B为null,但是A还是被B的 A属性引用,A的引用数还是1,无法被回收。
可达性分析算法(跟踪搜索算法)
对象到GCRoots根节点如果存在直接或者间接引用,那对象就不是垃圾。 GRRoots根节点是什么?
- 虚拟机栈中局部变量表存储的对象的引用。
- 方法区中静态属性的引用。
- 方法区中常量的引用。
- 本地方法栈中JNI引用,即Native方法中局部变量引用的对象。
什么时候GC
堆内存分为老年代和新生代。 而新生代又分为Eden区和Survivor区。 一般情况下,对象创建出来都在Eden区,处分Eden大小放不下的大对象直接进入Old区。整个大小通过虚拟机参数pretenureSizeThreadhold参数指定。 当Eden区达到90%使用空间时,发生minorGC,也叫youngGC, 会将年轻代中存活对象年代信息增加1,通过复制算法,放入survivor的s0或者s1. 发不下的进入老年代。 当Old区对象放满后,有新对象放入,发生majorGC,也叫FullGC。
标记清除算法
标记: 根据可达性分析算法,标记内存中有哪些对象可以回收,哪些对象还可以继续使用。 清除: 清除掉跟GcRoot跟节点不可达的对象。 缺点: 标记清除效率低、随着对象的不断增多,效率就会下降,比如新生代中对象声明周期都很短,就不适合。 清除之后内存空间不连续,即有空间碎片,而太多的空间碎片可能导致新对象放不下。从而频繁GC.
复制算法
将内存划分为大小相等的两块,每次只使用其中一块 当使用的这一块空间不够时,将存活的对象放到另一块,然后将本块清空。 缺点: 内存代价太高,有效使用空间永远只有一半。 优点: 没有内存碎片。在对象存活率低的场景下非常适用。 年轻代对象存活率低,surivivor区中的from和to就是这种复制算法。 而老年代对象存活率比较高,就不适用。
标记整理算法
在标记整理的基础上多做了一步,清除之后将存活对象移动到内存空间一端而空余出另一端,解决内存空间不连续问题。 适用老年代的垃圾回收。
垃圾回收器选择
100M以下内存,或者单核CPU或者没有停顿时间要求,serial系列。 如果响应时间要求高,系统内存CPU满足要求,适用CMS,G1
并行和并发的概念
并行 parallel
1) 同一时间做多件事情的能力 2) 多核CPU上不同的线程同时运行。
并发 concurrent
1) 同一时间,应对 dealing with 一件事情的能力 2) 线程轮行适用CPU的做法。 3) 在同一个CPU上,多个线程抢占CPU资源,轮流执行,由于任务调度器将CPU时间切片分的很小 人类感觉是同时运行的。 即:微观串行,宏观并行。
垃圾收集器
- serial (串行垃圾回收器 )
1) 年轻代复制算法、老年代标记整理算法。 2) 单线程进行垃圾回收,垃圾回收过程中会STW.
- Parallel (并行垃圾回收器) - 1.8的默认垃圾收集器
1) 年轻代复制算法、老年代标记整理算法。 2) 多个线程并行进行垃圾回收,会STW,与serial相比,减少了STW的时间。
- CMS(并发垃圾收集器)
1) 用户线程和垃圾收集线程同时执行。(不一定是并行,有可能是并发) 2) 不需要停顿用户线程,没有STW
- G1垃圾回收器
1) 将内存分割成不同的区域 2) 并发进行垃圾回收
STW
进行垃圾回收时,需要停止工作线程。 在进行可达性分析的时候,如果不进行STW : 1) 新生成的对象,这些对象还没有指向引用,可能被误标记为垃圾。 2) 已经是垃圾的对象,有可能没有被标记。
CMS垃圾收集器
- 初始标记:只标记能直接关联到的对象,速度很快,需要STW.
- 并发标记: 同时执行GCROOT Tracing,耗时最长的过程。
- 重新标记: 重新标记因为并发标记是,用户程序运行而导致的标记发生移动的部分对象。一般比初始标记时间长点,但是比并发标记时间短。 需要STW.
- 清除垃圾对象
优点: 并发标记,停顿低。 缺点: 虽然不会停顿用户线程,但是会占用一部分线程数和CPU资源,占用资源就意味着会增加用户代码执行时间。 使用的是标记清除算法,容易产生空间碎片,等空间碎片过多,会给内存分配大对象产生影响。不得不提前进行一次FullGc。
三色标记法(CMS垃圾收集器并发标记时的算法)
- 基于GcRoots,一开始所有对象都是白色的,白色的意思: 没有被GC扫描,扫描结束后可以被回收。
- 将GCRoots直接关联的对象设置为灰色。灰色的意思: 被GC扫描过过,但是至少有一个引用没有被扫描。
- 遍历灰色对象引用,灰色对象本身变为黑色,引用变为灰色。黑色的意思: 对象本身和他的直接引用都被扫描过,不能被回收。
- 重复上一步,直到没有灰色对象。
- 结束,白色为垃圾对象。
JVM参数
- -XX:printGC 打印GC基本信息
- -XX:+PrintGCDetails 打印GC详细信息
- -XX:PrintGCTimeStamps 打印GC发生时间戳
- -X:loggc:log/gc.log 输出gc信息的log文件位置
- -XX:+PrintHeapAtGC 每次GC后,都打印堆信息
- -XX:+TraceClassLoading 监控类的加载
- -xmx -xms 最大,最小堆内存设置
- -xmn 设置新生代内存大小
- -XX:NewRatio 新生代老年代比例
- -XX:SurvivorRatio 设置survivor 和 eden的比例
- -XX:+HeapDumpOnOutMemeoryError 当发生OOM异常时,导出堆信息到指定文件。
- -XX:+HeapDumpPath 堆信息文件的路径
- -Xss:栈内存大小设置
获取对象在内存中大小分布情况
openjdk 开源layout 可以打印出当前对象的内存信息
<!--jol--> <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>Object obj = new Object()占多少内存空间?
@Test public void testLayoutObj(){ Hello obj = new Hello(); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); // 从返回结果看,一个对象包含了对象头信息和对象指向的类信息,即指针 // 对象头宝包含了 4字节的年代信息,4字节的 hashcode信息,4字节的锁信息。 // 类指针为4字节。 所以如果一个对象没有成员变量,jvm也没有开启压缩,他的大小为16字节 // com.bczhaoa.mybatisplustest.OtherTest.TestJvm$Hello object internals: // OFFSET SIZE TYPE DESCRIPTION VALUE // 0 4 (object header) 09 00 00 00 (00001001 00000000 00000000 00000000) (9) // 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) // 8 4 (object header) 90 b5 13 1e (10010000 10110101 00010011 00011110) (504608144) // 12 4 com.bczhaoa.mybatisplustest.OtherTest.TestJvm Hello.this$0 (object) // Instance size: 16 bytes // Space losses: 0 bytes internal + 0 bytes external = 0 bytes total }如果Object有一个int 一个 String 类型的属性呢。
class Hello{ int a; String b="testsssss"; } @Test public void testLayoutObj(){ Hello obj = new Hello(); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); // 从返回结果看,一个对象包含了对象头信息和对象指向的类信息,即指针 // 对象头宝包含了 4字节的年代信息,4字节的 hashcode信息,4字节的锁信息。 // 类指针为4字节。 所以如果一个对象没有成员变量,jvm也没有开启压缩,他的大小为24字节 // 4个字节锁+4个年代信息+4个hash信息 + // 4个int信息+4个String信息+4个类指针 刚好24. // 是8的整数倍,如果在来个boolean属性, // 25不是8的整数本 会补到32 // com.bczhaoa.mybatisplustest.OtherTest.TestJvm$Hello object internals: // OFFSET SIZE TYPE DESCRIPTION VALUE // 0 4 (object header) 09 00 00 00 (00001001 00000000 00000000 00000000) (9) // 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) // 8 4 (object header) 90 b5 13 1e (10010000 10110101 00010011 00011110) (504608144) // 12 4 int Hello.a 0 // 16 4 java.lang.String Hello.b (object) // 20 4 com.bczhaoa.mybatisplustest.OtherTest.TestJvm Hello.this$0 (object) // Instance size: 24 bytes // Space losses: 0 bytes internal + 0 bytes external = 0 bytes total }
