Skip to content

Commit

Permalink
Gracefully handle OutOfMemory errors
Browse files Browse the repository at this point in the history
- Detect and report java.lang.OutOfMemory errors
- Fixes #1959
- Use -XX:+ExitOnOutOfMemoryError to ensure Java language server exits
  when an OutOfMemory error occurs (rather than staying up)
- Use -XX:+HeapDumpOnOutOfMemoryError & -XX:HeapDumpPath to generate a
  heap dump whose existence can notify the client that an OutOfMemory
  error has occured
- Once OutOfMemory error is detected, clean up the heap dumps

Signed-off-by: Roland Grunberg <[email protected]>
  • Loading branch information
rgrunber committed Aug 27, 2021
1 parent 8c4fa79 commit 42b0470
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 8 deletions.
13 changes: 13 additions & 0 deletions document/_java.outOfMemory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# The Language Server Crashes Due to an Out Of Memory Error

If you are working with large Java project, this may lead to the language server running out of memory.

If you get an Out of Memory Error, but aren't working with a large project, then there may be a memory leak in the language server.
Please [file a issue](https://github.com/redhat-developer/vscode-java/issues) with a description of what you were doing if this is the case.

## How to increase the amount of memory available to the Java Language Server

1. Go to settings
2. Navigate to the setting `java.jdt.ls.vmargs`
3. Add `-Xmx2G` to the setting string. This allows the the language server to use at most 2 Gigabytes of memory.
4. If the problem persists, you can increase the `2G` to `4G` or higher
2 changes: 2 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,5 +249,7 @@ export namespace Commands {

export const NOT_COVERED_EXECUTION = '_java.notCoveredExecution';

export const OUT_OF_MEMORY = '_java.outOfMemory';

export const RUNTIME_VALIDATION_OPEN = 'java.runtimeValidation.open';
}
69 changes: 64 additions & 5 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { workspace, extensions, ExtensionContext, window, commands, ViewColumn,
import { ExecuteCommandParams, ExecuteCommandRequest, LanguageClientOptions, RevealOutputChannelOn, ErrorHandler, Message, ErrorAction, CloseAction, DidChangeConfigurationNotification, CancellationToken } from 'vscode-languageclient';
import { LanguageClient } from 'vscode-languageclient/node';
import { collectJavaExtensions, isContributedPartUpdated } from './plugin';
import { prepareExecutable } from './javaServerStarter';
import { HEAP_DUMP_LOCATION, prepareExecutable } from './javaServerStarter';
import * as requirements from './requirements';
import { initialize as initializeRecommendation } from './recommendation';
import { Commands } from './commands';
Expand Down Expand Up @@ -37,9 +37,13 @@ let clientLogFile;

export class ClientErrorHandler implements ErrorHandler {
private restarts: number[];
private globalStoragePath: string;
private heapDumpFolder: string;

constructor(private name: string) {
constructor(private name: string, globalStoragePath: string) {
this.restarts = [];
this.globalStoragePath = globalStoragePath;
this.heapDumpFolder = getHeapDumpFolderFromSettings() || globalStoragePath;
}

public error(_error: Error, _message: Message, count: number): ErrorAction {
Expand All @@ -54,6 +58,17 @@ export class ClientErrorHandler implements ErrorHandler {

public closed(): CloseAction {
this.restarts.push(Date.now());
const heapProfileGlob = new glob.GlobSync(`${this.heapDumpFolder}/java_*.hprof`);
if (heapProfileGlob.found.length) {
// Only clean heap dumps that are generated in the default location.
// The default location is the extension global storage
// This means that if users change the folder where the heap dumps are placed,
// then they will be able to read the heap dumps,
// since they aren't immediately deleted.
cleanUpHeapDumps(this.globalStoragePath);
showOOMMessage();
return CloseAction.DoNotRestart;
}
if (this.restarts.length < 5) {
logger.error(`The ${this.name} server crashed and will restart.`);
return CloseAction.Restart;
Expand All @@ -78,6 +93,46 @@ export class ClientErrorHandler implements ErrorHandler {
}
}

/**
* Deletes all the heap dumps generated by Out Of Memory errors
*
* @returns when the heap dumps have been deleted
*/
export async function cleanUpHeapDumps(globalStoragePath: string): Promise<void> {
const heapProfileGlob = new glob.GlobSync(`${globalStoragePath}/java_*.hprof`);
for (const heapProfile of heapProfileGlob.found) {
await fse.remove(heapProfile);
}
}

/**
* Shows a message about the server crashing due to an out of memory issue
*/
async function showOOMMessage(): Promise<void> {
const DOCS = 'More info...';
const result = await window.showErrorMessage('The Java Language Server crashed due to an Out Of Memory Error, and will not be restarted. ', //
DOCS);
if (result === DOCS) {
await commands.executeCommand(Commands.OUT_OF_MEMORY);
}
}

const HEAP_DUMP_FOLDER_EXTRACTOR = new RegExp(`${HEAP_DUMP_LOCATION}(?:'([^']+)'|"([^"]+)"|([^\\s]+))`);

/**
* Returns the heap dump folder defined in the user's preferences, or undefined if the user does not set the heap dump folder
*
* @returns the heap dump folder defined in the user's preferences, or undefined if the user does not set the heap dump folder
*/
function getHeapDumpFolderFromSettings(): string {
const jvmArgs: string = getJavaConfiguration().get('jdt.ls.vmargs');
const results = HEAP_DUMP_FOLDER_EXTRACTOR.exec(jvmArgs);
if (!results || !results[0]) {
return undefined;
}
return results[1] || results[2] || results[3];
}

export class OutputInfoCollector implements OutputChannel {
private channel: OutputChannel = null;

Expand Down Expand Up @@ -122,6 +177,9 @@ export function activate(context: ExtensionContext): Promise<ExtensionAPI> {
context.subscriptions.push(commands.registerCommand(Commands.NOT_COVERED_EXECUTION, async () => {
markdownPreviewProvider.show(context.asAbsolutePath(path.join('document', `_java.notCoveredExecution.md`)), 'Not Covered Maven Plugin Execution', "", context);
}));
context.subscriptions.push(commands.registerCommand(Commands.OUT_OF_MEMORY, async () => {
markdownPreviewProvider.show(context.asAbsolutePath(path.join('document', `_java.outOfMemory.md`)), 'Out Of Memory', "", context);
}));

let storagePath = context.storagePath;
if (!storagePath) {
Expand Down Expand Up @@ -209,7 +267,7 @@ export function activate(context: ExtensionContext): Promise<ExtensionAPI> {
}
},
revealOutputChannelOn: RevealOutputChannelOn.Never,
errorHandler: new ClientErrorHandler(extensionName),
errorHandler: new ClientErrorHandler(extensionName, context.globalStorageUri.fsPath),
initializationFailedHandler: error => {
logger.error(`Failed to initialize ${extensionName} due to ${error && error.toString()}`);
return true;
Expand All @@ -219,12 +277,13 @@ export function activate(context: ExtensionContext): Promise<ExtensionAPI> {
};

apiManager.initialize(requirements, serverMode);
const globalStoragePath = context.globalStorageUri.fsPath;

if (requireSyntaxServer) {
if (process.env['SYNTAXLS_CLIENT_PORT']) {
syntaxClient.initialize(requirements, clientOptions, resolve);
syntaxClient.initialize(requirements, clientOptions, resolve, globalStoragePath);
} else {
syntaxClient.initialize(requirements, clientOptions, resolve, prepareExecutable(requirements, syntaxServerWorkspacePath, getJavaConfig(requirements.java_home), context, true));
syntaxClient.initialize(requirements, clientOptions, resolve, globalStoragePath, prepareExecutable(requirements, syntaxServerWorkspacePath, getJavaConfig(requirements.java_home), context, true));
}
syntaxClient.start();
serverStatusBarProvider.showLightWeightStatus();
Expand Down
33 changes: 32 additions & 1 deletion src/javaServerStarter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,26 @@ import { RequirementsData } from './requirements';
import { getJavaEncoding, IS_WORKSPACE_VMARGS_ALLOWED, getKey, getJavaagentFlag } from './settings';
import { logger } from './log';
import { getJavaConfiguration, deleteDirectory, ensureExists, getTimestamp } from './utils';
import { workspace, ExtensionContext } from 'vscode';
import { workspace, ExtensionContext, window } from 'vscode';

declare var v8debug;
const DEBUG = (typeof v8debug === 'object') || startedInDebugMode();

/**
* Argument that tells the program where to generate the heap dump that is created when an OutOfMemoryError is raised and `HEAP_DUMP` has been passed
*/
export const HEAP_DUMP_LOCATION = '-XX:HeapDumpPath=';

/**
* Argument that tells the program to crash when an OutOfMemoryError is raised
*/
export const CRASH_ON_OOM = '-XX:+ExitOnOutOfMemoryError';

/**
* Argument that tells the program to generate a heap dump file when an OutOfMemoryError is raised
*/
export const HEAP_DUMP = '-XX:+HeapDumpOnOutOfMemoryError';

export function prepareExecutable(requirements: RequirementsData, workspacePath, javaConfig, context: ExtensionContext, isSyntaxServer: boolean): Executable {
const executable: Executable = Object.create(null);
const options: ExecutableOptions = Object.create(null);
Expand Down Expand Up @@ -94,6 +109,22 @@ function prepareParams(requirements: RequirementsData, javaConfiguration, worksp
}

parseVMargs(params, vmargs);

if (vmargs.indexOf(CRASH_ON_OOM) < 0) {
params.push(CRASH_ON_OOM);
}
if (vmargs.indexOf(HEAP_DUMP) < 0) {
params.push(HEAP_DUMP);
}
if (vmargs.indexOf(HEAP_DUMP_LOCATION) < 0) {
params.push(`${HEAP_DUMP_LOCATION}${context.globalStorageUri.fsPath}`);
} else {
window.showWarningMessage('Heap dump location has been modified. '
+ 'vscode-java won\'t delete the heap dumps. '
+ 'vscode-java\'s Out Of Memory detection won\'t work properly, '
+ 'unless you manually delete the heap dumps after each Out Of Memory crash.');
}

// "OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify
// were deprecated in JDK 13 and will likely be removed in a future release."
// so only add -noverify for older versions
Expand Down
4 changes: 2 additions & 2 deletions src/syntaxLanguageClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class SyntaxLanguageClient {
private languageClient: LanguageClient;
private status: ClientStatus = ClientStatus.Uninitialized;

public initialize(requirements, clientOptions: LanguageClientOptions, resolve: (value: ExtensionAPI) => void, serverOptions?: ServerOptions) {
public initialize(requirements, clientOptions: LanguageClientOptions, resolve: (value: ExtensionAPI) => void, globalStoragePath: string, serverOptions?: ServerOptions) {
const newClientOptions: LanguageClientOptions = Object.assign({}, clientOptions, {
middleware: {
workspace: {
Expand All @@ -29,7 +29,7 @@ export class SyntaxLanguageClient {
}
}
},
errorHandler: new ClientErrorHandler(extensionName),
errorHandler: new ClientErrorHandler(extensionName, globalStoragePath),
initializationFailedHandler: error => {
logger.error(`Failed to initialize ${extensionName} due to ${error && error.toString()}`);
return true;
Expand Down

0 comments on commit 42b0470

Please sign in to comment.