赞
踩
Java源程序→编译→字节码文件→放到JVM上运行
总体机制的粗略描述:
类加载器(英文:ClassLoader)负责加载 .class 字节码文件,.class 字节码文件在文件开头有特定的文件标识。ClassLoader 只负责 *.class 字节码文件的加载,至于它是否可以运行,则由 Execution Engine 决定。
JVM 中类加载器分为四种:前三种为虚拟机自带的加载器。
启动类加载器(Bootstrap):使用 C++ 语言编写的类加载器,在Java环境下看不到
负责加载 $JAVA_HOME/jre/lib/rt.jar 里所有的 class。由 C++ 实现,不是 ClassLoader 子类
扩展类加载器(Extension):sun.misc.Launcher.ExtClassLoader
负责加载 Java 平台中扩展功能的一些 jar 包,包括 $JAVA_HOME/jre/lib/*.jar 或 -Djava.ext.dirs 参数指定目录下的 jar 包、以及 $JAVA_HOME/jre/lib/ext/classes 目录下的 class。
应用类加载器(AppClassLoader):sun.misc.Launcher.AppClassLoader
也叫系统类加载器,负责加载classpath中指定的 jar 包及目录中的 class
自定义类加载器:程序员自己开发一个类继承 java.lang.ClassLoader,定制类加载方式
父子关系1:启动类加载器是扩展类加载器的父加载器
父子关系2:扩展类加载器是应用类加载器的父加载器
// 1.获取Person类的Class对象 // 2.通过Class对象进一步获取它的类加载器对象 ClassLoader appClassLoader = Person.class.getClassLoader(); // 3.获取appClassLoader的全类名 String appClassLoaderName = appClassLoader.getClass().getName(); // 4.打印appClassLoader的全类名 // sun.misc.Launcher$AppClassLoader System.out.println("appClassLoaderName = " + appClassLoaderName); // 5.通过appClassLoader获取扩展类加载器(父加载器) ClassLoader extClassLoader = appClassLoader.getParent(); // 6.获取extClassLoader的全类名 String extClassLoaderName = extClassLoader.getClass().getName(); // 7.打印extClassLoader的全类名 // sun.misc.Launcher$ExtClassLoader System.out.println("extClassLoaderName = " + extClassLoaderName); // 8.通过extClassLoader获取启动类加载器(父加载器) ClassLoader bootClassLoader = extClassLoader.getParent(); // 9.由于启动类加载器是C语言开发的,在Java代码中无法实例化对象,所以只能返回null值 System.out.println("bootClassLoader = " + bootClassLoader);
public class Hello {
public static void main(String[] args){
System.out.println("AAA");
}
}
public class Hello {
public static void main(String[] args){
System.out.println("BBB");
}
}
public class Hello {
public static void main(String[] args){
System.out.println("CCC");
}
}
package java.lang;
public class String {
public String() {
System.out.println("嘿嘿,其实我是假的!");
}
}
@Test
public void testLoadString() {
// 目标:测试不同范围内全类名相同的两个类JVM如何加装
// 1.创建String对象
java.lang.String testInstance = new java.lang.String();
// 2.获取String对象的类加载器
ClassLoader classLoader = testInstance.getClass().getClassLoader();
System.out.println(classLoader);
}
本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序。因为 Java 诞生的时候是 C/C++ 横行的时候,要想立足,必须有能力调用 C/C++。于是就在内存中专门开辟了一块区域处理标记为 native 的代码,它的具体做法是 Native Method Stack 中登记 native 方法,在Execution Engine 执行时加载 native libraies。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过 Java 程序驱动打印机或者 Java 系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket 通信,也可以使用 Web Service 等等,不多做介绍。
专门负责在本地方法运行时,提供栈空间,存放本地方法每一次执行时创建的栈帧。它的具体做法是在 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载本地方法库。
native 方法举例:
public static native void yield();
也叫PC寄存器(Program Counter Register)。用于保存程序执行过程中,下一条即将执行的指令的地址。也就是说能够保存程序当前已经执行到的位置。这个位置由执行引擎读取下一条指令,是一个非常小的内存空间,从内存空间使用优化这个角度来看:几乎可以忽略不记。
作用:用于执行字节码文件中的指令。
执行指令的具体技术:
提高特定场景下性能。
直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 本机直接内存的分配不会受到 Java 堆大小的限制,受到本机总内存大小限制。 配置虚拟机参数时,不要忽略直接内存防止出现 OutOfMemoryError 异常。
直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显。直接内存 I/O 读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显。
TIP
永久代概念辨析:
- 从堆空间角度来说
- 新生代:从标准和实现层面都确定属于堆
- 老年代:从标准和实现层面都确定属于堆
- 永久代
- 名义上属于堆
- 实现上不属于堆
- 从方法区角度来说
- 方法区的具体实现:JDK 版本 ≤ 1.7 时,使用永久代作为方法区。
- 方法区的具体实现:JDK 版本 ≥ 1.8 时,使用元空间作为方法区。
本身含义:万物初始,一件事情的源头或基本组成部分。
举例:元素、元始天尊、每年1月称为元月、1月1日称为元旦、元认知、元无知、元知识
对比类和对象,类相当于是对象的元信息。
方法栈并不是某一个 JVM 的内存空间,而是我们描述方法被调用过程的一个逻辑概念。
在同一个线程内,method01()调用method02():
TIP
『栈』和『堆』这两个字辨析:
1、从英文单词角度来说
- 栈:stack
- 堆:heap
2、从数据结构角度来说
- 栈和堆一样:都是先进后出,后进先出的数据结构
3、从 JVM 内存空间结构角度来说
- 栈:通常指 Java 方法栈,存放方法每一次执行时生成的栈帧。
- 堆:JVM 中存放对象的内存空间。包括新生代、老年代、永久代等组成部分。
方法在本次执行过程中所用到的局部变量、动态链接、方法出口等信息。栈帧中主要保存3 类数据:
当一个方法 A 被调用时就产生了一个栈帧 F1,并被压入到栈中,
A 方法又调用了 B 方法,于是产生栈帧 F2 也被压入栈,
B 方法又调用了 C 方法,于是产生栈帧 F3 也被压入栈,
……
C 方法执行完毕后,弹出 F3 栈帧;
B 方法执行完毕后,弹出 F2 栈帧;
A 方法执行完毕后,弹出 F1栈帧;
……
遵循“先进后出”或者“后进先出”原则。
图示在一个栈中有两个栈帧:
栈帧 2 是最先被调用的方法,先入栈,
然后方法 2 又调用了方法 1,栈帧 1 处于栈顶的位置,
栈帧 2 处于栈底,执行完毕后,依次弹出栈帧 1 和栈帧 2,
线程结束,栈释放。
每执行一个方法都会产生一个栈帧,保存到栈的顶部,顶部栈就是当前方法,该方法执行完毕后会自动将此栈帧出栈。
请预测下面代码打印的结果:34
int n = 10;
n += (n++) + (++n);
System.out.println(n);
实际执行结果:32
使用 javap 命令查看字节码文件内容:
D:\record-video-original\day03\code>javap -c Demo03JavaStackExample.class
Compiled from “Demo03JavaStackExample.java”
public class Demo03JavaStackExample{
public Demo03JavaStackExample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object.": ()V
4: returnpublic static void main(java.lang.String[]);
Code:
0: bipush 10
2: istore_1
3: iload_1
4: iload_1
5: iinc 1, 1
8: iinc 1, 1
11: iload_1
12: iadd
13: iadd
14: istore_1
15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_1
19: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
22: return
}
内存执行过程分析:
java.lang.StackOverflowError
下面的例子是一个没有退出机制的递归:
public class StackOverFlowTest {
public static void main(String[] args) {
methodInvokeToDie();
}
public static void methodInvokeToDie() {
methodInvokeToDie();
}
}
抛出的异常信息:
Exception in thread “main” java.lang.StackOverflowError at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10) at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10) at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10) at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10) at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10)
原因总结:方法每一次调用都会在栈空间中申请一个栈帧,来保存本次方法执行时所需要用到的数据。但是一个没有退出机制的递归调用,会不断申请新的空间,而又不释放空间,这样迟早会把当前线程在栈内存中自己的空间耗尽。
某一个线程抛出『栈溢出异常』,会导致其他线程也崩溃吗?从以往的经验中我们判断应该是不会,下面通过代码来实际验证一下。
new Thread(()->{ while(true) { try { TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName() + " working"); } catch (InterruptedException e) { e.printStackTrace(); } } }, "thread-01").start(); new Thread(()->{ while(true) { try { TimeUnit.SECONDS.sleep(2); // 递归调用一个没有退出机制的递归方法 methodInvokeToDie(); System.out.println(Thread.currentThread().getName() + " working"); } catch (InterruptedException e) { e.printStackTrace(); } } }, "thread-02").start(); new Thread(()->{ while(true) { try { TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName() + " working"); } catch (InterruptedException e) { e.printStackTrace(); } } }, "thread-03").start();
02 线程抛异常终止后,01 和 03 线程仍然能够继续正常运行,说明 02 抛异常并没有影响到 01 和 03,说明线程对栈内存空间的使用方式是彼此隔离的。每个线程都是在自己独享的空间内运行,反过来也可以说,这个空间是当前线程私有的。
口诀:复制必交换,谁空谁为to
永久代 | 常量池 | |
---|---|---|
≤JDK1.6 | 有 | 在方法区 |
=JDK1.7 | 有,但开始逐步“去永久代” | 在堆 |
≥JDK1.8 | 无 | 在元空间 |
Java代码:
List<Object> list = new ArrayList<>();
while (true){
list.add(new Object());
}
附加 JVM 运行参数:
java.lang.OutOfMemoryError,也往往简称为 OOM。
我们可以参考下面的控制台日志打印:
[GC (Allocation Failure) 4478364K->4479044K(5161984K), 4.3454766 secs] [Full GC (Ergonomics) 4479044K->3862071K(5416448K), 39.3706285 secs] [Full GC (Ergonomics) 4410423K->4410422K(5416448K), 27.7039534 secs] [Full GC (Ergonomics) 4629575K->4621239K(5416448K), 24.9298221 secs] [Full GC (Allocation Failure) 4621239K->4621186K(5416448K), 29.0616791 secs] Exception in thread “main” java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.Arrays.copyOf(Arrays.java:3181) at java.util.ArrayList.grow(ArrayList.java:261) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227) at java.util.ArrayList.add(ArrayList.java:458) at com.atguigu.jvm.test.JavaHeapTest.main(JavaHeapTest.java:16)
查看下面程序在每个步骤中内存的状态:
public class Review { // 静态变量,类变量 public static Review review = new Review(); public void showMessage() { // 局部变量 Review reviewLocal = new Review(); } // 程序入口 public static void main(String[] args) { // 局部变量 Review reviewMain = new Review(); // 通过局部变量调用对象的方法 reviewMain.showMessage(); // 手动 GC System.gc(); } }
%JAVA_HOME%/bin/jconsole.exe
双击打开,选择要监控的进程:
如果想要重新打开新建连接窗口可以点击菜单项:
可以选择内存选项卡,在图表下拉列表中选择想要查看的具体的内存区域:
%JAVA_HOME%/bin/jvisualvm.exe
什么是GC?
GC 是 garbage collection 的缩写,意思是垃圾回收——把内存(特别是堆内存)中不再使用的空间释放掉;清理不再使用的对象。
为什么要GC?
堆内存是各个线程共享的空间,不能无节制的使用。服务器运行的时间通常都很长。累积的对象也会非常多。这些对象如果不做任何清理,任由它们数量不断累加,内存很快就会耗尽。所以GC就是要把不使用的对象都清理掉,把内存空间空出来,让项目可以持续运行下去。
什么样的对象是垃圾对象?
不再使用或获取不到的对象是垃圾对象。
如何把垃圾对象找出来?
办法1:引用计数法(不采用,不能解决循环引用问题)[了解]
办法2:可达性分析(从GC Roots对象出发,不可达的对象就是要清理的对象)[理解]
找到垃圾对象如何执行清理?
具体的GC算法
引用计数法是在对象每一次被引用时,都给这个对象专属的『引用计数器』+1。
当前引用被取消时,就给这个『引用计数器』-1。
当前『引用计数器』为零时,表示这个对象不再被引用了,需要让GC回收。
可是当对象之间存在交叉引用的时候,对象即使处于应该被回收的状态,也没法让『引用计数器』归零。
Member member01 = new Member();
Member member02 = new Member();
member01.setFriend(member02);
member02.setFriend(member01);
member01 = null;
member02 = null;
引用计数法的关键问题:该清理的对象清理不掉。
public class Customer { private List<Order> orderList; public List<Order> getOrderList() { return orderList; } public void setOrderList(List<Order> orderList) { this.orderList = orderList; } } public class Order { private Customer customer; public Customer getCustomer() { return customer; } public void setCustomer(Customer customer) { this.customer = customer; } }
核心原理:判断一个对象,是否存在从**『堆外』到『堆内』**的引用。
请看下面的例子:
class Employee {
public static final String SUBJECT = new String("Java");
}
public static void main(String[] args) {
// 1.创建Employee对象,并赋值给employee变量
Employee employee = new Employee();
}
GC Root 对象:就是作为根节点出发,顺着引用路径一直查找到堆空间内,找到堆空间中的对象。
基本垃圾回收算法有四种:引用计数法、标记清除法、标记压缩法、复制算法。现代流行的垃圾收集算法一般是由这四种中的其中几种算法相互组合而成。例如:分代算法、分区算法。
这里又看到一个『引用计数法』,但是和前面提到的不一样:
前面『引用计数法』:是标记垃圾对象的一种方法
这里『引用计数法』:是执行垃圾回收的一种方法
引用计数算法很简单,它实际上是通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。
引用计数垃圾收集机制,它只是在引用计数变化为0时即刻发生,而且只针对某一个对象以及它所依赖的其它对象。所以,我们一般也称呼引用计数垃圾收集为直接的垃圾收集机制。垃圾收集的开销被分摊到整个应用程序的运行当中了,而不是在进行垃圾收集时,要挂起整个应用的运行,直到对堆中所有对象的处理都结束。因此,采用引用计数的垃圾收集不属于严格意义上的"Stop-The-World"的垃圾收集机制。
优点:
缺点:
小结:
正是由于引用计数法不能解决对象间的循环引用问题,所以事实上并没有哪一款 JVM 产品采用这个机制。
TIP
Stop-The-World:字面意思让整个世界停止。在 GC 机制中,Stop-The-World 表示挂起整个 JVM 程序,等执行完垃圾回收之后,再继续执行 JVM 程序。Stop-The-World 通常也会简称为 STW。
它的做法是当堆中的有效内存空间被耗尽的时候,就会暂停、挂起整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
小结:
既然叫标记压缩算法,那么它也分为两个阶段,一个是标记(mark),一个是压缩(compact)。所谓压缩就是把存在碎片的空间连起来。
标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象移动到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题。
标记 : 标记的过程其实就是,从根对象开始遍历所有的对象,然后将所有存活的对象标记为可达的对象。
压缩 : 移动所有的可达对象到堆内存的同一个区域中,使他们紧凑的排列在一起,从而将所有非可达对象释放出来的空闲内存都集中在一起,通过这样的方式来达到减少内存碎片的目的。
小结
优点:标记压缩算法是对标记清除算法的优化,解决了碎片化的问题
缺点:还是效率问题,在标记清除算法上又多加了一步,效率可想而知了
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,并依次排列,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。
小结
执行GC前:
执行标记:
执行复制:
交换指针:
前面介绍了多种回收算法,每一种算法都有自己的优点也有缺点,谁都不能替代谁,所以根据垃圾回收对象的特点进行选择,才是明智的。
分代算法其实就是这样的,根据回收对象的特点进行选择。
上面介绍的分代收集算法是将对象的生命周期按长短划分为两个部分,而分区算法则将整个堆空间划分为连续的不同小区间,每个小区间独立使用,独立回收。这样做的好处是可以控制一次回收多少个小区间。在相同条件下,堆空间越大。一次GC耗时就越长,从而产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割为多个小块,根据目标停顿时间每次合理地回收若干个小区间(而不是整个堆),从而减少一次GC所产生的停顿。
串行:在一个线程内执行垃圾回收操作。
新生代串行回收器 SerialGC:采用复制算法实现,单线程垃圾回收,独占式垃圾回收器
老年代串行回收器 SerialOldGC:采用标记压缩算法,单线程独占式垃圾回收器
并行:在多个线程中执行垃圾回收操作。
新生代 ParNew 回收器:采用复制算法实现,多线程回收器,独占式垃圾回收器。
新生代 ParallelScavengeGC 回收器:采用复制算法多线程独占式回收器
老年代 ParallelOldGC 回收器: 采用标记压缩算法,多线程独占式回收器
CMS回收器
CMS全称 (Concurrent Mark Sweep),是一款并发的、使用标记-清除算法的垃圾回收器。对CPU资源非常敏感。
启用CMS回收器参数 :-XX:+UseConcMarkSweepGC。
使用场景:GC过程短暂停顿,适合对时延要求较高的服务,用户线程不允许长时间的停顿。
优点:最短回收停顿时间为目标的收集器。并发收集,低停顿。
缺点:服务长时间运行,造成严重的内存碎片化。算法实现比较复杂。
G1回收器
G1(Garbage-First)是一款面向服务端应用的并发垃圾回收器, 主要目标用于配备多颗CPU的服务器,治理大内存。是JDK1.7提供的一个新收集器,是当今收集器技术发展的最前沿成果之一。
G1计划是并发标记-清除收集器的长期替代品。
启用G1收集器参数:-XX:+UseG1GC启用G1收集器。
G1将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了, 它们都是一部分Region(不需要连续)的集合。
每块区域既有可能属于Old区、也有可能是Young区,因此不需要一次就对整个老年代/新生代回收。而是当线程并发寻找可回收的对象时,有些区块包含可回收的对象要比其他区块多很多。虽然在清理这些区块时G1仍然需要暂停应用线程,但可以用相对较少的时间优先回收垃圾较多的Region(这也是G1命名的来源)。这种方式保证了G1可以在有限的时间内获取尽可能高的收集效率。
特点:
名称 | 串行/并行/并发 | 回收算法 | 适用场景 | 可以与CMS配合 |
---|---|---|---|---|
SerialGC | 串行 | 复制 | 单CPU | 是 |
ParNewGC | 并行 | 复制 | 多CPU | 是 |
ParallelScavengeGC | 并行 | 复制 | 多CPU且关注吞吐量 | 否 |
名称 | 串行/并行/并发 | 回收算法 | 适用场景 |
---|---|---|---|
SerialOldGC | 串行 | 标记压缩 | 单CPU |
ParNewOldGC | 并行 | 标记压缩 | 多CPU |
CMS | 并发,几乎不会暂停用户线程 | 标记清除 | 多CPU且与用户线程共存 |
java.lang.Object 类中有一个方法:
protected void finalize() throws Throwable { }
方法体内是空的,说明如果子类不重写这个方法,那么不执行任何逻辑。
public class FinalizeTest { // 静态变量 public static FinalizeTest testObj; @Override protected void finalize() throws Throwable { // 重写 finalize() 方法 System.out.println(Thread.currentThread().getName() + " is working"); // 给待回收的对象(this)重新建立引用 testObj = this; } public static void main(String[] args) { // 1、创建 FinalizeTest 对象 FinalizeTest testObj = new FinalizeTest(); // 2、取消引用 testObj = null; // 3、执行 GC 操作 System.gc(); // ※ 让主线程等待一会儿,以便调用 finalize() 的线程能够执行 try { TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {} // 4、判断待回收的对象是否存在 if (FinalizeTest.testObj == null) { System.out.println("待回收的对象没有获救,还是要被 GC 清理"); } else { System.out.println("待回收的对象被成功解救"); } // 5、再次取消引用 FinalizeTest.testObj = null; // 6、再次执行 GC 操作 System.gc(); // 7、判断待回收的对象是否存在 if (FinalizeTest.testObj == null) { System.out.println("待回收的对象没有获救,还是要被 GC 清理"); } else { System.out.println("待回收的对象被成功解救"); } } }
执行效果:
Finalizer is working
待回收的对象被成功解救
待回收的对象没有获救,还是要被 GC 清理
System.out.print("最大堆大小:");
System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
System.out.print("当前堆大小:");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
System.out.println("==================================================");
byte[] b = null;
for (int i = 0; i < 10; i++) {
b = new byte[1 * 1024 * 1024];
}
执行前配置参数:-Xmx50m -Xms30m -XX:+PrintGCDetails
执行看到如下信息:
新生代和老年代的堆大小之和是Runtime.getRuntime().totalMemory()
System.out.println("=====================Begin========================="); System.out.print("最大堆大小:Xmx="); System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); System.out.print("剩余堆大小:free mem="); System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); System.out.print("当前堆大小:total mem="); System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); System.out.println("==================First Allocated==================="); byte[] b1 = new byte[5 * 1024 * 1024]; System.out.println("5MB array allocated"); System.out.print("剩余堆大小:free mem="); System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); System.out.print("当前堆大小:total mem="); System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); System.out.println("=================Second Allocated==================="); byte[] b2 = new byte[10 * 1024 * 1024]; System.out.println("10MB array allocated"); System.out.print("剩余堆大小:free mem="); System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); System.out.print("当前堆大小:total mem="); System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); System.out.println("=====================OOM========================="); System.out.println("OOM!!!"); System.gc(); System.out.println("第一个 40M 数组"); byte[] b3 = new byte[40 * 1024 * 1024]; System.out.println("第二个 40M 数组"); byte[] b4 = new byte[40 * 1024 * 1024]; System.out.println("第三个 40M 数组"); byte[] b5 = new byte[40 * 1024 * 1024];
JVM参数设置成最大堆内存100M,当前堆内存10M:-Xmx100m -Xms10m -XX:+PrintGCDetails
再次运行,可以看到minor GC和full GC日志:
参数名 | 参数作用 |
---|---|
-Xms | ★堆内存的初始大小 |
-Xmx | ★堆内存的最大值(最多能够向操作系统申请多少) |
-Xmn | 新生代大小 |
-XX:PermSize | 设置永久代(perm gen)初始值 |
-XX:MaxPermSize | 设置持久代最大值 |
-Xss | 每个线程的堆栈大小 |
-XX:NewRatio | 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) |
-XX:SurvivorRatio | Eden区与Survivor区的大小比值 |
TIP
-Xss 设置每个线程在Java 栈中占用空间的大小。
线程数量 = Java 栈空间 / 每个线程空间(-Xss 参数设置的值)
线程深度 = 每个线程空间 / 每个栈帧空间
如果项目希望线程深度更大,那么应该给每个线程设置较大空间;
如果项目线程深度不大,线程数量很多,那么应该给每个线程设置较小空间。
线程深度:在一个线程中能够连续调用的方法的数量。
双亲委派机制
TIP
如果咱妈给咱们设置的每个月零花钱的额度是 1000 元,那么为了少去烦她老人家,咱们一开始就是申请 1000 元全部。
JDK5.0 以后每个线程所占用的栈内存大小为 1M,以前是 256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 3000~5000 左右。一般小的应用, 如果栈不是很深, 应该是128k够用的。大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。
A 调用 finalize() 方法的效果是把一个已经被 GC 销毁的对象恢复过来。 B 调用 finalize() 方法的效果是针对一个即将被 GC 销毁的对象,在销毁之前调用 finalize() 方法。
|
| -Xss | 每个线程的堆栈大小 |
| -XX:NewRatio | 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) |
| -XX:SurvivorRatio | Eden区与Survivor区的大小比值 |
TIP
-Xss 设置每个线程在Java 栈中占用空间的大小。
线程数量 = Java 栈空间 / 每个线程空间(-Xss 参数设置的值)
线程深度 = 每个线程空间 / 每个栈帧空间
如果项目希望线程深度更大,那么应该给每个线程设置较大空间;
如果项目线程深度不大,线程数量很多,那么应该给每个线程设置较小空间。
线程深度:在一个线程中能够连续调用的方法的数量。
双亲委派机制
TIP
如果咱妈给咱们设置的每个月零花钱的额度是 1000 元,那么为了少去烦她老人家,咱们一开始就是申请 1000 元全部。
JDK5.0 以后每个线程所占用的栈内存大小为 1M,以前是 256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 3000~5000 左右。一般小的应用, 如果栈不是很深, 应该是128k够用的。大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。
A 调用 finalize() 方法的效果是把一个已经被 GC 销毁的对象恢复过来。 B 调用 finalize() 方法的效果是针对一个即将被 GC 销毁的对象,在销毁之前调用 finalize() 方法。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。