P13、JVM架构图
灰色的说明:
1、线程私有
2、内存占用非常少,不存在GC垃圾回收
亮色的说明:
1、所有线程共有,存在GC垃圾回收
2、方法区垃圾回收比较少,大多数是回收堆。
P14-P15、 类装载器
C是面向过程的,C++是面向对象的,支持多重继承,可以基于指针操作,需要手动释放内存解答完毕
(c++)-- =java
双亲委派
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
采用双亲委派的–个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样–个Object对象。
P16、本地接口
package jvm13;
/**
* @author: lnn
* @date: 2021/6/27 10:11
*/
public class T1 {
public static void main(String[] args) {
Thread t1=new Thread();
t1.start();
t1.start();
}
}
P17、PC寄存器
P18、小总结
主题: JVM
1 JVM系统架构图
2类加载器
System.out.println(myObject.getClass().getClassLoader().getParent().getParent());//null,Java看不到c++写的启动器
System.out.println(myObject.getClass().getClassLoader().getParent());//sun.misc.Launcher$ExtClassLoader@1540e19d
System.out.println(myObject.getClass().getClassLoader());//sun.misc.Launcher$AppClassLoader@18b4aac2
2.1有哪几种类加载器
2.2双亲委派
2.3沙箱安全机制
3 Native
3.1 native是一个关键字
3.2声明有,实现无,why?
4 PC寄存器
4.1记录了方法之间的调用和执行情况,类似排版值日表用来存储指向下一条指令的地址,也即将要执行的指令代码 它是当前线程所执行的字节码的行号指示器
P19、方法区
P20-P22、栈
栈管运行,堆管存储
程序=算法+数据结构
程序=算法+数据结构
队列:先进先出(FIFO)、排队打饭。
栈:先进后出,类似于子弹弹夹,最后一个塞进弹夹的第一个出来。
P23、堆、栈、方法区的交互关系
P24、Heap堆
P24-25、Heap堆
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为三部分:
- Young Generation Space 新生区 Young/New
- Tenure generation space 养老区 Old/ Tenure
- Permanent Space 永久区 Perm
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。堆内存 逻辑上分为三部分:新生+养老+永久
新生区:
新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分: 伊甸区(Eden space) 和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个: 0区 (Survivor 0 space.)和1区(Survivor 1 space) 。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生Ma jorGC (FullGC) ,进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生00M异常"OutOfMemoryError"
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
(1) Java虛拟机的堆内存设置不够,可以通过参数-xms、-Xmx来调整。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
P27 对象生命周期和GC
Java堆从GC的角度还可以细分为:新生代( Edeni区、From survivor区和To survivor区)和老年代。
MinorGC的过程(复制->清空-> 互换)
- eden、SurvivorFrom复制到SurvivorTo, 年龄+1
首先,当Eden区满的时候会触发第一 次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1- 清空eden、SurvivorFrom
然后,清空Eden和SurvivorFrom中的对象, 也即复制之后有交换,谁空谁是to- SurvivorTo和 SurvivorFrom互换
最后,SurvivorTo 和SurvivorFrom互换,原SurvivorTo成为 下一次GC时 的SurvivorFrom区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代
P28、永久带
方法区:
实际而言,方法区(Method Area) 和堆一样, 是各个线程共享的内存区域,它用于存储虛拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
对于HotSpot虚拟机,很多开发者习惯将方法区称之为“永久代Parmanent Gen)” ,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于是一个接口interface)的一个实现,jdk1.7的饭本中,已经将原本放在永久代的字符串常量池移走。
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class, Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。
P29、堆参数调整
在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。
元空间与永久代之间最大的区别在于:
永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。
堆内存调优简介
-xms: 设置初始分配大小,默认为物理内存的“1/64"
-Xmx: 最大分配内存,默认为物理内存的“1/4
-XX:+PrintGCDetails: 输出详细的GC处理日志
public static void main(String[] args) {
long maxMemory = Runtime.getRuntime().maxMemory();//返回 Java虚拟机试图使用的最大内存量。
long totalMemory = Runtime.getRuntime().totalMemory();//返回 Java虚拟机中的内存总量。
System.out.println("-Xmx:MAX_ MEMORY = " + maxMemory + " (字节)" + (maxMemory / (double) 1024 / 1024) + "MB");
System.out.println("-xms:TOTAL_ MEMORY = " + totalMemory + "(字节)" + (totalMemory / (double) 1024 / 1024) + "MB");
}
生产中调初始值xms和最大值xmx必须一样,避免内存忽高忽低造成卡顿
-xms1024m -Xmx1024m -XX:+PrintGCDetails
public static void main(String[] args) {
// sorce0001
/* while (true){
String str="aaaa";
str=str+new Random().nextInt(88888888)+new Random().nextInt(999999999);
}*/
// Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
byte[] bytes=new byte[40*1024*1024];
}
P30、GC收集日志信息
轻GC:
[GC (Allocation Failure)
[PSYoungGen: 488K->504K(2560K)] 756K->876K(9728K), 0.0004468 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
重GC:
[Full GC (Allocation Failure)
[PSYoungGen: 504K->0K(2560K)]
[ParOldGen: 372K->640K(7168K)] 876K->640K(9728K),
[Metaspace: 3433K->3433K(1056768K)], 0.0043485 secs]
[Times: user=0.20 sys=0.00, real=0.00 secs]
P31、GC算法之引用计数法
引用计数法:
缺点:
每次对对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗 ;
较难处理循环引用
JVM的实现一般不采用这种方式
P32、复制算法
HotSpot JVM把年轻代分为了三部分:
1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1,-般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。 对象在Survivor区中每熬过一 次Minor GC, 年龄就会增加1岁,当它的年龄增加到- -定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一-块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
原理:
从根集合(GC Root)开始,通过Tracing从From中找到存活对象,拷贝到To中;
From、To交换身份,下次内存分配从To开始;
缺点:空间换时间,空间利用率低。
复制算法它的缺点也是相当明显的。
1、它浪费了一半的内存,这太要命了。
2、如果对象的存活率很高,我们可以极端一点, 假设是100%存活,那么我们需要将所有对象都复制- -遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。
P33、标记清除
四大算法:计数、复制、标清、标整
缺点:
1、首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲
2、其次,主要的缺点则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随即的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这- -点,JVM就不得不维持-一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。
P34、标记压缩
缺点:
标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。
从效率上来说,标记/整理算法要低于复制算法。
内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
内存整齐度:复制算法=标记整理算法>标记清除算法。
内存利用率:标记整理算法=标记清除算法>复制算法。
可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法
相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程
难道就没有一种最优算法吗?猜猜看,下面还有
回答:无,没有最好的算法,只有最合适的算法。==========>分代收集算法。
年轻代(Young Gen)
年轻代特点是区域相对老年代较小,对像存活率低。(Young Gen)
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代的回收。而复制算法
内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代(Tenure Gen)
老年代的特点是区域较大,对像存活率高。
这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。
P35、JMM
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建-个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。