JVM(https://www.yuque.com/qingkongxiaguang/javase/hla7hr#41678ce6)

(1)程序计数器:可以看做是当前线程所执行字节码的行号指示器,而行号正好就指的是某一条指令,字节码解释器在工作时也会改变这个值,来指定下一条即将执行的指令。因为Java的多线程也是依靠时间片轮转算法进行的,因此一个CPU同一时间也只会处理一个线程,当某个线程的时间片消耗完成后,会自动切换到下一个线程继续执行,而当前线程的执行位置会被保存到当前线程的程序计数器中,当下次轮转到此线程时,又继续根据之前的执行位置继续向下执行。
(2)虚拟机栈:
虚拟机栈就是一个非常关键的部分,看名字就知道它是一个栈结构,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(其实就是栈里面的一个元素),栈帧中包括了当前方法的一些信息,比如局部变量表、操作数栈、动态链接、方法出口等。每个栈帧还保存了一个可以指向当前方法所在类的运行时常量池,目的是:当前方法中如果需要调用其他方法的时候,能够从运行时常量池中找到对应的符号引用,然后将符号引用转换为直接引用,然后就能直接调用对应方法,这就是动态链接
public class Main { public static void main(String[] args) { int res = a(); System.out.println(res); } public static int a(){ return b(); } public static int b(){ return c(); } public static int c(){ int a = 10; int b = 20; return a + b; } }
上述代码在JVM中的执行过程
(3)堆:存放对象和数组
(4)方法区:方法区也是整个Java应用程序共享的区域,它用于存储所有的类信息、常量、静态变量、动态编译缓存等数据,可以大致分为两个部分,一个是类信息表,一个是运行时常量池
深度了解String类对于常量池方面的优化
左边是在堆内存中直接创建了两个对象,所以str1不会等于str2,equals是比较的对象中的内容
右边是直接使用双引号赋值,会先在常量池中查找是否存在相同的字符串,若存在,则将引用直接指向该字符串;若不存在,则在常量池中生成一个字符串,再将引用指向该字符串
str1.intern();
该方法和上面的效果差不多,也是第一次调用会将堆中字符串复制并放入常量池中,第二次通过此方法获取字符串时,会查看常量池中是否包含,如果包含那么会直接返回常量池中字符串的地址,相当于直接从常量池中获取数据
public static void main(String[] args) { //不能直接写"abc",双引号的形式,写了就直接在常量池里面吧abc创好了 String str1 = new String("ab")+new String("c"); String str2 = new String("ab")+new String("c"); System.out.println(str1.intern() == str2.intern()); System.out.println(str1.equals(str2)); }
JDK1.7之后调用intern()方法时,如果常量池中没有对应的字符串,则不会再进行复制,而是将其直接修改为指向当前字符串堆中的的引用
容易糊的地方,new是对象,对象保存在Heap中,双引号赋值则是直接将值放在字符串常量池中
垃圾回收机制(重点)
(1)对象存活判定算法
(1.1)引用计数
-
- 每个对象都包含一个 引用计数器,用于存放引用计数(其实就是存放被引用的次数)
-
- 每当有一个地方引用此对象时,引用计数
+1
- 每当有一个地方引用此对象时,引用计数
-
- 当引用失效( 比如离开了局部变量的作用域或是引用被设定为
null
)时,引用计数-1
- 当引用失效( 比如离开了局部变量的作用域或是引用被设定为
- 当引用计数为
0
时,表示此对象不可能再被使用,因为这时我们已经没有任何方法可以得到此对象的引用了
public class Main { public static void main(String[] args) { Test a = new Test(); Test b = new Test(); a.another = b; b.another = a; //这里直接把a和b赋值为null,这样前面的两个对象我们不可能再得到了 a = b = null; } private static class Test{ Test another; } }
但从上述代码来看,引用计数法并不是最好的。在new a和b时,引用计数器+1,在对各自内部another内部再一次进行赋值时,引用计数器再次+1,最后赋为null时说明再也不可能获得到a和b的引用,但由于对内部对象进行赋值的操作,所以引用计数器永远为1,实际上a和b却无任何实际作用
(1.2)可达性分析算法
首先每个对象的引用都有机会成为树的根节点(GC Roots),可以被选定作为根节点条件如下:
-
-
- 位于虚拟机栈的栈帧中的本地变量表中所引用到的对象(其实就是我们方法中的局部变量)同样也包括本地方法栈中JNI引用的对象。
-
-
-
- 类的静态成员变量引用的对象。
-
-
-
- 方法区中,常量池里面引用的对象,比如我们之前提到的
String
类型对象。
- 方法区中,常量池里面引用的对象,比如我们之前提到的
-
-
-
- 被添加了锁的对象(比如synchronized关键字)
-
- 虚拟机内部需要用到的对象。
如果用GC Roots来分析(1.1)
(1.3)最终判定
虽然在经历了可达性分析算法之后基本可能判定哪些对象能够被回收,但是并不代表此对象一定会被回收,我们依然可以在最终判定阶段对其进行挽留。
/** * Called by the garbage collector on an object when garbage collection * determines that there are no more references to the object. * A subclass overrides the {@code finalize} method to dispose of * system resources or to perform other cleanup. * ... */ protected void finalize() throws Throwable { }
Object类的finalize()方法,此方法正是最终判定方法,如果子类重写了此方法,那么子类对象在被判定为可回收时,会进行二次确认,也就是执行finalize()方法,而在此方法中,当前对象是完全有可能重新建立GC Roots的!
注意finalize()
方法并不是在主线程调用的,而是虚拟机自动建立的一个低优先级的Finalizer
线程(正是因为优先级比较低,所以前面才需要等待1秒钟)进行处理
同时,同一个对象的finalize()
方法只会有一次调用机会,也就是说,如果我们连续两次这样操作,那么第二次,对象必定被回收
总结:
(2)垃圾回收算法
(2.1)分代收集机制
因此,Java虚拟机将堆内存划分为新生代、老年代和永久代(其中永久代是HotSpot虚拟机特有的概念,在JDK8之前方法区实际上就是采用的永久代作为实现,而在JDK8之后,方法区由元空间实现,并且使用的是本地内存,容量大小取决于物理机实际大小,之后会详细介绍)这里我们主要讨论的是新生代和老年代。
首先,所有新创建的对象,在一开始都会进入到新生代的Eden区(如果是大对象会被直接丢进老年代),在进行新生代区域的垃圾回收时,首先会对所有新生代区域的对象进行扫描,并回收那些不再使用对象:
第一轮GC结束之后,Eden中存活的对象进入到Survivor中的From,然后To区与From区进行一个交换,仅仅是区的交换,To区变为From区,From区变为To区
接着就是下一次垃圾回收了,操作与上面是一样的,不过这时由于我们From区域中已经存在对象了(存疑,不是已经换区了,From区怎么还有对象?),所以,在Eden区的存活对象复制到From区之后,所有To区域中的对象会进行年龄判定(每经历一轮GC年龄+1
,如果对象的年龄大于默认值为15
,那么会直接进入到老年代,否则移动到From区)
最后像上面一样交换To区和From区,之后不断重复以上步骤。
垃圾收集分类:
- Minor GC - 次要垃圾回收,主要进行新生代区域的垃圾收集。
- 触发条件:新生代的Eden区容量已满时。
- Major GC - 主要垃圾回收,主要进行老年代的垃圾收集。
- Full GC - 完全垃圾回收,对整个Java堆内存和方法区进行垃圾回收。
- 触发条件1:每次晋升到老年代的对象平均大小大于老年代剩余空间
- 触发条件2:Minor GC后存活的对象超过了老年代剩余空间
- 触发条件3:永久代内存不足(JDK8之前)
- 触发条件4:手动调用
System.gc()
方法
(2.2)空间分配担保
在一轮垃圾回收之后,新生代空间Eden有大量对象要转移至Survivor区,但此时Survivor区没有足够的空间来容纳。这是就需要空间分配担保机制,由老年代来接收无法容纳的对象。
老年代也不是没有条件的全部接收,首先先判断此次垃圾回收进入老年代数据的平均大小是否小于老年代的剩余大小,小于则说明也许可以放得下,因为是平均值,万一某个数据特别大呢?则进行
一次Full GC,腾出空间。实在不行则报出OOM异常,主要流程:
(2.3)具体如何实现垃圾回收过程
整个堆内存虽然以分代收集机制为主,其中具体收集过程为
(2.3.1)标记-清除算法
首先标记出所有需要回收的对象,然后再依次回收掉被标记的对象,或是标记出所有不需要回收的对象,只回收未标记的对象。虽然此方法非常简单,但是缺点也是非常明显的 ,首先如果内存中存在大量的对象,那么可能就会存在大量的标记,并且大规模进行清除并且一次标记清除之后,连续的内存空间可能会出现许许多多的空隙,碎片化会导致连续内存空间利用率降低。
(2.3.2)标记-复制算法
标记复制算法,实际上就是将内存区域划分为大小相同的两块区域,每次只使用其中的一块区域,每次垃圾回收结束后,将所有存活的对象全部复制到另一块区域中,并一次性清空当前区域。虽然浪费了一些时间进行复制操作,但是这样能够很好地解决对象大面积回收后空间碎片化严重的问题。 这种算法就非常适用于新生代(因为新生代的回收效率极高,一般不会留下太多的对象)的垃圾回收,而我们之前所说的新生代Survivor区(方便理解为什么From区和To区来回调换)其实就是这个思路, 包括8:1:1的比例也正是为了对标记复制算法进行优化而采取的。
(2.3.3)标记-整理算法
虽然标记-复制算法能够很好地应对新生代高回收率的场景,但是放到老年代,它就显得很鸡肋了。我们知道,一般长期都回收不到的对象,才有机会进入到老年代,所以老年代一般都是些钉子那么我们能否这样,在标记所有待回收对象之后,不急着去进行回收操作,而是将所有待回收的对象整齐排列在一段内存空间中,而需要回收的对象全部往后丢,这样,前半部分的所有对象都是无需进行回收的,而后半部分直接一次性清除即可。
虽然这样能保证内存空间充分使用,并且也没有标记复制算法那么繁杂,但是缺点也是显而易见的,它的效率比前两者都低。甚至,由于需要修改对象在内存中的位置,此时程序必须要暂停才可以,在极端情况下,可能会导致整个程序发生停顿(被称为“Stop The World”)。
所以,我们可以将标记清除算法和标记整理算法混合使用,在内存空间还不是很凌乱时,采用标记清除算法是没有多大问题的,当内存空间凌乱到一定程度后,我们可以进行一次标记整理算法。
(2.4)垃圾收集器
(2.5)元空间
1.8之前是使用堆内存,1.8及之后则是直接使用本地内存

(3)Java中几种引用类型
(3.1)强引用
直接new一个对象出来就是强引用,如果堆内存不足,JVM宁愿抛出OOM异常也不会回收强引用对象
(3.2)软引用
如果JVM认为会内存不足的问题时,JVM则会清理软引用指向的对象,防止出现OOM异常
public class Main { public static void main(String[] args) { //强引用写法:Object obj = new Object(); //软引用写法: SoftReference<Object> reference = new SoftReference<>(new Object()); //使用get方法就可以获取到软引用所指向的对象了 System.out.println(reference.get()); } }
可以看到软引用还存在一个带队列的构造方法,软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
(3.3)弱引用
弱引用比软引用的生命周期还要短,在进行垃圾回收时,不管当前内存空间是否充足,都会回收它的内存。
public class Main { public static void main(String[] args) { SoftReference<Object> softReference = new SoftReference<>(new Object()); WeakReference<Object> weakReference = new WeakReference<>(new Object()); //手动GC System.gc(); //软引用还能获取到 System.out.println("软引用对象:"+softReference.get()); //弱引用会被回收 System.out.println("弱引用对象:"+weakReference.get()); } }
public class Main { public static void main(String[] args) { Integer a = new Integer(1); WeakHashMap<Integer, String> weakHashMap = new WeakHashMap<>(); weakHashMap.put(a, "yyds"); System.out.println(weakHashMap); a = null; System.gc(); System.out.println(weakHashMap); } }
WeakHashMap
正是一种类似于弱引用的HashMap类,如果Map中的Key没有其他引用那么此Map会自动丢弃此键值对。
可以看到,当变量a的引用断开后,这时只有WeakHashMap本身对此对象存在引用,所以在GC之后,这个键值对就自动被舍弃了。所以说这玩意,就挺适合拿去做缓存的。
(3.4)虚引用
虚引用相当于没有引用,随时都有可能会被回收。
public class PhantomReference<T> extends Reference<T> { /** * Returns this reference object's referent. Because the referent of a * phantom reference is always inaccessible, this method always returns * <code>null</code>. * * @return <code>null</code> */ public T get() { return null; } /** * Creates a new phantom reference that refers to the given object and * is registered with the given queue. * * <p> It is possible to create a phantom reference with a <tt>null</tt> * queue, but such a reference is completely useless: Its <tt>get</tt> * method will always return null and, since it does not have a queue, it * will never be enqueued. * * @param referent the object the new phantom reference will refer to * @param q the queue with which the reference is to be registered, * or <tt>null</tt> if registration is not required */ public PhantomReference(T referent, ReferenceQueue<? super T> q) { super(referent, q); } }
也就是说我们无论调用多少次get()
方法得到的永远都是null
,因为虚引用本身就不算是个引用,相当于这个对象不存在任何引用,并且只能使用带队列的构造方法,以便对象被回收时接到通知。
双亲委派机制
Java的执行流程:
类加载器的主要有三个:
1.引导类加载器(Bootstrap ClassLoader):加载Java的核心库(jre/lib/rt.jar),同时加载另外两种类加载器,由C++编写;
2.扩展类加载器(Extensions ClassLoader):加载Java的扩展库(jre/ext/*.jar);
3.应用类加载器(Application ClassLoader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。
*4.可以自定义类加载器
首先双亲委派机制的作用:
- 首先,保证了java核心库的安全性。如果你也写了一个java.lang.String类,那么JVM只会按照上面的顺序加载jdk自带的String类,而不是你写的String类。
- 其次,还能保证同一个类不会被加载多次。
其次,双亲委派机制的执行过程:
当某个特定的类加载器在收到类加载的请求时,会遵循下面的规则顺序:
1.首先判断被加载的类是否已经加载过,如果是则结束,否则会将加载任务委托给自己的父亲;
2.父类加载器在收到类加载的请求时,也会先判断被加载的类是否已经加载过,如果是则结束,否则同样将加载任务委托给自己的父亲
3.不断的循环进行步骤2,直到将加载任务委托给Bootstrap ClassLoader为止。此时,Bootstrap ClassLoader会先判断被加载的类是否已经加载过,如果是则结束;
请注意,到这里为止,都只是在转移加载任务的请求,下面将会进行类加载。
4.Bootstrap ClassLoader会判断能否完成加载任务,如果能则直接加载,否则会将加载任务交给儿子类加载器;
5.儿子类加载器也会判断能否完成加载任务,如果能则直接加载,否则会再一次将加载任务交给儿子类加载器;
6.不断的循环进行步骤5,直到最后一个类加载器,如果这个类加载器仍然不能够加载这个类,就会抛出一个异常:ClassNotFoundException。
本文作者:YoProgrammer
本文链接:https://www.cnblogs.com/sakanayo/p/16637358.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步