JVM内存模型
https://blog.youkuaiyun.com/justloveyou_/article/details/71189093
Java程序在执行前首先会被编译成字节码文件,然后再由Java虚拟机执行这些字节码文件从而使得Java程序得以执行。
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些数据区域可以分为两个部分:一部分是线程共享的,一部分则是线程私有的。
线程私有的数据区
包括程序计数器、虚拟机栈和本地方法栈三个区域:
- 程序计数器:
(1)为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器去记录其正在执行的字节码指令地址。
(2)程序计数器是线程私有的一块较小的内存空间,其可以看做是当前线程所执行的字节码的行号指示器。
(3)如果线程正在执行一个 Java 方法,记录的是正在执行的字节码指令的地址;如果正在执行Native方法,则计数器的值为空。
(4)是唯一一个没有规定任何OutOfMemoryError的区域。因为它记录指令的偏移地址,这个值的范围是可知晓的,所以在程序计数器建立之初就能分配一个绝对不会溢出的内存。 - 虚拟机栈:
(1)虚拟机栈描述的是Java方法执行的内存模型。
(2)每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
(3)每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。
(4)一个线程拥有一个自己的栈,这个栈的大小决定了方法调用的可达深度,若线程请求的栈深度大于虚拟机允许的深度,则抛出StackOverFlowError异常。
(5)栈的大小可以是固定的,也可以动态扩展,但当扩展时无法申请到足够的内存,则抛出OutofMemoryError异常。 - 本地方法栈:
(1)与Java虚拟机栈非常相似,区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机执行Native方法服务。
(2)会抛出StackOverflowError和OutOfMemoryError异常。
一个Native Method就是一个java调用非java代码的接口,该方法由C/C++等实现,实现体在DLL中,JDK的源代码中并不包含。
线程共享的数据区
包括Java堆和方法区两个区域:
- Java堆(GC堆):
(1)存放对象实例,几乎所有的对象实例(和数组)都在这里分配内存。
(2)对象通过new等指令建立,不需要程序代码来显式释放,是垃圾收集器管理的主要区域,故也称为称为GC堆。
(3)可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
(4)既可以是固定大小的,也可以是可拓展的,并且主流虚拟机都是按可扩展来实现的。
(5)如果在堆中没有内存完成实例分配,并且堆也无法再拓展时,将会抛出OutOfMemoryError异常。
(6)由于现在的垃圾收集器基本都采用分代收集算法,所以为了方便垃圾回收Java堆还可以分为新生代和老年代:
① 新生代用于存放刚创建的对象以及年轻的对象,如果对象一直没有被回收,生存得足够长,对象就会被移入老年代。
② 新生代又可进一步细分为eden、survivorSpace0和survivorSpace1。刚创建的对象都放入eden,s0和s1都至少经过一次GC并幸存,如果幸存对象经过一定时间仍存在,则进入老年代。 - 方法区(非堆):
(1)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区有个别称non-heap(非堆)。
(2)在JVM启动的时候被创建,关闭JVM就会释放这个区域的内存。
(3)和Java堆类似,实际物理内存空间可以不连续;可以选择固定大小或者扩展。
(4)方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,抛出OutOfMemoryError异常。
(5)方法区的回收:
① 回收目标主要是针对常量池的回收和对类型的卸载。
② 回收废弃常量与回收Java堆中的对象非常类似。假如一个字符串“abc”已经进入了常量池中,但是没有任何String对象引用常量池中的“abc”常量,如果在这时候发生内存回收,而且必要的话,这个常量就会被回收。
③ 类需要同时满足下面3个条件才能算是“无用的类”:
A. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
B. 加载该类的ClassLoader已经被回收;
C. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
④ 虚拟机可以对满足上述3个条件的无用类进行回收(卸载),这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。
Java堆是Java代码可及的内存,是留给开发人员使用的;而非堆是JVM留给自己用的。
垃圾回收机制
https://www.jianshu.com/p/23f8249886c6
https://blog.youkuaiyun.com/justloveyou_/article/details/71216049
https://blog.youkuaiyun.com/zhangzhi1979815592/article/details/105535978
垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
垃圾判断算法
- 引用计数法:
(1)给每个对象添加一个计数器,引用该对象时计数器加1,引用失效时(超过了生命周期或者被设置为一个新值)计数器减1。
(2)用对象计数器是否为0来判断对象是否可被回收。
(3)优点:
① 实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,来进行回收。
② 在垃圾回收过程中,应用无需挂起,如果申请内存时,内存不足,则立刻报outofmember错误。
③ 区域性,更新对象的计数器,只是影响到该对象,不会扫描全部对象。
(4)缺点:
① 每次对象被引用时,都需要去更新计数器,有一点时间开销。
② 浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。
③ 最大的缺点是无法解决循环引用的问题,如果a和b两个对象存在相互引用,即使a和b都为null,a和b永远不会被回收。 - 可达性分析算法:
(1)通过判断对象的引用链是否可达来决定对象是否可以被回收。
(2)把所有的引用关系看作一张图,通过一系列的名为“GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。
(3)当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
(4)在Java中,可作为GC Root的对象包括以下几种(点击查看样例):
① 虚拟机栈(栈帧中的局部变量表)中引用的对象;
② 本地方法栈中Native方法引用的对象;
③ 方法区中类静态属性引用的对象;
④ 方法区中常量引用的对象。
垃圾回收算法
- 标记-清除算法(Mark-Swap):
(1)标记-清除算法分为标记和清除两个阶段。
(2)该算法首先从根集合进行扫描,标记存活的对象;标记完毕后,再扫描整个空间,回收未标记的对象。
(3)优点:
① 解决了引用计数器算法中的循环引用的问题,没有从root节点引用的对象都会被回收;
② 实现简单。
(4)缺点:
① 两次扫描遍历,效率不高;
② 并且在GC时,需要暂停应用程序,对于交互性要求比较高的应用,体验非常差;
③ 不移动对象,因此清除之后会产生内存碎片,可能会导致以后在程序运行过程中无法找到足够的连续内存而提前触发另一次垃圾回收。 - 复制算法:
(1)将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
(2)当这块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。保证了内存的连续可用。
(3)在标记清除算法基础上演化而来,解决标记清除算法的内存碎片问题。
(4)适用于对象存活率低的场景,比如新生代:
① 新生代中的对象每次回收都基本上只有10%左右的对象存活,所以需要复制的对象很少。
② 将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间(Eden:Survivor1:Survivor2 = 8:1:1),每次使用Eden和其中一块Survivor,少了10%的可用内存。
(5)优点:内存分配时不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
(6)缺点:实际可用内存“少了”,存在浪费。 - 标记-整理算法:
(1)过程类似标记清除算法,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,类似于磁盘整理的过程。
(2)该垃圾回收算法适用于对象存活率高的场景,如老年代。
(3)优点:避免了“复制算法”的缺点,也不会产生内存碎片,充分利用空间。
(4)缺点:效率比较低。 - 分代收集算法:
(1)严格来说并不是一种算法,而是融合上述3种基础的算法思想,根据对象存活周期的不同将内存划分为几块。
(2)在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
(3)在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理算法或者标记-整理算法来进行回收。
永久代(Permanent Generation):主要用于存放静态文件,如Java类、方法等。
永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如使用反射、动态代理、CGLib等bytecode框架时,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。
垃圾回收类型
- Minor GC:对新生代进行回收,不会影响到年老代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。
- Full GC:也叫Major GC,对整个堆进行回收,包括新生代和老年代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代被写满、永久代(Perm)被写满和System.gc()被显式调用等。
垃圾收集器
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
- Serial收集器(复制算法):新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
- Serial Old收集器(标记-整理算法):老年代单线程收集器,Serial收集器的老年代版本;
- ParNew收集器(复制算法):新生代并行收集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
- Parallel Scavenge收集器(复制算法):新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
- Parallel Old收集器 (标记-整理算法):老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
- CMS(Concurrent Mark Sweep)收集器(标记-清除算法):老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
- G1(Garbage First)收集器(标记-整理算法):Java堆(包括新生代,老年代)并行收集器,G1收集器是JDK1.7提供的一个新收集器。
垃圾收集器回收的是无任何引用的对象占据的内存空间而不是对象本身。
内存分配与回收策略
- 对象优先在Eden分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。
- 大对象直接进入老年代。所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。
- 长期存活的对象将进入老年代。当对象在新生代中经历过一定次数(默认为15)的Minor GC后,就会被晋升到老年代中。
- 动态对象年龄判定。为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
内存泄漏
https://blog.youkuaiyun.com/weter_drop/article/details/89387564
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点:
- 这些对象是可达的,即在有向图中,存在通路可以与其相连;
- 这些对象是无用的,即程序以后不会再使用这些对象。
如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
引起内存泄漏的情况:
- 静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。
- 各种连接,如数据库连接、网络连接和IO连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
- 变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。
- 内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
- 改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露。
反射机制
https://blog.youkuaiyun.com/a745233700/article/details/82893076
Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。
Java属于先编译再运行的语言,程序中对象的类型在编译期就确定下来了,而当程序在运行时可能需要动态加载某些类,这些类因为之前用不到,所以没有被加载到JVM。通过反射,可以在运行时动态地创建对象并调用其属性,不需要提前在编译期知道运行的对象是谁。
优缺点
- 优点: 增加程序灵活性,复用率。
(1)增加程序的灵活性,避免将程序写死到代码里。
(2)代码简洁,提高代码的复用率,外部调用方便 - 缺点:性能较差,存在安全隐患。
(1)反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;
(2)使类的内部暴露,因此可能会破坏封装性而导致安全问题。
用途
- 反编译:.class–>.java。
- 通过反射机制访问java对象的属性,方法,构造方法等,我们在使用IDE,比如Ecplise时,当我们输入一个对象或者类,并想调用他的属性和方法是,一按点号,编译器就会自动列出他的属性或者方法,这里就是用到反射。
- 反射最重要的用途就是开发各种通用框架。比如很多框架(Spring)都是配置化的(比如通过XML文件配置Bean),为了保证框架的通用性,他们可能需要根据配置文件加载不同的类或者对象,调用不同的方法,这个时候就必须使用到反射了,运行时动态加载需要的加载的对象。比如,加载数据库驱动的,用到的也是反射。
Class.forName("com.mysql.jdbc.Driver"); // 动态加载mysql驱动
基本使用方法
- 获得Class,主要有三种方法:
Student stu1 = new Student();
(1)Object–>getClass
Class stuClass = stu1.getClass();//获取Class对象
(2)任何数据类型都有一个“静态”的class属性
Class stuClass2 = Student.class;
(3)Class.forName(String className),最常用
Class stuClass3 = Class.forName("包名.Student");//注意此字符串必须是真实路径,就是带包名的类路径,包名.类名
- 判断是否为某个类的实例:
一般的,我们使用instanceof关键字来判断是否为某个类的实例。同时我们也可以借助反射中Class对象的isInstance()方法来判断时候为某个类的实例,他是一个native方法。
public native boolean isInstance(Object obj);
- 创建实例,通过反射来生成对象主要有两种方法:
(1)使用Class对象的newInstance()方法来创建Class对象对应类的实例。
Class<?> c = String.class;
Object str = c.newInstance();
(2)先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建对象,这种方法可以用指定的构造器构造类的实例。
//获取String的Class对象
Class<?> c = String.class;
//通过Class对象获取指定的Constructor构造器对象
Constructor constructor=c.getConstructor(String.class);
//根据构造器创建实例:
Object obj = constructor.newInstance(“hello reflection”);
①②③④⑤⑥⑦⑧⑨⑩